Improve autocomplete sorting (#5798)

This commit is contained in:
Hetu Nandu 2021-07-20 15:32:56 +05:30 committed by GitHub
parent 6e20e2b659
commit ba06d797de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 3153 additions and 1027 deletions

View File

@ -31,7 +31,7 @@ describe("Dynamic input autocomplete", () => {
// Tests if data tree entities are sorted
cy.get(`${dynamicInputLocators.hints} li`)
.eq(1)
.should("have.text", "Aditya.backgroundColor");
.should("have.text", "input.text");
// Tests if "No suggestions" message will pop if you type any garbage
cy.get(dynamicInputLocators.input)

View File

@ -0,0 +1,82 @@
import {
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
} from "../constants/ReduxActionConstants";
import _ from "lodash";
import { DataTree } from "../entities/DataTree/dataTreeFactory";
import { DependencyMap } from "../utils/DynamicBindingUtils";
import { Diff } from "deep-diff";
export const FIRST_EVAL_REDUX_ACTIONS = [
// Pages
ReduxActionTypes.FETCH_PAGE_SUCCESS,
ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS,
];
export const EVALUATE_REDUX_ACTIONS = [
...FIRST_EVAL_REDUX_ACTIONS,
// Actions
ReduxActionTypes.FETCH_ACTIONS_SUCCESS,
ReduxActionTypes.FETCH_PLUGIN_FORM_CONFIGS_SUCCESS,
ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionErrorTypes.FETCH_ACTIONS_ERROR,
ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR,
ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS,
ReduxActionTypes.SUBMIT_CURL_FORM_SUCCESS,
ReduxActionTypes.CREATE_ACTION_SUCCESS,
ReduxActionTypes.UPDATE_ACTION_PROPERTY,
ReduxActionTypes.DELETE_ACTION_SUCCESS,
ReduxActionTypes.COPY_ACTION_SUCCESS,
ReduxActionTypes.MOVE_ACTION_SUCCESS,
ReduxActionTypes.RUN_ACTION_SUCCESS,
ReduxActionErrorTypes.RUN_ACTION_ERROR,
ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS,
ReduxActionErrorTypes.EXECUTE_ACTION_ERROR,
// App Data
ReduxActionTypes.SET_APP_MODE,
ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE,
ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE,
// Widgets
ReduxActionTypes.UPDATE_LAYOUT,
ReduxActionTypes.UPDATE_WIDGET_PROPERTY,
ReduxActionTypes.UPDATE_WIDGET_NAME_SUCCESS,
// Widget Meta
ReduxActionTypes.SET_META_PROP,
ReduxActionTypes.RESET_WIDGET_META,
// Batches
ReduxActionTypes.BATCH_UPDATES_SUCCESS,
];
export const shouldProcessBatchedAction = (action: ReduxAction<unknown>) => {
if (
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
Array.isArray(action.payload)
) {
const batchedActionTypes = action.payload.map(
(batchedAction) => batchedAction.type,
);
return (
_.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length > 0
);
}
return true;
};
export const setEvaluatedTree = (
dataTree: DataTree,
updates: Diff<DataTree, DataTree>[],
): ReduxAction<{ dataTree: DataTree; updates: Diff<DataTree, DataTree>[] }> => {
return {
type: ReduxActionTypes.SET_EVALUATED_TREE,
payload: { dataTree, updates },
};
};
export const setDependencyMap = (
inverseDependencyMap: DependencyMap,
): ReduxAction<{ inverseDependencyMap: DependencyMap }> => {
return {
type: ReduxActionTypes.SET_EVALUATION_INVERSE_DEPENDENCY_MAP,
payload: { inverseDependencyMap },
};
};

View File

@ -1,5 +1,5 @@
import CodeMirror from "codemirror";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
export enum EditorModes {
TEXT = "text/plain",
@ -40,18 +40,23 @@ export const EditorThemes: Record<EditorTheme, string> = {
[EditorTheme.DARK]: "duotone-dark",
};
export type HintEntityInformation = {
entityName?: string;
expectedType?: string;
entityType?: ENTITY_TYPE.ACTION | ENTITY_TYPE.WIDGET;
};
export type HintHelper = (
editor: CodeMirror.Editor,
data: DataTree,
additionalData?: Record<string, Record<string, unknown>>,
customDataTree?: Record<string, Record<string, unknown>>,
) => Hinter;
export type Hinter = {
showHint: (
editor: CodeMirror.Editor,
expected: string,
entityName: string,
entityInformation: HintEntityInformation,
additionalData?: any,
) => any;
) => boolean;
update?: (data: DataTree) => void;
trigger?: (editor: CodeMirror.Editor) => void;
};

View File

@ -1,3 +1,6 @@
import CodeMirror from "codemirror";
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
export const removeNewLineChars = (inputValue: any) => {
return inputValue && inputValue.replace(/(\r\n|\n|\r)/gm, "");
};
@ -10,3 +13,43 @@ export const getInputValue = (inputValue: any) => {
}
return inputValue;
};
const computeCursorIndex = (editor: CodeMirror.Editor) => {
const cursor = editor.getCursor();
let cursorIndex = cursor.ch;
if (cursor.line > 0) {
for (let lineIndex = 0; lineIndex < cursor.line; lineIndex++) {
const line = editor.getLine(lineIndex);
cursorIndex = cursorIndex + line.length + 1;
}
}
return cursorIndex;
};
export const checkIfCursorInsideBinding = (
editor: CodeMirror.Editor,
): boolean => {
let cursorBetweenBinding = false;
const value = editor.getValue();
const cursorIndex = computeCursorIndex(editor);
const stringSegments = getDynamicStringSegments(value);
// count of chars processed
let cumulativeCharCount = 0;
stringSegments.forEach((segment: string) => {
const start = cumulativeCharCount;
const dynamicStart = segment.indexOf("{{");
const dynamicDoesStart = dynamicStart > -1;
const dynamicEnd = segment.indexOf("}}");
const dynamicDoesEnd = dynamicEnd > -1;
const dynamicStartIndex = dynamicStart + start + 2;
const dynamicEndIndex = dynamicEnd + start;
if (
dynamicDoesStart &&
cursorIndex >= dynamicStartIndex &&
((dynamicDoesEnd && cursorIndex <= dynamicEndIndex) ||
(!dynamicDoesEnd && cursorIndex >= dynamicStartIndex))
) {
cursorBetweenBinding = true;
}
cumulativeCharCount = start + segment.length;
});
return cursorBetweenBinding;
};

View File

@ -1,22 +1,22 @@
import CodeMirror from "codemirror";
import { HintHelper } from "components/editorComponents/CodeEditor/EditorConfig";
import { CommandsCompletion } from "utils/autocomplete/TernServer";
import { checkIfCursorInsideBinding } from "./hintHelpers";
import { generateQuickCommands } from "./generateQuickCommands";
import { Datasource } from "entities/Datasource";
import AnalyticsUtil from "utils/AnalyticsUtil";
import log from "loglevel";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { checkIfCursorInsideBinding } from "components/editorComponents/CodeEditor/codeEditorUtils";
export const commandsHelper: HintHelper = (editor, data: any) => {
export const commandsHelper: HintHelper = (editor, data: DataTree) => {
let entitiesForSuggestions = Object.values(data).filter(
(entity: any) => entity.ENTITY_TYPE && entity.ENTITY_TYPE !== "APPSMITH",
(entity: any) =>
entity.ENTITY_TYPE && entity.ENTITY_TYPE !== ENTITY_TYPE.APPSMITH,
);
return {
showHint: (
editor: CodeMirror.Editor,
_: string,
entityName: string,
{ entityType },
{
datasources,
executeCommand,
@ -31,11 +31,11 @@ export const commandsHelper: HintHelper = (editor, data: any) => {
update: (value: string) => void;
},
): boolean => {
const currentEntityType = data[entityName]?.ENTITY_TYPE || "ACTION";
const currentEntityType = entityType || ENTITY_TYPE.ACTION;
entitiesForSuggestions = entitiesForSuggestions.filter((entity: any) => {
return currentEntityType === "WIDGET"
? entity.ENTITY_TYPE !== "WIDGET"
: entity.ENTITY_TYPE !== "ACTION";
return currentEntityType === ENTITY_TYPE.WIDGET
? entity.ENTITY_TYPE !== ENTITY_TYPE.WIDGET
: entity.ENTITY_TYPE !== ENTITY_TYPE.ACTION;
});
const cursorBetweenBinding = checkIfCursorInsideBinding(editor);
const value = editor.getValue();

View File

@ -1,14 +1,14 @@
import CodeMirror from "codemirror";
import TernServer from "utils/autocomplete/TernServer";
import KeyboardShortcuts from "constants/KeyboardShortcuts";
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
import { HintHelper } from "components/editorComponents/CodeEditor/EditorConfig";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { customTreeTypeDefCreator } from "../../../utils/autocomplete/customTreeTypeDefCreator";
import { customTreeTypeDefCreator } from "utils/autocomplete/customTreeTypeDefCreator";
import { checkIfCursorInsideBinding } from "components/editorComponents/CodeEditor/codeEditorUtils";
export const bindingHint: HintHelper = (editor, dataTree, additionalData) => {
if (additionalData) {
const customTreeDef = customTreeTypeDefCreator(additionalData);
export const bindingHint: HintHelper = (editor, dataTree, customDataTree) => {
if (customDataTree) {
const customTreeDef = customTreeTypeDefCreator(customDataTree);
TernServer.updateDef("customDataTree", customTreeDef);
}
@ -16,11 +16,8 @@ export const bindingHint: HintHelper = (editor, dataTree, additionalData) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available
...editor.options.extraKeys,
[KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (
cm: CodeMirror.Editor,
expected: string,
entity: string,
) => TernServer.complete(cm, expected, entity),
[KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (cm: CodeMirror.Editor) =>
TernServer.complete(cm),
[KeyboardShortcuts.CodeEditor.ShowTypeAndInfo]: (cm: CodeMirror.Editor) => {
TernServer.showType(cm);
},
@ -29,15 +26,12 @@ export const bindingHint: HintHelper = (editor, dataTree, additionalData) => {
},
});
return {
showHint: (
editor: CodeMirror.Editor,
expected: string,
entityName: string,
): boolean => {
showHint: (editor: CodeMirror.Editor, entityInformation): boolean => {
TernServer.setEntityInformation(entityInformation);
const shouldShow = checkIfCursorInsideBinding(editor);
if (shouldShow) {
AnalyticsUtil.logEvent("AUTO_COMPLETE_SHOW", {});
TernServer.complete(editor, expected, entityName);
TernServer.complete(editor);
return true;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -47,45 +41,3 @@ export const bindingHint: HintHelper = (editor, dataTree, additionalData) => {
},
};
};
const computeCursorIndex = (editor: CodeMirror.Editor) => {
const cursor = editor.getCursor();
let cursorIndex = cursor.ch;
if (cursor.line > 0) {
for (let lineIndex = 0; lineIndex < cursor.line; lineIndex++) {
const line = editor.getLine(lineIndex);
cursorIndex = cursorIndex + line.length + 1;
}
}
return cursorIndex;
};
export const checkIfCursorInsideBinding = (
editor: CodeMirror.Editor,
): boolean => {
let cursorBetweenBinding = false;
const value = editor.getValue();
const cursorIndex = computeCursorIndex(editor);
const stringSegments = getDynamicStringSegments(value);
// count of chars processed
let cumulativeCharCount = 0;
stringSegments.forEach((segment: string) => {
const start = cumulativeCharCount;
const dynamicStart = segment.indexOf("{{");
const dynamicDoesStart = dynamicStart > -1;
const dynamicEnd = segment.indexOf("}}");
const dynamicDoesEnd = dynamicEnd > -1;
const dynamicStartIndex = dynamicStart + start + 2;
const dynamicEndIndex = dynamicEnd + start;
if (
dynamicDoesStart &&
cursorIndex >= dynamicStartIndex &&
((dynamicDoesEnd && cursorIndex <= dynamicEndIndex) ||
(!dynamicDoesEnd && cursorIndex >= dynamicStartIndex))
) {
cursorBetweenBinding = true;
}
cumulativeCharCount = start + segment.length;
});
return cursorBetweenBinding;
};

View File

@ -18,6 +18,7 @@ import { WrappedFieldInputProps } from "redux-form";
import _ from "lodash";
import {
DataTree,
ENTITY_TYPE,
EvaluationSubstitutionType,
} from "entities/DataTree/dataTreeFactory";
import { Skin } from "constants/DefaultTheme";
@ -30,6 +31,7 @@ import {
EditorSize,
EditorTheme,
EditorThemes,
HintEntityInformation,
Hinter,
HintHelper,
MarkHelper,
@ -55,7 +57,7 @@ import {
getEvalValuePath,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import { removeNewLineChars, getInputValue } from "./codeEditorUtils";
import { getInputValue, removeNewLineChars } from "./codeEditorUtils";
import { commandsHelper } from "./commandsHelper";
import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
import Button from "components/ads/Button";
@ -373,13 +375,27 @@ class CodeEditor extends Component<Props, State> {
handleAutocompleteVisibility = (cm: CodeMirror.Editor) => {
if (!this.state.isFocused) return;
const expected = this.props.expected ? this.props.expected : "";
const { entityName } = getEntityNameAndPropertyPath(
this.props.dataTreePath || "",
);
const { dataTreePath, dynamicData, expected } = this.props;
const entityInformation: HintEntityInformation = {
expectedType: expected,
};
if (dataTreePath) {
const { entityName } = getEntityNameAndPropertyPath(dataTreePath);
entityInformation.entityName = entityName;
const entity = dynamicData[entityName];
if (entity && "ENTITY_TYPE" in entity) {
const entityType = entity.ENTITY_TYPE;
if (
entityType === ENTITY_TYPE.WIDGET ||
entityType === ENTITY_TYPE.ACTION
) {
entityInformation.entityType = entityType;
}
}
}
let hinterOpen = false;
for (let i = 0; i < this.hinters.length; i++) {
hinterOpen = this.hinters[i].showHint(cm, expected, entityName, {
hinterOpen = this.hinters[i].showHint(cm, entityInformation, {
datasources: this.props.datasources.list,
pluginIdToImageLocation: this.props.pluginIdToImageLocation,
recentEntities: this.props.recentEntities,

View File

@ -26,6 +26,7 @@ import {
EditorTheme,
TabBehaviour,
EditorSize,
HintHelper,
} from "components/editorComponents/CodeEditor/EditorConfig";
import { bindingMarker } from "components/editorComponents/CodeEditor/markHelpers";
import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers";
@ -218,7 +219,7 @@ class EmbeddedDatasourcePathComponent extends React.Component<Props> {
};
};
handleDatasourceHint = () => {
handleDatasourceHint = (): HintHelper => {
const { datasourceList } = this.props;
return () => {
return {
@ -270,7 +271,7 @@ class EmbeddedDatasourcePathComponent extends React.Component<Props> {
}
},
showHint: () => {
return;
return false;
},
};
};

View File

@ -133,6 +133,7 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) {
border={CodeEditorBorder.BOTTOM_SIDE}
className={`t--${field}.key.${index}`}
dataTreePath={`${props.dataTreePath}[${index}].key`}
expected={FIELD_VALUES.API_ACTION.params}
hoverInteraction
name={`${field}.key`}
placeholder={`Key ${index + 1}`}

View File

@ -23,14 +23,14 @@ const FIELD_VALUES: Record<
isRequired: "boolean",
isVisible: "boolean",
isDisabled: "boolean",
// onDateSelected: "Function Call",
onDateSelected: "Function Call",
},
DATE_PICKER_WIDGET2: {
defaultDate: "string | null", //TODO:Vicky validate this property
defaultDate: "string", //TODO:Vicky validate this property
isRequired: "boolean",
isVisible: "boolean",
isDisabled: "boolean",
// onDateSelected: "Function Call",
onDateSelected: "Function Call",
},
TABLE_WIDGET: {
tableData: "Array<Object>",
@ -40,8 +40,8 @@ const FIELD_VALUES: Record<
exportExcel: "boolean",
exportCsv: "boolean",
defaultSelectedRow: "string",
// onRowSelected: "Function Call",
// onPageChange: "Function Call",
onRowSelected: "Function Call",
onPageChange: "Function Call",
},
VIDEO_WIDGET: {
url: "string",
@ -59,7 +59,7 @@ const FIELD_VALUES: Record<
defaultOptionValue: "string",
isRequired: "boolean",
isVisible: "boolean",
// onSelectionChange: "Function Call",
onSelectionChange: "Function Call",
},
TABS_WIDGET: {
selectedTab: "string",
@ -86,7 +86,7 @@ const FIELD_VALUES: Record<
isRequired: "boolean",
isVisible: "boolean",
isDisabled: "boolean",
// onTextChanged: "Function Call",
onTextChanged: "Function Call",
},
DROP_DOWN_WIDGET: {
label: "string",
@ -95,7 +95,7 @@ const FIELD_VALUES: Record<
defaultOptionValue: "string",
isRequired: "boolean",
isVisible: "boolean",
// onOptionChange: "Function Call",
onOptionChange: "Function Call",
},
FORM_BUTTON_WIDGET: {
text: "string",
@ -103,7 +103,7 @@ const FIELD_VALUES: Record<
disabledWhenInvalid: "boolean",
resetFormOnClick: "boolean",
isVisible: "boolean",
// onClick: "Function Call",
onClick: "Function Call",
},
MAP_WIDGET: {
mapCenter: "{ lat: number, long: number }",
@ -112,30 +112,29 @@ const FIELD_VALUES: Record<
enablePickLocation: "boolean",
enableCreateMarker: "boolean",
isVisible: "boolean",
// onMarkerClick: "Function Call",
// onCreateMarker: "Function Call",
onMarkerClick: "Function Call",
onCreateMarker: "Function Call",
},
BUTTON_WIDGET: {
text: "string",
buttonStyle: "PRIMARY_BUTTON | SECONDARY_BUTTON | DANGER_BUTTON",
isVisible: "boolean",
// onClick: "Function Call",
onClick: "Function Call",
},
RICH_TEXT_EDITOR_WIDGET: {
defaultText: "string",
isVisible: "boolean",
isDisabled: "boolean",
// onTextChange: "Function Call",
onTextChange: "Function Call",
},
FILE_PICKER_WIDGET: {
label: "string",
maxNumFiles: "number",
maxFileSize: "number",
allowedFileTypes: "Array<string>",
isRequired: "boolean",
isVisible: "boolean",
// onFilesSelected: "Function Call",
onFilesSelected: "Function Call",
},
CHECKBOX_WIDGET: {
label: "string",
@ -143,7 +142,7 @@ const FIELD_VALUES: Record<
isRequired: "boolean",
isDisabled: "boolean",
isVisible: "boolean",
// onCheckChange: "Function Call",
onCheckChange: "Function Call",
},
SWITCH_WIDGET: {
label: "string",
@ -151,7 +150,7 @@ const FIELD_VALUES: Record<
isDisabled: "boolean",
isVisible: "boolean",
alignWidget: "LEFT | RIGHT",
// onChange: "Function Call",
onChange: "Function Call",
},
FORM_WIDGET: {
backgroundColor: "string",

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"!name": "../../node-forge/lib/aes.js",
"!name": "LIB/node-forge",
"!define": {
"forge.aes.Algorithm.prototype.initialize.!0": {
"key": {

View File

@ -1,5 +1,5 @@
{
"!name": "lodash",
"!name": "LIB/lodash",
"_": {
"chunk": {
"!url": "https://lodash.com/docs/4.17.15#chunk",

View File

@ -1,5 +1,5 @@
{
"!name": "moment",
"!name": "LIB/moment",
"moment": {
"!type": "fn(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean) -> Moment",
"!url": "https://momentjs.com/docs/#/parsing/",

View File

@ -1,5 +1,5 @@
{
"!name": "xmlParser",
"!name": "LIB/xmlParser",
"xmlParser": {
"parse": {
"!doc": "converts xml string to json object",

View File

@ -37,11 +37,11 @@ export const CodemirrorHintStyles = createGlobalStyle<{
letter-spacing: -0.24px;
&:hover {
background: ${(props) =>
props.editorTheme === EditorTheme.LIGHT ? "#6A86CE" : "#157A96"};
props.editorTheme === EditorTheme.LIGHT ? "#E8E8E8" : "#157A96"};
border-radius: 0px;
color: #fff;
color: #090707;
&:after {
color: #fff;
color: #090707;
}
}
}
@ -139,7 +139,11 @@ export const CodemirrorHintStyles = createGlobalStyle<{
padding-left: ${(props) => props.theme.spaces[11]}px !important;
&:hover{
background: ${(props) =>
props.editorTheme === EditorTheme.LIGHT ? "#6A86CE" : "#157A96"};
props.editorTheme === EditorTheme.LIGHT ? "#E8E8E8" : "#157A96"};
color: #090707;
&:after {
color: #090707;
}
}
}
.CodeMirror-Tern-completion:before {
@ -214,6 +218,13 @@ export const CodemirrorHintStyles = createGlobalStyle<{
&:after {
color: #fff;
}
&:hover {
background: #6A86CE;
color: #fff;
&:after {
color: #fff;
}
}
}
.CodeMirror-Tern-hint-doc {
display: none;

View File

@ -282,7 +282,6 @@ const PropertyControl = memo((props: Props) => {
config.validationMessage = "";
delete config.dataTreePath;
delete config.evaluatedValue;
delete config.expected;
}
const isDynamic: boolean = isPathADynamicProperty(

View File

@ -10,7 +10,6 @@ import {
import {
EvaluationReduxAction,
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants";
@ -21,16 +20,7 @@ import {
import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory";
import { GracefulWorkerService } from "utils/WorkerUtil";
import Worker from "worker-loader!../workers/evaluation.worker";
import {
EVAL_WORKER_ACTIONS,
EvalError,
EvalErrorTypes,
EvaluationError,
getEvalErrorPath,
getEvalValuePath,
PropertyEvalErrorTypeDebugMessage,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import { EVAL_WORKER_ACTIONS } from "utils/DynamicBindingUtils";
import log from "loglevel";
import { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, {
@ -38,319 +28,24 @@ import PerformanceTracker, {
} from "../utils/PerformanceTracker";
import * as Sentry from "@sentry/react";
import { Action } from "redux";
import _ from "lodash";
import { ENTITY_TYPE, Message } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { AppState } from "reducers";
import {
getEntityNameAndPropertyPath,
isAction,
isWidget,
} from "workers/evaluationUtils";
import moment from "moment/moment";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import AppsmithConsole from "utils/AppsmithConsole";
import AnalyticsUtil from "utils/AnalyticsUtil";
EVALUATE_REDUX_ACTIONS,
FIRST_EVAL_REDUX_ACTIONS,
setDependencyMap,
setEvaluatedTree,
shouldProcessBatchedAction,
} from "actions/evaluationActions";
import {
createMessage,
ERROR_EVAL_ERROR_GENERIC,
ERROR_EVAL_TRIGGER,
} from "constants/messages";
import { getAppMode } from "selectors/applicationSelectors";
import { APP_MODE } from "reducers/entityReducers/appReducer";
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
import TernServer from "utils/autocomplete/TernServer";
import store from "store";
import { logDebuggerErrorAnalytics } from "actions/debuggerActions";
evalErrorHandler,
logSuccessfulBindings,
postEvalActionDispatcher,
updateTernDefinitions,
} from "./PostEvaluationSagas";
let widgetTypeConfigMap: WidgetTypeConfigMap;
const worker = new GracefulWorkerService(Worker);
const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors;
function getLatestEvalPropertyErrors(
currentDebuggerErrors: Record<string, Message>,
dataTree: DataTree,
evaluationOrder: Array<string>,
) {
const updatedDebuggerErrors: Record<string, Message> = {
...currentDebuggerErrors,
};
for (const evaluatedPath of evaluationOrder) {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
evaluatedPath,
);
const entity = dataTree[entityName];
if (isWidget(entity) || isAction(entity)) {
if (propertyPath in entity.logBlackList) {
continue;
}
const allEvalErrors: EvaluationError[] = _.get(
entity,
getEvalErrorPath(evaluatedPath, false),
[],
);
const evaluatedValue = _.get(
entity,
getEvalValuePath(evaluatedPath, false),
);
const evalErrors = allEvalErrors.filter(
(error) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
const idField = isWidget(entity) ? entity.widgetId : entity.actionId;
const nameField = isWidget(entity) ? entity.widgetName : entity.name;
const entityType = isWidget(entity)
? ENTITY_TYPE.WIDGET
: ENTITY_TYPE.ACTION;
const debuggerKey = idField + "-" + propertyPath;
// if dataTree has error but debugger does not -> add
// if debugger has error and data tree has error -> update error
// if debugger has error but data tree does not -> remove
// if debugger or data tree does not have an error -> no change
if (evalErrors.length) {
// TODO Rank and set the most critical error
const error = evalErrors[0];
const errorMessages = evalErrors.map((e) => ({
message: e.errorMessage,
}));
if (!(debuggerKey in updatedDebuggerErrors)) {
store.dispatch(
logDebuggerErrorAnalytics({
eventName: "DEBUGGER_NEW_ERROR",
entityId: idField,
entityName: nameField,
entityType,
propertyPath,
errorMessages,
}),
);
}
const analyticsData = isWidget(entity)
? {
widgetType: entity.type,
}
: {};
// Add or update
updatedDebuggerErrors[debuggerKey] = {
logType: LOG_TYPE.EVAL_ERROR,
text: PropertyEvalErrorTypeDebugMessage[error.errorType](
propertyPath,
),
messages: errorMessages,
severity: error.severity,
timestamp: moment().format("hh:mm:ss"),
source: {
id: idField,
name: nameField,
type: entityType,
propertyPath: propertyPath,
},
state: {
[propertyPath]: evaluatedValue,
},
analytics: analyticsData,
};
} else if (debuggerKey in updatedDebuggerErrors) {
store.dispatch(
logDebuggerErrorAnalytics({
eventName: "DEBUGGER_RESOLVED_ERROR",
entityId: idField,
entityName: nameField,
entityType,
propertyPath:
updatedDebuggerErrors[debuggerKey].source?.propertyPath ?? "",
errorMessages: updatedDebuggerErrors[debuggerKey].messages ?? [],
}),
);
// Remove
delete updatedDebuggerErrors[debuggerKey];
}
}
}
return updatedDebuggerErrors;
}
function* evalErrorHandler(
errors: EvalError[],
dataTree?: DataTree,
evaluationOrder?: Array<string>,
): any {
if (dataTree && evaluationOrder) {
const currentDebuggerErrors: Record<string, Message> = yield select(
getDebuggerErrors,
);
const evalPropertyErrors = getLatestEvalPropertyErrors(
currentDebuggerErrors,
dataTree,
evaluationOrder,
);
yield put({
type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
payload: evalPropertyErrors,
});
}
errors.forEach((error) => {
switch (error.type) {
case EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR: {
if (error.context) {
// Add more info about node for the toast
const { entityType, node } = error.context;
Toaster.show({
text: `${error.message} Node was: ${node}`,
variant: Variant.danger,
});
AppsmithConsole.error({
text: `${error.message} Node was: ${node}`,
});
// Send the generic error message to sentry for better grouping
Sentry.captureException(new Error(error.message), {
tags: {
node,
entityType,
},
// Level is warning because it could be a user error
level: Sentry.Severity.Warning,
});
// Log an analytics event for cyclical dep errors
AnalyticsUtil.logEvent("CYCLICAL_DEPENDENCY_ERROR", {
node,
entityType,
// Level is warning because it could be a user error
level: Sentry.Severity.Warning,
});
}
break;
}
case EvalErrorTypes.EVAL_TREE_ERROR: {
Toaster.show({
text: createMessage(ERROR_EVAL_ERROR_GENERIC),
variant: Variant.danger,
});
break;
}
case EvalErrorTypes.BAD_UNEVAL_TREE_ERROR: {
Sentry.captureException(error);
break;
}
case EvalErrorTypes.EVAL_TRIGGER_ERROR: {
log.debug(error);
Toaster.show({
text: createMessage(ERROR_EVAL_TRIGGER, error.message),
variant: Variant.danger,
showDebugButton: true,
});
AppsmithConsole.error({
text: createMessage(ERROR_EVAL_TRIGGER, error.message),
});
break;
}
case EvalErrorTypes.EVAL_PROPERTY_ERROR: {
log.debug(error);
break;
}
default: {
Sentry.captureException(error);
log.debug(error);
}
}
});
}
function* logSuccessfulBindings(
unEvalTree: DataTree,
dataTree: DataTree,
evaluationOrder: string[],
) {
const appMode = yield select(getAppMode);
if (appMode === APP_MODE.PUBLISHED) return;
if (!evaluationOrder) return;
evaluationOrder.forEach((evaluatedPath) => {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
evaluatedPath,
);
const entity = dataTree[entityName];
if (isAction(entity) || isWidget(entity)) {
const unevalValue = _.get(unEvalTree, evaluatedPath);
const entityType = isAction(entity) ? entity.pluginType : entity.type;
const isABinding = _.find(entity.dynamicBindingPathList, {
key: propertyPath,
});
const logBlackList = entity.logBlackList;
const errors: EvaluationError[] = _.get(
dataTree,
getEvalErrorPath(evaluatedPath),
[],
) as EvaluationError[];
const criticalErrors = errors.filter(
(error) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
const hasErrors = criticalErrors.length > 0;
if (isABinding && !hasErrors && !(propertyPath in logBlackList)) {
AnalyticsUtil.logEvent("BINDING_SUCCESS", {
unevalValue,
entityType,
propertyPath,
});
}
}
});
}
// Update only the changed entities on tern. We will pick up the updated
// entities from the evaluation order and create a new def from them.
// When there is a top level entity removed in removedPaths,
// we will remove its def
function* updateTernDefinitions(
dataTree: DataTree,
evaluationOrder: string[],
isFirstEvaluation: boolean,
) {
const updatedEntities: Set<string> = new Set();
// If it is the first evaluation, we want to add everything in the data tree
if (isFirstEvaluation) {
Object.keys(dataTree).forEach((key) => updatedEntities.add(key));
} else {
evaluationOrder.forEach((path) => {
const { entityName } = getEntityNameAndPropertyPath(path);
updatedEntities.add(entityName);
});
}
updatedEntities.forEach((entityName) => {
const entity = dataTree[entityName];
if (entity) {
const { def, name } = dataTreeTypeDefCreator(entity, entityName);
TernServer.updateDef(name, def);
}
});
// removedPaths.forEach((path) => {
// // No '.' means that the path is an entity name
// if (path.split(".").length === 1) {
// TernServer.removeDef(path);
// }
// });
}
function* postEvalActionDispatcher(
actions: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
) {
for (const action of actions) {
yield put(action);
}
}
function* evaluateTreeSaga(
postEvalActions?: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
isFirstEvaluation = false,
@ -382,10 +77,7 @@ function* evaluateTreeSaga(
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE,
);
yield put({
type: ReduxActionTypes.SET_EVALUATED_TREE,
payload: { dataTree, updates },
});
yield put(setEvaluatedTree(dataTree, updates));
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE,
);
@ -408,10 +100,7 @@ function* evaluateTreeSaga(
isFirstEvaluation,
);
yield put({
type: ReduxActionTypes.SET_EVALUATION_INVERSE_DEPENDENCY_MAP,
payload: { inverseDependencyMap: dependencies },
});
yield put(setDependencyMap(dependencies));
if (postEvalActions && postEvalActions.length) {
yield call(postEvalActionDispatcher, postEvalActions);
}
@ -495,62 +184,6 @@ export function* validateProperty(
});
}
const FIRST_EVAL_REDUX_ACTIONS = [
// Pages
ReduxActionTypes.FETCH_PAGE_SUCCESS,
ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS,
];
const EVALUATE_REDUX_ACTIONS = [
...FIRST_EVAL_REDUX_ACTIONS,
// Actions
ReduxActionTypes.FETCH_ACTIONS_SUCCESS,
ReduxActionTypes.FETCH_PLUGIN_FORM_CONFIGS_SUCCESS,
ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionErrorTypes.FETCH_ACTIONS_ERROR,
ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR,
ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS,
ReduxActionTypes.SUBMIT_CURL_FORM_SUCCESS,
ReduxActionTypes.CREATE_ACTION_SUCCESS,
ReduxActionTypes.UPDATE_ACTION_PROPERTY,
ReduxActionTypes.DELETE_ACTION_SUCCESS,
ReduxActionTypes.COPY_ACTION_SUCCESS,
ReduxActionTypes.MOVE_ACTION_SUCCESS,
ReduxActionTypes.RUN_ACTION_SUCCESS,
ReduxActionErrorTypes.RUN_ACTION_ERROR,
ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS,
ReduxActionErrorTypes.EXECUTE_ACTION_ERROR,
// App Data
ReduxActionTypes.SET_APP_MODE,
ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE,
ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE,
// Widgets
ReduxActionTypes.UPDATE_LAYOUT,
ReduxActionTypes.UPDATE_WIDGET_PROPERTY,
ReduxActionTypes.UPDATE_WIDGET_NAME_SUCCESS,
// Widget Meta
ReduxActionTypes.SET_META_PROP,
ReduxActionTypes.RESET_WIDGET_META,
// Batches
ReduxActionTypes.BATCH_UPDATES_SUCCESS,
];
const shouldProcessAction = (action: ReduxAction<unknown>) => {
if (
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
Array.isArray(action.payload)
) {
const batchedActionTypes = action.payload.map(
(batchedAction) => batchedAction.type,
);
return (
_.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length > 0
);
}
return true;
};
function evalQueueBuffer() {
let canTake = false;
let postEvalActions: any = [];
@ -571,7 +204,7 @@ function evalQueueBuffer() {
};
const put = (action: EvaluationReduxAction<unknown | unknown[]>) => {
if (!shouldProcessAction(action)) {
if (!shouldProcessBatchedAction(action)) {
return;
}
canTake = true;
@ -607,11 +240,14 @@ function* evaluationChangeListenerSaga() {
const action: EvaluationReduxAction<unknown | unknown[]> = yield take(
evtActionChannel,
);
if (shouldProcessAction(action)) {
if (FIRST_EVAL_REDUX_ACTIONS.includes(action.type)) {
yield call(evaluateTreeSaga, initAction.postEvalActions, true);
} else {
if (shouldProcessBatchedAction(action)) {
yield call(evaluateTreeSaga, action.postEvalActions);
}
}
// TODO(hetu) need an action to stop listening and evaluate (exit app)
}
}
export default function* evaluationSagaListeners() {

View File

@ -0,0 +1,326 @@
import { ENTITY_TYPE, Message } from "entities/AppsmithConsole";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import {
getEntityNameAndPropertyPath,
isAction,
isWidget,
} from "workers/evaluationUtils";
import {
EvalError,
EvalErrorTypes,
EvaluationError,
getEvalErrorPath,
getEvalValuePath,
PropertyEvalErrorTypeDebugMessage,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import _ from "lodash";
import LOG_TYPE from "../entities/AppsmithConsole/logtype";
import moment from "moment/moment";
import { put, select } from "redux-saga/effects";
import {
ReduxAction,
ReduxActionTypes,
ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import AppsmithConsole from "../utils/AppsmithConsole";
import * as Sentry from "@sentry/react";
import AnalyticsUtil from "../utils/AnalyticsUtil";
import {
createMessage,
ERROR_EVAL_ERROR_GENERIC,
ERROR_EVAL_TRIGGER,
} from "constants/messages";
import log from "loglevel";
import { AppState } from "reducers";
import { getAppMode } from "selectors/applicationSelectors";
import { APP_MODE } from "reducers/entityReducers/appReducer";
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
import TernServer from "utils/autocomplete/TernServer";
import { logDebuggerErrorAnalytics } from "actions/debuggerActions";
import store from "../store";
const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors;
function getLatestEvalPropertyErrors(
currentDebuggerErrors: Record<string, Message>,
dataTree: DataTree,
evaluationOrder: Array<string>,
) {
const updatedDebuggerErrors: Record<string, Message> = {
...currentDebuggerErrors,
};
for (const evaluatedPath of evaluationOrder) {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
evaluatedPath,
);
const entity = dataTree[entityName];
if (isWidget(entity) || isAction(entity)) {
if (propertyPath in entity.logBlackList) {
continue;
}
const allEvalErrors: EvaluationError[] = _.get(
entity,
getEvalErrorPath(evaluatedPath, false),
[],
);
const evaluatedValue = _.get(
entity,
getEvalValuePath(evaluatedPath, false),
);
const evalErrors = allEvalErrors.filter(
(error) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
const idField = isWidget(entity) ? entity.widgetId : entity.actionId;
const nameField = isWidget(entity) ? entity.widgetName : entity.name;
const entityType = isWidget(entity)
? ENTITY_TYPE.WIDGET
: ENTITY_TYPE.ACTION;
const debuggerKey = idField + "-" + propertyPath;
// if dataTree has error but debugger does not -> add
// if debugger has error and data tree has error -> update error
// if debugger has error but data tree does not -> remove
// if debugger or data tree does not have an error -> no change
if (evalErrors.length) {
// TODO Rank and set the most critical error
const error = evalErrors[0];
const errorMessages = evalErrors.map((e) => ({
message: e.errorMessage,
}));
if (!(debuggerKey in updatedDebuggerErrors)) {
store.dispatch(
logDebuggerErrorAnalytics({
eventName: "DEBUGGER_NEW_ERROR",
entityId: idField,
entityName: nameField,
entityType,
propertyPath,
errorMessages,
}),
);
}
const analyticsData = isWidget(entity)
? {
widgetType: entity.type,
}
: {};
// Add or update
updatedDebuggerErrors[debuggerKey] = {
logType: LOG_TYPE.EVAL_ERROR,
text: PropertyEvalErrorTypeDebugMessage[error.errorType](
propertyPath,
),
messages: errorMessages,
severity: error.severity,
timestamp: moment().format("hh:mm:ss"),
source: {
id: idField,
name: nameField,
type: entityType,
propertyPath: propertyPath,
},
state: {
[propertyPath]: evaluatedValue,
},
analytics: analyticsData,
};
} else if (debuggerKey in updatedDebuggerErrors) {
store.dispatch(
logDebuggerErrorAnalytics({
eventName: "DEBUGGER_RESOLVED_ERROR",
entityId: idField,
entityName: nameField,
entityType,
propertyPath:
updatedDebuggerErrors[debuggerKey].source?.propertyPath ?? "",
errorMessages: updatedDebuggerErrors[debuggerKey].messages ?? [],
}),
);
// Remove
delete updatedDebuggerErrors[debuggerKey];
}
}
}
return updatedDebuggerErrors;
}
export function* evalErrorHandler(
errors: EvalError[],
dataTree?: DataTree,
evaluationOrder?: Array<string>,
): any {
if (dataTree && evaluationOrder) {
const currentDebuggerErrors: Record<string, Message> = yield select(
getDebuggerErrors,
);
const evalPropertyErrors = getLatestEvalPropertyErrors(
currentDebuggerErrors,
dataTree,
evaluationOrder,
);
yield put({
type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
payload: evalPropertyErrors,
});
}
errors.forEach((error) => {
switch (error.type) {
case EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR: {
if (error.context) {
// Add more info about node for the toast
const { entityType, node } = error.context;
Toaster.show({
text: `${error.message} Node was: ${node}`,
variant: Variant.danger,
});
AppsmithConsole.error({
text: `${error.message} Node was: ${node}`,
});
// Send the generic error message to sentry for better grouping
Sentry.captureException(new Error(error.message), {
tags: {
node,
entityType,
},
// Level is warning because it could be a user error
level: Sentry.Severity.Warning,
});
// Log an analytics event for cyclical dep errors
AnalyticsUtil.logEvent("CYCLICAL_DEPENDENCY_ERROR", {
node,
entityType,
// Level is warning because it could be a user error
level: Sentry.Severity.Warning,
});
}
break;
}
case EvalErrorTypes.EVAL_TREE_ERROR: {
Toaster.show({
text: createMessage(ERROR_EVAL_ERROR_GENERIC),
variant: Variant.danger,
});
break;
}
case EvalErrorTypes.BAD_UNEVAL_TREE_ERROR: {
Sentry.captureException(error);
break;
}
case EvalErrorTypes.EVAL_TRIGGER_ERROR: {
log.debug(error);
Toaster.show({
text: createMessage(ERROR_EVAL_TRIGGER, error.message),
variant: Variant.danger,
showDebugButton: true,
});
AppsmithConsole.error({
text: createMessage(ERROR_EVAL_TRIGGER, error.message),
});
break;
}
case EvalErrorTypes.EVAL_PROPERTY_ERROR: {
log.debug(error);
break;
}
default: {
Sentry.captureException(error);
log.debug(error);
}
}
});
}
export function* logSuccessfulBindings(
unEvalTree: DataTree,
dataTree: DataTree,
evaluationOrder: string[],
) {
const appMode = yield select(getAppMode);
if (appMode === APP_MODE.PUBLISHED) return;
if (!evaluationOrder) return;
evaluationOrder.forEach((evaluatedPath) => {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
evaluatedPath,
);
const entity = dataTree[entityName];
if (isAction(entity) || isWidget(entity)) {
const unevalValue = _.get(unEvalTree, evaluatedPath);
const entityType = isAction(entity) ? entity.pluginType : entity.type;
const isABinding = _.find(entity.dynamicBindingPathList, {
key: propertyPath,
});
const logBlackList = entity.logBlackList;
const errors: EvaluationError[] = _.get(
dataTree,
getEvalErrorPath(evaluatedPath),
[],
) as EvaluationError[];
const criticalErrors = errors.filter(
(error) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
const hasErrors = criticalErrors.length > 0;
if (isABinding && !hasErrors && !(propertyPath in logBlackList)) {
AnalyticsUtil.logEvent("BINDING_SUCCESS", {
unevalValue,
entityType,
propertyPath,
});
}
}
});
}
export function* postEvalActionDispatcher(
actions: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
) {
for (const action of actions) {
yield put(action);
}
}
// Update only the changed entities on tern. We will pick up the updated
// entities from the evaluation order and create a new def from them.
// When there is a top level entity removed in removedPaths,
// we will remove its def
export function* updateTernDefinitions(
dataTree: DataTree,
evaluationOrder: string[],
isFirstEvaluation: boolean,
) {
const updatedEntities: Set<string> = new Set();
// If it is the first evaluation, we want to add everything in the data tree
if (isFirstEvaluation) {
TernServer.resetServer();
Object.keys(dataTree).forEach((key) => updatedEntities.add(key));
} else {
evaluationOrder.forEach((path) => {
const { entityName } = getEntityNameAndPropertyPath(path);
updatedEntities.add(entityName);
});
}
updatedEntities.forEach((entityName) => {
const entity = dataTree[entityName];
if (entity) {
const { def, name } = dataTreeTypeDefCreator(entity, entityName);
TernServer.updateDef(name, def);
}
});
// removedPaths.forEach((path) => {
// // No '.' means that the path is an entity name
// if (path.split(".").length === 1) {
// TernServer.removeDef(path);
// }
// });
}

View File

@ -2,10 +2,6 @@ import { generateTypeDef } from "utils/autocomplete/dataTreeTypeDefCreator";
import { DataTreeAction } from "entities/DataTree/dataTreeFactory";
import _ from "lodash";
// const isLoading = {
// "!type": "bool",
// "!doc": "Boolean value indicating if the entity is in loading state",
// };
const isVisible = {
"!type": "bool",
"!doc": "Boolean value indicating if the widget is in visible state",
@ -14,7 +10,6 @@ const isVisible = {
export const entityDefinitions = {
ACTION: (entity: DataTreeAction) => {
const dataDef = generateTypeDef(entity.data);
const responseMetaDef = generateTypeDef(entity.responseMeta);
let data: Record<string, any> = {
"!doc": "The response of the action",
};
@ -23,21 +18,16 @@ export const entityDefinitions = {
} else {
data = { ...data, ...dataDef };
}
let responseMeta: Record<string, any> = {
"!doc": "The response meta of the action",
};
if (_.isString(responseMetaDef)) {
responseMeta["!type"] = responseMetaDef;
} else {
responseMeta = { ...responseMeta, ...responseMetaDef };
}
return {
"!doc":
"Actions allow you to connect your widgets to your backend data in a secure manner.",
"!url": "https://docs.appsmith.com/v/v1.2.1/framework-reference/run",
isLoading: "bool",
data,
responseMeta,
responseMeta: {
"!doc": "The response meta of the action",
"!type": "?",
},
run: "fn(onSuccess: fn() -> void, onError: fn() -> void) -> void",
};
},
@ -137,8 +127,6 @@ export const entityDefinitions = {
isVisible: isVisible,
text: "string",
isDisabled: "bool",
recaptchaToken: "string",
googleRecaptchaKey: "string",
},
DATE_PICKER_WIDGET: {
"!doc":
@ -219,8 +207,6 @@ export const entityDefinitions = {
isVisible: isVisible,
text: "string",
isDisabled: "bool",
recaptchaToken: "string",
googleRecaptchaKey: "string",
},
MAP_WIDGET: {
isVisible: isVisible,
@ -319,6 +305,7 @@ export const GLOBAL_DEFS = {
};
export const GLOBAL_FUNCTIONS = {
"!name": "DATA_TREE.APPSMITH.FUNCTIONS",
navigateTo: {
"!doc": "Action to navigate the user to another page or url",
"!type": "fn(pageNameOrUrl: string, params: {}, target?: string) -> void",

View File

@ -1,5 +1,7 @@
import TernServer from "./TernServer";
import TernServer, { Completion, createCompletionHeader } from "./TernServer";
import { MockCodemirrorEditor } from "../../../test/__mocks__/CodeMirrorEditorMock";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import _ from "lodash";
describe("Tern server", () => {
it("Check whether the correct value is being sent to tern", () => {
@ -157,3 +159,133 @@ describe("Tern server", () => {
});
});
});
describe("Tern server sorting", () => {
const contextCompletion: Completion = {
text: "context",
type: "STRING",
origin: "[doc]",
data: {
doc: "",
},
};
const sameEntityCompletion: Completion = {
text: "sameEntity.tableData",
type: "ARRAY",
origin: "DATA_TREE.WIDGET.TABLE_WIDGET.sameEntity",
data: {
doc: "",
},
};
const sameTypeCompletion: Completion = {
text: "sameType.selectedRow",
type: "OBJECT",
origin: "DATA_TREE.WIDGET.TABLE_WIDGET.sameType",
data: {
doc: "",
},
};
const diffTypeCompletion: Completion = {
text: "diffType.tableData",
type: "ARRAY",
origin: "DATA_TREE.WIDGET.TABLE_WIDGET.diffType",
data: {
doc: "",
},
};
const sameTypeDiffEntityTypeCompletion: Completion = {
text: "diffEntity.data",
type: "OBJECT",
origin: "DATA_TREE.ACTION.ACTION.diffEntity",
data: {
doc: "",
},
};
const dataTreeCompletion: Completion = {
text: "otherDataTree",
type: "STRING",
origin: "DATA_TREE.WIDGET.TEXT_WIDGET.otherDataTree",
data: {
doc: "",
},
};
const functionCompletion: Completion = {
text: "otherDataFunction",
type: "FUNCTION",
origin: "DATA_TREE.APPSMITH.FUNCTIONS",
data: {
doc: "",
},
};
const ecmascriptCompletion: Completion = {
text: "otherJS",
type: "OBJECT",
origin: "ecmascript",
data: {
doc: "",
},
};
const libCompletion: Completion = {
text: "libValue",
type: "OBJECT",
origin: "LIB/lodash",
data: {
doc: "",
},
};
const unknownCompletion: Completion = {
text: "unknownSuggestion",
type: "UNKNOWN",
origin: "unknown",
data: {
doc: "",
},
};
const completions = [
sameEntityCompletion,
sameTypeCompletion,
contextCompletion,
libCompletion,
unknownCompletion,
diffTypeCompletion,
sameTypeDiffEntityTypeCompletion,
ecmascriptCompletion,
functionCompletion,
dataTreeCompletion,
];
it("shows best match results", () => {
TernServer.setEntityInformation({
entityName: "sameEntity",
entityType: ENTITY_TYPE.WIDGET,
expectedType: "object",
});
const sortedCompletions = TernServer.sortCompletions(
_.shuffle(completions),
true,
"",
);
expect(sortedCompletions[0]).toStrictEqual(contextCompletion);
expect(sortedCompletions).toEqual(
expect.arrayContaining([
createCompletionHeader("Best Match"),
sameTypeDiffEntityTypeCompletion,
createCompletionHeader("Search Results"),
dataTreeCompletion,
]),
);
expect(sortedCompletions).toEqual(
expect.not.arrayContaining([diffTypeCompletion]),
);
});
});

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// Heavily inspired from https://github.com/codemirror/CodeMirror/blob/master/addon/tern/tern.js
import tern, { Server, Def } from "tern";
import ecma from "tern/defs/ecmascript.json";
import ecma from "constants/defs/ecmascript.json";
import lodash from "constants/defs/lodash.json";
import base64 from "constants/defs/base64-js.json";
import moment from "constants/defs/moment.json";
@ -9,6 +9,7 @@ import xmlJs from "constants/defs/xmlParser.json";
import forge from "constants/defs/forge.json";
import CodeMirror, { Hint, Pos, cmpPos } from "codemirror";
import {
getDynamicBindings,
getDynamicStringSegments,
isDynamicValue,
} from "utils/DynamicBindingUtils";
@ -16,6 +17,10 @@ import {
GLOBAL_DEFS,
GLOBAL_FUNCTIONS,
} from "utils/autocomplete/EntityDefinitions";
import { HintEntityInformation } from "components/editorComponents/CodeEditor/EditorConfig";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import SortRules from "./dataTypeSortRules";
import _ from "lodash";
const DEFS: Def[] = [
GLOBAL_FUNCTIONS,
@ -77,8 +82,7 @@ class TernServer {
docs: TernDocs = Object.create(null);
cachedArgHints: ArgHints | null = null;
active: any;
expected?: string;
entityName?: string;
entityInformation: HintEntityInformation = {};
constructor() {
this.server = new tern.Server({
@ -87,9 +91,15 @@ class TernServer {
});
}
complete(cm: CodeMirror.Editor, expected: string, entityName: string) {
this.expected = expected;
this.entityName = entityName;
resetServer() {
this.server = new tern.Server({
async: true,
defs: DEFS,
});
this.docs = Object.create(null);
}
complete(cm: CodeMirror.Editor) {
cm.showHint({
hint: this.getHint.bind(this),
completeSingle: false,
@ -161,25 +171,36 @@ class TernServer {
) {
after = '"]';
}
const bindings = getDynamicBindings(cm.getValue());
const onlySingleBinding = bindings.stringSegments.length === 1;
const searchText = bindings.jsSnippets[0].trim();
for (let i = 0; i < data.completions.length; ++i) {
const completion = data.completions[i];
let className = this.typeToIcon(completion.type);
const dataType = this.getDataType(completion.type);
const entityName = this.entityName;
if (data.guess) className += " " + cls + "guess";
if (!entityName || !completion.name.includes(entityName)) {
let completionText = completion.name + after;
if (dataType === "FUNCTION") {
completionText = completionText + "()";
}
completions.push({
text: completion.name + after,
displayText: completion.displayName || completion.name,
text: completionText,
displayText: completionText,
className: className,
data: completion,
origin: completion.origin,
type: dataType,
isHeader: false,
});
}
}
completions = this.sortCompletions(completions);
const indexToBeSelected = completions.length > 1 ? 1 : 0;
completions = this.sortCompletions(
completions,
onlySingleBinding,
searchText,
);
const indexToBeSelected =
completions.length && completions[0].isHeader ? 1 : 0;
const obj = {
from: from,
to: to,
@ -242,56 +263,135 @@ class TernServer {
});
}
sortCompletions(completions: Completion[]) {
// Add data tree completions before others
sortCompletions(
completions: Completion[],
findBestMatch: boolean,
bestMatchSearch: string,
) {
const expectedDataType = this.getExpectedDataType();
const dataTreeCompletions = completions
.filter((c) => c.origin && c.origin.startsWith("DATA_TREE_"))
.sort((a: Completion, b: Completion) => {
const { entityName, entityType } = this.entityInformation;
type CompletionType =
| "DATA_TREE"
| "MATCHING_TYPE"
| "OTHER"
| "CONTEXT"
| "JS"
| "LIBRARY";
const completionType: Record<CompletionType, Completion[]> = {
MATCHING_TYPE: [],
DATA_TREE: [],
CONTEXT: [],
JS: [],
LIBRARY: [],
OTHER: [],
};
completions.forEach((completion) => {
if (entityName && completion.text.includes(entityName)) {
return;
}
if (completion.origin) {
if (completion.origin && completion.origin.startsWith("DATA_TREE")) {
if (completion.text.includes(".")) {
// nested paths (with ".") should only be used for best match
if (completion.type === expectedDataType) {
completionType.MATCHING_TYPE.push(completion);
}
} else if (
completion.origin === "DATA_TREE.APPSMITH.FUNCTIONS" &&
completion.type === expectedDataType
) {
// Global functions should be in best match as well as DataTree
completionType.MATCHING_TYPE.push(completion);
completionType.DATA_TREE.push(completion);
} else {
// All top level entities are set in data tree
completionType.DATA_TREE.push(completion);
}
return;
}
if (
completion.origin === "[doc]" ||
completion.origin === "customDataTree"
) {
// [doc] are variables defined in the current context
// customDataTree are implicit context defined by platform
completionType.CONTEXT.push(completion);
return;
}
if (
completion.origin === "ecmascript" ||
completion.origin === "base64-js"
) {
completionType.JS.push(completion);
return;
}
if (completion.origin.startsWith("LIB/")) {
completionType.LIBRARY.push(completion);
return;
}
}
// Generally keywords or other unCategorised completions
completionType.OTHER.push(completion);
});
completionType.DATA_TREE = completionType.DATA_TREE.sort(
(a: Completion, b: Completion) => {
if (a.type === "FUNCTION" && b.type !== "FUNCTION") {
return 1;
} else if (a.type !== "FUNCTION" && b.type === "FUNCTION") {
return -1;
}
return a.text.toLowerCase().localeCompare(b.text.toLowerCase());
},
);
completionType.MATCHING_TYPE = completionType.MATCHING_TYPE.filter((c) =>
c.text.toLowerCase().startsWith(bestMatchSearch.toLowerCase()),
);
if (findBestMatch && completionType.MATCHING_TYPE.length) {
const sortedMatches: Completion[] = [];
const groupedMatches = _.groupBy(completionType.MATCHING_TYPE, (c) => {
const [, , subType, name] = c.origin.split(".");
return c.text.replace(name, subType);
});
const sameDataType = dataTreeCompletions.filter(
(c) => c.type === expectedDataType,
);
const otherDataType = dataTreeCompletions.filter(
(c) => c.type !== expectedDataType,
);
if (otherDataType.length && sameDataType.length) {
const otherDataTitle: Completion = {
text: "Search results",
displayText: "Search results",
className: "CodeMirror-hint-header",
data: { doc: "" },
origin: "",
type: "UNKNOWN",
isHeader: true,
};
const sameDataTitle: Completion = {
text: "Best Match",
displayText: "Best Match",
className: "CodeMirror-hint-header",
data: { doc: "" },
origin: "",
type: "UNKNOWN",
isHeader: true,
};
sameDataType.unshift(sameDataTitle);
otherDataType.unshift(otherDataTitle);
SortRules[expectedDataType].forEach((rule) => {
if (Array.isArray(groupedMatches[rule])) {
sortedMatches.push(...groupedMatches[rule]);
}
const docCompletetions = completions.filter((c) => c.origin === "[doc]");
const otherCompletions = completions.filter(
(c) => c.origin !== "dataTree" && c.origin !== "[doc]",
});
sortedMatches.sort((a, b) => {
let aRank = 0;
let bRank = 0;
const entityTypeA: ENTITY_TYPE = a.origin.split(".")[1] as ENTITY_TYPE;
const entityTypeB: ENTITY_TYPE = b.origin.split(".")[1] as ENTITY_TYPE;
if (entityTypeA === entityType) {
aRank = aRank + 1;
}
if (entityTypeB === entityType) {
bRank = bRank + 1;
}
return aRank - bRank;
});
completionType.MATCHING_TYPE = _.take(sortedMatches, 3);
if (completionType.MATCHING_TYPE.length) {
completionType.MATCHING_TYPE.unshift(
createCompletionHeader("Best Match"),
);
completionType.DATA_TREE.unshift(
createCompletionHeader("Search Results"),
);
}
} else {
// Clear any matching type because we dont want to find best match
completionType.MATCHING_TYPE = [];
}
return [
...docCompletetions,
...sameDataType,
...otherDataType,
...otherCompletions,
...completionType.CONTEXT,
...completionType.MATCHING_TYPE,
...completionType.DATA_TREE,
...completionType.LIBRARY,
...completionType.JS,
...completionType.OTHER,
];
}
@ -306,21 +406,23 @@ class TernServer {
else return "OBJECT";
}
getExpectedDataType() {
const type = this.expected;
getExpectedDataType(): DataType {
const type = this.entityInformation.expectedType;
if (type === undefined) return "UNKNOWN";
if (
type === "Array<Object>" ||
type === "Array" ||
type === "Array<{ label: string, value: string }>" ||
type === "Array<x:string, y:number>"
)
) {
return "ARRAY";
}
if (type === "boolean") return "BOOLEAN";
if (type === "string") return "STRING";
if (type === "number") return "NUMBER";
if (type === "object" || type === "JSON") return "OBJECT";
if (type === undefined) return "UNKNOWN";
return undefined;
if (type === "Function Call") return "FUNCTION";
return "UNKNOWN";
}
typeToIcon(type: string) {
@ -417,6 +519,7 @@ class TernServer {
end?: any;
start?: any;
file?: any;
includeKeywords?: boolean;
},
pos?: CodeMirror.Position,
) {
@ -425,6 +528,7 @@ class TernServer {
const allowFragments = !query.fullDocs;
if (!allowFragments) delete query.fullDocs;
query.lineCharPositions = true;
query.includeKeywords = true;
if (!query.end) {
const lineValue = this.lineValue(doc);
const focusedValue = this.getFocusedDynamicValue(doc);
@ -712,6 +816,20 @@ class TernServer {
fadeOut(tooltip: HTMLElement) {
this.remove(tooltip);
}
setEntityInformation(entityInformation: HintEntityInformation) {
this.entityInformation = entityInformation;
}
}
export const createCompletionHeader = (name: string): Completion => ({
text: name,
displayText: name,
className: "CodeMirror-hint-header",
data: { doc: "" },
origin: "",
type: "UNKNOWN",
isHeader: true,
});
export default new TernServer();

View File

@ -1,5 +1,4 @@
import { generateReactKey } from "utils/generators";
import { getType, Types } from "utils/TypeHelpers";
import { generateTypeDef } from "./dataTreeTypeDefCreator";
let extraDefs: any = {};
@ -17,35 +16,3 @@ export const customTreeTypeDefCreator = (
extraDefs = {};
return { ...def };
};
export function generateTypeDef(
obj: any,
): string | Record<string, string | Record<string, unknown>> {
const type = getType(obj);
switch (type) {
case Types.ARRAY: {
const arrayType = generateTypeDef(obj[0]);
const name = generateReactKey();
extraDefs[name] = arrayType;
return `[${name}]`;
}
case Types.OBJECT: {
const objType: Record<string, string | Record<string, unknown>> = {};
Object.keys(obj).forEach((k) => {
objType[k] = generateTypeDef(obj[k]);
});
return objType;
}
case Types.STRING:
return "string";
case Types.NUMBER:
return "number";
case Types.BOOLEAN:
return "bool";
case Types.NULL:
case Types.UNDEFINED:
return "?";
default:
return "?";
}
}

View File

@ -1,7 +1,7 @@
import {
generateTypeDef,
dataTreeTypeDefCreator,
flattenObjKeys,
flattenDef,
} from "utils/autocomplete/dataTreeTypeDefCreator";
import {
DataTreeWidget,
@ -72,26 +72,37 @@ describe("dataTreeTypeDefCreator", () => {
expect(objType).toStrictEqual(expected);
});
it("flatten object", () => {
const options = {
it("flatten def", () => {
const def = {
entity1: {
someNumber: "number",
someString: "string",
someBool: "bool",
nested: {
someExtraNested: "string",
},
},
};
const expected = {
entity1: {
someNumber: "number",
someString: "string",
someBool: "bool",
nested: {
someExtraNested: "string",
},
},
"entity1.someNumber": "number",
"entity1.someString": "string",
"entity1.someBool": "bool",
"entity1.nested": {
someExtraNested: "string",
},
"entity1.nested.someExtraNested": "string",
};
const value = flattenObjKeys(options, "entity1");
const value = flattenDef(def, "entity1");
expect(value).toStrictEqual(expected);
});
});

View File

@ -1,58 +1,55 @@
import { DataTreeEntity, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { DataTreeEntity } from "entities/DataTree/dataTreeFactory";
import _ from "lodash";
import { generateReactKey } from "utils/generators";
import { entityDefinitions } from "utils/autocomplete/EntityDefinitions";
import { getType, Types } from "utils/TypeHelpers";
import { Def } from "tern";
import {
isAction,
isAppsmithEntity,
isTrueObject,
isWidget,
} from "workers/evaluationUtils";
// When there is a complex data type, we store it in extra def and refer to it
// in the def
let extraDefs: any = {};
const skipProperties = ["!doc", "!url", "!type"];
// Def names are encoded with information about the entity
// This so that we have more info about them
// when sorting results in autocomplete
// DATA_TREE.{entityType}.{entitySubType}.{entityName}
// eg DATA_TREE.WIDGET.TABLE_WIDGET.Table1
// or DATA_TREE.ACTION.ACTION.Api1
export const dataTreeTypeDefCreator = (
entity: DataTreeEntity,
entityName: string,
): { def: Def; name: string } => {
const defName = `DATA_TREE_${entityName}`;
const def: any = {
"!name": defName,
};
if (entity && "ENTITY_TYPE" in entity) {
if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) {
const def: any = {};
if (isWidget(entity)) {
const widgetType = entity.type;
if (widgetType in entityDefinitions) {
const definition = _.get(entityDefinitions, widgetType);
if (_.isFunction(definition)) {
const data = definition(entity);
const allData = flattenObjKeys(data, entityName);
for (const [key, value] of Object.entries(allData)) {
def[key] = value;
}
def[entityName] = definition(entity);
} else {
def[entityName] = definition;
const allFlattenData = flattenObjKeys(definition, entityName);
for (const [key, value] of Object.entries(allFlattenData)) {
def[key] = value;
}
flattenDef(def, entityName);
def["!name"] = `DATA_TREE.WIDGET.${widgetType}.${entityName}`;
}
} else if (isAction(entity)) {
def[entityName] = entityDefinitions.ACTION(entity);
flattenDef(def, entityName);
def["!name"] = `DATA_TREE.ACTION.ACTION.${entityName}`;
} else if (isAppsmithEntity(entity)) {
def["!name"] = "DATA_TREE.APPSMITH.APPSMITH";
def.appsmith = generateTypeDef(_.omit(entity, "ENTITY_TYPE"));
}
}
if (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION) {
const actionDefs = entityDefinitions.ACTION(entity);
def[entityName] = actionDefs;
const finalData = flattenObjKeys(actionDefs, entityName);
for (const [key, value] of Object.entries(finalData)) {
def[key] = value;
}
}
if (entity.ENTITY_TYPE === ENTITY_TYPE.APPSMITH) {
const options: any = generateTypeDef(_.omit(entity, "ENTITY_TYPE"));
def.appsmith = options;
}
}
if (Object.keys(extraDefs)) {
def["!define"] = { ...extraDefs };
extraDefs = {};
return { def, name: defName };
}
return { def, name: def["!name"] };
};
export function generateTypeDef(
@ -61,10 +58,8 @@ export function generateTypeDef(
const type = getType(obj);
switch (type) {
case Types.ARRAY: {
const arrayType = generateTypeDef(obj[0]);
const name = generateReactKey();
extraDefs[name] = arrayType;
return `[${name}]`;
const arrayType = getType(obj[0]);
return `[${arrayType}]`;
}
case Types.OBJECT: {
const objType: Record<string, string | Record<string, unknown>> = {};
@ -87,16 +82,21 @@ export function generateTypeDef(
}
}
export const flattenObjKeys = (
options: any,
parentKey: string,
results: any = {},
): any => {
const r: any = results;
for (const [key, value] of Object.entries(options)) {
if (!skipProperties.includes(key)) {
r[parentKey + "." + key] = value;
export const flattenDef = (def: Def, entityName: string): Def => {
const flattenedDef = def;
if (isTrueObject(def[entityName])) {
Object.entries(def[entityName]).forEach(([key, value]) => {
if (!key.startsWith("!")) {
flattenedDef[`${entityName}.${key}`] = value;
if (isTrueObject(value)) {
Object.entries(value).forEach(([subKey, subValue]) => {
if (!subKey.startsWith("!")) {
flattenedDef[`${entityName}.${key}.${subKey}`] = subValue;
}
});
}
}
return r;
});
}
return flattenedDef;
};

View File

@ -0,0 +1,100 @@
import { DataType } from "utils/autocomplete/TernServer";
const RULES: Record<DataType, Array<string>> = {
STRING: [
"INPUT_WIDGET.text",
"RICH_TEXT_EDITOR_WIDGET.text",
"DROP_DOWN_WIDGET.selectedOptionValue",
"DATE_PICKER_WIDGET_2.selectedDate",
"DATE_PICKER_WIDGET_2.formattedDate",
"TABLE_WIDGET.pageNo",
"TABLE_WIDGET.searchText",
"TABLE_WIDGET.pageSize",
"TABS_WIDGET.selectedTab",
"TABLE_WIDGET.selectedRowIndex",
"IFRAME_WIDGET.source",
"IFRAME_WIDGET.title",
"DROP_DOWN_WIDGET.selectedOptionLabel",
"BUTTON_WIDGET.recaptchaToken",
"IMAGE_WIDGET.image",
"TEXT_WIDGET.text",
"BUTTON_WIDGET.text",
"FORM_BUTTON_WIDGET.text",
"CHART_WIDGET.xAxisName",
"CHART_WIDGET.yAxisName",
"CONTAINER_WIDGET.backgroundColor",
"BUTTON_WIDGET.googleRecaptchaKey",
],
NUMBER: [
"TABLE_WIDGET.pageNo",
"TABLE_WIDGET.pageSize",
"INPUT_WIDGET.text",
"TABLE_WIDGET.selectedRowIndex",
"RICH_TEXT_EDITOR_WIDGET.text",
"DROP_DOWN_WIDGET.selectedOptionValue",
"DATE_PICKER_WIDGET_2.selectedDate",
"DATE_PICKER_WIDGET_2.formattedDate",
"TABLE_WIDGET.searchText",
"TABS_WIDGET.selectedTab",
"IFRAME_WIDGET.source",
"IFRAME_WIDGET.title",
"DROP_DOWN_WIDGET.selectedOptionLabel",
"IMAGE_WIDGET.image",
"TEXT_WIDGET.text",
"BUTTON_WIDGET.text",
"FORM_BUTTON_WIDGET.text",
"CHART_WIDGET.xAxisName",
"CHART_WIDGET.yAxisName",
"CONTAINER_WIDGET.backgroundColor",
],
OBJECT: ["ACTION.data"],
ARRAY: ["ACTION.data"],
BOOLEAN: [
"CHECKBOX_WIDGET.isChecked",
"SWITCH_WIDGET.isSwitchedOn",
"CONTAINER_WIDGET.isVisible",
"INPUT_WIDGET.isVisible",
"TABLE_WIDGET.isVisible",
"DROP_DOWN_WIDGET.isVisible",
"IMAGE_WIDGET.isVisible",
"TEXT_WIDGET.isVisible",
"BUTTON_WIDGET.isVisible",
"DATE_PICKER_WIDGET2.isVisible",
"CHECKBOX_WIDGET.isVisible",
"SWITCH_WIDGET.isVisible",
"RADIO_GROUP_WIDGET.isVisible",
"TABS_WIDGET.isVisible",
"MODAL_WIDGET.isVisible",
"RICH_TEXT_EDITOR_WIDGET.isVisible",
"FORM_WIDGET.isVisible",
"FORM_BUTTON_WIDGET.isVisible",
"FILE_PICKER_WIDGET.isVisible",
"LIST_WIDGET.isVisible",
"RATE_WIDGET.isVisible",
"IFRAME_WIDGET.isVisible",
"DIVIDER_WIDGET.isVisible",
"INPUT_WIDGET.isValid",
"INPUT_WIDGET.isDisabled",
"DROP_DOWN_WIDGET.isDisabled",
"BUTTON_WIDGET.isDisabled",
"DATE_PICKER_WIDGET2.isDisabled",
"CHECKBOX_WIDGET.isDisabled",
"SWITCH_WIDGET.isDisabled",
"RICH_TEXT_EDITOR_WIDGET.isDisabled",
"FORM_BUTTON_WIDGET.isDisabled",
"FILE_PICKER_WIDGET.isRequired",
"MODAL_WIDGET.isOpen",
],
FUNCTION: [
"ACTION.run()",
"storeValue()",
"showAlert()",
"navigateTo()",
"resetWidget()",
"download()",
"showModal()",
],
UNKNOWN: [],
};
export default RULES;

View File

@ -306,7 +306,6 @@ describe("substituteDynamicBindingWithValues", () => {
"wrongBinding": undefined,
"emptyBinding": null,
}`;
debugger;
const result = substituteDynamicBindingWithValues(
binding,
subBindings,

View File

@ -58,6 +58,7 @@ describe("Add functions", () => {
responseMeta: { isExecutionSuccess: false },
ENTITY_TYPE: ENTITY_TYPE.ACTION,
dependencyMap: {},
logBlackList: {},
},
};
const dataTreeWithFunctions = addFunctions(dataTree);

View File

@ -14,6 +14,7 @@ import { Diff } from "deep-diff";
import {
DataTree,
DataTreeAction,
DataTreeAppsmith,
DataTreeEntity,
DataTreeWidget,
ENTITY_TYPE,
@ -230,6 +231,16 @@ export function isAction(entity: DataTreeEntity): entity is DataTreeAction {
);
}
export function isAppsmithEntity(
entity: DataTreeEntity,
): entity is DataTreeAppsmith {
return (
typeof entity === "object" &&
"ENTITY_TYPE" in entity &&
entity.ENTITY_TYPE === ENTITY_TYPE.APPSMITH
);
}
// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing
// Check issue https://github.com/appsmithorg/appsmith/issues/719
export const removeFunctions = (value: any) => {
@ -577,7 +588,9 @@ export const addErrorToEntityProperty = (
// For the times when you need to know if something truly an object like { a: 1, b: 2}
// typeof, lodash.isObject and others will return false positives for things like array, null, etc
export const isTrueObject = (item: unknown): boolean => {
export const isTrueObject = (
item: unknown,
): item is Record<string, unknown> => {
return Object.prototype.toString.call(item) === "[object Object]";
};

File diff suppressed because it is too large Load Diff