Adding JS execution functionality.

This commit is contained in:
Satbir 2019-11-28 09:26:44 +05:30
parent 0ffe94a298
commit 48fa52d9ba
16 changed files with 298 additions and 39 deletions

View File

@ -3,6 +3,7 @@
<head> <head>
<script type="text/javascript" src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script> <script type="text/javascript" src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
<script type="text/javascript" src="/shims/realms-shim.umd.min.js"></script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<!-- in index.html, or however you manage your CSS files --> <!-- in index.html, or however you manage your CSS files -->
<link href="../node_modules/normalize.css/normalize.css" rel="stylesheet" /> <link href="../node_modules/normalize.css/normalize.css" rel="stylesheet" />

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@ export const PropertyPaneActionDropdownOptions: DropdownOption[] = [
// { label: "Run Query", value: "QUERY" }, // { label: "Run Query", value: "QUERY" },
]; ];
export interface ActionPayload { export interface BaseActionPayload {
actionId: string; actionId: string;
actionType: ActionType; actionType: ActionType;
contextParams: Record<string, string>; contextParams: Record<string, string>;
@ -35,34 +35,42 @@ export interface ActionPayload {
onError?: ActionPayload[]; onError?: ActionPayload[];
} }
export type ActionPayload =
| NavigateActionPayload
| SetValueActionPayload
| ExecuteJSActionPayload
| DownloadDataActionPayload
| SetValueActionPayload;
export type NavigationType = "NEW_TAB" | "INLINE"; export type NavigationType = "NEW_TAB" | "INLINE";
export interface NavigateActionPayload extends ActionPayload { export interface NavigateActionPayload extends BaseActionPayload {
pageUrl: string; pageUrl: string;
navigationType: NavigationType; navigationType: NavigationType;
} }
export interface ShowAlertActionPayload extends ActionPayload { export interface ShowAlertActionPayload extends BaseActionPayload {
header: string; header: string;
message: string; message: string;
alertType: AlertType; alertType: AlertType;
intent: MessageIntent; intent: MessageIntent;
} }
export interface SetValueActionPayload extends ActionPayload { export interface SetValueActionPayload extends BaseActionPayload {
header: string; header: string;
message: string; message: string;
alertType: AlertType; alertType: AlertType;
intent: MessageIntent; intent: MessageIntent;
} }
export interface ExecuteJSActionPayload extends ActionPayload { export interface ExecuteJSActionPayload extends BaseActionPayload {
jsFunctionId: string; jsFunctionId: string;
jsFunction: string;
} }
export type DownloadFiletype = "CSV" | "XLS" | "JSON" | "TXT"; export type DownloadFiletype = "CSV" | "XLS" | "JSON" | "TXT";
export interface DownloadDataActionPayload extends ActionPayload { export interface DownloadDataActionPayload extends BaseActionPayload {
data: JSON; data: JSON;
fileName: string; fileName: string;
fileType: DownloadFiletype; fileType: DownloadFiletype;

View File

@ -2,5 +2,6 @@
// TODO (hetu): Remove useless escapes and re-enable the above lint rule // TODO (hetu): Remove useless escapes and re-enable the above lint rule
export type NamePathBindingMap = Record<string, string>; export type NamePathBindingMap = Record<string, string>;
export const DATA_BIND_REGEX = /{{(\s*[\w\.\[\]\d]+\s*)}}/g; export const DATA_BIND_REGEX = /{{(\s*[\w\.\[\]\d]+\s*)}}/g;
export const DATA_BIND_JS_REGEX = /(.*?){{(\s*(.*?)\s*)}}(.*?)/g;
export const DATA_PATH_REGEX = /[\w\.\[\]\d]+/; export const DATA_PATH_REGEX = /[\w\.\[\]\d]+/;
/* eslint-enable no-useless-escape */ /* eslint-enable no-useless-escape */

View File

@ -121,6 +121,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
FETCH_PAGE_LIST_ERROR: "FETCH_PAGE_LIST_ERROR", FETCH_PAGE_LIST_ERROR: "FETCH_PAGE_LIST_ERROR",
FETCH_APPLICATION_LIST_ERROR: "FETCH_APPLICATION_LIST_ERROR", FETCH_APPLICATION_LIST_ERROR: "FETCH_APPLICATION_LIST_ERROR",
CREATE_APPLICATION_ERROR: "CREATE_APPLICATION_ERROR", CREATE_APPLICATION_ERROR: "CREATE_APPLICATION_ERROR",
SAVE_JS_EXECUTION_RECORD: "SAVE_JS_EXECUTION_RECORD",
}; };
export const ReduxFormActionTypes: { [key: string]: string } = { export const ReduxFormActionTypes: { [key: string]: string } = {

View File

@ -0,0 +1,51 @@
import RealmExecutor from "./RealmExecutor";
export type JSExecutorGlobal = Record<string, object>;
export interface JSExecutor {
execute: (src: string, data: JSExecutorGlobal) => string;
registerLibrary: (accessor: string, lib: any) => void;
unRegisterLibrary: (accessor: string) => void;
}
enum JSExecutorType {
REALM,
}
class JSExecutionManager {
currentExecutor: JSExecutor;
executors: Record<JSExecutorType, JSExecutor>;
registerLibrary(accessor: string, lib: any) {
Object.keys(this.executors).forEach(type => {
const executor = this.executors[(type as any) as JSExecutorType];
executor.registerLibrary(accessor, lib);
});
}
unRegisterLibrary(accessor: string) {
Object.keys(this.executors).forEach(type => {
const executor = this.executors[(type as any) as JSExecutorType];
executor.unRegisterLibrary(accessor);
});
}
switchExecutor(type: JSExecutorType) {
const executor = this.executors[type];
if (!executor) {
throw new Error("Executor does not exist");
}
this.currentExecutor = executor;
}
constructor() {
const realmExecutor = new RealmExecutor();
this.executors = {
[JSExecutorType.REALM]: realmExecutor,
};
this.currentExecutor = realmExecutor;
this.registerLibrary("_", window._);
}
evaluateSync(jsSrc: string, data: JSExecutorGlobal) {
return this.currentExecutor.execute(jsSrc, data);
}
}
const JSExecutionManagerSingleton = new JSExecutionManager();
export default JSExecutionManagerSingleton;

View File

@ -0,0 +1,42 @@
import { JSExecutorGlobal, JSExecutor } from "./JSExecutionManagerSingleton";
declare let Realm: any;
export default class RealmExecutor implements JSExecutor {
rootRealm: any;
creaetSafeObject: any;
extrinsics: any[] = [];
createSafeFunction: (unsafeFn: Function) => Function;
libraries: Record<string, any> = {};
constructor() {
this.rootRealm = Realm.makeRootRealm();
this.createSafeFunction = this.rootRealm.evaluate(`
(function createSafeFunction(unsafeFn) {
return function safeFn(...args) {
unsafeFn(...args);
}
})
`);
this.creaetSafeObject = this.rootRealm.evaluate(`
(function creaetSafeObject(unsafeObject) {
return JSON.parse(JSON.stringify(unsafeObject));
})
`);
}
registerLibrary(accessor: string, lib: any) {
this.rootRealm.global[accessor] = lib;
}
unRegisterLibrary(accessor: string) {
this.rootRealm.global[accessor] = null;
}
execute(sourceText: string, data: JSExecutorGlobal) {
const safeData = this.creaetSafeObject(data);
let result;
try {
result = this.rootRealm.evaluate(sourceText, safeData);
} catch (e) {
result = `Error: ${e}`;
}
return result;
}
}

View File

@ -8,6 +8,7 @@ import propertyPaneConfigReducer from "./propertyPaneConfigReducer";
import datasourceReducer from "./datasourceReducer"; import datasourceReducer from "./datasourceReducer";
import bindingsReducer from "./bindingsReducer"; import bindingsReducer from "./bindingsReducer";
import pageListReducer from "./pageListReducer"; import pageListReducer from "./pageListReducer";
import jsExecutionsReducer from "./jsExecutionsReducer";
const entityReducer = combineReducers({ const entityReducer = combineReducers({
canvasWidgets: canvasWidgetsReducer, canvasWidgets: canvasWidgetsReducer,
@ -19,6 +20,7 @@ const entityReducer = combineReducers({
datasources: datasourceReducer, datasources: datasourceReducer,
nameBindings: bindingsReducer, nameBindings: bindingsReducer,
pageList: pageListReducer, pageList: pageListReducer,
jsExecutions: jsExecutionsReducer,
}); });
export default entityReducer; export default entityReducer;

View File

@ -0,0 +1,21 @@
import { createReducer } from "../../utils/AppsmithUtils";
import {
ReduxActionTypes,
ReduxAction,
} from "../../constants/ReduxActionConstants";
export type JSExecutionRecord = Record<string, string>;
const initialState: JSExecutionRecord = {};
const jsExecutionsReducer = createReducer(initialState, {
[ReduxActionTypes.SAVE_JS_EXECUTION_RECORD]: (
state: JSExecutionRecord,
action: ReduxAction<JSExecutionRecord>,
) => {
return {
...state,
...action.payload,
};
},
});
export default jsExecutionsReducer;

View File

@ -12,7 +12,11 @@ import {
takeEvery, takeEvery,
takeLatest, takeLatest,
} from "redux-saga/effects"; } from "redux-saga/effects";
import { ActionPayload, PageAction } from "constants/ActionConstants"; import {
ActionPayload,
PageAction,
ExecuteJSActionPayload,
} from "constants/ActionConstants";
import ActionAPI, { import ActionAPI, {
ActionApiResponse, ActionApiResponse,
ActionCreateUpdateResponse, ActionCreateUpdateResponse,
@ -35,6 +39,7 @@ import {
extractDynamicBoundValue, extractDynamicBoundValue,
getDynamicBindings, getDynamicBindings,
isDynamicValue, isDynamicValue,
NameBindingsWithData,
} from "utils/DynamicBindingUtils"; } from "utils/DynamicBindingUtils";
import { validateResponse } from "./ErrorSagas"; import { validateResponse } from "./ErrorSagas";
import { getDataTree } from "selectors/entitiesSelector"; import { getDataTree } from "selectors/entitiesSelector";
@ -44,6 +49,8 @@ import {
} from "constants/messages"; } from "constants/messages";
import { getFormData } from "selectors/formSelectors"; import { getFormData } from "selectors/formSelectors";
import { API_EDITOR_FORM_NAME } from "constants/forms"; import { API_EDITOR_FORM_NAME } from "constants/forms";
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
export const getAction = ( export const getAction = (
state: AppState, state: AppState,
@ -90,6 +97,23 @@ export function* getActionParams(jsonPathKeys: string[] | undefined) {
return mapToPropList(dynamicBindings); return mapToPropList(dynamicBindings);
} }
function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) {
const nameBindingsWithData: NameBindingsWithData = yield select(
getNameBindingsWithData,
);
const result = JSExecutionManagerSingleton.evaluateSync(
jsAction.jsFunction,
nameBindingsWithData,
);
yield put({
type: ReduxActionTypes.SAVE_JS_EXECUTION_RECORD,
payload: {
[jsAction.jsFunctionId]: result,
},
});
}
export function* executeAPIQueryActionSaga(apiAction: ActionPayload) { export function* executeAPIQueryActionSaga(apiAction: ActionPayload) {
try { try {
const api: PageAction = yield select(getAction, apiAction.actionId); const api: PageAction = yield select(getAction, apiAction.actionId);
@ -193,6 +217,11 @@ export function* executeActionSaga(actionPayloads: ActionPayload[]): any {
return call(executeAPIQueryActionSaga, actionPayload); return call(executeAPIQueryActionSaga, actionPayload);
case "QUERY": case "QUERY":
return call(executeAPIQueryActionSaga, actionPayload); return call(executeAPIQueryActionSaga, actionPayload);
case "JS_FUNCTION":
return call(
executeJSActionSaga,
actionPayload as ExecuteJSActionPayload,
);
default: default:
return undefined; return undefined;
} }

View File

@ -7,7 +7,10 @@ import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigRe
import { WidgetCardProps } from "widgets/BaseWidget"; import { WidgetCardProps } from "widgets/BaseWidget";
import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer"; import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer";
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; import {
enhanceWithDynamicValuesAndValidations,
NameBindingsWithData,
} from "utils/DynamicBindingUtils";
import { getDataTree } from "./entitiesSelector"; import { getDataTree } from "./entitiesSelector";
import { import {
FlattenedWidgetProps, FlattenedWidgetProps,
@ -17,6 +20,7 @@ import { PageListReduxState } from "reducers/entityReducers/pageListReducer";
import { OccupiedSpace } from "constants/editorConstants"; import { OccupiedSpace } from "constants/editorConstants";
import { WidgetTypes } from "constants/WidgetConstants"; import { WidgetTypes } from "constants/WidgetConstants";
import { getNameBindingsWithData } from "./nameBindingsWithDataSelector";
const getEditorState = (state: AppState) => state.ui.editor; const getEditorState = (state: AppState) => state.ui.editor;
const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig;
@ -112,12 +116,13 @@ export const getWidgetCards = createSelector(
export const getValidatedDynamicProps = createSelector( export const getValidatedDynamicProps = createSelector(
getDataTree, getDataTree,
(entities: DataTree) => { getNameBindingsWithData,
(entities: DataTree, nameBindingsWithData: NameBindingsWithData) => {
const widgets = { ...entities.canvasWidgets }; const widgets = { ...entities.canvasWidgets };
Object.keys(widgets).forEach(widgetKey => { Object.keys(widgets).forEach(widgetKey => {
widgets[widgetKey] = enhanceWithDynamicValuesAndValidations( widgets[widgetKey] = enhanceWithDynamicValuesAndValidations(
widgets[widgetKey], widgets[widgetKey],
entities, nameBindingsWithData,
true, true,
); );
}); });

View File

@ -0,0 +1,24 @@
import { DataTree } from "reducers";
import { NameBindingsWithData } from "utils/DynamicBindingUtils";
import { JSONPath } from "jsonpath-plus";
import { createSelector } from "reselect";
import { getDataTree } from "./entitiesSelector";
export const getNameBindingsWithData = createSelector(
getDataTree,
(dataTree: DataTree): NameBindingsWithData => {
const nameBindingsWithData: Record<string, object> = {};
Object.keys(dataTree.nameBindings).forEach(key => {
const nameBindings = dataTree.nameBindings[key];
const evaluatedValue = JSONPath({
path: nameBindings,
json: dataTree,
})[0];
if (evaluatedValue && key !== "undefined") {
nameBindingsWithData[key] = evaluatedValue;
}
});
return nameBindingsWithData;
},
);

View File

@ -1,12 +1,15 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { AppState, DataTree } from "reducers"; import { AppState } from "reducers";
import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer"; import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer";
import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer"; import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer"; import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer";
import { getDataTree } from "./entitiesSelector"; import {
import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; enhanceWithDynamicValuesAndValidations,
NameBindingsWithData,
} from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget"; import { WidgetProps } from "widgets/BaseWidget";
import { getNameBindingsWithData } from "./nameBindingsWithDataSelector";
const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => const getPropertyPaneState = (state: AppState): PropertyPaneReduxState =>
state.ui.propertyPane; state.ui.propertyPane;
@ -35,10 +38,17 @@ export const getCurrentWidgetProperties = createSelector(
export const getWidgetPropsWithValidations = createSelector( export const getWidgetPropsWithValidations = createSelector(
getCurrentWidgetProperties, getCurrentWidgetProperties,
getDataTree, getNameBindingsWithData,
(widget: WidgetProps | undefined, dataTree: DataTree) => { (
widget: WidgetProps | undefined,
nameBindigsWithData: NameBindingsWithData,
) => {
if (!widget) return undefined; if (!widget) return undefined;
return enhanceWithDynamicValuesAndValidations(widget, dataTree, false); return enhanceWithDynamicValuesAndValidations(
widget,
nameBindigsWithData,
false,
);
}, },
); );

View File

@ -1,22 +1,67 @@
import _ from "lodash"; import _ from "lodash";
import { DataTree } from "reducers";
import { JSONPath } from "jsonpath-plus";
import { WidgetProps } from "widgets/BaseWidget"; import { WidgetProps } from "widgets/BaseWidget";
import { DATA_BIND_REGEX, DATA_PATH_REGEX } from "constants/BindingsConstants"; import { DATA_BIND_JS_REGEX } from "constants/BindingsConstants";
import ValidationFactory from "./ValidationFactory"; import ValidationFactory from "./ValidationFactory";
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
export type NameBindingsWithData = Record<string, object>;
export const isDynamicValue = (value: string): boolean => export const isDynamicValue = (value: string): boolean =>
DATA_BIND_REGEX.test(value); DATA_BIND_JS_REGEX.test(value);
//{{}}{{}}}
function parseDynamicString(dynamicString: string): string[] {
let parsedDynamicValues = [];
const indexOfDoubleParanStart = dynamicString.indexOf("{{");
if (indexOfDoubleParanStart === -1) {
return [dynamicString];
}
//{{}}{{}}}
const firstString = dynamicString.substring(0, indexOfDoubleParanStart);
firstString && parsedDynamicValues.push(firstString);
let rest = dynamicString.substring(
indexOfDoubleParanStart,
dynamicString.length,
);
//{{}}{{}}}
let sum = 0;
for (let i = 0; i <= rest.length - 1; i++) {
const char = rest[i];
const prevChar = rest[i - 1];
if (char === "{") {
sum++;
} else if (char === "}") {
sum--;
if (prevChar === "}" && sum === 0) {
parsedDynamicValues.push(rest.substring(0, i + 1));
rest = rest.substring(i + 1, rest.length);
if (rest) {
parsedDynamicValues = parsedDynamicValues.concat(
parseDynamicString(rest),
);
break;
}
}
}
}
if (sum !== 0 && dynamicString !== "") {
return [dynamicString];
}
return parsedDynamicValues;
}
export const getDynamicBindings = ( export const getDynamicBindings = (
dynamicString: string, dynamicString: string,
): { bindings: string[]; paths: string[] } => { ): { bindings: string[]; paths: string[] } => {
// Get the {{binding}} bound values // Get the {{binding}} bound values
const bindings = dynamicString.match(DATA_BIND_REGEX) || []; const bindings = parseDynamicString(dynamicString);
// Get the "binding" path values // Get the "binding" path values
const paths = bindings.map(p => { const paths = bindings.map(binding => {
const matches = p.match(DATA_PATH_REGEX); const length = binding.length;
if (matches) return matches[0]; const matches = binding.match(DATA_BIND_JS_REGEX);
if (matches) {
return binding.substring(2, length - 2);
}
return ""; return "";
}); });
return { bindings, paths }; return { bindings, paths };
@ -24,17 +69,10 @@ export const getDynamicBindings = (
// Paths are expected to have "{name}.{path}" signature // Paths are expected to have "{name}.{path}" signature
export const extractDynamicBoundValue = ( export const extractDynamicBoundValue = (
dataTree: DataTree, data: NameBindingsWithData,
path: string, path: string,
): any => { ): any => {
// Remove the name in the binding return JSExecutionManagerSingleton.evaluateSync(path, data);
const splitPath = path.split(".");
// Find the dataTree path of the name
const bindingPath = dataTree.nameBindings[splitPath[0]];
// Create the full path
const fullPath = `${bindingPath}.${splitPath.slice(1).join(".")}`;
// Search with JSONPath
return JSONPath({ path: fullPath, json: dataTree })[0];
}; };
// For creating a final value where bindings could be in a template format // For creating a final value where bindings could be in a template format
@ -57,13 +95,16 @@ export const createDynamicValueString = (
export const getDynamicValue = ( export const getDynamicValue = (
dynamicBinding: string, dynamicBinding: string,
dataTree: DataTree, data: NameBindingsWithData,
): any => { ): any => {
// Get the {{binding}} bound values // Get the {{binding}} bound values
const { bindings, paths } = getDynamicBindings(dynamicBinding); const { bindings, paths } = getDynamicBindings(dynamicBinding);
if (bindings.length) { if (bindings.length) {
// Get the Data Tree value of those "binding "paths // Get the Data Tree value of those "binding "paths
const values = paths.map(p => extractDynamicBoundValue(dataTree, p)); const values = paths.map((p, i) => {
return p ? extractDynamicBoundValue(data, p) : bindings[i];
});
// if it is just one binding, no need to create template string // if it is just one binding, no need to create template string
if (bindings.length === 1) return values[0]; if (bindings.length === 1) return values[0];
// else return a string template with bindings // else return a string template with bindings
@ -74,17 +115,19 @@ export const getDynamicValue = (
export const enhanceWithDynamicValuesAndValidations = ( export const enhanceWithDynamicValuesAndValidations = (
widget: WidgetProps, widget: WidgetProps,
entities: DataTree, nameBindingsWithData: NameBindingsWithData,
replaceWithParsed: boolean, replaceWithParsed: boolean,
): WidgetProps => { ): WidgetProps => {
if (!widget) return widget; if (!widget) return widget;
const properties = { ...widget }; const properties = { ...widget };
const invalidProps: Record<string, boolean> = {}; const invalidProps: Record<string, boolean> = {};
const t0 = performance.now();
Object.keys(widget).forEach((property: string) => { Object.keys(widget).forEach((property: string) => {
let value = widget[property]; let value = widget[property];
// Check for dynamic bindings // Check for dynamic bindings
if (widget.dynamicBindings && property in widget.dynamicBindings) { if (widget.dynamicBindings && property in widget.dynamicBindings) {
value = getDynamicValue(value, entities); value = getDynamicValue(value, nameBindingsWithData);
} }
// Pass it through validation and parse // Pass it through validation and parse
const { isValid, parsed } = ValidationFactory.validateWidgetProperty( const { isValid, parsed } = ValidationFactory.validateWidgetProperty(
@ -97,5 +140,14 @@ export const enhanceWithDynamicValuesAndValidations = (
// Replace if flag is turned on // Replace if flag is turned on
if (replaceWithParsed) properties[property] = parsed; if (replaceWithParsed) properties[property] = parsed;
}); });
const t1 = performance.now();
console.log(
"Evaluations for " +
widget.widgetName +
" took " +
(t1 - t0) +
" milliseconds.",
);
console.trace();
return { ...properties, invalidProps }; return { ...properties, invalidProps };
}; };

View File

@ -2,7 +2,7 @@ import React from "react";
import _ from "lodash"; import _ from "lodash";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants"; import { WidgetType } from "constants/WidgetConstants";
import { ActionPayload } from "constants/ActionConstants"; import { ActionPayload, BaseActionPayload } from "constants/ActionConstants";
import { AutoResizer } from "react-base-table"; import { AutoResizer } from "react-base-table";
import "react-base-table/styles.css"; import "react-base-table/styles.css";
import { forIn } from "lodash"; import { forIn } from "lodash";
@ -94,7 +94,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
export type PaginationType = "PAGES" | "INFINITE_SCROLL"; export type PaginationType = "PAGES" | "INFINITE_SCROLL";
export interface TableAction extends ActionPayload { export interface TableAction extends BaseActionPayload {
actionName: string; actionName: string;
} }

2
app/client/typings/Realm/index.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
// import * as React from "react";
declare module "Realm";