diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Autocomplete/Autocomplete_JS_spec.ts b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Autocomplete/Autocomplete_JS_spec.ts index 8eaae35552..ba60e09412 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Autocomplete/Autocomplete_JS_spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Autocomplete/Autocomplete_JS_spec.ts @@ -2,7 +2,13 @@ import { WIDGET } from "../../../../locators/WidgetLocators"; import { ObjectsRegistry } from "../../../../support/Objects/Registry"; 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 { myVar1: [], @@ -16,12 +22,11 @@ const jsObjectBody = `export default { }`; describe("Autocomplete tests", () => { - before(() => { + it("1. Verify widgets autocomplete: ButtonGroup & Document viewer widget", () => { 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 jsEditor.CreateJSObject(jsObjectBody, { paste: true, @@ -30,31 +35,120 @@ describe("Autocomplete tests", () => { shouldCreateNewJSObj: true, }); - const lineNumber = 5; - cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click(); + // focus on 5th line + cy.get(`:nth-child(5) > .CodeMirror-line`).click(); + // 1. Button group widget autocomplete verification cy.get(CommonLocators._codeMirrorTextArea) .focus() .type(`ButtonGroup1.`); - cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("groupButtons"); + agHelper.AssertElementText(CommonLocators._hints, "groupButtons"); cy.get(CommonLocators._codeMirrorTextArea) .focus() .type(`groupButtons.`); - cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("groupButton1"); + agHelper.AssertElementText(CommonLocators._hints, "groupButton1"); - cy.get(CommonLocators._codeMirrorTextArea).focus().type(` - eval`); + // 2. Document view widget autocomplete verification + 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( - "not.have.value", - "eval()", - ); + agHelper.AssertElementText(CommonLocators._hints, "docUrl"); }); - 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 jsEditor.CreateJSObject(jsObjectBody, { paste: true, @@ -65,15 +159,16 @@ describe("Autocomplete tests", () => { const lineNumber = 5; - const array = [ + const users = [ { label: "a", value: "b" }, { label: "a", value: "b" }, ]; 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 @@ -86,16 +181,36 @@ describe("Autocomplete tests", () => { .focus() .type(`${codeToType}`, { parseSpecialCharSequences: false }) .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) .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 jsEditor.CreateJSObject(jsObjectBody, { paste: true, @@ -104,18 +219,18 @@ describe("Autocomplete tests", () => { shouldCreateNewJSObj: true, }); - const lineNumber = 5; - - const codeToType = "this."; - - cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click(); + cy.get(`:nth-child(${5}) > .CodeMirror-line`).click(); cy.get(CommonLocators._codeMirrorTextArea) .focus() - .type(`${codeToType}`); + .type("Api1.data.u"); - ["myFun2()", "myVar1", "myVar2"].forEach((element, index) => { - cy.get(`.CodeMirror-hints > :nth-child(${index + 1})`).contains(element); - }); + agHelper.AssertElementText(CommonLocators._hints, "users"); + + cy.get(CommonLocators._codeMirrorTextArea) + .focus() + .type("sers[0].e"); + + agHelper.AssertElementText(CommonLocators._hints, "email"); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_MultiSelect_Button_Text_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_MultiSelect_Button_Text_spec.js index 02660fff4d..79c97c0937 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_MultiSelect_Button_Text_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_MultiSelect_Button_Text_spec.js @@ -11,7 +11,7 @@ import { } from "../../../../locators/WidgetLocators"; const widgetsToTest = { - [WIDGET.MULTISELECT_WIDGET]: { + [WIDGET.MULTISELECT]: { testCases: [ { input: @@ -39,7 +39,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig]) => { }); 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) .find(".t--js-toggle") .click(); @@ -60,7 +60,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig]) => { cy.wrap(item).should("contain.text", "BLUE"); }); const inputs = testConfig.testCases; - cy.get(getWidgetSelector(WIDGET.BUTTON_WIDGET)) + cy.get(getWidgetSelector(WIDGET.BUTTON)) .scrollIntoView() .click({ force: true }); cy.wait("@updateLayout"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/AllInputWidgets_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/AllInputWidgets_spec.js index dedf4dded1..a108557947 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/AllInputWidgets_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/AllInputWidgets_spec.js @@ -9,7 +9,7 @@ import { } from "../../../../locators/WidgetLocators"; const widgetsToTest = { - [WIDGET.INPUT_WIDGET_V2]: { + [WIDGET.INPUT_V2]: { testCases: [ { input: "test", expected: "test", clearBeforeType: true }, { input: "12", expected: "test12", clearBeforeType: false }, @@ -29,7 +29,7 @@ const widgetsToTest = { widgetName: "Input widget", widgetPrefixName: "Input", }, - [WIDGET.PHONE_INPUT_WIDGET]: { + [WIDGET.PHONE_INPUT]: { testCases: [ { input: "9999999999", @@ -50,7 +50,7 @@ const widgetsToTest = { widgetName: "Phone Input widget", widgetPrefixName: "PhoneInput", }, - [WIDGET.CURRENCY_INPUT_WIDGET]: { + [WIDGET.CURRENCY_INPUT]: { testCases: [ { 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.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 }); }); 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 (widgetSelector === WIDGET.INPUT_WIDGET_V2) { + // if (widgetSelector === WIDGET.INPUT_V2) { // cy.openPropertyPane(widgetSelector); // cy.selectDropdownValue(".t--property-control-datatype", "Text"); // cy.get(".t--property-control-required label") @@ -119,7 +119,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => { // } // Set onClick action, storing value - cy.openPropertyPane(WIDGET.BUTTON_WIDGET); + cy.openPropertyPane(WIDGET.BUTTON); cy.get(PROPERTY_SELECTOR.onClick) .find(".t--js-toggle") .click(); @@ -147,7 +147,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => { 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 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", () => { // Set onClick action, storing value - cy.openPropertyPane(WIDGET.BUTTON_WIDGET); + cy.openPropertyPane(WIDGET.BUTTON); // cy.get(PROPERTY_SELECTOR.onClick) // .find(".t--js-toggle") // .click(); @@ -177,7 +177,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => { 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 @@ -190,7 +190,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => { }); 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(getWidgetSelector(WIDGET.TEXT)).click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/AllWidgets_default_meta_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/AllWidgets_default_meta_spec.js index 6e90d509fa..090859ec18 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/AllWidgets_default_meta_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/AllWidgets_default_meta_spec.js @@ -13,7 +13,7 @@ import { } from "../../../../locators/WidgetLocators"; const widgetsToTest = { - [WIDGET.MULTISELECT_WIDGET]: { + [WIDGET.MULTISELECT]: { widgetName: "MultiSelect", widgetPrefixName: "MultiSelect1", textBindingValue: "{{MultiSelect1.selectedOptionValues}}", @@ -61,7 +61,7 @@ const widgetsToTest = { selectAndReset(); }, }, - [WIDGET.CURRENCY_INPUT_WIDGET]: { + [WIDGET.CURRENCY_INPUT]: { widgetName: "CurrencyInput", widgetPrefixName: "CurrencyInput1", textBindingValue: testdata.currencyBindingValue, @@ -435,7 +435,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig]) => { it("2. Bind Button on click and Text widget content", () => { // Set onClick assertWidgetReset, storing value - cy.openPropertyPane(WIDGET.BUTTON_WIDGET); + cy.openPropertyPane(WIDGET.BUTTON); cy.get(PROPERTY_SELECTOR.onClick) .find(".t--js-toggle") diff --git a/app/client/cypress/locators/WidgetLocators.ts b/app/client/cypress/locators/WidgetLocators.ts index 0b95f2cef3..ec4dd00e63 100644 --- a/app/client/cypress/locators/WidgetLocators.ts +++ b/app/client/cypress/locators/WidgetLocators.ts @@ -1,12 +1,12 @@ export const WIDGET = { - INPUT_WIDGET_V2: "inputwidgetv2", + INPUT_V2: "inputwidgetv2", TEXT: "textwidget", - PHONE_INPUT_WIDGET: "phoneinputwidget", - CURRENCY_INPUT_WIDGET: "currencyinputwidget", - BUTTON_WIDGET: "buttonwidget", - MULTISELECT_WIDGET: "multiselectwidgetv2", - BUTTON_GROUP_WIDGET: "buttongroupwidget", - TREESELECT_WIDGET: "singleselecttreewidget", + PHONE_INPUT: "phoneinputwidget", + CURRENCY_INPUT: "currencyinputwidget", + BUTTON: "buttonwidget", + MULTISELECT: "multiselectwidgetv2", + BUTTON_GROUP: "buttongroupwidget", + TREESELECT: "singleselecttreewidget", TAB: "tabswidget", TABLE: "tablewidgetv2", SWITCHGROUP: "switchgroupwidget", @@ -23,6 +23,7 @@ export const WIDGET = { PHONEINPUT: "phoneinputwidget", CAMERA: "camerawidget", FILEPICKER: "filepickerwidgetv2", + DOCUMENT_VIEWER: "documentviewerwidget", } as const; // property pane element selector are maintained here diff --git a/app/client/cypress/support/Pages/AggregateHelper.ts b/app/client/cypress/support/Pages/AggregateHelper.ts index 4e9668c5c3..f739c9a49b 100644 --- a/app/client/cypress/support/Pages/AggregateHelper.ts +++ b/app/client/cypress/support/Pages/AggregateHelper.ts @@ -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("//") ? cy.xpath(selector) : cy.get(selector); diff --git a/app/client/cypress/support/Pages/ApiPage.ts b/app/client/cypress/support/Pages/ApiPage.ts index 2a7ac88ab0..5cd614daa5 100644 --- a/app/client/cypress/support/Pages/ApiPage.ts +++ b/app/client/cypress/support/Pages/ApiPage.ts @@ -42,7 +42,7 @@ export class ApiPage { _saveAsDS = ".t--store-as-datasource"; CreateApi( - apiName: string = "", + apiName = "", apiVerb: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET", ) { cy.get(this.locator._createNew).click({ force: true }); @@ -67,11 +67,11 @@ export class ApiPage { CreateAndFillApi( url: string, - apiname: string = "", + apiName = "", apiVerb: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET", queryTimeout = 30000, ) { - this.CreateApi(apiname, apiVerb); + this.CreateApi(apiName, apiVerb); this.EnterURL(url); this.agHelper.AssertAutoSave(); //this.agHelper.Sleep(2000);// Added because api name edit takes some time to reflect in api sidebar after the call passes. diff --git a/app/client/cypress/support/Pages/JSEditor.ts b/app/client/cypress/support/Pages/JSEditor.ts index 5c3cc56271..e7569f59b6 100644 --- a/app/client/cypress/support/Pages/JSEditor.ts +++ b/app/client/cypress/support/Pages/JSEditor.ts @@ -6,6 +6,7 @@ export interface ICreateJSObjectOptions { toRun: boolean; shouldCreateNewJSObj: boolean; lineNumber?: number; + prettify?: boolean; } const DEFAULT_CREATE_JS_OBJECT_OPTIONS = { paste: true, @@ -124,15 +125,14 @@ export class JSEditor { completeReplace, lineNumber = 4, paste, + prettify = true, shouldCreateNewJSObj, toRun, } = options; shouldCreateNewJSObj && this.NavigateToNewJSEditor(); if (!completeReplace) { - const downKeys = Array.from(new Array(lineNumber), () => "{downarrow}") - .toString() - .replaceAll(",", ""); + const downKeys = "{downarrow}".repeat(lineNumber); cy.get(this.locator._codeMirrorTextArea) .first() .focus() @@ -161,8 +161,11 @@ export class JSEditor { }); this.agHelper.AssertAutoSave(); - this.agHelper.ActionContextMenuWithInPane("Prettify Code"); - this.agHelper.AssertAutoSave(); //Ample wait due to open bug # 10284 + // Ample wait due to open bug # 10284 + if (prettify) { + this.agHelper.ActionContextMenuWithInPane("Prettify Code"); + this.agHelper.AssertAutoSave(); + } if (toRun) { //clicking 1 times & waits for 2 second for result to be populated! diff --git a/app/client/src/constants/defs/browser.json b/app/client/src/constants/defs/browser.json new file mode 100644 index 0000000000..d0a93d4764 --- /dev/null +++ b/app/client/src/constants/defs/browser.json @@ -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 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" + } + } + } +} \ No newline at end of file diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx index b4a307b557..cc312a06a5 100644 --- a/app/client/src/pages/Editor/JSEditor/Form.tsx +++ b/app/client/src/pages/Editor/JSEditor/Form.tsx @@ -170,7 +170,13 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) { event: React.MouseEvent | KeyboardEvent, ) => { event.preventDefault(); - selectedJSActionOption.data && executeJSAction(selectedJSActionOption.data); + if ( + !disableRunFunctionality && + !isExecutingCurrentJSAction && + selectedJSActionOption.data + ) { + executeJSAction(selectedJSActionOption.data); + } }; useEffect(() => { @@ -179,9 +185,13 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) { } else { setDisableRunFunctionality(false); } - setSelectedJSActionOption(getJSActionOption(activeJSAction, jsActions)); }, [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(() => { if (selectedJSActionOption.label) { const funcName = `${selectedJSActionOption.label}()`; diff --git a/app/client/src/utils/autocomplete/AutocompleteSortRules.ts b/app/client/src/utils/autocomplete/AutocompleteSortRules.ts index 828bca5309..68a3ec55b8 100644 --- a/app/client/src/utils/autocomplete/AutocompleteSortRules.ts +++ b/app/client/src/utils/autocomplete/AutocompleteSortRules.ts @@ -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 * Max score - 0 @@ -280,6 +298,7 @@ export class ScoredCompletion { new JSLibraryRule(), new GlobalJSRule(), new BlockSuggestionsRule(), + new HideInternalDefsRule(), ]; completion: Completion; diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts index 7c054a6ebc..637a82cf03 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -1,4 +1,7 @@ -import { generateTypeDef } from "utils/autocomplete/dataTreeTypeDefCreator"; +import { + ExtraDef, + generateTypeDef, +} from "utils/autocomplete/dataTreeTypeDefCreator"; import { DataTreeAction, DataTreeAppsmith, @@ -6,6 +9,7 @@ import { import _ from "lodash"; import { EVALUATION_PATH } from "utils/DynamicBindingUtils"; import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer"; +import { Def } from "tern"; import { ButtonGroupWidgetProps } from "widgets/ButtonGroupWidget/widget"; const isVisible = { @@ -14,9 +18,10 @@ const isVisible = { }; export const entityDefinitions = { - APPSMITH: (entity: DataTreeAppsmith) => { + APPSMITH: (entity: DataTreeAppsmith, extraDefsToDefine: ExtraDef) => { const generatedTypeDef = generateTypeDef( _.omit(entity, "ENTITY_TYPE", EVALUATION_PATH), + extraDefsToDefine, ); if ( typeof generatedTypeDef === "object" && @@ -39,11 +44,13 @@ export const entityDefinitions = { } return generatedTypeDef; }, - ACTION: (entity: DataTreeAction) => { - const dataDef = generateTypeDef(entity.data); - let data: Record = { + ACTION: (entity: DataTreeAction, extraDefsToDefine: ExtraDef) => { + const dataDef = generateTypeDef(entity.data, extraDefsToDefine); + + let data: Def = { "!doc": "The response of the action", }; + if (_.isString(dataDef)) { data["!type"] = dataDef; } else { @@ -59,8 +66,7 @@ export const entityDefinitions = { "!doc": "The response meta of the action", "!type": "?", }, - run: - "fn(onSuccess: fn() -> void, onError: fn() -> void) -> +Promise[:t=[!0..:t]]", + run: "fn(params: ?) -> +Promise[:t=[!0..:t]]", clear: "fn() -> +Promise[:t=[!0..:t]]", }; }, @@ -102,17 +108,20 @@ export const entityDefinitions = { "!doc": "Selected country code for Currency type input", }, }, - TABLE_WIDGET: (widget: any) => ({ + TABLE_WIDGET: (widget: any, extraDefsToDefine?: ExtraDef) => ({ "!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", "!url": "https://docs.appsmith.com/widget-reference/table", - selectedRow: generateTypeDef(widget.selectedRow), - selectedRows: generateTypeDef(widget.selectedRows), + selectedRow: generateTypeDef(widget.selectedRow, extraDefsToDefine), + selectedRows: generateTypeDef(widget.selectedRows, extraDefsToDefine), selectedRowIndices: generateTypeDef(widget.selectedRowIndices), triggeredRow: generateTypeDef(widget.triggeredRow), selectedRowIndex: "number", - tableData: generateTypeDef(widget.tableData), - filteredTableData: generateTypeDef(widget.filteredTableData), + tableData: generateTypeDef(widget.tableData, extraDefsToDefine), + filteredTableData: generateTypeDef( + widget.filteredTableData, + extraDefsToDefine, + ), pageNo: "number", pageSize: "number", isVisible: isVisible, @@ -123,17 +132,17 @@ export const entityDefinitions = { order: ["asc", "desc"], }, }), - TABLE_WIDGET_V2: (widget: any) => ({ + TABLE_WIDGET_V2: (widget: any, extraDefsToDefine?: ExtraDef) => ({ "!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", "!url": "https://docs.appsmith.com/widget-reference/table", - selectedRow: generateTypeDef(widget.selectedRow), - selectedRows: generateTypeDef(widget.selectedRows), + selectedRow: generateTypeDef(widget.selectedRow, extraDefsToDefine), + selectedRows: generateTypeDef(widget.selectedRows, extraDefsToDefine), selectedRowIndices: generateTypeDef(widget.selectedRowIndices), triggeredRow: generateTypeDef(widget.triggeredRow), updatedRow: generateTypeDef(widget.updatedRow), selectedRowIndex: "number", - tableData: generateTypeDef(widget.tableData), + tableData: generateTypeDef(widget.tableData, extraDefsToDefine), pageNo: "number", pageSize: "number", isVisible: isVisible, @@ -143,7 +152,7 @@ export const entityDefinitions = { column: "string", order: ["asc", "desc"], }, - updatedRows: generateTypeDef(widget.updatedRows), + updatedRows: generateTypeDef(widget.updatedRows, extraDefsToDefine), updatedRowIndices: generateTypeDef(widget.updatedRowIndices), triggeredRowIndex: generateTypeDef(widget.triggeredRowIndex), }), @@ -341,12 +350,12 @@ export const entityDefinitions = { yAxisName: "string", selectedDataPoint: "$__chartDataPoint__$", }, - FORM_WIDGET: (widget: any) => ({ + FORM_WIDGET: (widget: any, extraDefsToDefine?: ExtraDef) => ({ "!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", "!url": "https://docs.appsmith.com/widget-reference/form", isVisible: isVisible, - data: generateTypeDef(widget.data), + data: generateTypeDef(widget.data, extraDefsToDefine), hasChanges: "bool", }), FORM_BUTTON_WIDGET: { @@ -389,7 +398,7 @@ export const entityDefinitions = { files: "[$__file__$]", isDisabled: "bool", }, - LIST_WIDGET: (widget: any) => ({ + LIST_WIDGET: (widget: any, extraDefsToDefine?: ExtraDef) => ({ "!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.", "!url": "https://docs.appsmith.com/widget-reference/list", @@ -399,9 +408,9 @@ export const entityDefinitions = { }, isVisible: isVisible, gridGap: "number", - selectedItem: generateTypeDef(widget.selectedItem), - items: generateTypeDef(widget.items), - listData: generateTypeDef(widget.listData), + selectedItem: generateTypeDef(widget.selectedItem, extraDefsToDefine), + items: generateTypeDef(widget.items, extraDefsToDefine), + listData: generateTypeDef(widget.listData, extraDefsToDefine), pageNo: generateTypeDef(widget.pageNo), pageSize: generateTypeDef(widget.pageSize), }), @@ -632,6 +641,12 @@ export const entityDefinitions = { isVisible: isVisible, 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", + }, }; /* diff --git a/app/client/src/utils/autocomplete/TernServer.ts b/app/client/src/utils/autocomplete/TernServer.ts index 4ab26a3b20..b915ed3e11 100644 --- a/app/client/src/utils/autocomplete/TernServer.ts +++ b/app/client/src/utils/autocomplete/TernServer.ts @@ -7,6 +7,7 @@ import base64 from "constants/defs/base64-js.json"; import moment from "constants/defs/moment.json"; import xmlJs from "constants/defs/xmlParser.json"; import forge from "constants/defs/forge.json"; +import browser from "constants/defs/browser.json"; import CodeMirror, { Hint, Pos, cmpPos } from "codemirror"; import { getDynamicStringSegments, @@ -19,10 +20,12 @@ import { import { FieldEntityInformation } from "components/editorComponents/CodeEditor/EditorConfig"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { AutocompleteSorter } from "./AutocompleteSortRules"; +import { getCompletionsForKeyword } from "./keywordCompletion"; const DEFS: Def[] = [ // @ts-expect-error: Types are not available ecma, + browser, GLOBAL_FUNCTIONS, GLOBAL_DEFS, lodash, @@ -38,7 +41,7 @@ const hintDelay = 1700; export type Completion = Hint & { origin: string; - type: AutocompleteDataType; + type: AutocompleteDataType | string; data: { doc: string; }; @@ -231,6 +234,15 @@ class TernServer { ) { 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) { const completion = data.completions[i]; let className = typeToIcon(completion.type, completion.isKeyword); @@ -258,6 +270,12 @@ class TernServer { element.setAttribute("keyword", data.displayText); element.innerHTML = data.displayText; }; + // Add relevant keyword completions + const keywordCompletions = getCompletionsForKeyword( + codeMirrorCompletion, + cursorHorizontalPos, + ); + completions = [...completions, ...keywordCompletions]; } completions.push(codeMirrorCompletion); } diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts index d5cafa9047..09ce377cee 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts @@ -88,6 +88,88 @@ describe("dataTreeTypeDefCreator", () => { 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", () => { const def = { entity1: { diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts index b7f9a685bf..b59494285f 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts @@ -1,5 +1,5 @@ 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 { getType, Types } from "utils/TypeHelpers"; import { Def } from "tern"; @@ -11,11 +11,11 @@ import { isWidget, } from "workers/evaluationUtils"; import { DataTreeDefEntityInformation } from "utils/autocomplete/TernServer"; + +export type ExtraDef = Record; + 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 // This so that we have more info about them // when sorting results in autocomplete @@ -26,6 +26,9 @@ export const dataTreeTypeDefCreator = ( dataTree: DataTree, isJSEditorEnabled: boolean, ): { def: Def; entityInfo: Map } => { + // 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 = { "!name": "DATA_TREE", }; @@ -37,7 +40,7 @@ export const dataTreeTypeDefCreator = ( if (widgetType in entityDefinitions) { const definition = get(entityDefinitions, widgetType); if (isFunction(definition)) { - def[entityName] = definition(entity); + def[entityName] = definition(entity, extraDefsToDefine); } else { def[entityName] = definition; } @@ -48,21 +51,21 @@ export const dataTreeTypeDefCreator = ( }); } } else if (isAction(entity)) { - def[entityName] = entityDefinitions.ACTION(entity); + def[entityName] = entityDefinitions.ACTION(entity, extraDefsToDefine); flattenDef(def, entityName); entityMap.set(entityName, { type: ENTITY_TYPE.ACTION, subType: "ACTION", }); } else if (isAppsmithEntity(entity)) { - def.appsmith = (entityDefinitions.APPSMITH as any)(entity); + def.appsmith = entityDefinitions.APPSMITH(entity, extraDefsToDefine); entityMap.set("appsmith", { type: ENTITY_TYPE.APPSMITH, subType: ENTITY_TYPE.APPSMITH, }); } else if (isJSAction(entity) && isJSEditorEnabled) { const metaObj = entity.meta; - const jsProperty: Def = {}; + const jsPropertiesDef: Def = {}; for (const key in metaObj) { // const jsFunctionObj = metaObj[key]; @@ -72,42 +75,64 @@ export const dataTreeTypeDefCreator = ( // we will also need to check performance implications here const argsTypeString = getFunctionsArgsType([]); - jsProperty[key] = argsTypeString; + jsPropertiesDef[key] = argsTypeString; } for (let i = 0; i < entity.variables.length; i++) { const varKey = entity.variables[i]; const varValue = entity[varKey]; - jsProperty[varKey] = generateTypeDef(varValue); + jsPropertiesDef[varKey] = generateTypeDef(varValue, extraDefsToDefine); } - def[entityName] = jsProperty; + def[entityName] = jsPropertiesDef; flattenDef(def, entityName); entityMap.set(entityName, { type: ENTITY_TYPE.JSACTION, subType: "JSACTION", }); } - if (Object.keys(extraDefs)) { - def["!define"] = { ...extraDefs }; - extraDefs = {}; - } }); + if (Object.keys(extraDefsToDefine)) { + def["!define"] = { ...extraDefsToDefine }; + } + return { def, entityInfo: entityMap }; }; -export function generateTypeDef(obj: any): string | Def { - const type = getType(obj); - switch (type) { +export function generateTypeDef( + value: unknown, + extraDefsToDefine?: ExtraDef, + depth = 0, +): Def | string { + switch (getType(value)) { case Types.ARRAY: { - const arrayType = getType(obj[0]); - return `[${arrayType}]`; + const array = value as [unknown]; + 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: { const objType: Def = {}; - Object.keys(obj).forEach((k) => { - objType[k] = generateTypeDef(obj[k]); + const object = value as Record; + Object.keys(object).forEach((k) => { + objType[k] = generateTypeDef(object[k], extraDefsToDefine, depth); }); return objType; } diff --git a/app/client/src/utils/autocomplete/keywordCompletion.ts b/app/client/src/utils/autocomplete/keywordCompletion.ts new file mode 100644 index 0000000000..564aa960b3 --- /dev/null +++ b/app/client/src/utils/autocomplete/keywordCompletion.ts @@ -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; +};