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 // Tests if data tree entities are sorted
cy.get(`${dynamicInputLocators.hints} li`) cy.get(`${dynamicInputLocators.hints} li`)
.eq(1) .eq(1)
.should("have.text", "Aditya.backgroundColor"); .should("have.text", "input.text");
// Tests if "No suggestions" message will pop if you type any garbage // Tests if "No suggestions" message will pop if you type any garbage
cy.get(dynamicInputLocators.input) 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 CodeMirror from "codemirror";
import { DataTree } from "entities/DataTree/dataTreeFactory"; import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
export enum EditorModes { export enum EditorModes {
TEXT = "text/plain", TEXT = "text/plain",
@ -40,18 +40,23 @@ export const EditorThemes: Record<EditorTheme, string> = {
[EditorTheme.DARK]: "duotone-dark", [EditorTheme.DARK]: "duotone-dark",
}; };
export type HintEntityInformation = {
entityName?: string;
expectedType?: string;
entityType?: ENTITY_TYPE.ACTION | ENTITY_TYPE.WIDGET;
};
export type HintHelper = ( export type HintHelper = (
editor: CodeMirror.Editor, editor: CodeMirror.Editor,
data: DataTree, data: DataTree,
additionalData?: Record<string, Record<string, unknown>>, customDataTree?: Record<string, Record<string, unknown>>,
) => Hinter; ) => Hinter;
export type Hinter = { export type Hinter = {
showHint: ( showHint: (
editor: CodeMirror.Editor, editor: CodeMirror.Editor,
expected: string, entityInformation: HintEntityInformation,
entityName: string,
additionalData?: any, additionalData?: any,
) => any; ) => boolean;
update?: (data: DataTree) => void; update?: (data: DataTree) => void;
trigger?: (editor: CodeMirror.Editor) => 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) => { export const removeNewLineChars = (inputValue: any) => {
return inputValue && inputValue.replace(/(\r\n|\n|\r)/gm, ""); return inputValue && inputValue.replace(/(\r\n|\n|\r)/gm, "");
}; };
@ -10,3 +13,43 @@ export const getInputValue = (inputValue: any) => {
} }
return inputValue; 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 CodeMirror from "codemirror";
import { HintHelper } from "components/editorComponents/CodeEditor/EditorConfig"; import { HintHelper } from "components/editorComponents/CodeEditor/EditorConfig";
import { CommandsCompletion } from "utils/autocomplete/TernServer"; import { CommandsCompletion } from "utils/autocomplete/TernServer";
import { checkIfCursorInsideBinding } from "./hintHelpers";
import { generateQuickCommands } from "./generateQuickCommands"; import { generateQuickCommands } from "./generateQuickCommands";
import { Datasource } from "entities/Datasource"; import { Datasource } from "entities/Datasource";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import log from "loglevel"; 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( 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 { return {
showHint: ( showHint: (
editor: CodeMirror.Editor, editor: CodeMirror.Editor,
_: string, { entityType },
entityName: string,
{ {
datasources, datasources,
executeCommand, executeCommand,
@ -31,11 +31,11 @@ export const commandsHelper: HintHelper = (editor, data: any) => {
update: (value: string) => void; update: (value: string) => void;
}, },
): boolean => { ): boolean => {
const currentEntityType = data[entityName]?.ENTITY_TYPE || "ACTION"; const currentEntityType = entityType || ENTITY_TYPE.ACTION;
entitiesForSuggestions = entitiesForSuggestions.filter((entity: any) => { entitiesForSuggestions = entitiesForSuggestions.filter((entity: any) => {
return currentEntityType === "WIDGET" return currentEntityType === ENTITY_TYPE.WIDGET
? entity.ENTITY_TYPE !== "WIDGET" ? entity.ENTITY_TYPE !== ENTITY_TYPE.WIDGET
: entity.ENTITY_TYPE !== "ACTION"; : entity.ENTITY_TYPE !== ENTITY_TYPE.ACTION;
}); });
const cursorBetweenBinding = checkIfCursorInsideBinding(editor); const cursorBetweenBinding = checkIfCursorInsideBinding(editor);
const value = editor.getValue(); const value = editor.getValue();

View File

@ -1,14 +1,14 @@
import CodeMirror from "codemirror"; import CodeMirror from "codemirror";
import TernServer from "utils/autocomplete/TernServer"; import TernServer from "utils/autocomplete/TernServer";
import KeyboardShortcuts from "constants/KeyboardShortcuts"; import KeyboardShortcuts from "constants/KeyboardShortcuts";
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
import { HintHelper } from "components/editorComponents/CodeEditor/EditorConfig"; import { HintHelper } from "components/editorComponents/CodeEditor/EditorConfig";
import AnalyticsUtil from "utils/AnalyticsUtil"; 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) => { export const bindingHint: HintHelper = (editor, dataTree, customDataTree) => {
if (additionalData) { if (customDataTree) {
const customTreeDef = customTreeTypeDefCreator(additionalData); const customTreeDef = customTreeTypeDefCreator(customDataTree);
TernServer.updateDef("customDataTree", customTreeDef); TernServer.updateDef("customDataTree", customTreeDef);
} }
@ -16,11 +16,8 @@ export const bindingHint: HintHelper = (editor, dataTree, additionalData) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available // @ts-ignore: No types available
...editor.options.extraKeys, ...editor.options.extraKeys,
[KeyboardShortcuts.CodeEditor.OpenAutocomplete]: ( [KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (cm: CodeMirror.Editor) =>
cm: CodeMirror.Editor, TernServer.complete(cm),
expected: string,
entity: string,
) => TernServer.complete(cm, expected, entity),
[KeyboardShortcuts.CodeEditor.ShowTypeAndInfo]: (cm: CodeMirror.Editor) => { [KeyboardShortcuts.CodeEditor.ShowTypeAndInfo]: (cm: CodeMirror.Editor) => {
TernServer.showType(cm); TernServer.showType(cm);
}, },
@ -29,15 +26,12 @@ export const bindingHint: HintHelper = (editor, dataTree, additionalData) => {
}, },
}); });
return { return {
showHint: ( showHint: (editor: CodeMirror.Editor, entityInformation): boolean => {
editor: CodeMirror.Editor, TernServer.setEntityInformation(entityInformation);
expected: string,
entityName: string,
): boolean => {
const shouldShow = checkIfCursorInsideBinding(editor); const shouldShow = checkIfCursorInsideBinding(editor);
if (shouldShow) { if (shouldShow) {
AnalyticsUtil.logEvent("AUTO_COMPLETE_SHOW", {}); AnalyticsUtil.logEvent("AUTO_COMPLETE_SHOW", {});
TernServer.complete(editor, expected, entityName); TernServer.complete(editor);
return true; return true;
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // 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 _ from "lodash";
import { import {
DataTree, DataTree,
ENTITY_TYPE,
EvaluationSubstitutionType, EvaluationSubstitutionType,
} from "entities/DataTree/dataTreeFactory"; } from "entities/DataTree/dataTreeFactory";
import { Skin } from "constants/DefaultTheme"; import { Skin } from "constants/DefaultTheme";
@ -30,6 +31,7 @@ import {
EditorSize, EditorSize,
EditorTheme, EditorTheme,
EditorThemes, EditorThemes,
HintEntityInformation,
Hinter, Hinter,
HintHelper, HintHelper,
MarkHelper, MarkHelper,
@ -55,7 +57,7 @@ import {
getEvalValuePath, getEvalValuePath,
PropertyEvaluationErrorType, PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils"; } from "utils/DynamicBindingUtils";
import { removeNewLineChars, getInputValue } from "./codeEditorUtils"; import { getInputValue, removeNewLineChars } from "./codeEditorUtils";
import { commandsHelper } from "./commandsHelper"; import { commandsHelper } from "./commandsHelper";
import { getEntityNameAndPropertyPath } from "workers/evaluationUtils"; import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
import Button from "components/ads/Button"; import Button from "components/ads/Button";
@ -373,13 +375,27 @@ class CodeEditor extends Component<Props, State> {
handleAutocompleteVisibility = (cm: CodeMirror.Editor) => { handleAutocompleteVisibility = (cm: CodeMirror.Editor) => {
if (!this.state.isFocused) return; if (!this.state.isFocused) return;
const expected = this.props.expected ? this.props.expected : ""; const { dataTreePath, dynamicData, expected } = this.props;
const { entityName } = getEntityNameAndPropertyPath( const entityInformation: HintEntityInformation = {
this.props.dataTreePath || "", 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; let hinterOpen = false;
for (let i = 0; i < this.hinters.length; i++) { 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, datasources: this.props.datasources.list,
pluginIdToImageLocation: this.props.pluginIdToImageLocation, pluginIdToImageLocation: this.props.pluginIdToImageLocation,
recentEntities: this.props.recentEntities, recentEntities: this.props.recentEntities,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{ {
"!name": "moment", "!name": "LIB/moment",
"moment": { "moment": {
"!type": "fn(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean) -> Moment", "!type": "fn(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean) -> Moment",
"!url": "https://momentjs.com/docs/#/parsing/", "!url": "https://momentjs.com/docs/#/parsing/",
@ -469,7 +469,7 @@
"!doc": "A global locale configuration can be problematic when passing around moments that may need to be formatted into different locale" "!doc": "A global locale configuration can be problematic when passing around moments that may need to be formatted into different locale"
}, },
"localeData": { "localeData": {
"!type": "fn() -> Locale" "!type": "fn() -> Locale"
}, },
"toObject": { "toObject": {
"!type": "fn() -> MomentObjectOutput", "!type": "fn() -> MomentObjectOutput",
@ -727,4 +727,4 @@
"!doc": "Returns am/pm string for particular time-of-day in upper/lower case" "!doc": "Returns am/pm string for particular time-of-day in upper/lower case"
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -10,7 +10,6 @@ import {
import { import {
EvaluationReduxAction, EvaluationReduxAction,
ReduxAction, ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes, ReduxActionTypes,
ReduxActionWithoutPayload, ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
@ -21,16 +20,7 @@ import {
import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory"; import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory";
import { GracefulWorkerService } from "utils/WorkerUtil"; import { GracefulWorkerService } from "utils/WorkerUtil";
import Worker from "worker-loader!../workers/evaluation.worker"; import Worker from "worker-loader!../workers/evaluation.worker";
import { import { EVAL_WORKER_ACTIONS } from "utils/DynamicBindingUtils";
EVAL_WORKER_ACTIONS,
EvalError,
EvalErrorTypes,
EvaluationError,
getEvalErrorPath,
getEvalValuePath,
PropertyEvalErrorTypeDebugMessage,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import log from "loglevel"; import log from "loglevel";
import { WidgetProps } from "widgets/BaseWidget"; import { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, { import PerformanceTracker, {
@ -38,319 +28,24 @@ import PerformanceTracker, {
} from "../utils/PerformanceTracker"; } from "../utils/PerformanceTracker";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { Action } from "redux"; 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 { import {
getEntityNameAndPropertyPath, EVALUATE_REDUX_ACTIONS,
isAction, FIRST_EVAL_REDUX_ACTIONS,
isWidget, setDependencyMap,
} from "workers/evaluationUtils"; setEvaluatedTree,
import moment from "moment/moment"; shouldProcessBatchedAction,
import { Toaster } from "components/ads/Toast"; } from "actions/evaluationActions";
import { Variant } from "components/ads/common";
import AppsmithConsole from "utils/AppsmithConsole";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { import {
createMessage, evalErrorHandler,
ERROR_EVAL_ERROR_GENERIC, logSuccessfulBindings,
ERROR_EVAL_TRIGGER, postEvalActionDispatcher,
} from "constants/messages"; updateTernDefinitions,
import { getAppMode } from "selectors/applicationSelectors"; } from "./PostEvaluationSagas";
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";
let widgetTypeConfigMap: WidgetTypeConfigMap; let widgetTypeConfigMap: WidgetTypeConfigMap;
const worker = new GracefulWorkerService(Worker); 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( function* evaluateTreeSaga(
postEvalActions?: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>, postEvalActions?: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
isFirstEvaluation = false, isFirstEvaluation = false,
@ -382,10 +77,7 @@ function* evaluateTreeSaga(
PerformanceTracker.startAsyncTracking( PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE, PerformanceTransactionName.SET_EVALUATED_TREE,
); );
yield put({ yield put(setEvaluatedTree(dataTree, updates));
type: ReduxActionTypes.SET_EVALUATED_TREE,
payload: { dataTree, updates },
});
PerformanceTracker.stopAsyncTracking( PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE, PerformanceTransactionName.SET_EVALUATED_TREE,
); );
@ -408,10 +100,7 @@ function* evaluateTreeSaga(
isFirstEvaluation, isFirstEvaluation,
); );
yield put({ yield put(setDependencyMap(dependencies));
type: ReduxActionTypes.SET_EVALUATION_INVERSE_DEPENDENCY_MAP,
payload: { inverseDependencyMap: dependencies },
});
if (postEvalActions && postEvalActions.length) { if (postEvalActions && postEvalActions.length) {
yield call(postEvalActionDispatcher, postEvalActions); 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() { function evalQueueBuffer() {
let canTake = false; let canTake = false;
let postEvalActions: any = []; let postEvalActions: any = [];
@ -571,7 +204,7 @@ function evalQueueBuffer() {
}; };
const put = (action: EvaluationReduxAction<unknown | unknown[]>) => { const put = (action: EvaluationReduxAction<unknown | unknown[]>) => {
if (!shouldProcessAction(action)) { if (!shouldProcessBatchedAction(action)) {
return; return;
} }
canTake = true; canTake = true;
@ -607,11 +240,14 @@ function* evaluationChangeListenerSaga() {
const action: EvaluationReduxAction<unknown | unknown[]> = yield take( const action: EvaluationReduxAction<unknown | unknown[]> = yield take(
evtActionChannel, evtActionChannel,
); );
if (shouldProcessAction(action)) { if (FIRST_EVAL_REDUX_ACTIONS.includes(action.type)) {
yield call(evaluateTreeSaga, action.postEvalActions); 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() { 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 { DataTreeAction } from "entities/DataTree/dataTreeFactory";
import _ from "lodash"; import _ from "lodash";
// const isLoading = {
// "!type": "bool",
// "!doc": "Boolean value indicating if the entity is in loading state",
// };
const isVisible = { const isVisible = {
"!type": "bool", "!type": "bool",
"!doc": "Boolean value indicating if the widget is in visible state", "!doc": "Boolean value indicating if the widget is in visible state",
@ -14,7 +10,6 @@ const isVisible = {
export const entityDefinitions = { export const entityDefinitions = {
ACTION: (entity: DataTreeAction) => { ACTION: (entity: DataTreeAction) => {
const dataDef = generateTypeDef(entity.data); const dataDef = generateTypeDef(entity.data);
const responseMetaDef = generateTypeDef(entity.responseMeta);
let data: Record<string, any> = { let data: Record<string, any> = {
"!doc": "The response of the action", "!doc": "The response of the action",
}; };
@ -23,21 +18,16 @@ export const entityDefinitions = {
} else { } else {
data = { ...data, ...dataDef }; 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 { return {
"!doc": "!doc":
"Actions allow you to connect your widgets to your backend data in a secure manner.", "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", "!url": "https://docs.appsmith.com/v/v1.2.1/framework-reference/run",
isLoading: "bool", isLoading: "bool",
data, data,
responseMeta, responseMeta: {
"!doc": "The response meta of the action",
"!type": "?",
},
run: "fn(onSuccess: fn() -> void, onError: fn() -> void) -> void", run: "fn(onSuccess: fn() -> void, onError: fn() -> void) -> void",
}; };
}, },
@ -137,8 +127,6 @@ export const entityDefinitions = {
isVisible: isVisible, isVisible: isVisible,
text: "string", text: "string",
isDisabled: "bool", isDisabled: "bool",
recaptchaToken: "string",
googleRecaptchaKey: "string",
}, },
DATE_PICKER_WIDGET: { DATE_PICKER_WIDGET: {
"!doc": "!doc":
@ -219,8 +207,6 @@ export const entityDefinitions = {
isVisible: isVisible, isVisible: isVisible,
text: "string", text: "string",
isDisabled: "bool", isDisabled: "bool",
recaptchaToken: "string",
googleRecaptchaKey: "string",
}, },
MAP_WIDGET: { MAP_WIDGET: {
isVisible: isVisible, isVisible: isVisible,
@ -319,6 +305,7 @@ export const GLOBAL_DEFS = {
}; };
export const GLOBAL_FUNCTIONS = { export const GLOBAL_FUNCTIONS = {
"!name": "DATA_TREE.APPSMITH.FUNCTIONS",
navigateTo: { navigateTo: {
"!doc": "Action to navigate the user to another page or url", "!doc": "Action to navigate the user to another page or url",
"!type": "fn(pageNameOrUrl: string, params: {}, target?: string) -> void", "!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 { MockCodemirrorEditor } from "../../../test/__mocks__/CodeMirrorEditorMock";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import _ from "lodash";
describe("Tern server", () => { describe("Tern server", () => {
it("Check whether the correct value is being sent to tern", () => { 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 */ /* eslint-disable @typescript-eslint/ban-ts-comment */
// Heavily inspired from https://github.com/codemirror/CodeMirror/blob/master/addon/tern/tern.js // Heavily inspired from https://github.com/codemirror/CodeMirror/blob/master/addon/tern/tern.js
import tern, { Server, Def } from "tern"; 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 lodash from "constants/defs/lodash.json";
import base64 from "constants/defs/base64-js.json"; import base64 from "constants/defs/base64-js.json";
import moment from "constants/defs/moment.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 forge from "constants/defs/forge.json";
import CodeMirror, { Hint, Pos, cmpPos } from "codemirror"; import CodeMirror, { Hint, Pos, cmpPos } from "codemirror";
import { import {
getDynamicBindings,
getDynamicStringSegments, getDynamicStringSegments,
isDynamicValue, isDynamicValue,
} from "utils/DynamicBindingUtils"; } from "utils/DynamicBindingUtils";
@ -16,6 +17,10 @@ import {
GLOBAL_DEFS, GLOBAL_DEFS,
GLOBAL_FUNCTIONS, GLOBAL_FUNCTIONS,
} from "utils/autocomplete/EntityDefinitions"; } 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[] = [ const DEFS: Def[] = [
GLOBAL_FUNCTIONS, GLOBAL_FUNCTIONS,
@ -77,8 +82,7 @@ class TernServer {
docs: TernDocs = Object.create(null); docs: TernDocs = Object.create(null);
cachedArgHints: ArgHints | null = null; cachedArgHints: ArgHints | null = null;
active: any; active: any;
expected?: string; entityInformation: HintEntityInformation = {};
entityName?: string;
constructor() { constructor() {
this.server = new tern.Server({ this.server = new tern.Server({
@ -87,9 +91,15 @@ class TernServer {
}); });
} }
complete(cm: CodeMirror.Editor, expected: string, entityName: string) { resetServer() {
this.expected = expected; this.server = new tern.Server({
this.entityName = entityName; async: true,
defs: DEFS,
});
this.docs = Object.create(null);
}
complete(cm: CodeMirror.Editor) {
cm.showHint({ cm.showHint({
hint: this.getHint.bind(this), hint: this.getHint.bind(this),
completeSingle: false, completeSingle: false,
@ -161,25 +171,36 @@ class TernServer {
) { ) {
after = '"]'; 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) { for (let i = 0; i < data.completions.length; ++i) {
const completion = data.completions[i]; const completion = data.completions[i];
let className = this.typeToIcon(completion.type); let className = this.typeToIcon(completion.type);
const dataType = this.getDataType(completion.type); const dataType = this.getDataType(completion.type);
const entityName = this.entityName;
if (data.guess) className += " " + cls + "guess"; if (data.guess) className += " " + cls + "guess";
if (!entityName || !completion.name.includes(entityName)) { let completionText = completion.name + after;
completions.push({ if (dataType === "FUNCTION") {
text: completion.name + after, completionText = completionText + "()";
displayText: completion.displayName || completion.name,
className: className,
data: completion,
origin: completion.origin,
type: dataType,
});
} }
completions.push({
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 = { const obj = {
from: from, from: from,
to: to, to: to,
@ -242,56 +263,135 @@ class TernServer {
}); });
} }
sortCompletions(completions: Completion[]) { sortCompletions(
// Add data tree completions before others completions: Completion[],
findBestMatch: boolean,
bestMatchSearch: string,
) {
const expectedDataType = this.getExpectedDataType(); const expectedDataType = this.getExpectedDataType();
const dataTreeCompletions = completions const { entityName, entityType } = this.entityInformation;
.filter((c) => c.origin && c.origin.startsWith("DATA_TREE_")) type CompletionType =
.sort((a: Completion, b: Completion) => { | "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") { if (a.type === "FUNCTION" && b.type !== "FUNCTION") {
return 1; return 1;
} else if (a.type !== "FUNCTION" && b.type === "FUNCTION") { } else if (a.type !== "FUNCTION" && b.type === "FUNCTION") {
return -1; return -1;
} }
return a.text.toLowerCase().localeCompare(b.text.toLowerCase()); 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( SortRules[expectedDataType].forEach((rule) => {
(c) => c.type === expectedDataType, if (Array.isArray(groupedMatches[rule])) {
); sortedMatches.push(...groupedMatches[rule]);
const otherDataType = dataTreeCompletions.filter( }
(c) => c.type !== expectedDataType, });
);
if (otherDataType.length && sameDataType.length) { sortedMatches.sort((a, b) => {
const otherDataTitle: Completion = { let aRank = 0;
text: "Search results", let bRank = 0;
displayText: "Search results", const entityTypeA: ENTITY_TYPE = a.origin.split(".")[1] as ENTITY_TYPE;
className: "CodeMirror-hint-header", const entityTypeB: ENTITY_TYPE = b.origin.split(".")[1] as ENTITY_TYPE;
data: { doc: "" }, if (entityTypeA === entityType) {
origin: "", aRank = aRank + 1;
type: "UNKNOWN", }
isHeader: true, if (entityTypeB === entityType) {
}; bRank = bRank + 1;
const sameDataTitle: Completion = { }
text: "Best Match", return aRank - bRank;
displayText: "Best Match", });
className: "CodeMirror-hint-header", completionType.MATCHING_TYPE = _.take(sortedMatches, 3);
data: { doc: "" }, if (completionType.MATCHING_TYPE.length) {
origin: "", completionType.MATCHING_TYPE.unshift(
type: "UNKNOWN", createCompletionHeader("Best Match"),
isHeader: true, );
}; completionType.DATA_TREE.unshift(
sameDataType.unshift(sameDataTitle); createCompletionHeader("Search Results"),
otherDataType.unshift(otherDataTitle); );
}
} else {
// Clear any matching type because we dont want to find best match
completionType.MATCHING_TYPE = [];
} }
const docCompletetions = completions.filter((c) => c.origin === "[doc]");
const otherCompletions = completions.filter(
(c) => c.origin !== "dataTree" && c.origin !== "[doc]",
);
return [ return [
...docCompletetions, ...completionType.CONTEXT,
...sameDataType, ...completionType.MATCHING_TYPE,
...otherDataType, ...completionType.DATA_TREE,
...otherCompletions, ...completionType.LIBRARY,
...completionType.JS,
...completionType.OTHER,
]; ];
} }
@ -306,21 +406,23 @@ class TernServer {
else return "OBJECT"; else return "OBJECT";
} }
getExpectedDataType() { getExpectedDataType(): DataType {
const type = this.expected; const type = this.entityInformation.expectedType;
if (type === undefined) return "UNKNOWN";
if ( if (
type === "Array<Object>" || type === "Array<Object>" ||
type === "Array" || type === "Array" ||
type === "Array<{ label: string, value: string }>" || type === "Array<{ label: string, value: string }>" ||
type === "Array<x:string, y:number>" type === "Array<x:string, y:number>"
) ) {
return "ARRAY"; return "ARRAY";
}
if (type === "boolean") return "BOOLEAN"; if (type === "boolean") return "BOOLEAN";
if (type === "string") return "STRING"; if (type === "string") return "STRING";
if (type === "number") return "NUMBER"; if (type === "number") return "NUMBER";
if (type === "object" || type === "JSON") return "OBJECT"; if (type === "object" || type === "JSON") return "OBJECT";
if (type === undefined) return "UNKNOWN"; if (type === "Function Call") return "FUNCTION";
return undefined; return "UNKNOWN";
} }
typeToIcon(type: string) { typeToIcon(type: string) {
@ -417,6 +519,7 @@ class TernServer {
end?: any; end?: any;
start?: any; start?: any;
file?: any; file?: any;
includeKeywords?: boolean;
}, },
pos?: CodeMirror.Position, pos?: CodeMirror.Position,
) { ) {
@ -425,6 +528,7 @@ class TernServer {
const allowFragments = !query.fullDocs; const allowFragments = !query.fullDocs;
if (!allowFragments) delete query.fullDocs; if (!allowFragments) delete query.fullDocs;
query.lineCharPositions = true; query.lineCharPositions = true;
query.includeKeywords = true;
if (!query.end) { if (!query.end) {
const lineValue = this.lineValue(doc); const lineValue = this.lineValue(doc);
const focusedValue = this.getFocusedDynamicValue(doc); const focusedValue = this.getFocusedDynamicValue(doc);
@ -712,6 +816,20 @@ class TernServer {
fadeOut(tooltip: HTMLElement) { fadeOut(tooltip: HTMLElement) {
this.remove(tooltip); 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(); export default new TernServer();

View File

@ -1,5 +1,4 @@
import { generateReactKey } from "utils/generators"; import { generateTypeDef } from "./dataTreeTypeDefCreator";
import { getType, Types } from "utils/TypeHelpers";
let extraDefs: any = {}; let extraDefs: any = {};
@ -17,35 +16,3 @@ export const customTreeTypeDefCreator = (
extraDefs = {}; extraDefs = {};
return { ...def }; 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 { import {
generateTypeDef, generateTypeDef,
dataTreeTypeDefCreator, dataTreeTypeDefCreator,
flattenObjKeys, flattenDef,
} from "utils/autocomplete/dataTreeTypeDefCreator"; } from "utils/autocomplete/dataTreeTypeDefCreator";
import { import {
DataTreeWidget, DataTreeWidget,
@ -72,26 +72,37 @@ describe("dataTreeTypeDefCreator", () => {
expect(objType).toStrictEqual(expected); expect(objType).toStrictEqual(expected);
}); });
it("flatten object", () => { it("flatten def", () => {
const options = { const def = {
someNumber: "number", entity1: {
someString: "string", someNumber: "number",
someBool: "bool", someString: "string",
nested: { someBool: "bool",
someExtraNested: "string", nested: {
someExtraNested: "string",
},
}, },
}; };
const expected = { const expected = {
entity1: {
someNumber: "number",
someString: "string",
someBool: "bool",
nested: {
someExtraNested: "string",
},
},
"entity1.someNumber": "number", "entity1.someNumber": "number",
"entity1.someString": "string", "entity1.someString": "string",
"entity1.someBool": "bool", "entity1.someBool": "bool",
"entity1.nested": { "entity1.nested": {
someExtraNested: "string", someExtraNested: "string",
}, },
"entity1.nested.someExtraNested": "string",
}; };
const value = flattenObjKeys(options, "entity1"); const value = flattenDef(def, "entity1");
expect(value).toStrictEqual(expected); 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 _ from "lodash";
import { generateReactKey } from "utils/generators";
import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; import { entityDefinitions } from "utils/autocomplete/EntityDefinitions";
import { getType, Types } from "utils/TypeHelpers"; import { getType, Types } from "utils/TypeHelpers";
import { Def } from "tern"; 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 = {}; 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 = ( export const dataTreeTypeDefCreator = (
entity: DataTreeEntity, entity: DataTreeEntity,
entityName: string, entityName: string,
): { def: Def; name: string } => { ): { def: Def; name: string } => {
const defName = `DATA_TREE_${entityName}`; const def: any = {};
const def: any = { if (isWidget(entity)) {
"!name": defName, const widgetType = entity.type;
}; if (widgetType in entityDefinitions) {
if (entity && "ENTITY_TYPE" in entity) { const definition = _.get(entityDefinitions, widgetType);
if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { if (_.isFunction(definition)) {
const widgetType = entity.type; def[entityName] = definition(entity);
if (widgetType in entityDefinitions) { } else {
const definition = _.get(entityDefinitions, widgetType); def[entityName] = definition;
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}`;
} }
if (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION) { } else if (isAction(entity)) {
const actionDefs = entityDefinitions.ACTION(entity); def[entityName] = entityDefinitions.ACTION(entity);
def[entityName] = actionDefs; flattenDef(def, entityName);
const finalData = flattenObjKeys(actionDefs, entityName); def["!name"] = `DATA_TREE.ACTION.ACTION.${entityName}`;
for (const [key, value] of Object.entries(finalData)) { } else if (isAppsmithEntity(entity)) {
def[key] = value; def["!name"] = "DATA_TREE.APPSMITH.APPSMITH";
} def.appsmith = generateTypeDef(_.omit(entity, "ENTITY_TYPE"));
}
if (entity.ENTITY_TYPE === ENTITY_TYPE.APPSMITH) {
const options: any = generateTypeDef(_.omit(entity, "ENTITY_TYPE"));
def.appsmith = options;
}
} }
def["!define"] = { ...extraDefs }; if (Object.keys(extraDefs)) {
extraDefs = {}; def["!define"] = { ...extraDefs };
return { def, name: defName }; extraDefs = {};
}
return { def, name: def["!name"] };
}; };
export function generateTypeDef( export function generateTypeDef(
@ -61,10 +58,8 @@ export function generateTypeDef(
const type = getType(obj); const type = getType(obj);
switch (type) { switch (type) {
case Types.ARRAY: { case Types.ARRAY: {
const arrayType = generateTypeDef(obj[0]); const arrayType = getType(obj[0]);
const name = generateReactKey(); return `[${arrayType}]`;
extraDefs[name] = arrayType;
return `[${name}]`;
} }
case Types.OBJECT: { case Types.OBJECT: {
const objType: Record<string, string | Record<string, unknown>> = {}; const objType: Record<string, string | Record<string, unknown>> = {};
@ -87,16 +82,21 @@ export function generateTypeDef(
} }
} }
export const flattenObjKeys = ( export const flattenDef = (def: Def, entityName: string): Def => {
options: any, const flattenedDef = def;
parentKey: string, if (isTrueObject(def[entityName])) {
results: any = {}, Object.entries(def[entityName]).forEach(([key, value]) => {
): any => { if (!key.startsWith("!")) {
const r: any = results; flattenedDef[`${entityName}.${key}`] = value;
for (const [key, value] of Object.entries(options)) { if (isTrueObject(value)) {
if (!skipProperties.includes(key)) { Object.entries(value).forEach(([subKey, subValue]) => {
r[parentKey + "." + key] = value; 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, "wrongBinding": undefined,
"emptyBinding": null, "emptyBinding": null,
}`; }`;
debugger;
const result = substituteDynamicBindingWithValues( const result = substituteDynamicBindingWithValues(
binding, binding,
subBindings, subBindings,

View File

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

View File

@ -14,6 +14,7 @@ import { Diff } from "deep-diff";
import { import {
DataTree, DataTree,
DataTreeAction, DataTreeAction,
DataTreeAppsmith,
DataTreeEntity, DataTreeEntity,
DataTreeWidget, DataTreeWidget,
ENTITY_TYPE, 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 // 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 // Check issue https://github.com/appsmithorg/appsmith/issues/719
export const removeFunctions = (value: any) => { 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} // 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 // 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]"; return Object.prototype.toString.call(item) === "[object Object]";
}; };

File diff suppressed because it is too large Load Diff