fix: autocomplete fixes & enhancement (#15857)

This commit is contained in:
Rishabh Rathod 2022-08-22 11:17:24 +05:30 committed by Rishabh-Rathod
parent 1660092c79
commit fd9719aa4e
16 changed files with 789 additions and 112 deletions

View File

@ -2,7 +2,13 @@ import { WIDGET } from "../../../../locators/WidgetLocators";
import { ObjectsRegistry } from "../../../../support/Objects/Registry"; import { ObjectsRegistry } from "../../../../support/Objects/Registry";
const explorer = require("../../../../locators/explorerlocators.json"); const explorer = require("../../../../locators/explorerlocators.json");
const { CommonLocators, EntityExplorer, JSEditor: jsEditor } = ObjectsRegistry; const {
AggregateHelper: agHelper,
ApiPage,
CommonLocators,
EntityExplorer,
JSEditor: jsEditor,
} = ObjectsRegistry;
const jsObjectBody = `export default { const jsObjectBody = `export default {
myVar1: [], myVar1: [],
@ -16,12 +22,11 @@ const jsObjectBody = `export default {
}`; }`;
describe("Autocomplete tests", () => { describe("Autocomplete tests", () => {
before(() => { it("1. Verify widgets autocomplete: ButtonGroup & Document viewer widget", () => {
cy.get(explorer.addWidget).click(); cy.get(explorer.addWidget).click();
EntityExplorer.DragDropWidgetNVerify(WIDGET.BUTTON_GROUP_WIDGET, 300, 500); EntityExplorer.DragDropWidgetNVerify(WIDGET.BUTTON_GROUP, 200, 200);
}); EntityExplorer.DragDropWidgetNVerify(WIDGET.DOCUMENT_VIEWER, 200, 500);
it("1. ButtonGroup autocomplete & Eval shouldn't show up", () => {
// create js object // create js object
jsEditor.CreateJSObject(jsObjectBody, { jsEditor.CreateJSObject(jsObjectBody, {
paste: true, paste: true,
@ -30,31 +35,120 @@ describe("Autocomplete tests", () => {
shouldCreateNewJSObj: true, shouldCreateNewJSObj: true,
}); });
const lineNumber = 5; // focus on 5th line
cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click(); cy.get(`:nth-child(5) > .CodeMirror-line`).click();
// 1. Button group widget autocomplete verification
cy.get(CommonLocators._codeMirrorTextArea) cy.get(CommonLocators._codeMirrorTextArea)
.focus() .focus()
.type(`ButtonGroup1.`); .type(`ButtonGroup1.`);
cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("groupButtons"); agHelper.AssertElementText(CommonLocators._hints, "groupButtons");
cy.get(CommonLocators._codeMirrorTextArea) cy.get(CommonLocators._codeMirrorTextArea)
.focus() .focus()
.type(`groupButtons.`); .type(`groupButtons.`);
cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("groupButton1"); agHelper.AssertElementText(CommonLocators._hints, "groupButton1");
cy.get(CommonLocators._codeMirrorTextArea).focus().type(` // 2. Document view widget autocomplete verification
eval`); cy.get(CommonLocators._codeMirrorTextArea)
.focus()
.type("{backspace}".repeat("ButtonGroup1.groupButtons.".length)) // remove "ButtonGroup1.groupButtons."
.wait(20)
.type(`DocumentViewer1.`);
cy.get(`.CodeMirror-hints > :nth-child(1)`).should( agHelper.AssertElementText(CommonLocators._hints, "docUrl");
"not.have.value",
"eval()",
);
}); });
it("2. Local variables autocompletion support", () => { it("2. Verify browser JavaScript APIs in autocomplete ", () => {
// create js object
jsEditor.CreateJSObject(jsObjectBody, {
paste: true,
completeReplace: true,
toRun: false,
shouldCreateNewJSObj: true,
prettify: false,
});
// focus on 5th line
cy.get(`:nth-child(5) > .CodeMirror-line`).click();
const JSAPIsToTest = [
// console API verification
{
type: "console",
expected: "console",
shouldBePresent: true,
},
// crypto API verification
{
type: "crypto",
expected: "crypto",
shouldBePresent: true,
},
// eval function verification
{
type: "eval",
expected: "eval()",
shouldBePresent: false,
},
{
type: "Blob",
expected: "Blob()",
shouldBePresent: true,
},
{
type: "FormData",
expected: "FormData()",
shouldBePresent: true,
},
{
type: "FileReader",
expected: "FileReader()",
shouldBePresent: true,
},
];
JSAPIsToTest.forEach((test, index) => {
const deleteCharCount = (JSAPIsToTest[index - 1]?.type || " ").length;
cy.get(CommonLocators._codeMirrorTextArea)
.focus()
// remove previously typed code
.type(deleteCharCount ? "{backspace}".repeat(deleteCharCount) : " ")
.wait(20)
.type(test.type);
cy.get(CommonLocators._hints)
.eq(0)
.should(
test.shouldBePresent ? "have.text" : "not.have.text",
test.expected,
);
});
});
it("3. JSObject this. autocomplete", () => {
// create js object
jsEditor.CreateJSObject(jsObjectBody, {
paste: true,
completeReplace: true,
toRun: false,
shouldCreateNewJSObj: true,
});
// focus on 5th line
cy.get(`:nth-child(5) > .CodeMirror-line`).click();
cy.get(CommonLocators._codeMirrorTextArea)
.focus()
.type("this.");
["myFun2()", "myVar1", "myVar2"].forEach((element, index) => {
cy.get(`.CodeMirror-hints > :nth-child(${index + 1})`).contains(element);
});
});
it("4. Local variables & complex data autocompletion test", () => {
// create js object // create js object
jsEditor.CreateJSObject(jsObjectBody, { jsEditor.CreateJSObject(jsObjectBody, {
paste: true, paste: true,
@ -65,15 +159,16 @@ describe("Autocomplete tests", () => {
const lineNumber = 5; const lineNumber = 5;
const array = [ const users = [
{ label: "a", value: "b" }, { label: "a", value: "b" },
{ label: "a", value: "b" }, { label: "a", value: "b" },
]; ];
const codeToType = ` const codeToType = `
const arr = ${JSON.stringify(array)}; const users = ${JSON.stringify(users)};
const data = { userCollection: [{ users }, { users }] };
arr.map(callBack) users.map(callBack)
`; `;
// component re-render cause DOM element of cy.get to lost // component re-render cause DOM element of cy.get to lost
@ -86,16 +181,36 @@ describe("Autocomplete tests", () => {
.focus() .focus()
.type(`${codeToType}`, { parseSpecialCharSequences: false }) .type(`${codeToType}`, { parseSpecialCharSequences: false })
.type(`{upArrow}{upArrow}`) .type(`{upArrow}{upArrow}`)
.type(`const callBack = (item) => item.l`); .type(`const callBack = (user) => user.l`);
cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("label"); agHelper.AssertElementText(CommonLocators._hints, "label");
cy.get(CommonLocators._codeMirrorTextArea) cy.get(CommonLocators._codeMirrorTextArea)
.focus() .focus()
.type(`label`); .type(`abel;`);
cy.get(CommonLocators._codeMirrorTextArea)
.focus()
.type(`data.`);
agHelper.AssertElementText(CommonLocators._hints, "userCollection");
cy.get(CommonLocators._codeMirrorTextArea)
.focus()
.type(`userCollection[0].`);
agHelper.AssertElementText(CommonLocators._hints, "users");
cy.get(CommonLocators._codeMirrorTextArea)
.focus()
.type(`users[0].`);
agHelper.AssertElementText(CommonLocators._hints, "label");
}); });
it("3. JSObject this. autocomplete", () => { it("5. Api data with array of object autocompletion test", () => {
ApiPage.CreateAndFillApi("https://mock-api.appsmith.com/users");
ApiPage.RunAPI();
// create js object // create js object
jsEditor.CreateJSObject(jsObjectBody, { jsEditor.CreateJSObject(jsObjectBody, {
paste: true, paste: true,
@ -104,18 +219,18 @@ describe("Autocomplete tests", () => {
shouldCreateNewJSObj: true, shouldCreateNewJSObj: true,
}); });
const lineNumber = 5; cy.get(`:nth-child(${5}) > .CodeMirror-line`).click();
const codeToType = "this.";
cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click();
cy.get(CommonLocators._codeMirrorTextArea) cy.get(CommonLocators._codeMirrorTextArea)
.focus() .focus()
.type(`${codeToType}`); .type("Api1.data.u");
["myFun2()", "myVar1", "myVar2"].forEach((element, index) => { agHelper.AssertElementText(CommonLocators._hints, "users");
cy.get(`.CodeMirror-hints > :nth-child(${index + 1})`).contains(element);
}); cy.get(CommonLocators._codeMirrorTextArea)
.focus()
.type("sers[0].e");
agHelper.AssertElementText(CommonLocators._hints, "email");
}); });
}); });

View File

@ -11,7 +11,7 @@ import {
} from "../../../../locators/WidgetLocators"; } from "../../../../locators/WidgetLocators";
const widgetsToTest = { const widgetsToTest = {
[WIDGET.MULTISELECT_WIDGET]: { [WIDGET.MULTISELECT]: {
testCases: [ testCases: [
{ {
input: input:
@ -39,7 +39,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig]) => {
}); });
it("2. Bind Button on click and Text widget content", function() { it("2. Bind Button on click and Text widget content", function() {
cy.openPropertyPane(WIDGET.BUTTON_WIDGET); cy.openPropertyPane(WIDGET.BUTTON);
cy.get(PROPERTY_SELECTOR.onClick) cy.get(PROPERTY_SELECTOR.onClick)
.find(".t--js-toggle") .find(".t--js-toggle")
.click(); .click();
@ -60,7 +60,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig]) => {
cy.wrap(item).should("contain.text", "BLUE"); cy.wrap(item).should("contain.text", "BLUE");
}); });
const inputs = testConfig.testCases; const inputs = testConfig.testCases;
cy.get(getWidgetSelector(WIDGET.BUTTON_WIDGET)) cy.get(getWidgetSelector(WIDGET.BUTTON))
.scrollIntoView() .scrollIntoView()
.click({ force: true }); .click({ force: true });
cy.wait("@updateLayout"); cy.wait("@updateLayout");

View File

@ -9,7 +9,7 @@ import {
} from "../../../../locators/WidgetLocators"; } from "../../../../locators/WidgetLocators";
const widgetsToTest = { const widgetsToTest = {
[WIDGET.INPUT_WIDGET_V2]: { [WIDGET.INPUT_V2]: {
testCases: [ testCases: [
{ input: "test", expected: "test", clearBeforeType: true }, { input: "test", expected: "test", clearBeforeType: true },
{ input: "12", expected: "test12", clearBeforeType: false }, { input: "12", expected: "test12", clearBeforeType: false },
@ -29,7 +29,7 @@ const widgetsToTest = {
widgetName: "Input widget", widgetName: "Input widget",
widgetPrefixName: "Input", widgetPrefixName: "Input",
}, },
[WIDGET.PHONE_INPUT_WIDGET]: { [WIDGET.PHONE_INPUT]: {
testCases: [ testCases: [
{ {
input: "9999999999", input: "9999999999",
@ -50,7 +50,7 @@ const widgetsToTest = {
widgetName: "Phone Input widget", widgetName: "Phone Input widget",
widgetPrefixName: "PhoneInput", widgetPrefixName: "PhoneInput",
}, },
[WIDGET.CURRENCY_INPUT_WIDGET]: { [WIDGET.CURRENCY_INPUT]: {
testCases: [ testCases: [
{ input: "1233", expected: "1,233", clearBeforeType: true }, { input: "1233", expected: "1,233", clearBeforeType: true },
{ {
@ -102,14 +102,14 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => {
cy.dragAndDropToCanvas(widgetSelector, { x: 300, y: 200 }); cy.dragAndDropToCanvas(widgetSelector, { x: 300, y: 200 });
cy.get(getWidgetSelector(widgetSelector)).should("exist"); cy.get(getWidgetSelector(widgetSelector)).should("exist");
cy.dragAndDropToCanvas(WIDGET.BUTTON_WIDGET, { x: 300, y: 400 }); cy.dragAndDropToCanvas(WIDGET.BUTTON, { x: 300, y: 400 });
cy.dragAndDropToCanvas(WIDGET.TEXT, { x: 300, y: 600 }); cy.dragAndDropToCanvas(WIDGET.TEXT, { x: 300, y: 600 });
}); });
it("2. StoreValue should have complete input value", () => { it("2. StoreValue should have complete input value", () => {
// if default input widget type is changed from text to any other type then uncomment below code. // if default input widget type is changed from text to any other type then uncomment below code.
// if (widgetSelector === WIDGET.INPUT_WIDGET_V2) { // if (widgetSelector === WIDGET.INPUT_V2) {
// cy.openPropertyPane(widgetSelector); // cy.openPropertyPane(widgetSelector);
// cy.selectDropdownValue(".t--property-control-datatype", "Text"); // cy.selectDropdownValue(".t--property-control-datatype", "Text");
// cy.get(".t--property-control-required label") // cy.get(".t--property-control-required label")
@ -119,7 +119,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => {
// } // }
// Set onClick action, storing value // Set onClick action, storing value
cy.openPropertyPane(WIDGET.BUTTON_WIDGET); cy.openPropertyPane(WIDGET.BUTTON);
cy.get(PROPERTY_SELECTOR.onClick) cy.get(PROPERTY_SELECTOR.onClick)
.find(".t--js-toggle") .find(".t--js-toggle")
.click(); .click();
@ -147,7 +147,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => {
cy.get(getWidgetInputSelector(widgetSelector)).type(`${input}`); cy.get(getWidgetInputSelector(widgetSelector)).type(`${input}`);
} }
cy.get(getWidgetSelector(WIDGET.BUTTON_WIDGET)).click(); cy.get(getWidgetSelector(WIDGET.BUTTON)).click();
// Assert if the Text widget contains the whole value, test // Assert if the Text widget contains the whole value, test
cy.get(getWidgetSelector(WIDGET.TEXT)).should("have.text", expected); cy.get(getWidgetSelector(WIDGET.TEXT)).should("have.text", expected);
@ -156,7 +156,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => {
it("3. Api params getting correct input values", () => { it("3. Api params getting correct input values", () => {
// Set onClick action, storing value // Set onClick action, storing value
cy.openPropertyPane(WIDGET.BUTTON_WIDGET); cy.openPropertyPane(WIDGET.BUTTON);
// cy.get(PROPERTY_SELECTOR.onClick) // cy.get(PROPERTY_SELECTOR.onClick)
// .find(".t--js-toggle") // .find(".t--js-toggle")
// .click(); // .click();
@ -177,7 +177,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => {
cy.get(getWidgetInputSelector(widgetSelector)).type(`${input}`); cy.get(getWidgetInputSelector(widgetSelector)).type(`${input}`);
} }
cy.get(getWidgetSelector(WIDGET.BUTTON_WIDGET)).click(); cy.get(getWidgetSelector(WIDGET.BUTTON)).click();
// Assert if the Api request contains the expected value // Assert if the Api request contains the expected value
@ -190,7 +190,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => {
}); });
it("4. Delete all the widgets on canvas", () => { it("4. Delete all the widgets on canvas", () => {
cy.get(getWidgetSelector(WIDGET.BUTTON_WIDGET)).click(); cy.get(getWidgetSelector(WIDGET.BUTTON)).click();
cy.get("body").type(`{del}`, { force: true }); cy.get("body").type(`{del}`, { force: true });
cy.get(getWidgetSelector(WIDGET.TEXT)).click(); cy.get(getWidgetSelector(WIDGET.TEXT)).click();

View File

@ -13,7 +13,7 @@ import {
} from "../../../../locators/WidgetLocators"; } from "../../../../locators/WidgetLocators";
const widgetsToTest = { const widgetsToTest = {
[WIDGET.MULTISELECT_WIDGET]: { [WIDGET.MULTISELECT]: {
widgetName: "MultiSelect", widgetName: "MultiSelect",
widgetPrefixName: "MultiSelect1", widgetPrefixName: "MultiSelect1",
textBindingValue: "{{MultiSelect1.selectedOptionValues}}", textBindingValue: "{{MultiSelect1.selectedOptionValues}}",
@ -61,7 +61,7 @@ const widgetsToTest = {
selectAndReset(); selectAndReset();
}, },
}, },
[WIDGET.CURRENCY_INPUT_WIDGET]: { [WIDGET.CURRENCY_INPUT]: {
widgetName: "CurrencyInput", widgetName: "CurrencyInput",
widgetPrefixName: "CurrencyInput1", widgetPrefixName: "CurrencyInput1",
textBindingValue: testdata.currencyBindingValue, textBindingValue: testdata.currencyBindingValue,
@ -435,7 +435,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig]) => {
it("2. Bind Button on click and Text widget content", () => { it("2. Bind Button on click and Text widget content", () => {
// Set onClick assertWidgetReset, storing value // Set onClick assertWidgetReset, storing value
cy.openPropertyPane(WIDGET.BUTTON_WIDGET); cy.openPropertyPane(WIDGET.BUTTON);
cy.get(PROPERTY_SELECTOR.onClick) cy.get(PROPERTY_SELECTOR.onClick)
.find(".t--js-toggle") .find(".t--js-toggle")

View File

@ -1,12 +1,12 @@
export const WIDGET = { export const WIDGET = {
INPUT_WIDGET_V2: "inputwidgetv2", INPUT_V2: "inputwidgetv2",
TEXT: "textwidget", TEXT: "textwidget",
PHONE_INPUT_WIDGET: "phoneinputwidget", PHONE_INPUT: "phoneinputwidget",
CURRENCY_INPUT_WIDGET: "currencyinputwidget", CURRENCY_INPUT: "currencyinputwidget",
BUTTON_WIDGET: "buttonwidget", BUTTON: "buttonwidget",
MULTISELECT_WIDGET: "multiselectwidgetv2", MULTISELECT: "multiselectwidgetv2",
BUTTON_GROUP_WIDGET: "buttongroupwidget", BUTTON_GROUP: "buttongroupwidget",
TREESELECT_WIDGET: "singleselecttreewidget", TREESELECT: "singleselecttreewidget",
TAB: "tabswidget", TAB: "tabswidget",
TABLE: "tablewidgetv2", TABLE: "tablewidgetv2",
SWITCHGROUP: "switchgroupwidget", SWITCHGROUP: "switchgroupwidget",
@ -23,6 +23,7 @@ export const WIDGET = {
PHONEINPUT: "phoneinputwidget", PHONEINPUT: "phoneinputwidget",
CAMERA: "camerawidget", CAMERA: "camerawidget",
FILEPICKER: "filepickerwidgetv2", FILEPICKER: "filepickerwidgetv2",
DOCUMENT_VIEWER: "documentviewerwidget",
} as const; } as const;
// property pane element selector are maintained here // property pane element selector are maintained here

View File

@ -110,7 +110,7 @@ export class AggregateHelper {
}); });
} }
public AssertElementText(selector: string, text: string, index: number = 0) { public AssertElementText(selector: string, text: string, index = 0) {
const locator = selector.startsWith("//") const locator = selector.startsWith("//")
? cy.xpath(selector) ? cy.xpath(selector)
: cy.get(selector); : cy.get(selector);

View File

@ -42,7 +42,7 @@ export class ApiPage {
_saveAsDS = ".t--store-as-datasource"; _saveAsDS = ".t--store-as-datasource";
CreateApi( CreateApi(
apiName: string = "", apiName = "",
apiVerb: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET", apiVerb: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET",
) { ) {
cy.get(this.locator._createNew).click({ force: true }); cy.get(this.locator._createNew).click({ force: true });
@ -67,11 +67,11 @@ export class ApiPage {
CreateAndFillApi( CreateAndFillApi(
url: string, url: string,
apiname: string = "", apiName = "",
apiVerb: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET", apiVerb: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET",
queryTimeout = 30000, queryTimeout = 30000,
) { ) {
this.CreateApi(apiname, apiVerb); this.CreateApi(apiName, apiVerb);
this.EnterURL(url); this.EnterURL(url);
this.agHelper.AssertAutoSave(); this.agHelper.AssertAutoSave();
//this.agHelper.Sleep(2000);// Added because api name edit takes some time to reflect in api sidebar after the call passes. //this.agHelper.Sleep(2000);// Added because api name edit takes some time to reflect in api sidebar after the call passes.

View File

@ -6,6 +6,7 @@ export interface ICreateJSObjectOptions {
toRun: boolean; toRun: boolean;
shouldCreateNewJSObj: boolean; shouldCreateNewJSObj: boolean;
lineNumber?: number; lineNumber?: number;
prettify?: boolean;
} }
const DEFAULT_CREATE_JS_OBJECT_OPTIONS = { const DEFAULT_CREATE_JS_OBJECT_OPTIONS = {
paste: true, paste: true,
@ -124,15 +125,14 @@ export class JSEditor {
completeReplace, completeReplace,
lineNumber = 4, lineNumber = 4,
paste, paste,
prettify = true,
shouldCreateNewJSObj, shouldCreateNewJSObj,
toRun, toRun,
} = options; } = options;
shouldCreateNewJSObj && this.NavigateToNewJSEditor(); shouldCreateNewJSObj && this.NavigateToNewJSEditor();
if (!completeReplace) { if (!completeReplace) {
const downKeys = Array.from(new Array(lineNumber), () => "{downarrow}") const downKeys = "{downarrow}".repeat(lineNumber);
.toString()
.replaceAll(",", "");
cy.get(this.locator._codeMirrorTextArea) cy.get(this.locator._codeMirrorTextArea)
.first() .first()
.focus() .focus()
@ -161,8 +161,11 @@ export class JSEditor {
}); });
this.agHelper.AssertAutoSave(); this.agHelper.AssertAutoSave();
this.agHelper.ActionContextMenuWithInPane("Prettify Code"); // Ample wait due to open bug # 10284
this.agHelper.AssertAutoSave(); //Ample wait due to open bug # 10284 if (prettify) {
this.agHelper.ActionContextMenuWithInPane("Prettify Code");
this.agHelper.AssertAutoSave();
}
if (toRun) { if (toRun) {
//clicking 1 times & waits for 2 second for result to be populated! //clicking 1 times & waits for 2 second for result to be populated!

View File

@ -0,0 +1,245 @@
{
"!name": "browser",
"console": {
"assert": {
"!type": "fn(assertion: bool, text: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.assert",
"!doc": "Writes an error message to the console if the assertion is false."
},
"clear": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Console/clear",
"!doc": " Clear the console."
},
"count": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.count",
"!doc": "Logs the number of times that this particular call to count() has been called."
},
"debug": "console.log",
"dir": {
"!type": "fn(object: ?)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.dir",
"!doc": "Displays an interactive list of the properties of the specified JavaScript object."
},
"error": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.error",
"!doc": "Outputs an error message to the Web Console."
},
"group": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.group",
"!doc": "Creates a new inline group in the Web Console log."
},
"groupCollapsed": {
"!type": "fn(label?: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.groupCollapsed",
"!doc": "Creates a new inline group in the Web Console log."
},
"groupEnd": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.groupEnd",
"!doc": "Exits the current inline group in the Web Console."
},
"info": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.info",
"!doc": "Outputs an informational message to the Web Console."
},
"log": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.log",
"!doc": "Outputs a message to the Web Console."
},
"table": {
"!type": "fn(data: []|?, columns?: [])",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Console/table",
"!doc": " Displays tabular data as a table."
},
"time": {
"!type": "fn(label: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.time",
"!doc": "Starts a timer you can use to track how long an operation takes."
},
"timeEnd": {
"!type": "fn(label: string)",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.timeEnd",
"!doc": "Stops a timer that was previously started by calling console.time()."
},
"trace": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console.trace",
"!doc": "Outputs a stack trace to the Web Console."
},
"warn": {
"!type": "fn(...msg: ?)",
"!url": "https://developer.mozilla.org/en/docs/DOM/console.warn",
"!doc": "Outputs a warning message to the Web Console."
},
"!url": "https://developer.mozilla.org/en/docs/Web/API/Console",
"!doc": "The console object provides access to the browser's debugging console. The specifics of how it works vary from browser to browser, but there is a de facto set of features that are typically provided."
},
"crypto": {
"getRandomValues": {
"!type": "fn([number])",
"!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
"!doc": "This methods lets you get cryptographically random values."
},
"!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
"!doc": "This methods lets you get cryptographically random values."
},
"Blob": {
"!type": "fn(parts: [?], options?: ?)",
"prototype": {
"size": {
"!type": "number",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Blob/size",
"!doc": "The size, in bytes, of the data contained in the Blob object. Read only."
},
"type": {
"!type": "string",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/Blob/type",
"!doc": "An ASCII-encoded string, in all lower case, indicating the MIME type of the data contained in the Blob. If the type is unknown, this string is empty. Read only."
},
"slice": {
"!type": "fn(start: number, end?: number, type?: string) -> +Blob",
"!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
"!doc": "Returns a new Blob object containing the data in the specified range of bytes of the source Blob."
}
},
"!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
"!doc": "A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system."
},
"FileReader": {
"!type": "fn()",
"prototype": {
"abort": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Aborts the read operation. Upon return, the readyState will be DONE."
},
"readAsArrayBuffer": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing an ArrayBuffer."
},
"readAsBinaryString": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing raw binary data."
},
"readAsDataURL": {
"!type": "fn(blob: +Blob)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing a data: url."
},
"readAsText": {
"!type": "fn(blob: +Blob, encoding?: string)",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Starts reading the contents of the specified Blob, producing a string."
},
"EMPTY": "number",
"LOADING": "number",
"DONE": "number",
"error": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The error that occurred while reading the file. Read only."
},
"readyState": {
"!type": "number",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Indicates the state of the FileReader. This will be one of the State constants. Read only."
},
"result": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The file's contents. This property is only valid after the read operation is complete, and the format of the data depends on which of the methods was used to initiate the read operation. Read only."
},
"onabort": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read operation is aborted."
},
"onerror": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when an error occurs."
},
"onload": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read operation is successfully completed."
},
"onloadend": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when the read is completed, whether successful or not. This is called after either onload or onerror."
},
"onloadstart": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called when reading the data is about to begin."
},
"onprogress": {
"!type": "?",
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "Called periodically while the data is being read."
}
},
"!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
"!doc": "The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read. File objects may be obtained from a FileList object returned as a result of a user selecting files using the <input> element, from a drag and drop operation's DataTransfer object, or from the mozGetAsFile() API on an HTMLCanvasElement."
},
"FormData": {
"!type": "fn()",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData",
"prototype": {
"append": {
"!type": "fn(name: string, value: string|+Blob, filename?: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/append",
"!doc": "Appends a new value onto an existing key inside a FormData object, or adds the key if it does not already exist."
},
"delete": {
"!type": "fn(name: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/delete",
"!doc": "Deletes a key/value pair from a FormData object."
},
"entries": {
"!type": "fn() -> +iter[:t=[number, string|+Blob]]",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries",
"!doc": "Returns an iterator allowing to go through all key/value pairs contained in this object."
},
"get": {
"!type": "fn(name: string) -> string|+Blob",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/get",
"!doc": "Returns the first value associated with a given key from within a FormData object."
},
"getAll": {
"!type": "fn(name: string) -> [string|+Blob]",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/getAll",
"!doc": "Returns an array of all the values associated with a given key from within a FormData."
},
"has": {
"!type": "fn(name: string) -> bool",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/has",
"!doc": "Returns a boolean stating whether a FormData object contains a certain key/value pair."
},
"set": {
"!type": "fn(name: string, value: string|+Blob, filename?: string)",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/set",
"!doc": "Sets a new value for an existing key inside a FormData object, or adds the key/value if it does not already exist."
},
"keys": {
"!type": "fn() -> +iter[:t=number]",
"!doc": "Returns an iterator allowing to go through all keys of the key/value pairs contained in this object.",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/keys"
},
"values": {
"!type": "fn() -> +iter[:t=string|blob]",
"!doc": "Returns an iterator allowing to go through all values of the key/value pairs contained in this object.",
"!url": "https://developer.mozilla.org/en-US/docs/Web/API/FormData/values"
}
}
}
}

View File

@ -170,7 +170,13 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) {
event: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent, event: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent,
) => { ) => {
event.preventDefault(); event.preventDefault();
selectedJSActionOption.data && executeJSAction(selectedJSActionOption.data); if (
!disableRunFunctionality &&
!isExecutingCurrentJSAction &&
selectedJSActionOption.data
) {
executeJSAction(selectedJSActionOption.data);
}
}; };
useEffect(() => { useEffect(() => {
@ -179,9 +185,13 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) {
} else { } else {
setDisableRunFunctionality(false); setDisableRunFunctionality(false);
} }
setSelectedJSActionOption(getJSActionOption(activeJSAction, jsActions));
}, [parseErrors, jsActions, activeJSActionId]); }, [parseErrors, jsActions, activeJSActionId]);
useEffect(() => {
// update the selectedJSActionOption when there is addition or removal of jsAction or function
setSelectedJSActionOption(getJSActionOption(activeJSAction, jsActions));
}, [jsActions, activeJSActionId]);
const blockCompletions = useMemo(() => { const blockCompletions = useMemo(() => {
if (selectedJSActionOption.label) { if (selectedJSActionOption.label) {
const funcName = `${selectedJSActionOption.label}()`; const funcName = `${selectedJSActionOption.label}()`;

View File

@ -43,6 +43,24 @@ export class AndRule implements AutocompleteRule {
} }
} }
/**
* Set score to -Infinity for internal defs to be hidden from autocompletion like $__dropdownOption__$
* Max score - 0
* Min score - -Infinity
*/
class HideInternalDefsRule implements AutocompleteRule {
static threshold = -Infinity;
computeScore(completion: Completion): number {
let score = 0;
if (completion.text.includes("$__") && completion.text.includes("__$")) {
score = HideInternalDefsRule.threshold;
}
return score;
}
}
/** /**
* Set score to -Infinity for paths to be blocked from autocompletion * Set score to -Infinity for paths to be blocked from autocompletion
* Max score - 0 * Max score - 0
@ -280,6 +298,7 @@ export class ScoredCompletion {
new JSLibraryRule(), new JSLibraryRule(),
new GlobalJSRule(), new GlobalJSRule(),
new BlockSuggestionsRule(), new BlockSuggestionsRule(),
new HideInternalDefsRule(),
]; ];
completion: Completion; completion: Completion;

View File

@ -1,4 +1,7 @@
import { generateTypeDef } from "utils/autocomplete/dataTreeTypeDefCreator"; import {
ExtraDef,
generateTypeDef,
} from "utils/autocomplete/dataTreeTypeDefCreator";
import { import {
DataTreeAction, DataTreeAction,
DataTreeAppsmith, DataTreeAppsmith,
@ -6,6 +9,7 @@ import {
import _ from "lodash"; import _ from "lodash";
import { EVALUATION_PATH } from "utils/DynamicBindingUtils"; import { EVALUATION_PATH } from "utils/DynamicBindingUtils";
import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer"; import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer";
import { Def } from "tern";
import { ButtonGroupWidgetProps } from "widgets/ButtonGroupWidget/widget"; import { ButtonGroupWidgetProps } from "widgets/ButtonGroupWidget/widget";
const isVisible = { const isVisible = {
@ -14,9 +18,10 @@ const isVisible = {
}; };
export const entityDefinitions = { export const entityDefinitions = {
APPSMITH: (entity: DataTreeAppsmith) => { APPSMITH: (entity: DataTreeAppsmith, extraDefsToDefine: ExtraDef) => {
const generatedTypeDef = generateTypeDef( const generatedTypeDef = generateTypeDef(
_.omit(entity, "ENTITY_TYPE", EVALUATION_PATH), _.omit(entity, "ENTITY_TYPE", EVALUATION_PATH),
extraDefsToDefine,
); );
if ( if (
typeof generatedTypeDef === "object" && typeof generatedTypeDef === "object" &&
@ -39,11 +44,13 @@ export const entityDefinitions = {
} }
return generatedTypeDef; return generatedTypeDef;
}, },
ACTION: (entity: DataTreeAction) => { ACTION: (entity: DataTreeAction, extraDefsToDefine: ExtraDef) => {
const dataDef = generateTypeDef(entity.data); const dataDef = generateTypeDef(entity.data, extraDefsToDefine);
let data: Record<string, any> = {
let data: Def = {
"!doc": "The response of the action", "!doc": "The response of the action",
}; };
if (_.isString(dataDef)) { if (_.isString(dataDef)) {
data["!type"] = dataDef; data["!type"] = dataDef;
} else { } else {
@ -59,8 +66,7 @@ export const entityDefinitions = {
"!doc": "The response meta of the action", "!doc": "The response meta of the action",
"!type": "?", "!type": "?",
}, },
run: run: "fn(params: ?) -> +Promise[:t=[!0.<i>.:t]]",
"fn(onSuccess: fn() -> void, onError: fn() -> void) -> +Promise[:t=[!0.<i>.:t]]",
clear: "fn() -> +Promise[:t=[!0.<i>.:t]]", clear: "fn() -> +Promise[:t=[!0.<i>.:t]]",
}; };
}, },
@ -102,17 +108,20 @@ export const entityDefinitions = {
"!doc": "Selected country code for Currency type input", "!doc": "Selected country code for Currency type input",
}, },
}, },
TABLE_WIDGET: (widget: any) => ({ TABLE_WIDGET: (widget: any, extraDefsToDefine?: ExtraDef) => ({
"!doc": "!doc":
"The Table is the hero widget of Appsmith. You can display data from an API in a table, trigger an action when a user selects a row and even work with large paginated data sets", "The Table is the hero widget of Appsmith. You can display data from an API in a table, trigger an action when a user selects a row and even work with large paginated data sets",
"!url": "https://docs.appsmith.com/widget-reference/table", "!url": "https://docs.appsmith.com/widget-reference/table",
selectedRow: generateTypeDef(widget.selectedRow), selectedRow: generateTypeDef(widget.selectedRow, extraDefsToDefine),
selectedRows: generateTypeDef(widget.selectedRows), selectedRows: generateTypeDef(widget.selectedRows, extraDefsToDefine),
selectedRowIndices: generateTypeDef(widget.selectedRowIndices), selectedRowIndices: generateTypeDef(widget.selectedRowIndices),
triggeredRow: generateTypeDef(widget.triggeredRow), triggeredRow: generateTypeDef(widget.triggeredRow),
selectedRowIndex: "number", selectedRowIndex: "number",
tableData: generateTypeDef(widget.tableData), tableData: generateTypeDef(widget.tableData, extraDefsToDefine),
filteredTableData: generateTypeDef(widget.filteredTableData), filteredTableData: generateTypeDef(
widget.filteredTableData,
extraDefsToDefine,
),
pageNo: "number", pageNo: "number",
pageSize: "number", pageSize: "number",
isVisible: isVisible, isVisible: isVisible,
@ -123,17 +132,17 @@ export const entityDefinitions = {
order: ["asc", "desc"], order: ["asc", "desc"],
}, },
}), }),
TABLE_WIDGET_V2: (widget: any) => ({ TABLE_WIDGET_V2: (widget: any, extraDefsToDefine?: ExtraDef) => ({
"!doc": "!doc":
"The Table is the hero widget of Appsmith. You can display data from an API in a table, trigger an action when a user selects a row and even work with large paginated data sets", "The Table is the hero widget of Appsmith. You can display data from an API in a table, trigger an action when a user selects a row and even work with large paginated data sets",
"!url": "https://docs.appsmith.com/widget-reference/table", "!url": "https://docs.appsmith.com/widget-reference/table",
selectedRow: generateTypeDef(widget.selectedRow), selectedRow: generateTypeDef(widget.selectedRow, extraDefsToDefine),
selectedRows: generateTypeDef(widget.selectedRows), selectedRows: generateTypeDef(widget.selectedRows, extraDefsToDefine),
selectedRowIndices: generateTypeDef(widget.selectedRowIndices), selectedRowIndices: generateTypeDef(widget.selectedRowIndices),
triggeredRow: generateTypeDef(widget.triggeredRow), triggeredRow: generateTypeDef(widget.triggeredRow),
updatedRow: generateTypeDef(widget.updatedRow), updatedRow: generateTypeDef(widget.updatedRow),
selectedRowIndex: "number", selectedRowIndex: "number",
tableData: generateTypeDef(widget.tableData), tableData: generateTypeDef(widget.tableData, extraDefsToDefine),
pageNo: "number", pageNo: "number",
pageSize: "number", pageSize: "number",
isVisible: isVisible, isVisible: isVisible,
@ -143,7 +152,7 @@ export const entityDefinitions = {
column: "string", column: "string",
order: ["asc", "desc"], order: ["asc", "desc"],
}, },
updatedRows: generateTypeDef(widget.updatedRows), updatedRows: generateTypeDef(widget.updatedRows, extraDefsToDefine),
updatedRowIndices: generateTypeDef(widget.updatedRowIndices), updatedRowIndices: generateTypeDef(widget.updatedRowIndices),
triggeredRowIndex: generateTypeDef(widget.triggeredRowIndex), triggeredRowIndex: generateTypeDef(widget.triggeredRowIndex),
}), }),
@ -341,12 +350,12 @@ export const entityDefinitions = {
yAxisName: "string", yAxisName: "string",
selectedDataPoint: "$__chartDataPoint__$", selectedDataPoint: "$__chartDataPoint__$",
}, },
FORM_WIDGET: (widget: any) => ({ FORM_WIDGET: (widget: any, extraDefsToDefine?: ExtraDef) => ({
"!doc": "!doc":
"Form is used to capture a set of data inputs from a user. Forms are used specifically because they reset the data inputs when a form is submitted and disable submission for invalid data inputs", "Form is used to capture a set of data inputs from a user. Forms are used specifically because they reset the data inputs when a form is submitted and disable submission for invalid data inputs",
"!url": "https://docs.appsmith.com/widget-reference/form", "!url": "https://docs.appsmith.com/widget-reference/form",
isVisible: isVisible, isVisible: isVisible,
data: generateTypeDef(widget.data), data: generateTypeDef(widget.data, extraDefsToDefine),
hasChanges: "bool", hasChanges: "bool",
}), }),
FORM_BUTTON_WIDGET: { FORM_BUTTON_WIDGET: {
@ -389,7 +398,7 @@ export const entityDefinitions = {
files: "[$__file__$]", files: "[$__file__$]",
isDisabled: "bool", isDisabled: "bool",
}, },
LIST_WIDGET: (widget: any) => ({ LIST_WIDGET: (widget: any, extraDefsToDefine?: ExtraDef) => ({
"!doc": "!doc":
"Containers are used to group widgets together to form logical higher order widgets. Containers let you organize your page better and move all the widgets inside them together.", "Containers are used to group widgets together to form logical higher order widgets. Containers let you organize your page better and move all the widgets inside them together.",
"!url": "https://docs.appsmith.com/widget-reference/list", "!url": "https://docs.appsmith.com/widget-reference/list",
@ -399,9 +408,9 @@ export const entityDefinitions = {
}, },
isVisible: isVisible, isVisible: isVisible,
gridGap: "number", gridGap: "number",
selectedItem: generateTypeDef(widget.selectedItem), selectedItem: generateTypeDef(widget.selectedItem, extraDefsToDefine),
items: generateTypeDef(widget.items), items: generateTypeDef(widget.items, extraDefsToDefine),
listData: generateTypeDef(widget.listData), listData: generateTypeDef(widget.listData, extraDefsToDefine),
pageNo: generateTypeDef(widget.pageNo), pageNo: generateTypeDef(widget.pageNo),
pageSize: generateTypeDef(widget.pageSize), pageSize: generateTypeDef(widget.pageSize),
}), }),
@ -632,6 +641,12 @@ export const entityDefinitions = {
isVisible: isVisible, isVisible: isVisible,
progress: "number", progress: "number",
}, },
DOCUMENT_VIEWER_WIDGET: {
"!doc": "Document viewer widget is used to show documents on a page",
"!url": "https://docs.appsmith.com/reference/widgets/document-viewer",
isVisible: isVisible,
docUrl: "string",
},
}; };
/* /*

View File

@ -7,6 +7,7 @@ import base64 from "constants/defs/base64-js.json";
import moment from "constants/defs/moment.json"; import moment from "constants/defs/moment.json";
import xmlJs from "constants/defs/xmlParser.json"; import xmlJs from "constants/defs/xmlParser.json";
import forge from "constants/defs/forge.json"; import forge from "constants/defs/forge.json";
import browser from "constants/defs/browser.json";
import CodeMirror, { Hint, Pos, cmpPos } from "codemirror"; import CodeMirror, { Hint, Pos, cmpPos } from "codemirror";
import { import {
getDynamicStringSegments, getDynamicStringSegments,
@ -19,10 +20,12 @@ import {
import { FieldEntityInformation } from "components/editorComponents/CodeEditor/EditorConfig"; import { FieldEntityInformation } from "components/editorComponents/CodeEditor/EditorConfig";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { AutocompleteSorter } from "./AutocompleteSortRules"; import { AutocompleteSorter } from "./AutocompleteSortRules";
import { getCompletionsForKeyword } from "./keywordCompletion";
const DEFS: Def[] = [ const DEFS: Def[] = [
// @ts-expect-error: Types are not available // @ts-expect-error: Types are not available
ecma, ecma,
browser,
GLOBAL_FUNCTIONS, GLOBAL_FUNCTIONS,
GLOBAL_DEFS, GLOBAL_DEFS,
lodash, lodash,
@ -38,7 +41,7 @@ const hintDelay = 1700;
export type Completion = Hint & { export type Completion = Hint & {
origin: string; origin: string;
type: AutocompleteDataType; type: AutocompleteDataType | string;
data: { data: {
doc: string; doc: string;
}; };
@ -231,6 +234,15 @@ class TernServer {
) { ) {
after = '"]'; after = '"]';
} }
// Actual char space
const trimmedFocusedValueLength = focusedValue.trim().length;
// end.ch counts tab space as 1 instead of 2 space chars in string
// For eg: lets take string ` ab`. Here, end.ch = 3 & trimmedFocusedValueLength = 2
// hence tabSpacesCount = end.ch - trimmedFocusedValueLength
const tabSpacesCount = end.ch - trimmedFocusedValueLength;
const cursorHorizontalPos =
tabSpacesCount * 2 + trimmedFocusedValueLength - 2;
for (let i = 0; i < data.completions.length; ++i) { for (let i = 0; i < data.completions.length; ++i) {
const completion = data.completions[i]; const completion = data.completions[i];
let className = typeToIcon(completion.type, completion.isKeyword); let className = typeToIcon(completion.type, completion.isKeyword);
@ -258,6 +270,12 @@ class TernServer {
element.setAttribute("keyword", data.displayText); element.setAttribute("keyword", data.displayText);
element.innerHTML = data.displayText; element.innerHTML = data.displayText;
}; };
// Add relevant keyword completions
const keywordCompletions = getCompletionsForKeyword(
codeMirrorCompletion,
cursorHorizontalPos,
);
completions = [...completions, ...keywordCompletions];
} }
completions.push(codeMirrorCompletion); completions.push(codeMirrorCompletion);
} }

View File

@ -88,6 +88,88 @@ describe("dataTreeTypeDefCreator", () => {
expect(objType).toStrictEqual(expected); expect(objType).toStrictEqual(expected);
}); });
it("creates a correct def for a complex array of object", () => {
const data = [
{
nested: [
{
nested: [
{
nested: [
{
nested: [
{
nested: [
{
nested: [
{
name: "",
email: "",
},
],
},
],
},
],
},
],
},
],
},
],
},
];
const expected = "[def_6]";
const extraDef = {};
const expectedExtraDef = {
def_1: { nested: "[?]" },
def_2: { nested: "[def_1]" },
def_3: { nested: "[def_2]" },
def_4: { nested: "[def_3]" },
def_5: { nested: "[def_4]" },
def_6: { nested: "[def_5]" },
};
const dataType = generateTypeDef(data, extraDef);
expect(dataType).toStrictEqual(expected);
expect(extraDef).toStrictEqual(expectedExtraDef);
const extraDef2 = {};
const expected2 = "[def_10]";
const dataType2 = generateTypeDef(
data[0].nested[0].nested[0].nested,
extraDef2,
);
const expectedExtraDef2 = {
def_7: { name: "string", email: "string" },
def_8: { nested: "[def_7]" },
def_9: { nested: "[def_8]" },
def_10: { nested: "[def_9]" },
};
expect(dataType2).toStrictEqual(expected2);
expect(extraDef2).toStrictEqual(expectedExtraDef2);
});
it("creates a correct def for an array of array of object", () => {
const array = [[{ name: "", email: "" }]];
const expected = "[[def_11]]";
const extraDefsToDefine = {};
const expectedExtraDef = {
def_11: { name: "string", email: "string" },
};
const objType = generateTypeDef(array, extraDefsToDefine);
expect(objType).toStrictEqual(expected);
expect(extraDefsToDefine).toStrictEqual(expectedExtraDef);
});
it("flatten def", () => { it("flatten def", () => {
const def = { const def = {
entity1: { entity1: {

View File

@ -1,5 +1,5 @@
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { get, isFunction } from "lodash"; import { uniqueId, get, isFunction, isObject } from "lodash";
import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; import { entityDefinitions } from "utils/autocomplete/EntityDefinitions";
import { getType, Types } from "utils/TypeHelpers"; import { getType, Types } from "utils/TypeHelpers";
import { Def } from "tern"; import { Def } from "tern";
@ -11,11 +11,11 @@ import {
isWidget, isWidget,
} from "workers/evaluationUtils"; } from "workers/evaluationUtils";
import { DataTreeDefEntityInformation } from "utils/autocomplete/TernServer"; import { DataTreeDefEntityInformation } from "utils/autocomplete/TernServer";
export type ExtraDef = Record<string, Def | string>;
import { Variable } from "entities/JSCollection"; 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: Def = {};
// Def names are encoded with information about the entity // Def names are encoded with information about the entity
// This so that we have more info about them // This so that we have more info about them
// when sorting results in autocomplete // when sorting results in autocomplete
@ -26,6 +26,9 @@ export const dataTreeTypeDefCreator = (
dataTree: DataTree, dataTree: DataTree,
isJSEditorEnabled: boolean, isJSEditorEnabled: boolean,
): { def: Def; entityInfo: Map<string, DataTreeDefEntityInformation> } => { ): { def: Def; entityInfo: Map<string, DataTreeDefEntityInformation> } => {
// When there is a complex data type, we store it in extra def and refer to it in the def
const extraDefsToDefine: Def = {};
const def: Def = { const def: Def = {
"!name": "DATA_TREE", "!name": "DATA_TREE",
}; };
@ -37,7 +40,7 @@ export const dataTreeTypeDefCreator = (
if (widgetType in entityDefinitions) { if (widgetType in entityDefinitions) {
const definition = get(entityDefinitions, widgetType); const definition = get(entityDefinitions, widgetType);
if (isFunction(definition)) { if (isFunction(definition)) {
def[entityName] = definition(entity); def[entityName] = definition(entity, extraDefsToDefine);
} else { } else {
def[entityName] = definition; def[entityName] = definition;
} }
@ -48,21 +51,21 @@ export const dataTreeTypeDefCreator = (
}); });
} }
} else if (isAction(entity)) { } else if (isAction(entity)) {
def[entityName] = entityDefinitions.ACTION(entity); def[entityName] = entityDefinitions.ACTION(entity, extraDefsToDefine);
flattenDef(def, entityName); flattenDef(def, entityName);
entityMap.set(entityName, { entityMap.set(entityName, {
type: ENTITY_TYPE.ACTION, type: ENTITY_TYPE.ACTION,
subType: "ACTION", subType: "ACTION",
}); });
} else if (isAppsmithEntity(entity)) { } else if (isAppsmithEntity(entity)) {
def.appsmith = (entityDefinitions.APPSMITH as any)(entity); def.appsmith = entityDefinitions.APPSMITH(entity, extraDefsToDefine);
entityMap.set("appsmith", { entityMap.set("appsmith", {
type: ENTITY_TYPE.APPSMITH, type: ENTITY_TYPE.APPSMITH,
subType: ENTITY_TYPE.APPSMITH, subType: ENTITY_TYPE.APPSMITH,
}); });
} else if (isJSAction(entity) && isJSEditorEnabled) { } else if (isJSAction(entity) && isJSEditorEnabled) {
const metaObj = entity.meta; const metaObj = entity.meta;
const jsProperty: Def = {}; const jsPropertiesDef: Def = {};
for (const key in metaObj) { for (const key in metaObj) {
// const jsFunctionObj = metaObj[key]; // const jsFunctionObj = metaObj[key];
@ -72,42 +75,64 @@ export const dataTreeTypeDefCreator = (
// we will also need to check performance implications here // we will also need to check performance implications here
const argsTypeString = getFunctionsArgsType([]); const argsTypeString = getFunctionsArgsType([]);
jsProperty[key] = argsTypeString; jsPropertiesDef[key] = argsTypeString;
} }
for (let i = 0; i < entity.variables.length; i++) { for (let i = 0; i < entity.variables.length; i++) {
const varKey = entity.variables[i]; const varKey = entity.variables[i];
const varValue = entity[varKey]; const varValue = entity[varKey];
jsProperty[varKey] = generateTypeDef(varValue); jsPropertiesDef[varKey] = generateTypeDef(varValue, extraDefsToDefine);
} }
def[entityName] = jsProperty; def[entityName] = jsPropertiesDef;
flattenDef(def, entityName); flattenDef(def, entityName);
entityMap.set(entityName, { entityMap.set(entityName, {
type: ENTITY_TYPE.JSACTION, type: ENTITY_TYPE.JSACTION,
subType: "JSACTION", subType: "JSACTION",
}); });
} }
if (Object.keys(extraDefs)) {
def["!define"] = { ...extraDefs };
extraDefs = {};
}
}); });
if (Object.keys(extraDefsToDefine)) {
def["!define"] = { ...extraDefsToDefine };
}
return { def, entityInfo: entityMap }; return { def, entityInfo: entityMap };
}; };
export function generateTypeDef(obj: any): string | Def { export function generateTypeDef(
const type = getType(obj); value: unknown,
switch (type) { extraDefsToDefine?: ExtraDef,
depth = 0,
): Def | string {
switch (getType(value)) {
case Types.ARRAY: { case Types.ARRAY: {
const arrayType = getType(obj[0]); const array = value as [unknown];
return `[${arrayType}]`; if (depth > 5) {
return `[?]`;
}
const arrayElementType = generateTypeDef(
array[0],
extraDefsToDefine,
depth + 1,
);
if (isObject(arrayElementType)) {
if (extraDefsToDefine) {
const uniqueDefName = uniqueId("def_");
extraDefsToDefine[uniqueDefName] = arrayElementType;
return `[${uniqueDefName}]`;
}
return `[?]`;
}
return `[${arrayElementType}]`;
} }
case Types.OBJECT: { case Types.OBJECT: {
const objType: Def = {}; const objType: Def = {};
Object.keys(obj).forEach((k) => { const object = value as Record<string, unknown>;
objType[k] = generateTypeDef(obj[k]); Object.keys(object).forEach((k) => {
objType[k] = generateTypeDef(object[k], extraDefsToDefine, depth);
}); });
return objType; return objType;
} }

View File

@ -0,0 +1,144 @@
import { Completion } from "./TernServer";
export const getCompletionsForKeyword = (
completion: Completion,
cursorHorizontalPos: number,
) => {
const keywordName = completion.text;
// indentation needs to be positive number
const indentation = cursorHorizontalPos < 0 ? 0 : cursorHorizontalPos;
const indentationSpace = " ".repeat(indentation);
const completions = [];
switch (keywordName) {
// loops
case "for":
completions.push({
...completion,
name: "for-loop",
text: `for(let i=0;i < array.length;i++){\n${indentationSpace}\tconst element = array[i];\n${indentationSpace}}`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "For Loop");
element.innerHTML = completion.text;
},
});
completions.push({
...completion,
name: "for-in-loop",
text: `for(const key in object) {\n${indentationSpace}}`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "For-in Loop");
element.innerHTML = "forin";
},
});
completions.push({
...completion,
name: "for-of-loop",
text: `for(const iterator of object){\n${indentationSpace}}`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "For-of Loop");
element.innerHTML = "forof";
},
});
break;
case "while":
completions.push({
...completion,
name: "while-loop",
text: `while(condition){\n${indentationSpace}}`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "While Statement");
element.innerHTML = completion.text;
},
});
break;
case "do":
completions.push({
...completion,
name: "do-while-statement",
text: `do{\n\n${indentationSpace}} while (condition);`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "do-While Statement");
element.innerHTML = completion.text;
},
});
break;
// conditional statement
case "if":
completions.push({
...completion,
name: "if-statement",
text: `if(condition){\n\n${indentationSpace}}`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "if Statement");
element.innerHTML = completion.text;
},
});
break;
case "switch":
completions.push({
...completion,
name: "switch-statement",
text: `switch(key){\n${indentationSpace}\tcase value:\n${indentationSpace}\t\tbreak;\n${indentationSpace}\tdefault:\n${indentationSpace}\t\tbreak;\n${indentationSpace}}`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "Switch Statement");
element.innerHTML = completion.text;
},
});
break;
case "function":
completions.push({
...completion,
name: "function-statement",
text: `function name(params){\n\n${indentationSpace}}`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "Function Statement");
element.innerHTML = completion.text;
},
});
break;
case "try":
completions.push({
...completion,
name: "try-catch",
text: `try{\n\n${indentationSpace}}catch(error){\n\n${indentationSpace}}`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "Try-catch Statement");
element.innerHTML = "try-catch";
},
});
break;
case "throw":
completions.push({
...completion,
name: "throw-exception",
text: `throw new Error("");`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "Throw Exception");
element.innerHTML = completion.text;
},
});
break;
case "new":
completions.push({
...completion,
name: "new-statement",
text: `const name = new type(arguments);`,
render: (element: HTMLElement) => {
element.setAttribute("keyword", "new Statement");
element.innerHTML = completion.text;
},
});
break;
}
return completions;
};