fix: Improve Autocomplete for local vars, JSObject & ButtonGroup Widget
Add Autocomplete support for - local variables - JSObjects - ButtonGroupWidget Remove Autocomplete suggestion for - `eval` - undefined global values like `tabs`
This commit is contained in:
parent
1a6936435d
commit
e255593e28
|
|
@ -2,10 +2,10 @@ import { ObjectsRegistry } from "../../../../support/Objects/Registry";
|
|||
|
||||
const {
|
||||
AggregateHelper: agHelper,
|
||||
|
||||
DeployMode: deployMode,
|
||||
EntityExplorer: ee,
|
||||
JSEditor: jsEditor,
|
||||
CommonLocators: locator,
|
||||
DeployMode: deployMode,
|
||||
PropertyPane: propPane,
|
||||
} = ObjectsRegistry;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import { WIDGET } from "../../../../locators/WidgetLocators";
|
||||
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
|
||||
const explorer = require("../../../../locators/explorerlocators.json");
|
||||
|
||||
const { CommonLocators, EntityExplorer, JSEditor: jsEditor } = ObjectsRegistry;
|
||||
|
||||
const jsObjectBody = `export default {
|
||||
myVar1: [],
|
||||
myVar2: {},
|
||||
myFun1(){
|
||||
|
||||
},
|
||||
myFun2: async () => {
|
||||
//use async-await or promises
|
||||
}
|
||||
}`;
|
||||
|
||||
describe("Autocomplete tests", () => {
|
||||
before(() => {
|
||||
cy.get(explorer.addWidget).click();
|
||||
EntityExplorer.DragDropWidgetNVerify(WIDGET.BUTTON_GROUP_WIDGET, 300, 500);
|
||||
});
|
||||
|
||||
it("1. ButtonGroup autocomplete & Eval shouldn't show up", () => {
|
||||
// create js object
|
||||
jsEditor.CreateJSObject(jsObjectBody, {
|
||||
paste: true,
|
||||
completeReplace: true,
|
||||
toRun: false,
|
||||
shouldCreateNewJSObj: true,
|
||||
});
|
||||
|
||||
const lineNumber = 5;
|
||||
cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click();
|
||||
|
||||
cy.get(CommonLocators._codeMirrorTextArea)
|
||||
.focus()
|
||||
.type(`ButtonGroup1.`);
|
||||
|
||||
cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("groupButtons");
|
||||
|
||||
cy.get(CommonLocators._codeMirrorTextArea)
|
||||
.focus()
|
||||
.type(`groupButtons.`);
|
||||
|
||||
cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("groupButton1");
|
||||
|
||||
cy.get(CommonLocators._codeMirrorTextArea).focus().type(`
|
||||
eval`);
|
||||
|
||||
cy.get(`.CodeMirror-hints > :nth-child(1)`).should(
|
||||
"not.have.value",
|
||||
"eval()",
|
||||
);
|
||||
});
|
||||
|
||||
it("2. Local variables autocompletion support", () => {
|
||||
// create js object
|
||||
jsEditor.CreateJSObject(jsObjectBody, {
|
||||
paste: true,
|
||||
completeReplace: true,
|
||||
toRun: false,
|
||||
shouldCreateNewJSObj: true,
|
||||
});
|
||||
|
||||
const lineNumber = 5;
|
||||
|
||||
const array = [
|
||||
{ label: "a", value: "b" },
|
||||
{ label: "a", value: "b" },
|
||||
];
|
||||
|
||||
const codeToType = `
|
||||
const arr = ${JSON.stringify(array)};
|
||||
|
||||
arr.map(callBack)
|
||||
`;
|
||||
|
||||
// component re-render cause DOM element of cy.get to lost
|
||||
// added wait to finish re-render before cy.get
|
||||
cy.wait(100);
|
||||
|
||||
cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click();
|
||||
|
||||
cy.get(CommonLocators._codeMirrorTextArea)
|
||||
.focus()
|
||||
.type(`${codeToType}`, { parseSpecialCharSequences: false })
|
||||
.type(`{upArrow}{upArrow}`)
|
||||
.type(`const callBack = (item) => item.l`);
|
||||
|
||||
cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("label");
|
||||
|
||||
cy.get(CommonLocators._codeMirrorTextArea)
|
||||
.focus()
|
||||
.type(`label`);
|
||||
});
|
||||
|
||||
it("3. JSObject this. autocomplete", () => {
|
||||
// create js object
|
||||
jsEditor.CreateJSObject(jsObjectBody, {
|
||||
paste: true,
|
||||
completeReplace: true,
|
||||
toRun: false,
|
||||
shouldCreateNewJSObj: true,
|
||||
});
|
||||
|
||||
const lineNumber = 5;
|
||||
|
||||
const codeToType = "this.";
|
||||
|
||||
cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click();
|
||||
|
||||
cy.get(CommonLocators._codeMirrorTextArea)
|
||||
.focus()
|
||||
.type(`${codeToType}`);
|
||||
|
||||
["myFun2()", "myVar1", "myVar2"].forEach((element, index) => {
|
||||
cy.get(`.CodeMirror-hints > :nth-child(${index + 1})`).contains(element);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ export const WIDGET = {
|
|||
CURRENCY_INPUT_WIDGET: "currencyinputwidget",
|
||||
BUTTON_WIDGET: "buttonwidget",
|
||||
MULTISELECT_WIDGET: "multiselectwidgetv2",
|
||||
BUTTON_GROUP_WIDGET: "buttongroupwidget",
|
||||
TREESELECT_WIDGET: "singleselecttreewidget",
|
||||
TAB: "tabswidget",
|
||||
TABLE: "tablewidgetv2",
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ export interface ICreateJSObjectOptions {
|
|||
completeReplace: boolean;
|
||||
toRun: boolean;
|
||||
shouldCreateNewJSObj: boolean;
|
||||
lineNumber?: number;
|
||||
}
|
||||
const DEFAULT_CREATE_JS_OBJECT_OPTIONS = {
|
||||
paste: true,
|
||||
completeReplace: false,
|
||||
toRun: true,
|
||||
shouldCreateNewJSObj: true,
|
||||
lineNumber: 4,
|
||||
};
|
||||
|
||||
export class JSEditor {
|
||||
|
|
@ -118,30 +120,29 @@ export class JSEditor {
|
|||
JSCode: string,
|
||||
options: ICreateJSObjectOptions = DEFAULT_CREATE_JS_OBJECT_OPTIONS,
|
||||
) {
|
||||
const { completeReplace, paste, shouldCreateNewJSObj, toRun } = options;
|
||||
const {
|
||||
completeReplace,
|
||||
lineNumber = 4,
|
||||
paste,
|
||||
shouldCreateNewJSObj,
|
||||
toRun,
|
||||
} = options;
|
||||
|
||||
shouldCreateNewJSObj && this.NavigateToNewJSEditor();
|
||||
if (!completeReplace) {
|
||||
const downKeys = Array.from(new Array(lineNumber), () => "{downarrow}")
|
||||
.toString()
|
||||
.replaceAll(",", "");
|
||||
cy.get(this.locator._codeMirrorTextArea)
|
||||
.first()
|
||||
.focus()
|
||||
.type("{downarrow}{downarrow}{downarrow}{downarrow} ");
|
||||
.type(`${downKeys} `);
|
||||
} else {
|
||||
cy.get(this.locator._codeMirrorTextArea)
|
||||
.first()
|
||||
.focus()
|
||||
.type(this.selectAllJSObjectContentShortcut)
|
||||
.type("{backspace}", { force: true });
|
||||
|
||||
// .type("{uparrow}", { force: true })
|
||||
// .type("{ctrl}{shift}{downarrow}", { force: true })
|
||||
// .type("{del}",{ force: true });
|
||||
|
||||
// cy.get(this.locator._codthis.eeditorTarget).contains('export').click().closest(this.locator._codthis.eeditorTarget)
|
||||
// .type("{uparrow}", { force: true })
|
||||
// .type("{ctrl}{shift}{downarrow}", { force: true })
|
||||
// .type("{backspace}",{ force: true });
|
||||
//.type("{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow} ")
|
||||
}
|
||||
|
||||
cy.get(this.locator._codeMirrorTextArea)
|
||||
|
|
|
|||
|
|
@ -45,9 +45,10 @@ export const EditorThemes: Record<EditorTheme, string> = {
|
|||
export type FieldEntityInformation = {
|
||||
entityName?: string;
|
||||
expectedType?: AutocompleteDataType;
|
||||
entityType?: ENTITY_TYPE.ACTION | ENTITY_TYPE.WIDGET | ENTITY_TYPE.JSACTION;
|
||||
entityType?: ENTITY_TYPE;
|
||||
entityId?: string;
|
||||
propertyPath?: string;
|
||||
blockCompletions?: Array<{ parentPath: string; subPath: string }>;
|
||||
};
|
||||
|
||||
export type HintHelper = (
|
||||
|
|
|
|||
|
|
@ -28,8 +28,20 @@ export const bindingHint: HintHelper = (editor, dataTree, customDataTree) => {
|
|||
},
|
||||
});
|
||||
return {
|
||||
showHint: (editor: CodeMirror.Editor, entityInformation): boolean => {
|
||||
TernServer.setEntityInformation(entityInformation);
|
||||
showHint: (
|
||||
editor: CodeMirror.Editor,
|
||||
entityInformation,
|
||||
additionalData,
|
||||
): boolean => {
|
||||
if (additionalData && additionalData.blockCompletions) {
|
||||
TernServer.setEntityInformation({
|
||||
...entityInformation,
|
||||
blockCompletions: additionalData.blockCompletions,
|
||||
});
|
||||
} else {
|
||||
TernServer.setEntityInformation(entityInformation);
|
||||
}
|
||||
|
||||
const entityType = entityInformation?.entityType;
|
||||
let shouldShow = false;
|
||||
if (entityType === ENTITY_TYPE.JSACTION) {
|
||||
|
|
|
|||
|
|
@ -101,17 +101,8 @@ import { getMoveCursorLeftKey } from "./utils/cursorLeftMovement";
|
|||
import { interactionAnalyticsEvent } from "utils/AppsmithUtils";
|
||||
import { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator";
|
||||
|
||||
interface ReduxStateProps {
|
||||
datasources: any;
|
||||
dynamicData: DataTree;
|
||||
pluginIdToImageLocation: Record<string, string>;
|
||||
recentEntities: string[];
|
||||
}
|
||||
|
||||
interface ReduxDispatchProps {
|
||||
executeCommand: (payload: any) => void;
|
||||
startingEntityUpdation: () => void;
|
||||
}
|
||||
type ReduxStateProps = ReturnType<typeof mapStateToProps>;
|
||||
type ReduxDispatchProps = ReturnType<typeof mapDispatchToProps>;
|
||||
|
||||
export type CodeEditorExpected = {
|
||||
type: string;
|
||||
|
|
@ -141,6 +132,7 @@ export type EditorStyleProps = {
|
|||
evaluationSubstitutionType?: EvaluationSubstitutionType;
|
||||
popperPlacement?: Placement;
|
||||
popperZIndex?: Indices;
|
||||
blockCompletions?: FieldEntityInformation["blockCompletions"];
|
||||
};
|
||||
/**
|
||||
* line => Line to which the gutter is added
|
||||
|
|
@ -190,9 +182,7 @@ export type EditorProps = EditorStyleProps &
|
|||
customGutter?: CodeEditorGutter;
|
||||
};
|
||||
|
||||
type Props = ReduxStateProps &
|
||||
EditorProps &
|
||||
ReduxDispatchProps & { dispatch?: () => void };
|
||||
interface Props extends ReduxStateProps, EditorProps, ReduxDispatchProps {}
|
||||
|
||||
type State = {
|
||||
isFocused: boolean;
|
||||
|
|
@ -525,12 +515,16 @@ class CodeEditor extends Component<Props, State> {
|
|||
|
||||
handleEditorFocus = (cm: CodeMirror.Editor) => {
|
||||
this.setState({ isFocused: true });
|
||||
|
||||
if (!cm.state.completionActive) {
|
||||
const entityInformation: FieldEntityInformation = this.getEntityInformation();
|
||||
const entityInformation = this.getEntityInformation();
|
||||
const { blockCompletions } = this.props;
|
||||
this.hinters
|
||||
.filter((hinter) => hinter.fireOnFocus)
|
||||
.forEach(
|
||||
(hinter) => hinter.showHint && hinter.showHint(cm, entityInformation),
|
||||
(hinter) =>
|
||||
hinter.showHint &&
|
||||
hinter.showHint(cm, entityInformation, blockCompletions),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -656,10 +650,12 @@ class CodeEditor extends Component<Props, State> {
|
|||
|
||||
handleAutocompleteVisibility = (cm: CodeMirror.Editor) => {
|
||||
if (!this.state.isFocused) return;
|
||||
const entityInformation: FieldEntityInformation = this.getEntityInformation();
|
||||
const entityInformation = this.getEntityInformation();
|
||||
const { blockCompletions } = this.props;
|
||||
let hinterOpen = false;
|
||||
for (let i = 0; i < this.hinters.length; i++) {
|
||||
hinterOpen = this.hinters[i].showHint(cm, entityInformation, {
|
||||
blockCompletions,
|
||||
datasources: this.props.datasources.list,
|
||||
pluginIdToImageLocation: this.props.pluginIdToImageLocation,
|
||||
recentEntities: this.props.recentEntities,
|
||||
|
|
@ -957,14 +953,14 @@ class CodeEditor extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState): ReduxStateProps => ({
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
dynamicData: getDataTreeForAutocomplete(state),
|
||||
datasources: state.entities.datasources,
|
||||
pluginIdToImageLocation: getPluginIdToImageLocation(state),
|
||||
recentEntities: getRecentEntityIds(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
executeCommand: (payload: SlashCommandPayload) =>
|
||||
dispatch(executeCommandAction(payload)),
|
||||
startingEntityUpdation: () => dispatch(startingEntityUpdation()),
|
||||
|
|
|
|||
|
|
@ -1227,11 +1227,6 @@
|
|||
"!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/isFinite",
|
||||
"!doc": "Determines whether the passed value is a finite number."
|
||||
},
|
||||
"eval": {
|
||||
"!type": "fn(code: string) -> ?",
|
||||
"!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/eval",
|
||||
"!doc": "Evaluates JavaScript code represented as a string."
|
||||
},
|
||||
"encodeURI": {
|
||||
"!type": "fn(uri: string) -> string",
|
||||
"!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURI",
|
||||
|
|
|
|||
|
|
@ -181,6 +181,24 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) {
|
|||
}
|
||||
setSelectedJSActionOption(getJSActionOption(activeJSAction, jsActions));
|
||||
}, [parseErrors, jsActions, activeJSActionId]);
|
||||
|
||||
const blockCompletions = useMemo(() => {
|
||||
if (selectedJSActionOption.label) {
|
||||
const funcName = `${selectedJSActionOption.label}()`;
|
||||
return [
|
||||
{
|
||||
parentPath: "this",
|
||||
subPath: funcName,
|
||||
},
|
||||
{
|
||||
parentPath: currentJSCollection.name,
|
||||
subPath: funcName,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}, [selectedJSActionOption.label, currentJSCollection.name]);
|
||||
|
||||
return (
|
||||
<FormWrapper>
|
||||
<JSObjectHotKeys runActiveJSFunction={handleRunAction}>
|
||||
|
|
@ -224,6 +242,7 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) {
|
|||
title: "Code",
|
||||
panelComponent: (
|
||||
<CodeEditor
|
||||
blockCompletions={blockCompletions}
|
||||
className={"js-editor"}
|
||||
customGutter={JSGutters}
|
||||
dataTreePath={`${currentJSCollection.name}.body`}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,33 @@ export class AndRule implements AutocompleteRule {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set score to -Infinity for paths to be blocked from autocompletion
|
||||
* Max score - 0
|
||||
* Min score - -Infinity
|
||||
*/
|
||||
class BlockSuggestionsRule implements AutocompleteRule {
|
||||
static threshold = -Infinity;
|
||||
|
||||
computeScore(completion: Completion): number {
|
||||
let score = 0;
|
||||
const { currentFieldInfo } = AutocompleteSorter;
|
||||
const { blockCompletions } = currentFieldInfo;
|
||||
|
||||
if (blockCompletions) {
|
||||
for (let index = 0; index < blockCompletions.length; index++) {
|
||||
const { subPath } = blockCompletions[index];
|
||||
if (completion.text === subPath) {
|
||||
score = BlockSuggestionsRule.threshold;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set score to -Infinity for suggestions like Table1.selectedRow.address
|
||||
* Max score - 0
|
||||
|
|
@ -252,6 +279,7 @@ export class ScoredCompletion {
|
|||
new DataTreeFunctionRule(),
|
||||
new JSLibraryRule(),
|
||||
new GlobalJSRule(),
|
||||
new BlockSuggestionsRule(),
|
||||
];
|
||||
completion: Completion;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
import _ from "lodash";
|
||||
import { EVALUATION_PATH } from "utils/DynamicBindingUtils";
|
||||
import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer";
|
||||
import { ButtonGroupWidgetProps } from "widgets/ButtonGroupWidget/widget";
|
||||
|
||||
const isVisible = {
|
||||
"!type": "bool",
|
||||
|
|
@ -173,7 +174,7 @@ export const entityDefinitions = {
|
|||
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
|
||||
},
|
||||
isDisabled: "bool",
|
||||
options: "[dropdownOption]",
|
||||
options: "[$__dropdownOption__$]",
|
||||
},
|
||||
SELECT_WIDGET: {
|
||||
"!doc":
|
||||
|
|
@ -195,7 +196,7 @@ export const entityDefinitions = {
|
|||
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
|
||||
},
|
||||
isDisabled: "bool",
|
||||
options: "[dropdownOption]",
|
||||
options: "[$__dropdownOption__$]",
|
||||
},
|
||||
MULTI_SELECT_WIDGET: {
|
||||
"!doc":
|
||||
|
|
@ -217,7 +218,7 @@ export const entityDefinitions = {
|
|||
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
|
||||
},
|
||||
isDisabled: "bool",
|
||||
options: "[dropdownOption]",
|
||||
options: "[$__dropdownOption__$]",
|
||||
},
|
||||
MULTI_SELECT_WIDGET_V2: {
|
||||
"!doc":
|
||||
|
|
@ -239,7 +240,7 @@ export const entityDefinitions = {
|
|||
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
|
||||
},
|
||||
isDisabled: "bool",
|
||||
options: "[dropdownOption]",
|
||||
options: "[$__dropdownOption__$]",
|
||||
},
|
||||
IMAGE_WIDGET: {
|
||||
"!doc":
|
||||
|
|
@ -264,6 +265,14 @@ export const entityDefinitions = {
|
|||
isDisabled: "bool",
|
||||
recaptchaToken: "string",
|
||||
},
|
||||
BUTTON_GROUP_WIDGET: (widget: ButtonGroupWidgetProps) => {
|
||||
return {
|
||||
"!doc":
|
||||
"The Button group widget represents a set of buttons in a group. Group can have simple buttons or menu buttons with drop-down items.",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/button-group",
|
||||
groupButtons: generateTypeDef(widget.groupButtons),
|
||||
};
|
||||
},
|
||||
DATE_PICKER_WIDGET: {
|
||||
"!doc":
|
||||
"Datepicker is used to capture the date and time from a user. It can be used to filter data base on the input date range as well as to capture personal information such as date of birth",
|
||||
|
|
@ -302,7 +311,7 @@ export const entityDefinitions = {
|
|||
"Radio widget lets the user choose only one option from a predefined set of options. It is quite similar to a SingleSelect Dropdown in its functionality",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/radio",
|
||||
isVisible: isVisible,
|
||||
options: "[dropdownOption]",
|
||||
options: "[$__dropdownOption__$]",
|
||||
selectedOptionValue: "string",
|
||||
isRequired: "bool",
|
||||
},
|
||||
|
|
@ -324,10 +333,13 @@ export const entityDefinitions = {
|
|||
"Chart widget is used to view the graphical representation of your data. Chart is the go-to widget for your data visualisation needs.",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/chart",
|
||||
isVisible: isVisible,
|
||||
chartData: "chartData",
|
||||
chartData: {
|
||||
seriesName: "string",
|
||||
data: "[$__chartDataPoint__$]",
|
||||
},
|
||||
xAxisName: "string",
|
||||
yAxisName: "string",
|
||||
selectedDataPoint: "chartDataPoint",
|
||||
selectedDataPoint: "$__chartDataPoint__$",
|
||||
},
|
||||
FORM_WIDGET: (widget: any) => ({
|
||||
"!doc":
|
||||
|
|
@ -348,16 +360,25 @@ export const entityDefinitions = {
|
|||
},
|
||||
MAP_WIDGET: {
|
||||
isVisible: isVisible,
|
||||
center: "latLong",
|
||||
markers: "[mapMarker]",
|
||||
selectedMarker: "mapMarker",
|
||||
center: {
|
||||
lat: "number",
|
||||
long: "number",
|
||||
title: "string",
|
||||
},
|
||||
markers: "[$__mapMarker__$]",
|
||||
selectedMarker: {
|
||||
lat: "number",
|
||||
long: "number",
|
||||
title: "string",
|
||||
description: "string",
|
||||
},
|
||||
},
|
||||
FILE_PICKER_WIDGET: {
|
||||
"!doc":
|
||||
"Filepicker widget is used to allow users to upload files from their local machines to any cloud storage via API. Cloudinary and Amazon S3 have simple APIs for cloud storage uploads",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/filepicker",
|
||||
isVisible: isVisible,
|
||||
files: "[file]",
|
||||
files: "[$__file__$]",
|
||||
isDisabled: "bool",
|
||||
},
|
||||
FILE_PICKER_WIDGET_V2: {
|
||||
|
|
@ -365,7 +386,7 @@ export const entityDefinitions = {
|
|||
"Filepicker widget is used to allow users to upload files from their local machines to any cloud storage via API. Cloudinary and Amazon S3 have simple APIs for cloud storage uploads",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/filepicker",
|
||||
isVisible: isVisible,
|
||||
files: "[file]",
|
||||
files: "[$__file__$]",
|
||||
isDisabled: "bool",
|
||||
},
|
||||
LIST_WIDGET: (widget: any) => ({
|
||||
|
|
@ -436,7 +457,7 @@ export const entityDefinitions = {
|
|||
},
|
||||
isDisabled: "bool",
|
||||
isValid: "bool",
|
||||
options: "[dropdownOption]",
|
||||
options: "[$__dropdownOption__$]",
|
||||
},
|
||||
MULTI_SELECT_TREE_WIDGET: {
|
||||
"!doc":
|
||||
|
|
@ -455,7 +476,7 @@ export const entityDefinitions = {
|
|||
},
|
||||
isDisabled: "bool",
|
||||
isValid: "bool",
|
||||
options: "[dropdownOption]",
|
||||
options: "[$__dropdownOption__$]",
|
||||
},
|
||||
ICON_BUTTON_WIDGET: {
|
||||
"!doc":
|
||||
|
|
@ -470,7 +491,7 @@ export const entityDefinitions = {
|
|||
isVisible: isVisible,
|
||||
isDisabled: "bool",
|
||||
isValid: "bool",
|
||||
options: "[dropdownOption]",
|
||||
options: "[$__dropdownOption__$]",
|
||||
selectedValues: "[string]",
|
||||
},
|
||||
STATBOX_WIDGET: {
|
||||
|
|
@ -515,7 +536,13 @@ export const entityDefinitions = {
|
|||
"Map Chart widget shows the graphical representation of your data on the map.",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/map-chart",
|
||||
isVisible: isVisible,
|
||||
selectedDataPoint: "mapChartDataPoint",
|
||||
selectedDataPoint: {
|
||||
id: "string",
|
||||
label: "string",
|
||||
originalId: "string",
|
||||
shortLabel: "string",
|
||||
value: "number",
|
||||
},
|
||||
},
|
||||
INPUT_WIDGET_V2: {
|
||||
"!doc":
|
||||
|
|
@ -607,46 +634,31 @@ export const entityDefinitions = {
|
|||
},
|
||||
};
|
||||
|
||||
/*
|
||||
$__name__$ is just to reduce occurrences of global def showing up in auto completion for user as `$` is less commonly used as entityName/
|
||||
|
||||
GLOBAL_DEFS are maintained to support definition for array of objects which currently aren't supported by our generateTypeDef.
|
||||
*/
|
||||
export const GLOBAL_DEFS = {
|
||||
dropdownOption: {
|
||||
$__dropdownOption__$: {
|
||||
label: "string",
|
||||
value: "string",
|
||||
},
|
||||
tabs: {
|
||||
id: "string",
|
||||
label: "string",
|
||||
},
|
||||
chartDataPoint: {
|
||||
$__chartDataPoint__$: {
|
||||
x: "string",
|
||||
y: "string",
|
||||
},
|
||||
chartData: {
|
||||
seriesName: "string",
|
||||
data: "[chartDataPoint]",
|
||||
},
|
||||
latLong: {
|
||||
lat: "number",
|
||||
long: "number",
|
||||
title: "string",
|
||||
},
|
||||
mapMarker: {
|
||||
lat: "number",
|
||||
long: "number",
|
||||
title: "string",
|
||||
description: "string",
|
||||
},
|
||||
file: {
|
||||
$__file__$: {
|
||||
data: "string",
|
||||
dataFormat: "string",
|
||||
name: "text",
|
||||
type: "file",
|
||||
},
|
||||
mapChartDataPoint: {
|
||||
id: "string",
|
||||
label: "string",
|
||||
originalId: "string",
|
||||
shortLabel: "string",
|
||||
value: "number",
|
||||
$__mapMarker__$: {
|
||||
lat: "number",
|
||||
long: "number",
|
||||
title: "string",
|
||||
description: "string",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ describe("Tern server", () => {
|
|||
getCursor: () => ({ ch: 0, line: 0 }),
|
||||
getLine: () => "{{Api.}}",
|
||||
somethingSelected: () => false,
|
||||
getValue: () => "",
|
||||
} as unknown) as CodeMirror.Doc,
|
||||
changed: null,
|
||||
},
|
||||
|
|
@ -71,9 +72,10 @@ describe("Tern server", () => {
|
|||
input: {
|
||||
name: "test",
|
||||
doc: ({
|
||||
getCursor: () => ({ ch: 0, line: 1 }),
|
||||
getCursor: () => ({ ch: 0, line: 0 }),
|
||||
getLine: () => "{{Api.}}",
|
||||
somethingSelected: () => false,
|
||||
getValue: () => "",
|
||||
} as unknown) as CodeMirror.Doc,
|
||||
changed: null,
|
||||
},
|
||||
|
|
@ -83,13 +85,14 @@ describe("Tern server", () => {
|
|||
input: {
|
||||
name: "test",
|
||||
doc: ({
|
||||
getCursor: () => ({ ch: 3, line: 1 }),
|
||||
getCursor: () => ({ ch: 3, line: 0 }),
|
||||
getLine: () => "g {{Api.}}",
|
||||
somethingSelected: () => false,
|
||||
getValue: () => "",
|
||||
} as unknown) as CodeMirror.Doc,
|
||||
changed: null,
|
||||
},
|
||||
expectedOutput: { ch: 1, line: 0 },
|
||||
expectedOutput: { ch: 3, line: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -126,20 +129,20 @@ describe("Tern server", () => {
|
|||
input: {
|
||||
codeEditor: {
|
||||
value: "\n {{}}",
|
||||
cursor: { ch: 3, line: 1 },
|
||||
cursor: { ch: 3, line: 0 },
|
||||
doc: ({
|
||||
getCursor: () => ({ ch: 3, line: 1 }),
|
||||
getCursor: () => ({ ch: 3, line: 0 }),
|
||||
getLine: () => " {{}}",
|
||||
somethingSelected: () => false,
|
||||
} as unknown) as CodeMirror.Doc,
|
||||
},
|
||||
requestCallbackData: {
|
||||
completions: [{ name: "Api1" }],
|
||||
start: { ch: 2, line: 1 },
|
||||
end: { ch: 6, line: 1 },
|
||||
start: { ch: 2, line: 0 },
|
||||
end: { ch: 6, line: 0 },
|
||||
},
|
||||
},
|
||||
expectedOutput: { ch: 3, line: 1 },
|
||||
expectedOutput: { ch: 3, line: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,26 @@ type ArgHints = {
|
|||
doc: CodeMirror.Doc;
|
||||
};
|
||||
|
||||
type RequestQuery = {
|
||||
type: string;
|
||||
types?: boolean;
|
||||
docs?: boolean;
|
||||
urls?: boolean;
|
||||
origins?: boolean;
|
||||
caseInsensitive?: boolean;
|
||||
preferFunction?: boolean;
|
||||
end?: CodeMirror.Position;
|
||||
guess?: boolean;
|
||||
inLiteral?: boolean;
|
||||
fullDocs?: any;
|
||||
lineCharPositions?: any;
|
||||
start?: any;
|
||||
file?: any;
|
||||
includeKeywords?: boolean;
|
||||
depth?: number;
|
||||
sort?: boolean;
|
||||
};
|
||||
|
||||
export type DataTreeDefEntityInformation = {
|
||||
type: ENTITY_TYPE;
|
||||
subType: string;
|
||||
|
|
@ -341,23 +361,13 @@ class TernServer {
|
|||
|
||||
request(
|
||||
cm: CodeMirror.Editor,
|
||||
query: {
|
||||
type: string;
|
||||
types?: boolean;
|
||||
docs?: boolean;
|
||||
urls?: boolean;
|
||||
origins?: boolean;
|
||||
caseInsensitive?: boolean;
|
||||
preferFunction?: boolean;
|
||||
end?: CodeMirror.Position;
|
||||
guess?: boolean;
|
||||
inLiteral?: boolean;
|
||||
},
|
||||
query: RequestQuery | string,
|
||||
callbackFn: (error: any, data: any) => void,
|
||||
pos?: CodeMirror.Position,
|
||||
) {
|
||||
const doc = this.findDoc(cm.getDoc());
|
||||
const request = this.buildRequest(doc, query, pos);
|
||||
|
||||
// @ts-expect-error: Types are not available
|
||||
this.server.request(request, callbackFn);
|
||||
}
|
||||
|
|
@ -389,55 +399,28 @@ class TernServer {
|
|||
|
||||
buildRequest(
|
||||
doc: TernDoc,
|
||||
query: {
|
||||
type?: string;
|
||||
types?: boolean;
|
||||
docs?: boolean;
|
||||
urls?: boolean;
|
||||
origins?: boolean;
|
||||
fullDocs?: any;
|
||||
lineCharPositions?: any;
|
||||
end?: any;
|
||||
start?: any;
|
||||
file?: any;
|
||||
includeKeywords?: boolean;
|
||||
inLiteral?: boolean;
|
||||
depth?: number;
|
||||
sort?: boolean;
|
||||
},
|
||||
query: Partial<RequestQuery> | string,
|
||||
pos?: CodeMirror.Position,
|
||||
) {
|
||||
const files = [];
|
||||
let offsetLines = 0;
|
||||
if (typeof query == "string") query = { type: query };
|
||||
const allowFragments = !query.fullDocs;
|
||||
if (!allowFragments) delete query.fullDocs;
|
||||
query.lineCharPositions = true;
|
||||
query.includeKeywords = true;
|
||||
query.depth = 0;
|
||||
query.sort = true;
|
||||
if (!query.end) {
|
||||
const lineValue = this.lineValue(doc);
|
||||
const focusedValue = this.getFocusedDynamicValue(doc);
|
||||
const index = lineValue.indexOf(focusedValue);
|
||||
|
||||
const positions = pos || doc.doc.getCursor("end");
|
||||
const queryChPosition = positions.ch - index;
|
||||
|
||||
query.end = {
|
||||
...positions,
|
||||
line: 0,
|
||||
ch: queryChPosition,
|
||||
};
|
||||
|
||||
if (doc.doc.somethingSelected()) {
|
||||
query.start = doc.doc.getCursor("start");
|
||||
}
|
||||
if (query.end == null) {
|
||||
query.end = pos || doc.doc.getCursor("end");
|
||||
if (doc.doc.somethingSelected()) query.start = doc.doc.getCursor("start");
|
||||
}
|
||||
const startPos = query.start || query.end;
|
||||
|
||||
if (doc.changed) {
|
||||
if (
|
||||
doc.doc.lineCount() > bigDoc &&
|
||||
allowFragments &&
|
||||
allowFragments !== false &&
|
||||
doc.changed.to - doc.changed.from < 100 &&
|
||||
doc.changed.from <= startPos.line &&
|
||||
doc.changed.to > query.end.line
|
||||
|
|
@ -445,29 +428,36 @@ class TernServer {
|
|||
files.push(this.getFragmentAround(doc, startPos, query.end));
|
||||
query.file = "#0";
|
||||
offsetLines = files[0].offsetLines;
|
||||
if (query.start) {
|
||||
if (query.start != null)
|
||||
query.start = Pos(query.start.line - -offsetLines, query.start.ch);
|
||||
}
|
||||
query.end = Pos(query.end.line - offsetLines, query.end.ch);
|
||||
} else {
|
||||
files.push({
|
||||
type: "full",
|
||||
name: doc.name,
|
||||
text: this.getFocusedDynamicValue(doc),
|
||||
text: this.docValue(doc),
|
||||
});
|
||||
query.file = doc.name;
|
||||
doc.changed = null;
|
||||
}
|
||||
} else {
|
||||
query.file = doc.name;
|
||||
// this code is different from tern.js code
|
||||
// we noticed error `TernError: file doesn't contain line x`
|
||||
// which was due to file not being present for the case when a codeEditor is opened and 1st character is typed
|
||||
files.push({
|
||||
type: "full",
|
||||
name: doc.name,
|
||||
text: this.docValue(doc),
|
||||
});
|
||||
}
|
||||
for (const name in this.docs) {
|
||||
const cur = this.docs[name];
|
||||
if (cur.changed && cur !== doc) {
|
||||
if (cur.changed && (cur != doc || cur.name != doc.name)) {
|
||||
files.push({
|
||||
type: "full",
|
||||
name: cur.name,
|
||||
text: this.getFocusedDynamicValue(cur),
|
||||
text: this.docValue(cur),
|
||||
});
|
||||
cur.changed = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Def } from "tern";
|
||||
import { TruthyPrimitiveTypes } from "utils/TypeHelpers";
|
||||
import { generateTypeDef } from "./dataTreeTypeDefCreator";
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ export type AdditionalDynamicDataTree = Record<
|
|||
export const customTreeTypeDefCreator = (
|
||||
dataTree: AdditionalDynamicDataTree,
|
||||
) => {
|
||||
const def: any = {
|
||||
const def: Def = {
|
||||
"!name": "customDataTree",
|
||||
};
|
||||
Object.keys(dataTree).forEach((entityName) => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
generateTypeDef,
|
||||
dataTreeTypeDefCreator,
|
||||
flattenDef,
|
||||
getFunctionsArgsType,
|
||||
} from "utils/autocomplete/dataTreeTypeDefCreator";
|
||||
import {
|
||||
DataTreeWidget,
|
||||
|
|
@ -121,3 +122,49 @@ describe("dataTreeTypeDefCreator", () => {
|
|||
expect(value).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFunctionsArgsType", () => {
|
||||
const testCases = {
|
||||
testCase1: {
|
||||
arguments: [
|
||||
{ name: "a", value: undefined },
|
||||
{ name: "b", value: undefined },
|
||||
{ name: "c", value: undefined },
|
||||
{ name: "d", value: undefined },
|
||||
{ name: "", value: undefined },
|
||||
],
|
||||
expectedOutput: "fn(a: ?, b: ?, c: ?, d: ?)",
|
||||
},
|
||||
testCase2: {
|
||||
arguments: [],
|
||||
expectedOutput: "fn()",
|
||||
},
|
||||
testCase3: {
|
||||
arguments: [
|
||||
{ name: "a", value: undefined },
|
||||
{ name: "b", value: undefined },
|
||||
{ name: "", value: undefined },
|
||||
{ name: "", value: undefined },
|
||||
],
|
||||
expectedOutput: "fn(a: ?, b: ?)",
|
||||
},
|
||||
};
|
||||
|
||||
it("function with 4 args", () => {
|
||||
expect(getFunctionsArgsType(testCases.testCase1.arguments)).toEqual(
|
||||
testCases.testCase1.expectedOutput,
|
||||
);
|
||||
});
|
||||
|
||||
it("function with no args", () => {
|
||||
expect(getFunctionsArgsType(testCases.testCase2.arguments)).toEqual(
|
||||
testCases.testCase2.expectedOutput,
|
||||
);
|
||||
});
|
||||
|
||||
it("function with 2 args", () => {
|
||||
expect(getFunctionsArgsType(testCases.testCase3.arguments)).toEqual(
|
||||
testCases.testCase3.expectedOutput,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import {
|
||||
DataTree,
|
||||
ENTITY_TYPE,
|
||||
MetaArgs,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import _ from "lodash";
|
||||
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
|
||||
import { get, isFunction } from "lodash";
|
||||
import { entityDefinitions } from "utils/autocomplete/EntityDefinitions";
|
||||
import { getType, Types } from "utils/TypeHelpers";
|
||||
import { Def } from "tern";
|
||||
|
|
@ -15,9 +11,11 @@ import {
|
|||
isWidget,
|
||||
} from "workers/evaluationUtils";
|
||||
import { DataTreeDefEntityInformation } from "utils/autocomplete/TernServer";
|
||||
import { Variable } from "entities/JSCollection";
|
||||
|
||||
// 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: Def = {};
|
||||
// Def names are encoded with information about the entity
|
||||
// This so that we have more info about them
|
||||
// when sorting results in autocomplete
|
||||
|
|
@ -28,16 +26,17 @@ export const dataTreeTypeDefCreator = (
|
|||
dataTree: DataTree,
|
||||
isJSEditorEnabled: boolean,
|
||||
): { def: Def; entityInfo: Map<string, DataTreeDefEntityInformation> } => {
|
||||
const def: any = {
|
||||
const def: Def = {
|
||||
"!name": "DATA_TREE",
|
||||
};
|
||||
const entityMap: Map<string, DataTreeDefEntityInformation> = new Map();
|
||||
|
||||
Object.entries(dataTree).forEach(([entityName, entity]) => {
|
||||
if (isWidget(entity)) {
|
||||
const widgetType = entity.type;
|
||||
if (widgetType in entityDefinitions) {
|
||||
const definition = _.get(entityDefinitions, widgetType);
|
||||
if (_.isFunction(definition)) {
|
||||
const definition = get(entityDefinitions, widgetType);
|
||||
if (isFunction(definition)) {
|
||||
def[entityName] = definition(entity);
|
||||
} else {
|
||||
def[entityName] = definition;
|
||||
|
|
@ -49,7 +48,7 @@ export const dataTreeTypeDefCreator = (
|
|||
});
|
||||
}
|
||||
} else if (isAction(entity)) {
|
||||
def[entityName] = (entityDefinitions.ACTION as any)(entity);
|
||||
def[entityName] = entityDefinitions.ACTION(entity);
|
||||
flattenDef(def, entityName);
|
||||
entityMap.set(entityName, {
|
||||
type: ENTITY_TYPE.ACTION,
|
||||
|
|
@ -62,20 +61,27 @@ export const dataTreeTypeDefCreator = (
|
|||
subType: ENTITY_TYPE.APPSMITH,
|
||||
});
|
||||
} else if (isJSAction(entity) && isJSEditorEnabled) {
|
||||
const metaObj: Record<string, MetaArgs> = entity.meta;
|
||||
const jsOptions: Record<string, unknown> = {};
|
||||
const metaObj = entity.meta;
|
||||
const jsProperty: Def = {};
|
||||
|
||||
for (const key in metaObj) {
|
||||
jsOptions[key] =
|
||||
"fn(onSuccess: fn() -> void, onError: fn() -> void) -> void";
|
||||
// const jsFunctionObj = metaObj[key];
|
||||
// const { arguments: args } = jsFunctionObj;
|
||||
// const argsTypeString = getFunctionsArgsType(args);
|
||||
// As we don't show args we avoid to get args def of function
|
||||
// we will also need to check performance implications here
|
||||
|
||||
const argsTypeString = getFunctionsArgsType([]);
|
||||
jsProperty[key] = argsTypeString;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entity.variables.length; i++) {
|
||||
const varKey = entity.variables[i];
|
||||
const varValue = entity[varKey];
|
||||
jsOptions[varKey] = generateTypeDef(varValue);
|
||||
jsProperty[varKey] = generateTypeDef(varValue);
|
||||
}
|
||||
|
||||
def[entityName] = jsOptions;
|
||||
def[entityName] = jsProperty;
|
||||
flattenDef(def, entityName);
|
||||
entityMap.set(entityName, {
|
||||
type: ENTITY_TYPE.JSACTION,
|
||||
|
|
@ -87,12 +93,11 @@ export const dataTreeTypeDefCreator = (
|
|||
extraDefs = {};
|
||||
}
|
||||
});
|
||||
|
||||
return { def, entityInfo: entityMap };
|
||||
};
|
||||
|
||||
export function generateTypeDef(
|
||||
obj: any,
|
||||
): string | Record<string, string | Record<string, unknown>> {
|
||||
export function generateTypeDef(obj: any): string | Def {
|
||||
const type = getType(obj);
|
||||
switch (type) {
|
||||
case Types.ARRAY: {
|
||||
|
|
@ -100,7 +105,7 @@ export function generateTypeDef(
|
|||
return `[${arrayType}]`;
|
||||
}
|
||||
case Types.OBJECT: {
|
||||
const objType: Record<string, string | Record<string, unknown>> = {};
|
||||
const objType: Def = {};
|
||||
Object.keys(obj).forEach((k) => {
|
||||
objType[k] = generateTypeDef(obj[k]);
|
||||
});
|
||||
|
|
@ -138,3 +143,32 @@ export const flattenDef = (def: Def, entityName: string): Def => {
|
|||
}
|
||||
return flattenedDef;
|
||||
};
|
||||
|
||||
const VALID_VARIABLE_NAME_REGEX = /^([a-zA-Z_$][a-zA-Z\d_$]*)$/;
|
||||
|
||||
const isValidVariableName = (variableName: string) =>
|
||||
VALID_VARIABLE_NAME_REGEX.test(variableName);
|
||||
|
||||
export const getFunctionsArgsType = (args: Variable[]): string => {
|
||||
// skip same name args to avoiding creating invalid type
|
||||
const argNames = new Set<string>();
|
||||
// skip invalid args name
|
||||
args.forEach((arg) => {
|
||||
if (arg.name && isValidVariableName(arg.name)) argNames.add(arg.name);
|
||||
});
|
||||
const argNamesArray = [...argNames];
|
||||
const argsTypeString = argNamesArray.reduce(
|
||||
(accumulatedArgType, argName, currentIndex) => {
|
||||
switch (currentIndex) {
|
||||
case 0:
|
||||
return `${argName}: ?`;
|
||||
case 1:
|
||||
return `${accumulatedArgType}, ${argName}: ?`;
|
||||
default:
|
||||
return `${accumulatedArgType}, ${argName}: ?`;
|
||||
}
|
||||
},
|
||||
argNamesArray[0],
|
||||
);
|
||||
return argsTypeString ? `fn(${argsTypeString})` : `fn()`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ import {
|
|||
getUpdatedLocalUnEvalTreeAfterJSUpdates,
|
||||
parseJSActions,
|
||||
} from "workers/JSObject";
|
||||
import { lintTree } from "workers/Lint";
|
||||
import { lintTree } from "workers/Lint/index";
|
||||
|
||||
export default class DataTreeEvaluator {
|
||||
dependencyMap: DependencyMap = {};
|
||||
|
|
@ -808,6 +808,8 @@ export default class DataTreeEvaluator {
|
|||
entityType = entity.type;
|
||||
} else if (entity && isAction(entity)) {
|
||||
entityType = entity.pluginType;
|
||||
} else if (entity && isJSAction(entity)) {
|
||||
entityType = entity.ENTITY_TYPE;
|
||||
}
|
||||
this.errors.push({
|
||||
type: EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user