Improve autocomplete sorting (#5798)
This commit is contained in:
parent
6e20e2b659
commit
ba06d797de
|
|
@ -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)
|
||||
|
|
|
|||
82
app/client/src/actions/evaluationActions.ts
Normal file
82
app/client/src/actions/evaluationActions.ts
Normal 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 },
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
1360
app/client/src/constants/defs/ecmascript.json
Normal file
1360
app/client/src/constants/defs/ecmascript.json
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"!name": "../../node-forge/lib/aes.js",
|
||||
"!name": "LIB/node-forge",
|
||||
"!define": {
|
||||
"forge.aes.Algorithm.prototype.initialize.!0": {
|
||||
"key": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"!name": "lodash",
|
||||
"!name": "LIB/lodash",
|
||||
"_": {
|
||||
"chunk": {
|
||||
"!url": "https://lodash.com/docs/4.17.15#chunk",
|
||||
|
|
|
|||
|
|
@ -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/",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"!name": "xmlParser",
|
||||
"!name": "LIB/xmlParser",
|
||||
"xmlParser": {
|
||||
"parse": {
|
||||
"!doc": "converts xml string to json object",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -282,7 +282,6 @@ const PropertyControl = memo((props: Props) => {
|
|||
config.validationMessage = "";
|
||||
delete config.dataTreePath;
|
||||
delete config.evaluatedValue;
|
||||
delete config.expected;
|
||||
}
|
||||
|
||||
const isDynamic: boolean = isPathADynamicProperty(
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
326
app/client/src/sagas/PostEvaluationSagas.ts
Normal file
326
app/client/src/sagas/PostEvaluationSagas.ts
Normal 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);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 "?";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
100
app/client/src/utils/autocomplete/dataTypeSortRules.ts
Normal file
100
app/client/src/utils/autocomplete/dataTypeSortRules.ts
Normal 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;
|
||||
|
|
@ -306,7 +306,6 @@ describe("substituteDynamicBindingWithValues", () => {
|
|||
"wrongBinding": undefined,
|
||||
"emptyBinding": null,
|
||||
}`;
|
||||
debugger;
|
||||
const result = substituteDynamicBindingWithValues(
|
||||
binding,
|
||||
subBindings,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ describe("Add functions", () => {
|
|||
responseMeta: { isExecutionSuccess: false },
|
||||
ENTITY_TYPE: ENTITY_TYPE.ACTION,
|
||||
dependencyMap: {},
|
||||
logBlackList: {},
|
||||
},
|
||||
};
|
||||
const dataTreeWithFunctions = addFunctions(dataTree);
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue
Block a user