diff --git a/.version b/.version index d8b698973a..fb2c0766b7 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.12.0 +2.13.0 diff --git a/cypress-tests/cypress/constants/texts/common.js b/cypress-tests/cypress/constants/texts/common.js index 2132af075a..d59a94a7b5 100644 --- a/cypress-tests/cypress/constants/texts/common.js +++ b/cypress-tests/cypress/constants/texts/common.js @@ -170,7 +170,7 @@ export const commonText = { // iframeLinkLabel: "Get embeddable link for this application", // ifameLinkCopyButton: "copy", }, - groupInputFieldLabel: "Select Group" + groupInputFieldLabel: "Select Group", }; export const commonWidgetText = { @@ -199,7 +199,7 @@ export const commonWidgetText = { codeMirrorInputTrue: codeMirrorInputLabel(true), codeMirrorInputFalse: codeMirrorInputLabel("false"), - addEventHandlerLink: "+ Add event handler", + addEventHandlerLink: "Add handler", inspectorComponentLabel: "components", componentValueLabel: "Value", labelDefaultValue: "Default Value", diff --git a/cypress-tests/cypress/constants/texts/multipage.js b/cypress-tests/cypress/constants/texts/multipage.js index f827047c61..e5779bf5b6 100644 --- a/cypress-tests/cypress/constants/texts/multipage.js +++ b/cypress-tests/cypress/constants/texts/multipage.js @@ -15,7 +15,7 @@ export const multipageText = { optionEventHandler: "Event Handlers", eventModalTitle: "Page Events", labelEvents: "Events", - addEventHandlerLink: "+ Add event handler", + addEventHandlerLink: "Add handler", noEventHandlerInfo: "This page doesn't have any event handlers", optionDeletePage: "Delete Page", diff --git a/cypress-tests/cypress/e2e/editor/multipage/multipageHappypath.cy.js b/cypress-tests/cypress/e2e/editor/multipage/multipageHappypath.cy.js index 519ded4f4f..3142bf4b4a 100644 --- a/cypress-tests/cypress/e2e/editor/multipage/multipageHappypath.cy.js +++ b/cypress-tests/cypress/e2e/editor/multipage/multipageHappypath.cy.js @@ -193,7 +193,7 @@ describe("Multipage", () => { multipageText.labelEvents ); cy.get(multipageSelector.addEventHandlerLink).verifyVisibleElement( - "have.text", + "contain.text", multipageText.addEventHandlerLink ); cy.get(multipageSelector.noEventHandlerMessage).verifyVisibleElement( diff --git a/cypress-tests/cypress/e2e/editor/queries/runjsHappyPath.cy.js b/cypress-tests/cypress/e2e/editor/queries/runjsHappyPath.cy.js index bc1e3ea65f..4243b13786 100644 --- a/cypress-tests/cypress/e2e/editor/queries/runjsHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/editor/queries/runjsHappyPath.cy.js @@ -33,7 +33,7 @@ import { } from "Support/utils/events"; import { - selectQuery, + selectQueryFromLandingPage, deleteQuery, query, changeQueryToggles, @@ -66,17 +66,15 @@ describe("RunJS", () => { cy.createApp(); cy.viewport(1800, 1800); cy.dragAndDropWidget("Button"); - resizeQueryPanel("50"); + resizeQueryPanel("80"); }); it("should verify basic runjs", () => { const data = {}; data.customText = randomString(12); - selectQuery("Run JavaScript code"); + selectQueryFromLandingPage("runjs", "JavaScript"); addInputOnQueryField("runjs", "return true"); - query("create"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); query("preview"); verifypreview("raw", "true"); query("run"); @@ -92,7 +90,7 @@ describe("RunJS", () => { const data = {}; data.customText = randomString(12); - selectQuery("Run JavaScript code"); + selectQueryFromLandingPage("runjs", "JavaScript"); addInputOnQueryField( "runjs", `setTimeout(() => { @@ -101,7 +99,6 @@ describe("RunJS", () => { }, [0]) ` ); query("run"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); cy.get(commonWidgetSelector.sidebarinspector).click(); cy.get(".tooltip-inner").invoke("hide"); verifyNodeData("variables", "Object", "1 entry "); @@ -134,7 +131,6 @@ describe("RunJS", () => { "actions.showAlert('success', 'alert from runjs');" ); query("run"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Saved"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); cy.get(multipageSelector.sidebarPageButton).click(); @@ -154,8 +150,6 @@ describe("RunJS", () => { addInputOnQueryField("runjs", "actions.closeModal('modal1');"); query("run"); - cy.intercept("GET", "api/data_queries?**").as("addQuery"); - cy.wait("@addQuery"); cy.wait(200); cy.notVisible('[data-cy="modal-title"]'); @@ -164,7 +158,6 @@ describe("RunJS", () => { "actions.copyToClipboard('data from runjs');" ); query("run"); - cy.wait("@addQuery"); cy.window().then((win) => { win.navigator.clipboard.readText().then((text) => { @@ -176,7 +169,6 @@ describe("RunJS", () => { "actions.setLocalStorage('localStorage','data from runjs');" ); query("run"); - cy.wait("@addQuery"); cy.getAllLocalStorage().then((result) => { expect(result[Cypress.config().baseUrl].localStorage).to.deep.equal( @@ -189,8 +181,6 @@ describe("RunJS", () => { "actions.generateFile('runjscsv', 'csv', [{ name: 'John', email: 'john@tooljet.com' }])" ); query("run"); - cy.wait("@addQuery"); - cy.wait(3000); cy.readFile("cypress/downloads/runjscsv.csv", "utf-8") .should("contain", "name,email") @@ -201,11 +191,9 @@ describe("RunJS", () => { // "actions.goToApp('111234')" // ); // query("run"); - // cy.wait("@addQuery"); + addInputOnQueryField("runjs", "actions.logout()"); query("run"); - cy.wait("@addQuery"); - cy.wait(3000); cy.get('[data-cy="sign-in-header"]').should("be.visible"); }); @@ -213,10 +201,8 @@ describe("RunJS", () => { const data = {}; data.customText = randomString(12); - selectQuery("Run JavaScript code"); + selectQueryFromLandingPage("runjs", "JavaScript"); addInputOnQueryField("runjs", "return [page.handle,page.name]"); - query("create"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); query("preview"); verifypreview("raw", `["home","Home"]`); @@ -267,22 +253,20 @@ describe("RunJS", () => { const data = {}; data.customText = randomString(12); - selectQuery("Run JavaScript code"); + selectQueryFromLandingPage("runjs", "JavaScript"); addInputOnQueryField( "runjs", "actions.showAlert('success', 'alert from runjs');" ); - query("create"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); query("run"); - + openEditorSidebar("button1"); selectEvent("On Click", "Run query", 1); cy.get('[data-cy="query-selection-field"]').type("runjs1{enter}"); cy.get(commonWidgetSelector.draggableWidget("button1")).click(); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); renameQueryFromEditor("newrunjs"); - cy.wait(3000); + cy.waitForAutoSave(); cy.get('[data-cy="event-handler"]').click(); cy.get('[data-cy="query-selection-field"]').should("have.text", "newrunjs"); @@ -291,26 +275,25 @@ describe("RunJS", () => { }); it("should verify runjs toggle options", () => { + cy.intercept("PATCH", "api/data_queries/**").as("editQuery"); const data = {}; data.customText = randomString(12); - selectQuery("Run JavaScript code"); + selectQueryFromLandingPage("runjs", "JavaScript"); addInputOnQueryField( "runjs", "actions.showAlert('success', 'alert from runjs');" ); - query("create"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); changeQueryToggles("run-on-app-load"); - query("save"); + cy.wait(`@editQuery`); + cy.waitForAutoSave(); cy.reload(); - cy.wait(3000); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); changeQueryToggles("confirmation-before-run"); - query("save"); + cy.wait(`@editQuery`); + cy.waitForAutoSave(); cy.reload(); - cy.wait(3000); cy.get('[data-cy="modal-message"]').verifyVisibleElement( "have.text", "Do you want to run this query - runjs1?" @@ -318,13 +301,15 @@ describe("RunJS", () => { cy.get('[data-cy="modal-confirm-button"]').realClick(); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); + resizeQueryPanel("80"); changeQueryToggles("notification-on-success"); cy.get('[data-cy="success-message-input-field"]').clearAndTypeOnCodeMirror( "Success alert" ); - query("save"); + cy.get('[data-cy="runjs-input-field"]').realClick(); + cy.wait(1000); + cy.waitForAutoSave(); cy.reload(); - cy.wait(3000); cy.get('[data-cy="modal-confirm-button"]').realClick(); cy.verifyToastMessage(commonSelectors.toastMessage, "Success alert"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); diff --git a/cypress-tests/cypress/e2e/editor/queries/runpyHappyPath.cy.js b/cypress-tests/cypress/e2e/editor/queries/runpyHappyPath.cy.js index c3a2fa3cbe..707227dc22 100644 --- a/cypress-tests/cypress/e2e/editor/queries/runpyHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/editor/queries/runpyHappyPath.cy.js @@ -33,12 +33,12 @@ import { } from "Support/utils/events"; import { - selectQuery, - deleteQuery, + selectQueryFromLandingPage, query, changeQueryToggles, renameQueryFromEditor, addInputOnQueryField, + waitForQueryAction, } from "Support/utils/queries"; import { @@ -66,17 +66,17 @@ describe("runpy", () => { cy.createApp(); cy.viewport(1800, 1800); cy.dragAndDropWidget("Button"); - resizeQueryPanel("50"); + resizeQueryPanel("80"); + cy.intercept("PATCH", "api/data_queries/**").as("editQuery"); }); it("should verify basic runpy", () => { const data = {}; data.customText = randomString(12); - selectQuery("Run Python code"); + selectQueryFromLandingPage("runpy", "Python"); addInputOnQueryField("runpy", "True"); - query("create"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); + cy.waitForAutoSave(); query("preview"); verifypreview("raw", "true"); query("run"); @@ -92,14 +92,14 @@ describe("runpy", () => { const data = {}; data.customText = randomString(12); - selectQuery("Run Python code"); + selectQueryFromLandingPage("runpy", "Python"); addInputOnQueryField( "runpy", `actions.setVariable('var', 'test') actions.setPageVariable('pageVar', 'pageTest')` ); + cy.waitForAutoSave(); query("run"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); cy.get(commonWidgetSelector.sidebarinspector).click(); cy.get(".tooltip-inner").invoke("hide"); verifyNodeData("variables", "Object", "1 entry "); @@ -130,9 +130,12 @@ actions.unsetPageVariable('pageVar')` "actions.showAlert('success', 'alert from runpy')" ); query("run"); - // cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); - cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy"); + cy.verifyToastMessage( + commonSelectors.toastMessage, + "alert from runpy", + false + ); cy.get(multipageSelector.sidebarPageButton).click(); addNewPage("test_page"); cy.url().should("contain", "/test-page"); @@ -146,22 +149,21 @@ actions.unsetPageVariable('pageVar')` cy.waitForAutoSave(); addInputOnQueryField("runpy", "actions.showModal('modal1')"); query("run"); - cy.closeToastMessage(); cy.get('[data-cy="modal-title"]').should("be.visible"); cy.get('[data-cy="runpy-input-field"]').click({ force: true }); addInputOnQueryField("runpy", "actions.closeModal('modal1')"); - cy.wait(2000); + cy.wait(`@editQuery`); + cy.waitForAutoSave(); query("run"); - cy.intercept("GET", "api/data_queries?**").as("addQuery"); - cy.wait("@addQuery"); - cy.wait(10000); + waitForQueryAction("run"); cy.notVisible('[data-cy="modal-title"]'); addInputOnQueryField("runpy", "actions.copyToClipboard('data from runpy')"); + cy.wait(`@editQuery`); + cy.waitForAutoSave(); query("run"); - cy.wait("@addQuery"); - cy.wait(10000); + waitForQueryAction("run"); cy.window().then((win) => { win.navigator.clipboard.readText().then((text) => { expect(text).to.eq("data from runpy"); @@ -171,9 +173,10 @@ actions.unsetPageVariable('pageVar')` "runpy", "actions.setLocalStorage('localStorage','data from runpy')" ); + cy.wait(`@editQuery`); + cy.waitForAutoSave(); query("run"); - cy.wait("@addQuery"); - cy.wait(10000); + waitForQueryAction("run"); cy.getAllLocalStorage().then((result) => { expect(result[Cypress.config().baseUrl].localStorage).to.deep.equal( @@ -186,7 +189,7 @@ actions.unsetPageVariable('pageVar')` // "actions.generateFile('runpycsv', 'csv', [{ 'name': 'John', 'email': 'john@tooljet.com' }])" // ); // query("run"); - // cy.wait("@addQuery"); + // cy.verifyToastMessage( // commonSelectors.toastMessage, // "Query (runpy1) completed." @@ -203,11 +206,13 @@ actions.unsetPageVariable('pageVar')` // "actions.goToApp('111234')" // ); // query("run"); - // cy.wait("@addQuery"); + addInputOnQueryField("runpy", "actions.logout()"); + cy.wait(`@editQuery`); + cy.wait(200); + cy.waitForAutoSave(); query("run"); - cy.wait("@addQuery"); - cy.wait(3000); + waitForQueryAction("run"); cy.get('[data-cy="sign-in-header"]').should("be.visible"); }); @@ -215,10 +220,9 @@ actions.unsetPageVariable('pageVar')` const data = {}; data.customText = randomString(12); - selectQuery("Run Python code"); + selectQueryFromLandingPage("runpy", "Python"); addInputOnQueryField("runpy", "tj_globals.theme"); - query("create"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); + cy.waitForAutoSave(); query("preview"); verifypreview("raw", `{"name":"light"}`); @@ -236,8 +240,11 @@ actions.unsetPageVariable('pageVar')` verifypreview("raw", `Developer`); addInputOnQueryField("runpy", "tj_globals.currentUser.groups"); query("preview"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query completed."); - cy.wait(10000); + cy.verifyToastMessage( + commonSelectors.toastMessage, + "Query (runpy1) completed." + ); + waitForQueryAction("preview"); verifypreview("raw", `["all_users","admin"]`); if (Cypress.env("environment") != "Community") { addInputOnQueryField("runpy", "tj_globals.mode.value"); @@ -261,21 +268,20 @@ actions.unsetPageVariable('pageVar')` const data = {}; data.customText = randomString(12); - selectQuery("Run Python code"); + selectQueryFromLandingPage("runpy", "Python"); addInputOnQueryField( "runpy", "actions.showAlert('success', 'alert from runpy');" ); - query("create"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); + cy.waitForAutoSave(); query("run"); + openEditorSidebar("button1"); selectEvent("On Click", "Run query", 1); cy.get('[data-cy="query-selection-field"]').type("runpy1{enter}"); cy.get(commonWidgetSelector.draggableWidget("button1")).click(); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy"); renameQueryFromEditor("newrunpy"); - cy.wait(3000); cy.get('[data-cy="event-handler"]').click(); cy.get('[data-cy="query-selection-field"]').should("have.text", "newrunpy"); @@ -287,23 +293,26 @@ actions.unsetPageVariable('pageVar')` const data = {}; data.customText = randomString(12); - selectQuery("Run Python code"); + selectQueryFromLandingPage("runpy", "Python"); + cy.waitForAutoSave(); addInputOnQueryField( "runpy", "actions.showAlert('success', 'alert from runpy');" ); - query("create"); - cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); + cy.wait("@editQuery"); + cy.wait(200); + cy.waitForAutoSave(); changeQueryToggles("run-on-app-load"); - query("save"); + cy.wait("@editQuery"); + cy.waitForAutoSave(); cy.reload(); - cy.wait(3000); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy"); changeQueryToggles("confirmation-before-run"); - query("save"); + cy.wait("@editQuery"); + cy.wait(200); + cy.waitForAutoSave(); cy.reload(); - cy.wait(3000); cy.get('[data-cy="modal-message"]').verifyVisibleElement( "have.text", "Do you want to run this query - runpy1?" @@ -315,10 +324,12 @@ actions.unsetPageVariable('pageVar')` cy.get('[data-cy="success-message-input-field"]').clearAndTypeOnCodeMirror( "Success alert" ); - query("save"); + cy.forceClickOnCanvas(); + cy.wait("@editQuery"); + cy.wait(200); + cy.waitForAutoSave(); cy.reload(); - cy.wait(4000); - cy.get('[data-cy="modal-confirm-button"]').realClick(); + cy.get('[data-cy="modal-confirm-button"]', { timeout: 10000 }).realClick(); cy.verifyToastMessage(commonSelectors.toastMessage, "Success alert", false); cy.verifyToastMessage( commonSelectors.toastMessage, diff --git a/cypress-tests/cypress/e2e/editor/widget/componentsBasicHappypath.cy.js b/cypress-tests/cypress/e2e/editor/widget/componentsBasicHappypath.cy.js index 1a72e5dd07..973255ecf1 100644 --- a/cypress-tests/cypress/e2e/editor/widget/componentsBasicHappypath.cy.js +++ b/cypress-tests/cypress/e2e/editor/widget/componentsBasicHappypath.cy.js @@ -29,6 +29,7 @@ import { commonWidgetText, codeMirrorInputLabel, } from "Texts/common"; +import { resizeQueryPanel } from "Support/utils/dataSource"; describe("Basic components", () => { const data = {}; @@ -37,7 +38,6 @@ describe("Basic components", () => { cy.appUILogin(); cy.createApp(); cy.modifyCanvasSize(900, 900); - cy.get('[data-tooltip-id="tooltip-for-hide-query-editor"]').click(); cy.renameApp(data.appName); cy.intercept("GET", "/api/comments/*").as("loadComments"); }); @@ -617,7 +617,9 @@ describe("Basic components", () => { }); it("Should verify Tabs", () => { - cy.dragAndDropWidget("Tabs", 50, 50); + cy.viewport(1200, 1300); + resizeQueryPanel("0"); + cy.dragAndDropWidget("Tabs", 100, 100); verifyComponent("tabs1"); deleteComponentAndVerify("image1"); diff --git a/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js b/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js index 1d9e3403f3..e1c7dcdbcf 100644 --- a/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js +++ b/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js @@ -368,7 +368,7 @@ describe("Table", () => { verifyAndEnterColumnOptionInput("Text color", "red"); verifyAndEnterColumnOptionInput( "Cell Background Color", - "{backspace}{backspace}{backspace}{backspace}{backspace}yellow" + "{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}yellow" ); cy.get( '[data-cy="input-and-label-cell-background-color"] > .form-label' diff --git a/cypress-tests/cypress/support/utils/commonWidget.js b/cypress-tests/cypress/support/utils/commonWidget.js index 1473889932..2ff1feef33 100644 --- a/cypress-tests/cypress/support/utils/commonWidget.js +++ b/cypress-tests/cypress/support/utils/commonWidget.js @@ -67,7 +67,7 @@ export const verifyAndModifyToggleFx = ( export const addDefaultEventHandler = (message) => { cy.get(commonWidgetSelector.addEventHandlerLink) - .should("have.text", commonWidgetText.addEventHandlerLink) + .should("contain.text", commonWidgetText.addEventHandlerLink) .click(); cy.get(commonWidgetSelector.eventHandlerCard).click(); cy.get(commonWidgetSelector.alertMessageInputField) diff --git a/cypress-tests/cypress/support/utils/queries.js b/cypress-tests/cypress/support/utils/queries.js index 91db54985f..69912228a6 100644 --- a/cypress-tests/cypress/support/utils/queries.js +++ b/cypress-tests/cypress/support/utils/queries.js @@ -1,12 +1,12 @@ import { postgreSqlSelector } from "Selectors/postgreSql"; -export const selectQuery = (dbName) => { - cy.get(postgreSqlSelector.buttonAddNewQueries).click(); +export const selectQueryFromLandingPage = (dbName, label) => { cy.get( `[data-cy="${dbName.toLowerCase().replace(/\s+/g, "-")}-add-query-card"]` ) - .should("contain", dbName) + .should("contain", label) .click(); + cy.waitForAutoSave(); }; export const deleteQuery = (queryName) => { @@ -34,4 +34,12 @@ export const addInputOnQueryField = (field, data) => { .click() .clearAndTypeOnCodeMirror(`{backSpace}`); cy.get(`[data-cy="${field}-input-field"]`).clearAndTypeOnCodeMirror(data); + cy.forceClickOnCanvas(); +}; + +export const waitForQueryAction = (action) => { + cy.get(`[data-cy="query-${action}-button"]`, { timeout: 20000 }).should( + "not.have.class", + "button-loading" + ); }; diff --git a/cypress-tests/cypress/support/utils/table.js b/cypress-tests/cypress/support/utils/table.js index aaf26e8e59..048734d5e7 100644 --- a/cypress-tests/cypress/support/utils/table.js +++ b/cypress-tests/cypress/support/utils/table.js @@ -67,8 +67,10 @@ export const verifyAndEnterColumnOptionInput = (label, value) => { cy.get(`[data-cy="input-and-label-${cyParamName(label)}"]`) .realClick() .realPress(["Meta", "A"]) + .realType(`{backspace}{backspace}{backspace}{backspace}`) + .realPress(["Meta", "A"]) .realType( - `{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}${value}` + `{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}${value}` ); }; diff --git a/frontend/assets/translations/de.json b/frontend/assets/translations/de.json index 99fce5af3f..00b8125fe3 100644 --- a/frontend/assets/translations/de.json +++ b/frontend/assets/translations/de.json @@ -180,7 +180,7 @@ "table": "Tabelle", "pageIndex": "Seitenindex", "component": "Komponente", - "addHandler": "+ Handler hinzufügen", + "addHandler": "Handler hinzufügen", "addEventHandler": "+ Ereignishandler hinzufügen", "emptyMessage": "Dieser {{componentName}} hat keine Ereignishandler" } @@ -914,4 +914,4 @@ "tip": "Zurück zur Startseite" } } -} +} \ No newline at end of file diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index 127618df03..19170d2051 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -181,7 +181,7 @@ "events": "Events", "transformation": { "transformationToolTip": "Transformations can be enabled on queries to transform the query results. ToolJet allows you to transform the query results using two programming languages: JavaScript and Python", - "transformations": "Enable Transformations" + "transformations": "Transformations" } }, "inspector": { @@ -204,7 +204,7 @@ "table": "Table", "pageIndex": "Page index", "component": "Component", - "addHandler": "+ Add handler", + "addHandler": "Add handler", "addEventHandler": "+ Add event handler", "emptyMessage": "This {{componentName}} doesn't have any event handlers", "page": "Page" @@ -259,7 +259,7 @@ "enterLastName": "Enter Last Name", "enterEmail": "Enter email id", "enterFulltName": "Enter full name", - "inviteNewUsers":"Invite new users" + "inviteNewUsers": "Invite new users" }, "manageGroups": { "permissions": { diff --git a/frontend/assets/translations/es.json b/frontend/assets/translations/es.json index 5a64b05b8d..7776a1fb29 100644 --- a/frontend/assets/translations/es.json +++ b/frontend/assets/translations/es.json @@ -180,7 +180,7 @@ "table": "Tabla", "pageIndex": "Índice de página", "component": "Componente", - "addHandler": "+ Añadir manejador", + "addHandler": "Añadir manejador", "addEventHandler": "+ Añadir manejador de eventos", "emptyMessage": "Este {{componentName}} no tiene manejadores de eventos." } diff --git a/frontend/assets/translations/fr.json b/frontend/assets/translations/fr.json index f663547012..16de18b8bd 100644 --- a/frontend/assets/translations/fr.json +++ b/frontend/assets/translations/fr.json @@ -180,7 +180,7 @@ "table": "Table", "pageIndex": "Index de page", "component": "Composante", - "addHandler": "+ Ajouter le gestionnaire", + "addHandler": "Ajouter le gestionnaire", "addEventHandler": "+ Ajouter un gestionnaire d'événements", "emptyMessage": "Ce {{componentName}} n'a pas de gestionnaires d'événements" } @@ -909,10 +909,9 @@ "maxHeightOfCanvas": "Hauteur maximale de la toile", "backgroundColorOfCanvas": "Couleur d'arrière-plan de la toile" }, - "Back": - { + "Back": { "text": "Retour", "tip": "De retour à la maison" } } -} +} \ No newline at end of file diff --git a/frontend/assets/translations/it.json b/frontend/assets/translations/it.json index 386e1779db..652f20114c 100644 --- a/frontend/assets/translations/it.json +++ b/frontend/assets/translations/it.json @@ -180,7 +180,7 @@ "table": "Tabella", "pageIndex": "Indice delle Pagine", "component": "Componente", - "addHandler": "+ Aggiungi handler", + "addHandler": "Aggiungi handler", "addEventHandler": "+ Aggiungi handler eventi", "emptyMessage": "Questo {{componentName}} non ha handler eventi" } diff --git a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx index 04f8daf553..82f28105c9 100644 --- a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx +++ b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx @@ -305,7 +305,7 @@ export function CodeHinter({ className={`row${height === '150px' || height === '300px' ? ' tablr-gutter-x-0' : ''} custom-row`} style={{ width: width, display: codeShow ? 'flex' : 'none' }} > -
+
{ const dataQueries = useDataQueries(); @@ -44,7 +45,9 @@ export const CustomComponent = (props) => { if (e.data.message === 'UPDATE_DATA') { setCustomProps({ ...customPropRef.current, ...e.data.updatedObj }); } else if (e.data.message === 'RUN_QUERY') { - const filteredQuery = dataQueryRef.current.filter((query) => query.name === e.data.queryName); + const filteredQuery = dataQueryRef.current.filter( + (query) => query.name === e.data.queryName && isQueryRunnable(query) + ); const parameters = e.data.parameters ? JSON.parse(e.data.parameters) : {}; filteredQuery.length === 1 && fireEvent('onTrigger', { diff --git a/frontend/src/Editor/Components/Table/columns/index.jsx b/frontend/src/Editor/Components/Table/columns/index.jsx index 1808ce0b2e..c01424a662 100644 --- a/frontend/src/Editor/Components/Table/columns/index.jsx +++ b/frontend/src/Editor/Components/Table/columns/index.jsx @@ -99,10 +99,10 @@ export default function generateColumnsData({ const rowChangeSet = updatedChangeSet ? updatedChangeSet[cell.row.index] : null; let cellValue = rowChangeSet ? rowChangeSet[column.key || column.name] ?? cell.value : cell.value; - const rowData = tableData[cell.row.index]; + const rowData = tableData?.[cell?.row?.index]; if ( cell.row.index === 0 && - variablesExposedForPreview && + !_.isEmpty(variablesExposedForPreview) && !_.isEqual(variablesExposedForPreview[id]?.rowData, rowData) ) { const customResolvables = {}; diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 691f24ba0a..f6904c5b61 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -3,6 +3,7 @@ import { appService, authenticationService, appVersionService, orgEnvironmentVar import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { defaults, cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash'; +import { shallow } from 'zustand/shallow'; import { Container } from './Container'; import { EditorKeyHooks } from './EditorKeyHooks'; import { CustomDragLayer } from './CustomDragLayer'; @@ -21,6 +22,7 @@ import { debuggerActions, cloneComponents, removeSelectedComponent, + computeQueryState, } from '@/_helpers/appUtils'; import { Confirm } from './Viewer/Confirm'; import { Tooltip as ReactTooltip } from 'react-tooltip'; @@ -48,10 +50,10 @@ import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; +import { useAppDataStore } from '@/_stores/appDataStore'; import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; import { resetAllStores } from '@/_stores/utils'; import { setCookie } from '@/_helpers/cookie'; -import { shallow } from 'zustand/shallow'; setAutoFreeze(false); enablePatches(); @@ -61,6 +63,8 @@ class EditorComponent extends React.Component { super(props); resetAllStores(); const appId = this.props.params.id; + + useAppDataStore.getState().actions.setAppId(appId); useEditorStore.getState().actions.setIsEditorActive(true); const { socket } = createWebsocketConnection(appId); @@ -112,13 +116,10 @@ class EditorComponent extends React.Component { apps: [], queryConfirmationList: [], isSourceSelected: false, - isSaving: false, - isUnsavedQueriesAvailable: false, selectionInProgress: false, scrollOptions: {}, currentPageId: defaultPageId, pages: {}, - draftQuery: null, selectedDataSource: null, }; @@ -190,6 +191,7 @@ class EditorComponent extends React.Component { threshold: 0, }, }); + const globals = { ...this.props.currentState.globals, theme: { name: this.props.darkMode ? 'dark' : 'light' }, @@ -201,6 +203,16 @@ class EditorComponent extends React.Component { variables: {}, }; useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); + + this.appDataStoreListner = useAppDataStore.subscribe(({ isSaving } = {}) => { + if (isSaving !== this.state.isSaving) { + this.setState({ isSaving }); + } + }); + + this.dataQueriesStoreListner = useDataQueriesStore.subscribe(({ dataQueries }) => { + computeQueryState(dataQueries, this); + }, shallow); } /** @@ -252,9 +264,10 @@ class EditorComponent extends React.Component { initEventListeners() { this.socket?.addEventListener('message', (event) => { const data = event.data.replace(/^"(.+(?="$))"$/, '$1'); - if (data === 'versionReleased') this.fetchApp(); - else if (data === 'dataQueriesChanged') { - this.fetchDataQueries(this.props.editingVersion?.id); + if (data === 'versionReleased') { + this.fetchApp(); + // } else if (data === 'dataQueriesChanged') { //Commented since this need additional BE changes to work. + // this.fetchDataQueries(this.state.editingVersion?.id); //Also needs revamping to exclude notifying the client of their own changes. } else if (data === 'dataSourcesChanged') { this.fetchDataSources(this.props.editingVersion?.id); } @@ -266,6 +279,8 @@ class EditorComponent extends React.Component { this.socket && this.socket?.close(); this.subscription && this.subscription.unsubscribe(); if (config.ENABLE_MULTIPLAYER_EDITING) this.props?.provider?.disconnect(); + this.appDataStoreListner && this.appDataStoreListner(); + this.dataQueriesStoreListner && this.dataQueriesStoreListner(); useEditorStore.getState().actions.setIsEditorActive(false); } @@ -355,11 +370,7 @@ class EditorComponent extends React.Component { async () => { computeComponentState(this, this.state.appDefinition.pages[homePageId]?.components ?? {}).then(async () => { this.setWindowTitle(data.name); - useEditorStore.getState().actions.setShowComments(!!queryString.parse(this.props.location.search).threadId); - for (const event of dataDefinition.pages[homePageId]?.events ?? []) { - await this.handleEvent(event.eventId, event); - } }); } ); @@ -401,19 +412,13 @@ class EditorComponent extends React.Component { if (version?.id === this.state.app?.current_version_id) { (this.canUndo = false), (this.canRedo = false); } + useAppDataStore.getState().actions.setIsSaving(false); useAppVersionStore.getState().actions.updateEditingVersion(version); - this.setState( - { - isSaving: false, - }, - () => { - shouldWeEditVersion && this.saveEditingVersion(true); - this.fetchDataSources(this.props.editingVersion?.id); - this.fetchDataQueries(this.props.editingVersion?.id, true); - this.initComponentVersioning(); - } - ); + shouldWeEditVersion && this.saveEditingVersion(true); + this.fetchDataSources(this.props.editingVersion?.id); + this.fetchDataQueries(this.props.editingVersion?.id, true); + this.initComponentVersioning(); } }; @@ -437,10 +442,10 @@ class EditorComponent extends React.Component { this.fetchGlobalDataSources(); }; - /** - * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState - */ - dataQueriesChanged = () => { + dataQueriesChanged = (options) => { + /** + * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState + */ if (this.socket instanceof WebSocket && this.socket?.readyState === WebSocket.OPEN) { this.socket?.send( JSON.stringify({ @@ -448,9 +453,8 @@ class EditorComponent extends React.Component { data: { message: 'dataQueriesChanged', appId: this.state.appId }, }) ); - } else { - this.fetchDataQueries(this.props.editingVersion?.id); } + options?.isReloadSelf && this.fetchDataQueries(this.props.editingVersion?.id, true); }; switchSidebarTab = (tabIndex) => { @@ -509,10 +513,10 @@ class EditorComponent extends React.Component { this.currentVersion[this.state.currentPageId] = currentVersion - 1; if (!appDefinition) return; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { appDefinition, - isSaving: true, }, () => { this.props.ymap?.set('appDef', { @@ -540,10 +544,10 @@ class EditorComponent extends React.Component { this.currentVersion[this.state.currentPageId] = currentVersion + 1; if (!appDefinition) return; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { appDefinition, - isSaving: true, }, () => { this.props.ymap?.set('appDef', { @@ -569,10 +573,9 @@ class EditorComponent extends React.Component { if (opts?.versionChanged) { currentPageId = newDefinition.homePageId; - + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, currentPageId: currentPageId, appDefinition: newDefinition, appDefinitionLocalVersion: uuid(), @@ -592,9 +595,16 @@ class EditorComponent extends React.Component { }, this.handleAddPatch ); - this.setState({ isSaving: true, appDefinition: newDefinition, appDefinitionLocalVersion: uuid() }, () => { - if (!opts.skipAutoSave) this.autoSave(); - }); + useAppDataStore.getState().actions.setIsSaving(true); + this.setState( + { + appDefinition: newDefinition, + appDefinitionLocalVersion: uuid(), + }, + () => { + if (!opts.skipAutoSave) this.autoSave(); + } + ); }; handleInspectorView = () => { @@ -696,7 +706,8 @@ class EditorComponent extends React.Component { ); setStateAsync(_self, newDefinition).then(() => { computeComponentState(_self, _self.state.appDefinition.pages[currentPageId].components); - this.setState({ isSaving: true, appDefinitionLocalVersion: uuid() }); + useAppDataStore.getState().actions.setIsSaving(true); + this.setState({ appDefinitionLocalVersion: uuid() }); this.autoSave(); this.props.ymap?.set('appDef', { newDefinition: newDefinition.appDefinition, @@ -775,9 +786,9 @@ class EditorComponent extends React.Component { const hexCode = `${value?.[0]}${this.decimalToHex(value?.[1]?.a)}`; appDefinition.globalSettings[key] = hexCode; } + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition, }, () => { @@ -865,7 +876,7 @@ class EditorComponent extends React.Component { saveEditingVersion = (isUserSwitchedVersion = false) => { if (this.props.isVersionReleased && !isUserSwitchedVersion) { - this.setState({ isSaving: false }); + useAppDataStore.getState().actions.setIsSaving(false); } else if (!isEmpty(this.props?.editingVersion)) { appVersionService .save( @@ -885,14 +896,13 @@ class EditorComponent extends React.Component { saveError: false, }, () => { - this.setState({ - isSaving: false, - }); + useAppDataStore.getState().actions.setIsSaving(false); } ); }) .catch(() => { - this.setState({ saveError: true, isSaving: false }, () => { + useAppDataStore.getState().actions.setIsSaving(false); + this.setState({ saveError: true }, () => { toast.error('App could not save.'); }); }); @@ -945,10 +955,6 @@ class EditorComponent extends React.Component { runQuery = (queryId, queryName) => runQuery(this, queryId, queryName); - dataSourceModalHandler = () => { - this.dataSourceModalRef.current.dataSourceModalToggleStateHandler(); - }; - onAreaSelectionStart = (e) => { const isMultiSelect = e.inputEvent.shiftKey || this.state.selectedComponents.length > 0; this.setState((prevState) => { @@ -1030,9 +1036,9 @@ class EditorComponent extends React.Component { }, }; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: newAppDefinition, appDefinitionLocalVersion: uuid(), }, @@ -1089,10 +1095,10 @@ class EditorComponent extends React.Component { ? Object.keys(this.state.appDefinition.pages)[0] : this.state.appDefinition.homePageId; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { currentPageId: newCurrentPageId, - isSaving: true, appDefinition: newAppDefinition, appDefinitionLocalVersion: uuid(), isDeletingPage: false, @@ -1107,9 +1113,9 @@ class EditorComponent extends React.Component { }; updateHomePage = (pageId) => { + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: { ...this.state.appDefinition, homePageId: pageId, @@ -1178,9 +1184,9 @@ class EditorComponent extends React.Component { }, }; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: newAppDefinition, appDefinitionLocalVersion: uuid(), }, @@ -1203,9 +1209,9 @@ class EditorComponent extends React.Component { return; } + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: { ...this.state.appDefinition, pages: { @@ -1227,9 +1233,9 @@ class EditorComponent extends React.Component { }; updateOnPageLoadEvents = (pageId, events) => { + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: { ...this.state.appDefinition, pages: { @@ -1254,9 +1260,9 @@ class EditorComponent extends React.Component { showViewerNavigation: !this.state.appDefinition.showViewerNavigation, }; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: newAppDefinition, appDefinitionLocalVersion: uuid(), }, @@ -1284,9 +1290,9 @@ class EditorComponent extends React.Component { }, }; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: newAppDefinition, appDefinitionLocalVersion: uuid(), }, @@ -1308,9 +1314,9 @@ class EditorComponent extends React.Component { }, }; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: newAppDefinition, appDefinitionLocalVersion: uuid(), }, @@ -1332,9 +1338,9 @@ class EditorComponent extends React.Component { }, }; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: newAppDefinition, appDefinitionLocalVersion: uuid(), }, @@ -1410,9 +1416,9 @@ class EditorComponent extends React.Component { pages: pagesObj, }; + useAppDataStore.getState().actions.setIsSaving(true); this.setState( { - isSaving: true, appDefinition: newAppDefinition, appDefinitionLocalVersion: uuid(), }, @@ -1695,7 +1701,6 @@ class EditorComponent extends React.Component { allComponents={appDefinition.pages[this.state.currentPageId]?.components ?? {}} appId={appId} appDefinition={appDefinition} - dataSourceModalHandler={this.dataSourceModalHandler} editorRef={this} /> diff --git a/frontend/src/Editor/Header/EditAppName.jsx b/frontend/src/Editor/Header/EditAppName.jsx index a31c8ceb0e..35400201e9 100644 --- a/frontend/src/Editor/Header/EditAppName.jsx +++ b/frontend/src/Editor/Header/EditAppName.jsx @@ -3,7 +3,7 @@ import { ToolTip } from '@/_components'; import { appService } from '@/_services'; import { handleHttpErrorMessages, validateName } from '../../_helpers/utils'; -function EditAppName({ appId, appName, onNameChanged }) { +function EditAppName({ appId, appName = '', onNameChanged }) { const darkMode = localStorage.getItem('darkMode') === 'true'; const [name, setName] = React.useState(appName); diff --git a/frontend/src/Editor/Inspector/ActionConfigurationPanels/RunjsParamters.jsx b/frontend/src/Editor/Inspector/ActionConfigurationPanels/RunjsParamters.jsx index da2e220b93..c510c10017 100644 --- a/frontend/src/Editor/Inspector/ActionConfigurationPanels/RunjsParamters.jsx +++ b/frontend/src/Editor/Inspector/ActionConfigurationPanels/RunjsParamters.jsx @@ -19,7 +19,9 @@ function RunjsParameters({ event, darkMode, index, handlerChanged }) { return (
- + {dataQuery?.options?.parameters.map((param) => (
{param.name}
diff --git a/frontend/src/Editor/Inspector/EventManager.jsx b/frontend/src/Editor/Inspector/EventManager.jsx index 3cdf997c16..7a05d8db0c 100644 --- a/frontend/src/Editor/Inspector/EventManager.jsx +++ b/frontend/src/Editor/Inspector/EventManager.jsx @@ -13,8 +13,13 @@ import Select from '@/_ui/Select'; import defaultStyles from '@/_ui/Select/styles'; import { useTranslation } from 'react-i18next'; -import { useDataQueries } from '@/_stores/dataQueriesStore'; +import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; +import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle'; +import { Tooltip } from 'react-tooltip'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import RunjsParameters from './ActionConfigurationPanels/RunjsParamters'; +import { isQueryRunnable } from '@/_helpers/utils'; +import { shallow } from 'zustand/shallow'; export const EventManager = ({ component, @@ -27,8 +32,15 @@ export const EventManager = ({ popoverPlacement, pages, hideEmptyEventsAlert, + callerQueryId, }) => { - const dataQueries = useDataQueries(); + const dataQueries = useDataQueriesStore(({ dataQueries = [] }) => { + if (callerQueryId) { + //filter the same query getting attached to itself + return dataQueries.filter((query) => query.id != callerQueryId); + } + return dataQueries; + }, shallow); const [events, setEvents] = useState(() => component.component.definition.events || []); const [focusedEventIndex, setFocusedEventIndex] = useState(null); const { t } = useTranslation(); @@ -294,7 +306,7 @@ export const EventManager = ({
- {actionLookup[event.actionId].options?.length > 0 && ( + {actionLookup[event.actionId]?.options?.length > 0 && (
{t('editor.inspector.eventManager.actionOptions', 'Action options')}
@@ -419,15 +431,18 @@ export const EventManager = ({
ds.kind === selectedQuery?.kind) - .map((ds) => ({ label: ds.name, value: ds.id }))} - value={value.id} + className="w-100" + options={dataSources.map((ds) => ({ label: ds.name, value: ds.id }))} + value={value?.id} onChange={(value) => { const dataSource = dataSources.find((ds) => ds.id === value); onChange(dataSource); diff --git a/frontend/src/Editor/QueryManager/Components/DataSourceIcon.jsx b/frontend/src/Editor/QueryManager/Components/DataSourceIcon.jsx new file mode 100644 index 0000000000..bd34434ea6 --- /dev/null +++ b/frontend/src/Editor/QueryManager/Components/DataSourceIcon.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { getSvgIcon } from '@/_helpers/appUtils'; +import RunjsIcon from '@/Editor/Icons/runjs.svg'; +import RunTooljetDbIcon from '@/Editor/Icons/tooljetdb.svg'; +import RunpyIcon from '@/Editor/Icons/runpy.svg'; + +const DataSourceIcon = ({ source, height = 25, styles }) => { + const iconFile = source?.plugin?.iconFile?.data ?? source?.plugin?.icon_file?.data; + const Icon = () => getSvgIcon(source.kind, height, height, iconFile, styles); + + switch (source.kind) { + case 'runjs': + return ; + case 'runpy': + return ; + case 'tooljetdb': + return ; + default: + return ; + } +}; + +export default DataSourceIcon; diff --git a/frontend/src/Editor/QueryManager/Components/DataSourceLister.jsx b/frontend/src/Editor/QueryManager/Components/DataSourceLister.jsx deleted file mode 100644 index 95fb085c3b..0000000000 --- a/frontend/src/Editor/QueryManager/Components/DataSourceLister.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import RunjsIcon from '@/Editor/Icons/runjs.svg'; -import RunTooljetDbIcon from '@/Editor/Icons/tooljetdb.svg'; -import RunpyIcon from '@/Editor/Icons/runpy.svg'; -import AddIcon from '@assets/images/icons/add-source.svg'; -import { useTranslation } from 'react-i18next'; -import { getSvgIcon } from '@/_helpers/appUtils'; - -function DataSourceLister({ - dataSources, - staticDataSources, - changeDataSource, - handleBackButton, - darkMode, - dataSourceModalHandler, - showAddDatasourceBtn = true, - dataSourceBtnComponent = null, -}) { - const [allSources, setAllSources] = useState([...dataSources, ...staticDataSources]); - const { t } = useTranslation(); - const computedStyles = { - background: darkMode ? '#2f3c4c' : 'white', - color: darkMode ? 'white' : '#1f2936', - border: darkMode && '1px solid #2f3c4c', - }; - const handleChangeDataSource = (source) => { - changeDataSource(source); - handleBackButton(); - }; - - useEffect(() => { - setAllSources([...dataSources, ...staticDataSources]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataSources]); - - const fetchIconForSource = (source) => { - const iconFile = source?.plugin?.iconFile?.data ?? undefined; - const Icon = () => getSvgIcon(source.kind, 20, 20, iconFile); - - switch (source.kind) { - case 'runjs': - return ; - case 'runpy': - return ; - case 'tooljetdb': - return ; - default: - return ; - } - }; - - return ( -
- {showAddDatasourceBtn && dataSourceBtnComponent && dataSourceBtnComponent} - {allSources.map((source) => { - return ( -
{ - handleChangeDataSource(source); - }} - > - {fetchIconForSource(source)} -

- {' '} - {source.name} -

-
- ); - })} - {showAddDatasourceBtn && !dataSourceBtnComponent && ( -
- -

{t('editor.queryManager.addDatasource', 'Add datasource')}

-
- )} -
- ); -} - -export default DataSourceLister; diff --git a/frontend/src/Editor/QueryManager/Components/DataSourcePicker.jsx b/frontend/src/Editor/QueryManager/Components/DataSourcePicker.jsx new file mode 100644 index 0000000000..e7280e2659 --- /dev/null +++ b/frontend/src/Editor/QueryManager/Components/DataSourcePicker.jsx @@ -0,0 +1,175 @@ +import React, { useState, useEffect } from 'react'; +import Plus from '@/_ui/Icon/solidIcons/Plus'; +import Information from '@/_ui/Icon/solidIcons/Information'; +import Search from '@/_ui/Icon/solidIcons/Search'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { getWorkspaceId } from '@/_helpers/utils'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import { SearchBox as SearchBox2 } from '@/_components/SearchBox'; +import DataSourceIcon from './DataSourceIcon'; +import { isEmpty } from 'lodash'; +import { Col, Container, Row } from 'react-bootstrap'; +import { useDataQueriesActions } from '@/_stores/dataQueriesStore'; +import { useQueryPanelActions } from '@/_stores/queryPanelStore'; +import { Tooltip } from 'react-tooltip'; + +function DataSourcePicker({ dataSources, staticDataSources, darkMode, globalDataSources }) { + const allUserDefinedSources = [...dataSources, ...globalDataSources]; + const [searchTerm, setSearchTerm] = useState(); + const [filteredUserDefinedDataSources, setFilteredUserDefinedDataSources] = useState(allUserDefinedSources); + const navigate = useNavigate(); + const { createDataQuery } = useDataQueriesActions(); + const { setPreviewData } = useQueryPanelActions(); + + const handleChangeDataSource = (source) => { + createDataQuery(source); + setPreviewData(null); + }; + + useEffect(() => { + if (searchTerm) { + const formattedSearchTerm = searchTerm.toLowerCase(); + const filteredResults = allUserDefinedSources.filter( + ({ name, kind }) => + name.toLowerCase().includes(formattedSearchTerm) || kind.toLowerCase().includes(formattedSearchTerm) + ); + setFilteredUserDefinedDataSources(filteredResults); + } else { + setFilteredUserDefinedDataSources(allUserDefinedSources); + } + }, [searchTerm, globalDataSources, dataSources]); + + const handleAddClick = () => { + const workspaceId = getWorkspaceId(); + navigate(`/${workspaceId}/global-datasources`); + }; + + return ( + <> +

+ Connect to a datasource +

+

+ Select a datasource to start creating a new query. To know more about queries in ToolJet, you can read our +   + + documentation + +

+
+ +
+ {staticDataSources.map((source) => { + return ( + { + handleChangeDataSource(source); + }} + className="text-truncate" + data-cy={`${source.kind.toLowerCase().replace(/\s+/g, '-')}-add-query-card`} + > + {source.shortName} + + ); + })} +
+
+ + + + Add new + +
+ {isEmpty(allUserDefinedSources) ? ( + + ) : ( + + {allUserDefinedSources.length > 4 && ( + + )} + + {filteredUserDefinedDataSources.map((source) => ( + + { + handleChangeDataSource(source); + }} + data-tooltip-id="tooltip-for-query-panel-ds-picker-btn" + data-tooltip-content={source.name} + data-cy={`${String(source.name).toLowerCase().replace(/\s+/g, '-')}-add-query-card`} + > + + {source.name} + + + + ))} + + + )} +
+ + ); +} + +const EmptyDataSourceBanner = () => ( +
+
+ +
+
+ No global datasources have been added yet.
+ Add new datasources to connect to your app! 🚀 +
+
+); + +const SearchBox = ({ onSearch, darkMode, searchTerm, dataCy }) => { + const { t } = useTranslation(); + return ( + + + onSearch(e.target.value)} + onClearCallback={() => onSearch('')} + dataCy={dataCy} + /> + {/* + + */} + + + ); +}; + +export default DataSourcePicker; diff --git a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx new file mode 100644 index 0000000000..bc707d5ff9 --- /dev/null +++ b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx @@ -0,0 +1,266 @@ +import React, { useState, useEffect } from 'react'; +import Select, { components } from 'react-select'; +import { groupBy, isEmpty } from 'lodash'; +import { useNavigate } from 'react-router-dom'; +import DataSourceIcon from './DataSourceIcon'; +import { authenticationService } from '@/_services'; +import { getWorkspaceId } from '@/_helpers/utils'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore'; +import { useDataQueriesActions } from '@/_stores/dataQueriesStore'; +import { staticDataSources } from '../constants'; +import { useQueryPanelActions } from '@/_stores/queryPanelStore'; +import Search from '@/_ui/Icon/solidIcons/Search'; +import { Tooltip } from 'react-tooltip'; +import { DataBaseSources, ApiSources, CloudStorageSources } from '@/Editor/DataSourceManager/SourceComponents'; + +function DataSourceSelect({ darkMode, isDisabled, selectRef, closePopup }) { + const dataSources = useDataSources(); + const globalDataSources = useGlobalDataSources(); + const [userDefinedSources, setUserDefinedSources] = useState([...dataSources, ...globalDataSources]); + const [dataSourcesKinds, setDataSourcesKinds] = useState([]); + const [userDefinedSourcesOpts, setUserDefinedSourcesOpts] = useState([]); + const { createDataQuery } = useDataQueriesActions(); + const { setPreviewData } = useQueryPanelActions(); + const handleChangeDataSource = (source) => { + createDataQuery(source); + setPreviewData(null); + closePopup(); + }; + + console.log(dataSourcesKinds); + + useEffect(() => { + const allDataSources = [...dataSources, ...globalDataSources]; + setUserDefinedSources(allDataSources); + const dataSourceKindsList = [...DataBaseSources, ...ApiSources, ...CloudStorageSources]; + allDataSources.forEach(({ plugin }) => { + //plugin names are fetched from list data source api call only + if (isEmpty(plugin)) { + return; + } + dataSourceKindsList.push({ name: plugin.name, kind: plugin.pluginId }); + }); + setDataSourcesKinds(dataSourceKindsList); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataSources]); + + useEffect(() => { + setUserDefinedSourcesOpts( + Object.entries(groupBy(userDefinedSources, 'kind')).map(([kind, sources], index) => ({ + label: ( +
+ {index === 0 && ( +
+ Global datasources +
+ )} + + {dataSourcesKinds.find((dsk) => dsk.kind === kind)?.name || kind} +
+ ), + options: sources.map((source) => ({ + label: ( +
+ {source.name} + +
+ ), + value: source.id, + isNested: true, + source, + })), + })) + ); + }, [userDefinedSources]); + + const DataSourceOptions = [ + { + label: ( + + Defaults + + ), + isDisabled: true, + options: [ + ...staticDataSources.map((source) => ({ + label: ( +
+ {source.name} +
+ ), + value: source.id, + source, + })), + ], + }, + ...userDefinedSourcesOpts, + ]; + + const handleKeyDown = (event) => { + if (event.key === 'Escape') { + closePopup(); + } + }; + + return ( +
+ optionchanged('notificationDuration', e.target.value)} - placeholder={5} - className="form-control" - value={options.notificationDuration} - data-cy={'notification-duration-input-field'} - /> -
-
-
- ); - - const renderCustomToggle = ({ dataCy, action, translatedLabel, label }, index) => ( -
- + {renderEventManager()} +
); + }; - const renderQueryOptions = () => { - return ( -
-
- {Object.keys(customToggles).map((toggle, index) => renderCustomToggle(customToggles[toggle], index))} - {options?.showSuccessNotification && renderSuccessNotification()} -
- {renderEventManager()} + const renderChangeDataSource = () => { + const selectableDataSources = [...globalDataSources, ...dataSources].filter( + (ds) => ds.kind === selectedQuery?.kind + ); + if (isEmpty(selectableDataSources)) { + return ''; + } + return ( +
+
+ Datasource
- ); - }; - - const renderChangeDataSource = () => { - return ( -
-
- Change Datasource -
+
{ changeDataQuery(newDataSource); }} />
- ); - }; - - if (selectedQueryId !== selectedQuery?.id) return; - - return ( -
- {selectedDataSource === null ? renderDataSourcesList() : renderQueryElement()} - {selectedDataSource !== null ? renderQueryOptions() : null} - {selectedQuery?.data_source_id && mode === 'edit' && selectedDataSource !== null - ? renderChangeDataSource() - : null}
); - } -); + }; + + if (selectedQueryId !== selectedQuery?.id) return; + + return ( +
+ {selectedQuery?.data_source_id && selectedDataSource !== null ? renderChangeDataSource() : null} + + {selectedDataSource === null || !selectedQuery ? renderDataSourcesList() : renderQueryElement()} + {selectedDataSource !== null ? renderQueryOptions() : null} +
+ ); +}; + +const CustomToggleFlag = ({ dataCy, action, translatedLabel, label, value, toggleOption, darkMode, index }) => { + const [flag, setFlag] = useState(false); + + const { t } = useTranslation(); + + useEffect(() => { + setFlag(value); + }, [value]); + + return ( +
+ { + setFlag((state) => !state); + toggleOption(flag); + }} + action={action} + darkMode={darkMode} + label={t(translatedLabel, label)} + /> +
+ ); +}; diff --git a/frontend/src/Editor/QueryManager/Components/QueryManagerHeader.jsx b/frontend/src/Editor/QueryManager/Components/QueryManagerHeader.jsx index 8acb7b390c..a5e914f088 100644 --- a/frontend/src/Editor/QueryManager/Components/QueryManagerHeader.jsx +++ b/frontend/src/Editor/QueryManager/Components/QueryManagerHeader.jsx @@ -1,244 +1,245 @@ -import React, { useState, forwardRef } from 'react'; -import RunIcon from '../Icons/RunIcon'; -import BreadcrumbsIcon from '../Icons/BreadcrumbsIcon'; +import React, { useState, forwardRef, useRef, useEffect } from 'react'; import RenameIcon from '../Icons/RenameIcon'; -import PreviewIcon from '../Icons/PreviewIcon'; -import CreateIcon from '../Icons/CreateIcon'; +import FloppyDisk from '@/_ui/Icon/solidIcons/FloppyDisk'; +import Eye1 from '@/_ui/Icon/solidIcons/Eye1'; +import Play from '@/_ui/Icon/solidIcons/Play'; import cx from 'classnames'; import { toast } from 'react-hot-toast'; -import { Tooltip } from 'react-tooltip'; import { useTranslation } from 'react-i18next'; import { previewQuery, checkExistingQueryName, runQuery } from '@/_helpers/appUtils'; + import { useDataQueriesActions, useQueryCreationLoading, useQueryUpdationLoading } from '@/_stores/dataQueriesStore'; -import { useSelectedQuery, useSelectedDataSource, useUnsavedChanges } from '@/_stores/queryPanelStore'; -import ToggleQueryEditorIcon from '../Icons/ToggleQueryEditorIcon'; +import { + useSelectedQuery, + useSelectedDataSource, + usePreviewLoading, + useShowCreateQuery, + useNameInputFocussed, +} from '@/_stores/queryPanelStore'; import { useCurrentState } from '@/_stores/currentStateStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { shallow } from 'zustand/shallow'; +import { Tooltip } from 'react-tooltip'; +import { Button } from 'react-bootstrap'; -export const QueryManagerHeader = forwardRef( - ( - { - darkMode, - mode, - addNewQueryAndDeselectSelectedQuery, - updateDraftQueryName, - toggleQueryEditor, - previewLoading = false, - options, - appId, - editorRef, - }, - ref - ) => { - const { renameQuery, updateDataQuery, createDataQuery } = useDataQueriesActions(); - const selectedQuery = useSelectedQuery(); - const isCreationInProcess = useQueryCreationLoading(); - const isUpdationInProcess = useQueryUpdationLoading(); - const isUnsavedQueriesAvailable = useUnsavedChanges(); - const selectedDataSource = useSelectedDataSource(); - const { t } = useTranslation(); - const queryName = selectedQuery?.name ?? ''; - const [renamingQuery, setRenamingQuery] = useState(false); - const { queries } = useCurrentState((state) => ({ queries: state.queries }), shallow); - const { isVersionReleased, editingVersionId } = useAppVersionStore( - (state) => ({ - isVersionReleased: state.isVersionReleased, - editingVersionId: state.editingVersion?.id, - }), - shallow +export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef }, ref) => { + const { renameQuery } = useDataQueriesActions(); + const selectedQuery = useSelectedQuery(); + const selectedDataSource = useSelectedDataSource(); + const [showCreateQuery, setShowCreateQuery] = useShowCreateQuery(); + const queryName = selectedQuery?.name ?? ''; + const { queries } = useCurrentState((state) => ({ queries: state.queries }), shallow); + const { isVersionReleased } = useAppVersionStore( + (state) => ({ + isVersionReleased: state.isVersionReleased, + editingVersionId: state.editingVersion?.id, + }), + shallow + ); + + useEffect(() => { + if (selectedQuery?.name) { + setShowCreateQuery(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedQuery?.name]); + + const isInDraft = selectedQuery?.status === 'draft'; + + const executeQueryNameUpdation = (newName) => { + const { name } = selectedQuery; + if (name === newName || !newName) { + return false; + } + + const isNewQueryNameAlreadyExists = checkExistingQueryName(newName); + if (isNewQueryNameAlreadyExists) { + toast.error('Query name already exists'); + return false; + } + + if (newName) { + renameQuery(selectedQuery?.id, newName, editorRef); + return true; + } + }; + + const buttonLoadingState = (loading, disabled = false) => { + return cx( + `${loading ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`, + { 'theme-dark ': darkMode }, + { disabled: disabled || !selectedDataSource } ); + }; - const buttonText = mode === 'edit' ? 'Save' : 'Create'; - const buttonDisabled = isUpdationInProcess || isCreationInProcess; - - const executeQueryNameUpdation = (newName) => { - const { id, name } = selectedQuery; - if (name === newName) { - return setRenamingQuery(false); - } - const isNewQueryNameAlreadyExists = checkExistingQueryName(newName); - if (newName && !isNewQueryNameAlreadyExists) { - if (id === 'draftQuery') { - toast.success('Query Name Updated'); - updateDraftQueryName(newName); - } else { - renameQuery(selectedQuery?.id, newName, editorRef); - } - setRenamingQuery(false); - } else { - if (isNewQueryNameAlreadyExists) { - toast.error('Query name already exists'); - } - setRenamingQuery(false); - } + const previewButtonOnClick = () => { + const _options = { ...options }; + const query = { + data_source_id: selectedDataSource.id === 'null' ? null : selectedDataSource.id, + pluginId: selectedDataSource.pluginId, + options: _options, + kind: selectedDataSource.kind, + name: queryName, }; + const hasParamSupport = selectedQuery?.options?.hasParamSupport; + previewQuery(editorRef, query, false, undefined, hasParamSupport) + .then(() => { + ref.current.scrollIntoView(); + }) + .catch(({ error, data }) => { + console.log(error, data); + }); + }; - const createOrUpdateDataQuery = (shouldRunQuery = false) => { - if (selectedQuery?.id === 'draftQuery') return createDataQuery(appId, editingVersionId, options, shouldRunQuery); - if (isUnsavedQueriesAvailable) return updateDataQuery(options, shouldRunQuery); - shouldRunQuery && runQuery(editorRef, selectedQuery?.id, selectedQuery?.name); - }; - - const renderRenameInput = () => ( - { - event.persist(); - if (event.keyCode === 13) { - executeQueryNameUpdation(event.target.value); - } - }} - onBlur={({ target }) => executeQueryNameUpdation(target.value)} - /> - ); - - const renderBreadcrumb = () => { - if (selectedQuery === null) return; - return ( - <> - - {mode === 'create' ? 'New Query' : 'Queries'} - - - - -
- - {renamingQuery ? renderRenameInput() : queryName} - - {!isVersionReleased && ( - setRenamingQuery(true)} - > - - - )} -
- - ); - }; - - const buttonLoadingState = (loading, disabled = false) => { - return cx( - `${loading ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`, - { 'theme-dark ': darkMode }, - { disabled: disabled || !selectedDataSource } - ); - }; - - const previewButtonOnClick = () => { - const _options = { ...options }; - const query = { - data_source_id: selectedDataSource.id === 'null' ? null : selectedDataSource.id, - pluginId: selectedDataSource.pluginId, - options: _options, - kind: selectedDataSource.kind, - }; - const hasParamSupport = mode === 'create' || selectedQuery?.options?.hasParamSupport; - previewQuery(editorRef, query, false, undefined, hasParamSupport) - .then(() => { - ref.current.scrollIntoView(); - }) - .catch(({ error, data }) => { - console.log(error, data); - }); - }; - - const renderPreviewButton = () => { - return ( + const renderRunButton = () => { + const { isLoading } = queries[selectedQuery?.name] ?? false; + return ( + - ); - }; - - const renderSaveButton = () => { - return ( - - ); - }; - - const renderRunButton = () => { - const { isLoading } = queries[selectedQuery?.name] ?? false; - return ( - - ); - }; - - const renderButtons = () => { - if (selectedQuery === null) return; - return ( - <> - {renderPreviewButton()} - {renderSaveButton()} - {renderRunButton()} - - ); - }; - - return ( -
-
{renderBreadcrumb()}
-
- {renderButtons()} - - - - -
-
+ {isInDraft && } +
); - } -); + }; + + const renderButtons = () => { + if (selectedQuery === null || showCreateQuery) return; + return ( + <> + + {renderRunButton()} + + ); + }; + + return ( +
+
+ {selectedQuery && } +
+
{renderButtons()}
+
+ ); +}); + +const PreviewButton = ({ buttonLoadingState, onClick }) => { + const previewLoading = usePreviewLoading(); + const { t } = useTranslation(); + + return ( + + ); +}; + +const NameInput = ({ onInput, value, darkMode }) => { + const [isFocussed, setIsFocussed] = useNameInputFocussed(false); + const [name, setName] = useState(value); + const isVersionReleased = useAppVersionStore((state) => state.isVersionReleased); + const inputRef = useRef(); + + useEffect(() => { + setName(value); + }, [value]); + + useEffect(() => { + if (isFocussed) { + inputRef.current?.focus(); + inputRef.current?.select(); + } + }, [isFocussed]); + + const handleChange = (event) => { + const sanitizedValue = event.target.value.replace(/[ \t&]/g, ''); + setName(sanitizedValue); + }; + + const handleInput = (newName) => { + const result = onInput(newName); + if (!result) { + setName(value); + } + }; + + return ( +
+ + {isFocussed ? ( + { + event.persist(); + if (event.keyCode === 13) { + setIsFocussed(false); + handleInput(event.target.value); + } + }} + onBlur={({ target }) => { + setIsFocussed(false); + handleInput(target.value); + }} + /> + ) : ( + + )} + +
+ ); +}; diff --git a/frontend/src/Editor/QueryManager/Components/SuccessNotificationInputs.jsx b/frontend/src/Editor/QueryManager/Components/SuccessNotificationInputs.jsx new file mode 100644 index 0000000000..8599125619 --- /dev/null +++ b/frontend/src/Editor/QueryManager/Components/SuccessNotificationInputs.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { CodeHinter } from '../../CodeBuilder/CodeHinter'; + +export default function SuccessNotificationInputs({ currentState, options, darkMode, optionchanged }) { + const { t } = useTranslation(); + if (!options?.showSuccessNotification) { + return
; + } + return ( +
+
+ +
+ optionchanged('successMessage', value)} + placeholder={t('editor.queryManager.queryRanSuccessfully', 'Query ran successfully')} + cyLabel={'success-message'} + /> +
+
+
+ + {/*
*/} +
+ optionchanged('notificationDuration', e.target.value)} + placeholder={5} + className="form-control" + value={options.notificationDuration} + data-cy={'notification-duration-input-field'} + /> +
+
+
+ ); +} diff --git a/frontend/src/Editor/QueryManager/Components/Transformation.jsx b/frontend/src/Editor/QueryManager/Components/Transformation.jsx index b0b94c7386..b3d51bd0ed 100644 --- a/frontend/src/Editor/QueryManager/Components/Transformation.jsx +++ b/frontend/src/Editor/QueryManager/Components/Transformation.jsx @@ -16,6 +16,7 @@ import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles'; import { Button } from '@/_ui/LeftSidebar'; import { Tooltip as ReactTooltip } from 'react-tooltip'; import { authenticationService } from '@/_services'; +import Information from '@/_ui/Icon/solidIcons/Information'; import { useCurrentState } from '@/_stores/currentStateStore'; export const Transformation = ({ changeOption, options, darkMode, queryId }) => { @@ -76,10 +77,6 @@ return [row for row in data if row['amount'] > 1000] useEffect(() => { const selectedQueryId = localStorage.getItem('selectedQuery') ?? null; - if (queryId === 'draftQuery') { - setState(defaultValue); - return; - } if (selectedQueryId !== queryId) { const nonLangdefaultCode = getNonActiveTransformations(options?.transformationLanguage ?? 'javascript'); const finalState = _.merge( @@ -138,9 +135,12 @@ return [row for row in data if row['amount'] > 1000] }; }; - const popover = ( - -

+ const labelPopoverContent = ( + +

{t( 'editor.queryManager.transformation.transformationToolTip', 'Transformations can be enabled on queries to transform the query results. ToolJet allows you to transform the query results using two programming languages: JavaScript and Python' @@ -154,9 +154,136 @@ return [row for row in data if row['amount'] > 1000] ); - const popoverForRecommendation = ( - -

+ return ( +
+
+
+ + + {t('editor.queryManager.transformation.transformations', 'Transformations')} + + +
+
+
+ + + Enable + +
+ +
+
+
+
+
+

+
+
+
+ {enableTransformation && ( +
+
+
+
+ Language +
+ { - setLang(value); - changeOption('transformationLanguage', value); - changeOption('transformation', state[value]); - }} - placeholder={t('globals.select', 'Select') + '...'} - styles={computeSelectStyles(darkMode, 140)} - useCustomStyles={true} - /> -
- -
- -
- - {!isCopilotEnabled && ( - - )} -
-
- changeOption('transformation', value)} - componentName={`transformation`} - cyLabel={'transformation-input'} - callgpt={handleCallToGPT} - isCopilotEnabled={isCopilotEnabled} - /> -
- )} +
+ + + + + + + +
); }; diff --git a/frontend/src/Editor/QueryManager/QueryEditors/Restapi/BaseUrl.jsx b/frontend/src/Editor/QueryManager/QueryEditors/Restapi/BaseUrl.jsx index 903c3ad40c..858f6b3aa6 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/Restapi/BaseUrl.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/Restapi/BaseUrl.jsx @@ -8,9 +8,11 @@ export const BaseUrl = ({ dataSourceURL, theme }) => { style={{ padding: '5px', border: theme === 'default' ? '1px solid rgb(217 220 222)' : '1px solid white', + borderRightWidth: 0, background: theme === 'default' ? 'rgb(246 247 251)' : '#20211e', color: theme === 'default' ? '#9ca1a6' : '#9e9e9e', height: '32px', + borderRadius: '6px 0 0 6px', }} > {dataSourceURL} diff --git a/frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabBody.jsx b/frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabBody.jsx index 512b6f398b..012aa04d52 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabBody.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabBody.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import GroupHeader from './GroupHeader'; import TabContent from './TabContent'; export default ({ @@ -12,11 +11,9 @@ export default ({ onJsonBodyChange, componentName, bodyToggle, - setBodyToggle, }) => { return ( <> - { + const { t } = useTranslation(); const darkMode = localStorage.getItem('darkMode') === 'true'; return ( @@ -22,10 +27,9 @@ export default ({ options.map((option, index) => { return ( <> -
-
-
{index + 1}
-
+
+
+
-
+
-
{ removeKeyValuePair(paramType, index); }} > - - - - - -
+ +
@@ -88,25 +79,11 @@ export default ({ />
) : ( -
-
addNewKeyValuePair(paramType)} - role="button" - > - - - - - -
-
+
+ addNewKeyValuePair(paramType)}> + +   {t('editor.inspector.eventManager.addKeyValueParam', 'Add more')} +
)}
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabHeaders.jsx b/frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabHeaders.jsx index b5ee84cd3e..d7094bf8c8 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabHeaders.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabHeaders.jsx @@ -1,11 +1,9 @@ import React from 'react'; import TabContent from './TabContent'; -import GroupHeader from './GroupHeader'; export default ({ options = [], theme, removeKeyValuePair, addNewKeyValuePair, onChange, componentName }) => { return ( <> - { return ( <> - setKey(k)} defaultActiveKey="headers"> -
- +
+ {tabs.map((tab) => ( {tab} ))} + {key === 'body' && ( +
+ Raw JSON   + +
+ )}
- + diff --git a/frontend/src/Editor/QueryManager/QueryEditors/Restapi/index.jsx b/frontend/src/Editor/QueryManager/QueryEditors/Restapi/index.jsx index 23e9e03d3a..5c657e1067 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/Restapi/index.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/Restapi/index.jsx @@ -14,7 +14,7 @@ class Restapi extends React.Component { super(props); const options = defaults( { ...props.options }, - { headers: [], url_params: [], body: [], json_body: null, body_toggle: false } + { headers: [['', '']], url_params: [], body: [], json_body: null, body_toggle: false } ); this.state = { options, @@ -26,6 +26,9 @@ class Restapi extends React.Component { if (isEmpty(this.state.options['headers'])) { this.addNewKeyValuePair('headers'); } + if (isEmpty(this.state.options['method'])) { + changeOption(this, 'method', 'get'); + } setTimeout(() => { if (isEmpty(this.state.options['url_params'])) { this.addNewKeyValuePair('url_params'); @@ -54,7 +57,10 @@ class Restapi extends React.Component { const newOptions = { ...options, [option]: [...options[option], ['', '']] }; this.setState({ options: newOptions }, () => { - this.props.optionsChanged(newOptions); + //these values are set to empty array so that user can type in directly without adding new entry, hence no need to pass to parent state + if (!['headers', 'url_params', 'body'].includes(option)) { + this.props.optionsChanged(newOptions); + } }); }; @@ -88,7 +94,9 @@ class Restapi extends React.Component { handleChange = (key, keyIndex, idx) => (value) => { const lastPair = this.state.options[key][idx]; - if (this.state.options[key].length - 1 === idx && (lastPair[0] || lastPair[1])) this.addNewKeyValuePair(key); + if (this.state.options[key].length - 1 === idx && (lastPair[0] || lastPair[1])) { + this.addNewKeyValuePair(key); + } this.keyValuePairValueChanged(value, keyIndex, key, idx); }; @@ -98,11 +106,11 @@ class Restapi extends React.Component { control: (provided) => ({ ...provided, boxShadow: 'none', - backgroundColor: darkMode ? '#2b3547' : '#F1F3F5', - borderRadius: '6px 0 0 6px', + ...(darkMode && { backgroundColor: '#2b3547' }), + borderRadius: '6px', height: 32, minHeight: 32, - borderColor: darkMode ? 'inherit' : ' #D7DBDF', + borderColor: 'var(--slate7)', borderWidth: '1px', '&:hover': { backgroundColor: darkMode ? '' : '#F8F9FA', @@ -125,64 +133,72 @@ class Restapi extends React.Component { const currentValue = { label: options.method?.toUpperCase(), value: options.method }; return ( -
-
-
- { - changeOption(this, 'url', value); + changeOption(this, 'method', value); }} - placeholder="Enter request URL" - componentName={`${queryName}::url`} - mode="javascript" - lineNumbers={false} - height={'32px'} + value={currentValue} + defaultValue={{ label: 'GET', value: 'get' }} + placeholder="Method" + width={100} + height={32} + styles={this.customSelectStyles(this.props.darkMode, 91)} + useCustomStyles={true} />
-
-
-
- +
+
URL
+
+ {dataSourceURL && ( + + )} +
+ { + changeOption(this, 'url', value); + }} + placeholder={dataSourceURL ? 'Enter request endpoint' : 'Enter request URL'} + componentName={`${queryName}::url`} + mode="javascript" + lineNumbers={false} + height={'32px'} + /> +
+
+
+
+ +
+ +
); diff --git a/frontend/src/Editor/QueryManager/QueryEditors/Runjs/ParameterDetails.jsx b/frontend/src/Editor/QueryManager/QueryEditors/Runjs/ParameterDetails.jsx index 8f91f3f5a8..9cbd4c2568 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/Runjs/ParameterDetails.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/Runjs/ParameterDetails.jsx @@ -81,6 +81,7 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe onClick={() => setShowModal((show) => !show)} className="ms-2" id="runjs-param-add-btn" + data-cy={`runjs-add-param-button`} > @@ -93,11 +94,15 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe ); }; -export const PillButton = ({ name, onClick, onRemove, marginBottom }) => ( - +export const PillButton = ({ name, onClick, onRemove, marginBottom, className, size }) => ( + {onRemove && ( + + + + ); +}; + +const DataSourceSelector = ({ + sources: _sources, + search, + setSearch, + onFilterDatasourcesChange, + onBackBtnClick, + selectedDataSources, +}) => { + const searchBoxRef = useRef(null); + const [sources, setSources] = useState([]); + + useEffect(() => { + searchBoxRef.current.focus(); + }, []); + + useEffect(() => { + setSources( + _sources.filter((source) => { + if (!search || !source?.name) { + return true; + } + return source.name.toLowerCase().includes(search.toLowerCase()); + }) + ); + }, [_sources, search]); + + return ( +
+
+
+ +
+
+ setSearch(e.target.value)} + ref={searchBoxRef} + value={search} + /> +
+
+
+ {sources.map((source) => ( + + ))} +
+
+ ); +}; + +const MenuButton = ({ + id, + order, + text, + iconSrc, + disabled = false, + callback = () => null, + active, + noMargin = false, +}) => { + const handleOnClick = (e) => { + e.stopPropagation(); + callback(id, order); + }; + + return ( +
+ + + {active && } + +
+ ); +}; + +export default FilterandSortPopup; diff --git a/frontend/src/Editor/QueryPanel/QueryCard.jsx b/frontend/src/Editor/QueryPanel/QueryCard.jsx index 490ee7197a..9e147729ed 100644 --- a/frontend/src/Editor/QueryPanel/QueryCard.jsx +++ b/frontend/src/Editor/QueryPanel/QueryCard.jsx @@ -1,49 +1,33 @@ import React, { useState } from 'react'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; -import Tooltip from 'react-bootstrap/Tooltip'; -import { getSvgIcon, checkExistingQueryName } from '@/_helpers/appUtils'; -import { DataSourceTypes } from '../DataSourceManager/SourceComponents'; +import { Tooltip } from 'react-tooltip'; +import { checkExistingQueryName } from '@/_helpers/appUtils'; import { Confirm } from '../Viewer/Confirm'; import { toast } from 'react-hot-toast'; import { useDataQueriesActions, useDataQueriesStore } from '@/_stores/dataQueriesStore'; -import { useQueryPanelActions, useSelectedQuery, useUnsavedChanges } from '@/_stores/queryPanelStore'; +import { useQueryPanelActions, useSelectedQuery } from '@/_stores/queryPanelStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { shallow } from 'zustand/shallow'; +import Copy from '@/_ui/Icon/solidIcons/Copy'; +import DataSourceIcon from '../QueryManager/Components/DataSourceIcon'; +import { isQueryRunnable } from '@/_helpers/utils'; -export const QueryCard = ({ - dataQuery, - setSaveConfirmation, - setCancelData, - setDraftQuery, - darkMode = false, - editorRef, -}) => { +export const QueryCard = ({ dataQuery, darkMode = false, editorRef, appId }) => { const selectedQuery = useSelectedQuery(); - const isUnsavedChangesAvailable = useUnsavedChanges(); const { isDeletingQueryInProcess } = useDataQueriesStore(); - const { deleteDataQueries, renameQuery } = useDataQueriesActions(); - const { setSelectedQuery, setSelectedDataSource, setUnSavedChanges } = useQueryPanelActions(); + const { deleteDataQueries, renameQuery, duplicateQuery } = useDataQueriesActions(); + const { setSelectedQuery, setPreviewData } = useQueryPanelActions(); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); + const { isVersionReleased } = useAppVersionStore( (state) => ({ isVersionReleased: state.isVersionReleased, }), shallow ); + const [renamingQuery, setRenamingQuery] = useState(false); - const getSourceMetaData = (dataSource) => { - if (dataSource?.plugin_id) { - return dataSource.plugin?.manifest_file?.data.source; - } - - return DataSourceTypes.find((source) => source.kind === dataSource.kind); - }; - - const sourceMeta = getSourceMetaData(dataQuery); - const iconFile = dataQuery?.plugin?.iconFile?.data || dataQuery?.plugin?.icon_file?.data; - const icon = getSvgIcon(sourceMeta?.kind.toLowerCase(), 20, 20, iconFile); - let isSeletedQuery = false; if (selectedQuery) { isSeletedQuery = dataQuery.id === selectedQuery.id; @@ -54,24 +38,14 @@ export const QueryCard = ({ setShowDeleteConfirmation(true); }; - const cancelDeleteDataQuery = () => { - setShowDeleteConfirmation(false); - }; - const updateQueryName = (selectedQuery, newName) => { - const { id, name } = selectedQuery; + const { name } = selectedQuery; if (name === newName) { return setRenamingQuery(false); } const isNewQueryNameAlreadyExists = checkExistingQueryName(newName); if (newName && !isNewQueryNameAlreadyExists) { - if (id === 'draftQuery') { - toast.success('Query Name Updated'); - setDraftQuery((query) => ({ ...query, name: newName })); - setSelectedQuery('draftQuery', { ...dataQuery, name: newName }); - } else { - renameQuery(dataQuery?.id, newName, editorRef); - } + renameQuery(dataQuery?.id, newName, editorRef); setRenamingQuery(false); } else { if (isNewQueryNameAlreadyExists) { @@ -83,36 +57,24 @@ export const QueryCard = ({ const executeDataQueryDeletion = () => { setShowDeleteConfirmation(false); - if (dataQuery?.id === 'draftQuery') { - toast.success('Query Deleted'); - setDraftQuery(null); - setSelectedQuery(null); - setUnSavedChanges(false); - setSelectedDataSource(null); - return; - } deleteDataQueries(dataQuery?.id, editorRef); }; return ( <>
{ if (selectedQuery?.id === dataQuery?.id) return; - const stateToBeUpdated = { editingQuery: true, selectedQuery: dataQuery, draftQuery: null }; - if (isUnsavedChangesAvailable) { - setSaveConfirmation(true); - setCancelData(stateToBeUpdated); - } else { - setSelectedQuery(dataQuery?.id); - setDraftQuery(null); - } + setSelectedQuery(dataQuery?.id); + setPreviewData(null); }} role="button" > -
{icon}
+
+ +
{renamingQuery ? ( { + if (key === 'Enter') { + updateQueryName(selectedQuery, target.value); + } + }} onBlur={({ target }) => { updateQueryName(selectedQuery, target.value); }} @@ -135,7 +102,15 @@ export const QueryCard = ({ overlay={{dataQuery.name}} >
- {dataQuery.name} + + {dataQuery.name} + {' '} + + {!isQueryRunnable(dataQuery) && Draft}
)} @@ -147,7 +122,7 @@ export const QueryCard = ({ className={`col-auto ${renamingQuery && 'display-none'} rename-query`} onClick={() => setRenamingQuery(true)} > - +
+
duplicateQuery(dataQuery?.id, appId)}> + + + +
{isDeletingQueryInProcess ? (
) : ( - + )}
+
)}
- {showDeleteConfirmation ? ( - - ) : null} + setShowDeleteConfirmation(false)} + darkMode={darkMode} + /> ); }; diff --git a/frontend/src/Editor/QueryPanel/QueryDataPane.jsx b/frontend/src/Editor/QueryPanel/QueryDataPane.jsx index 5de0a51bf8..da6f74a7a5 100644 --- a/frontend/src/Editor/QueryPanel/QueryDataPane.jsx +++ b/frontend/src/Editor/QueryPanel/QueryDataPane.jsx @@ -1,99 +1,157 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { isEmpty } from 'lodash'; import { SearchBox } from '@/_components/SearchBox'; +import Minimize from '@/_ui/Icon/solidIcons/Minimize'; +import Search from '@/_ui/Icon/solidIcons/Search'; import Skeleton from 'react-loading-skeleton'; -import EmptyQueriesIllustration from '@assets/images/icons/no-queries-added.svg'; import { QueryCard } from './QueryCard'; import Fuse from 'fuse.js'; import cx from 'classnames'; +import { Tooltip } from 'react-tooltip'; import { useDataQueriesStore, useDataQueries } from '@/_stores/dataQueriesStore'; -import { useAppVersionStore } from '@/_stores/appVersionStore'; -import { shallow } from 'zustand/shallow'; +import FilterandSortPopup from './FilterandSortPopup'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import Plus from '@/_ui/Icon/solidIcons/Plus'; +import useShowPopover from '@/_hooks/useShowPopover'; +import DataSourceSelect from '../QueryManager/Components/DataSourceSelect'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import FolderEmpty from '@/_ui/Icon/solidIcons/FolderEmpty'; -export const QueryDataPane = ({ - setSaveConfirmation, - setCancelData, - draftQuery, - handleAddNewQuery, - setDraftQuery, - darkMode, - fetchDataQueries, - editorRef, -}) => { +export const QueryDataPane = ({ darkMode, fetchDataQueries, editorRef, appId, toggleQueryEditor }) => { const { t } = useTranslation(); const { loadingDataQueries } = useDataQueriesStore(); const dataQueries = useDataQueries(); const [filteredQueries, setFilteredQueries] = useState(dataQueries); - const { isVersionReleased } = useAppVersionStore( - (state) => ({ - isVersionReleased: state.isVersionReleased, - }), - shallow - ); - useEffect(() => { - setFilteredQueries(dataQueries); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(dataQueries)]); + const [showSearchBox, setShowSearchBox] = useState(false); + const searchBoxRef = useRef(null); + const [dataSourcesForFilters, setDataSourcesForFilters] = useState([]); + const [searchTermForFilters, setSearchTermForFilters] = useState(); + + useEffect(() => { + // Create a copy of the dataQueries array to perform filtering without modifying the original data. + let filteredDataQueries = [...dataQueries]; + + // Filter the dataQueries based on the selected data sources (dataSourcesForFilters). + if (!isEmpty(dataSourcesForFilters)) { + const excludedDataSources = ['runjs', 'runpy']; + filteredDataQueries = dataQueries.filter((query) => { + const queryDSId = excludedDataSources.includes(query.data_source_id) ? null : query.data_source_id; + return dataSourcesForFilters.some((source) => source.id == queryDSId && source.kind === query.kind); + }); + } + + // Apply additional filtering based on the search term (searchTermForFilters). + filterQueries(searchTermForFilters, filteredDataQueries); - const filterQueries = useCallback( - (value) => { - if (value) { - const fuse = new Fuse(dataQueries, { keys: ['name'] }); - const results = fuse.search(value); - let filterDataQueries = []; - results.every((result) => { - if (result.item.name === value) { - filterDataQueries = []; - filterDataQueries.push(result.item); - return false; - } - filterDataQueries.push(result.item); - return true; - }); - setFilteredQueries(filterDataQueries); - } else { - setFilteredQueries(dataQueries); - } - }, // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(dataQueries)] - ); + }, [JSON.stringify(dataQueries), dataSourcesForFilters, searchTermForFilters]); + + const handleFilterDatasourcesChange = (source) => { + const { id, kind } = source; + setDataSourcesForFilters((dataSourcesForFilters) => { + const exists = dataSourcesForFilters.some((item) => item.id === id && item.kind === kind); + return exists + ? dataSourcesForFilters.filter((item) => item.id !== id || item.kind !== kind) + : [...dataSourcesForFilters, source]; + }); + }; + + const filterQueries = (value, queries) => { + if (value) { + const fuse = new Fuse(queries, { keys: ['name'] }); + const results = fuse.search(value); + let filterDataQueries = []; + results.every((result) => { + if (result.item.name === value) { + filterDataQueries = []; + filterDataQueries.push(result.item); + return false; + } + filterDataQueries.push(result.item); + return true; + }); + setFilteredQueries(filterDataQueries); + } else { + setFilteredQueries(queries); + } + }; + + useEffect(() => { + showSearchBox && searchBoxRef.current.focus(); + }, [showSearchBox]); return (
-
+
-
-
+
+ + + setDataSourcesForFilters([])} + darkMode={darkMode} + /> + +
+ +
+
+
+
{ + setSearchTermForFilters(val.target.value); + }} + onClearCallback={() => setSearchTermForFilters('')} placeholder={t('globals.search', 'Search')} - customClass="query-manager-search-box-wrapper" + customClass="query-manager-search-box-wrapper flex-grow-1" + showClearButton /> + { + setSearchTermForFilters(''); + setShowSearchBox(false); + }} + data-cy={`query-search-close-button`} + > + Close +
-
{loadingDataQueries ? ( @@ -102,41 +160,26 @@ export const QueryDataPane = ({
) : ( -
+
- {draftQuery !== null ? ( - - ) : ( - '' - )} {filteredQueries.map((query) => ( ))}
- {filteredQueries.length === 0 && draftQuery === null && ( + {filteredQueries.length === 0 && (
- - - {dataQueries.length === 0 ? 'No queries added' : 'No queries found'} - + {filteredQueries.length === 0 ? : ''}
)} @@ -146,3 +189,67 @@ export const QueryDataPane = ({
); }; + +const EmptyDataSource = () => ( +
+
+ + + +
+ No queries have been added. +
+); + +const AddDataSourceButton = ({ darkMode, disabled }) => { + const [showMenu, setShowMenu] = useShowPopover(false, '#query-add-ds-popover', '#query-add-ds-popover-btn'); + const selectRef = useRef(); + + useEffect(() => { + if (showMenu) { + selectRef.current.focus(); + } + }, [showMenu]); + + return ( + + setShowMenu(false)} /> + + } + > + + { + e.stopPropagation(); + if (disabled) { + return; + } + setShowMenu((show) => !show); + }} + className="px-1 pe-3 ps-2 gap-0" + data-cy={`show-ds-popover-button`} + > + + Add + + + + ); +}; diff --git a/frontend/src/Editor/QueryPanel/QueryPanel.jsx b/frontend/src/Editor/QueryPanel/QueryPanel.jsx index 6dd633fca7..1c141c6aaf 100644 --- a/frontend/src/Editor/QueryPanel/QueryPanel.jsx +++ b/frontend/src/Editor/QueryPanel/QueryPanel.jsx @@ -2,12 +2,14 @@ import React, { useState, useRef, useCallback, useEffect } from 'react'; import { useEventListener } from '@/_hooks/use-event-listener'; import { Tooltip } from 'react-tooltip'; import { QueryDataPane } from './QueryDataPane'; -import { Confirm } from '../Viewer/Confirm'; import QueryManager from '../QueryManager/QueryManager'; import useWindowResize from '@/_hooks/useWindowResize'; -import { useQueryPanelActions, useUnsavedChanges, useSelectedQuery } from '@/_stores/queryPanelStore'; -import { useDataQueries } from '@/_stores/dataQueriesStore'; +import { useQueryPanelStore, useQueryPanelActions } from '@/_stores/queryPanelStore'; +import { useDataQueriesStore, useDataQueries } from '@/_stores/dataQueriesStore'; +import Maximize from '@/_ui/Icon/solidIcons/Maximize'; +import { cloneDeep, isEmpty, isEqual } from 'lodash'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; const QueryPanel = ({ dataQueriesChanged, @@ -17,14 +19,11 @@ const QueryPanel = ({ allComponents, appId, appDefinition, - dataSourceModalHandler, editorRef, onQueryPaneDragging, handleQueryPaneExpanding, }) => { - const { setSelectedQuery, updateQueryPanelHeight, setUnSavedChanges, setSelectedDataSource } = useQueryPanelActions(); - const isUnsavedQueriesAvailable = useUnsavedChanges(); - const selectedQuery = useSelectedQuery(); + const { updateQueryPanelHeight } = useQueryPanelActions(); const dataQueries = useDataQueries(); const queryManagerPreferences = useRef(JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {}); const queryPaneRef = useRef(null); @@ -36,28 +35,38 @@ const QueryPanel = ({ : queryManagerPreferences.current?.queryPanelHeight ?? 70 ); const [isTopOfQueryPanel, setTopOfQueryPanel] = useState(false); - const [showSaveConfirmation, setSaveConfirmation] = useState(false); - const [queryCancelData, setCancelData] = useState({}); - const [draftQuery, setDraftQuery] = useState(null); - const [editingQuery, setEditingQuery] = useState(dataQueries.length > 0); const [windowSize, isWindowResizing] = useWindowResize(); useEffect(() => { - if (!editingQuery && selectedQuery !== null && selectedQuery?.id !== 'draftQuery') { - setEditingQuery(true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedQuery?.id, editingQuery]); + const queryPanelStoreListner = useQueryPanelStore.subscribe(({ selectedQuery }, prevState) => { + if (isEmpty(prevState?.selectedQuery) || isEmpty(selectedQuery)) { + return; + } + + if (prevState?.selectedQuery?.id !== selectedQuery.id) { + return; + } + + //removing updated_at since this value changes whenever the data is updated in the BE + const formattedQuery = cloneDeep(selectedQuery); + delete formattedQuery.updated_at; + + const formattedPrevQuery = cloneDeep(prevState?.selectedQuery || {}); + delete formattedPrevQuery.updated_at; + + if (!isEqual(formattedQuery, formattedPrevQuery)) { + useDataQueriesStore.getState().actions.saveData(selectedQuery); + } + }); + + return queryPanelStoreListner; + }, []); useEffect(() => { handleQueryPaneExpanding(isExpanded); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isExpanded]); - useEffect(() => { - setEditingQuery(dataQueries.length > 0); - }, [dataQueries.length]); - useEffect(() => { onQueryPaneDragging(isDragging); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -73,14 +82,6 @@ const QueryPanel = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [windowSize.height, isExpanded, isWindowResizing]); - const createDraftQuery = useCallback((queryDetails, source) => { - setSelectedQuery(queryDetails.id, queryDetails); - setDraftQuery(queryDetails); - setSelectedDataSource(source); - setEditingQuery(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const onMouseUp = () => { setDragging(false); @@ -127,27 +128,6 @@ const QueryPanel = ({ useEventListener('mousemove', onMouseMove); useEventListener('mouseup', onMouseUp); - const handleAddNewQuery = useCallback(() => { - const stateToBeUpdated = { - selectedDataSource: null, - selectedQuery: null, - editingQuery: false, - isSourceSelected: false, - draftQuery: null, - }; - - if (isUnsavedQueriesAvailable) { - setSaveConfirmation(true); - setCancelData(stateToBeUpdated); - } else { - setSelectedDataSource(null); - setSelectedQuery(null); - setDraftQuery(null); - setEditingQuery(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isUnsavedQueriesAvailable]); - const toggleQueryEditor = useCallback(() => { queryManagerPreferences.current = { ...queryManagerPreferences.current, isExpanded: !isExpanded }; localStorage.setItem('queryManagerPreferences', JSON.stringify(queryManagerPreferences.current)); @@ -161,85 +141,47 @@ const QueryPanel = ({ }, [isExpanded]); const updateDataQueries = useCallback(() => { - setEditingQuery(true); - setDraftQuery(null); dataQueriesChanged(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const updateDraftQueryName = useCallback( - (newName) => { - setDraftQuery((query) => ({ ...query, name: newName })); - setSelectedQuery(draftQuery.id, { ...draftQuery, name: newName }); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [draftQuery] - ); - return ( <> - { - setSaveConfirmation(false); - setDraftQuery(null); - setSelectedQuery(queryCancelData?.selectedQuery?.id ?? null); - setSelectedDataSource(queryCancelData?.selectedDataSource ?? null); - setUnSavedChanges(false); - if (queryCancelData.hasOwnProperty('editingQuery')) { - setEditingQuery(queryCancelData.editingQuery); - } - }} - onCancel={() => { - setSaveConfirmation(false); - }} - confirmButtonText="Discard changes" - cancelButtonText="Continue editing" - callCancelFnOnConfirm={false} - darkMode={darkMode} - />
-
- QUERIES -
- - {isExpanded ? ( - - - - ) : ( - - - - )} - + + + +
+ Query Manager +
+
- + ); }; diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index d50821e8f6..c4d11cbfbb 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -23,6 +23,7 @@ import { stripTrailingSlash, getSubpath, excludeWorkspaceIdFromURL, + isQueryRunnable, redirectToDashboard, getWorkspaceId, } from '@/_helpers/utils'; @@ -184,7 +185,7 @@ class ViewerComponent extends React.Component { runQueries = (data_queries) => { data_queries.forEach((query) => { - if (query.options.runOnPageLoad) { + if (query.options.runOnPageLoad && isQueryRunnable(query)) { runQuery(this, query.id, query.name, undefined, 'view'); } }); diff --git a/frontend/src/Editor/Viewer/Confirm.jsx b/frontend/src/Editor/Viewer/Confirm.jsx index a9c3aeff36..5e35f3ee17 100644 --- a/frontend/src/Editor/Viewer/Confirm.jsx +++ b/frontend/src/Editor/Viewer/Confirm.jsx @@ -22,7 +22,7 @@ export function Confirm({ }, [show]); const handleClose = () => { - onCancel(); + onCancel && onCancel(); setShow(false); }; diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index fe0a90e2c4..d192529892 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -33,8 +33,11 @@ const DynamicForm = ({ optionsChanged, queryName, computeSelectStyles = false, + onBlur, + layout = 'vertical', }) => { const [computedProps, setComputedProps] = React.useState({}); + const isHorizontalLayout = layout === 'horizontal'; const currentState = useCurrentState(); const { isEditorActive } = useEditorStore( @@ -184,7 +187,8 @@ const DynamicForm = ({ value: options?.[key]?.value, ...(type === 'textarea' && { rows: rows }), ...(helpText && { helpText }), - onChange: (e) => optionchanged(key, e.target.value), + onChange: (e) => optionchanged(key, e.target.value, true), //shouldNotAutoSave is true because autosave should occur during onBlur, not after each character change (in optionchanged). + onblur: () => onBlur(), isGDS, workspaceVariables, }; @@ -356,56 +360,80 @@ const DynamicForm = ({ }; return ( -
+
{Object.keys(obj).map((key) => { const { label, type, encrypted, className } = obj[key]; const Element = getElement(type); + const isSpecificComponent = ['tooljetdb-operations'].includes(type); return ( -
-
- {label && ( - - )} - {(type === 'password' || encrypted) && selectedDataSource?.id && ( -
- handleEncryptedFieldsToggle(event, key)} +
+ {!isSpecificComponent && ( +
+ {label && ( +
- )} - {(type === 'password' || encrypted) && ( -
- - - Encrypted - -
- )} + {label} + + )} + {(type === 'password' || encrypted) && selectedDataSource?.id && ( +
+ handleEncryptedFieldsToggle(event, key)} + > + {computedProps?.[key]?.['disabled'] ? 'Edit' : 'Cancel'} + +
+ )} + {(type === 'password' || encrypted) && ( +
+ + + Encrypted + +
+ )} +
+ )} +
+
-
); })} @@ -421,17 +449,19 @@ const DynamicForm = ({ const selector = options?.[flipComponentDropdown?.key]?.value || options?.[flipComponentDropdown?.key]; return ( <> -
+
{flipComponentDropdown.commonFields && getLayout(flipComponentDropdown.commonFields)}
- {flipComponentDropdown.label && ( + {(flipComponentDropdown.label || isHorizontalLayout) && (