diff --git a/.version b/.version index 23a93836ae..5c18f9195b 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.24.5 +2.25.0 diff --git a/cypress-tests/cypress/constants/selectors/database.js b/cypress-tests/cypress/constants/selectors/database.js index 0dcf3df726..741aa5aa17 100644 --- a/cypress-tests/cypress/constants/selectors/database.js +++ b/cypress-tests/cypress/constants/selectors/database.js @@ -23,6 +23,7 @@ export const databaseSelectors = { tableKebabIcon: '[data-cy="table-kebab-icon"]', tableEditOption: '[data-cy="edit-option"]', + tableExportOption: '[data-cy="export-table-option"]', tableDeleteOption: '[data-cy="delete-option"]', editTableHeader: '[data-cy="edit-table-header"]', @@ -32,19 +33,25 @@ export const databaseSelectors = { deleteRecordButton: '[data-cy="delete-row-records-button"]', nameInputField: (value) => { - return `[data-cy="name-input-field-${value}"]` + return `[data-cy="name-input-field-${value}"]`; }, currentTable: (tableName) => { - return `[data-cy="${String(tableName).toLowerCase().replace(/\s+/g, "-")}-table"]`; + return `[data-cy="${String(tableName) + .toLowerCase() + .replace(/\s+/g, "-")}-table"]`; }, currentTableName: (tableName) => { - return `[data-cy="${String(tableName).toLowerCase().replace(/\s+/g, "-")}-table-name"]`; + return `[data-cy="${String(tableName) + .toLowerCase() + .replace(/\s+/g, "-")}-table-name"]`; }, columnHeader: (columnName) => { - return `[data-cy="${String(columnName).toLowerCase().replace(/\s+/g, "-")}-column-header"]`; + return `[data-cy="${String(columnName) + .toLowerCase() + .replace(/\s+/g, "-")}-column-header"]`; }, checkboxCell: (idColumn) => { - return `[data-cy="${idColumn}-checkbox-table-cell"]> div > input` + return `[data-cy="${idColumn}-checkbox-table-cell"]> div > input`; }, }; @@ -66,12 +73,15 @@ export const createNewRowSelectors = { serialDataTypeLabel: '[data-cy="integer-data-type-label"]', idColumnInputField: '[data-cy="id-input-field"]', - columnNameLabel: (columnName) => { - return `[data-cy="${String(columnName).toLowerCase().replace(/\s+/g, "-")}-column-name-label"]`; + return `[data-cy="${String(columnName) + .toLowerCase() + .replace(/\s+/g, "-")}-column-name-label"]`; }, columnNameInputField: (columnName) => { - return `[data-cy="${String(columnName).toLowerCase().replace(/\s+/g, "-")}-input-field"]`; + return `[data-cy="${String(columnName) + .toLowerCase() + .replace(/\s+/g, "-")}-input-field"]`; }, }; @@ -98,6 +108,20 @@ export const editRowSelectors = { idColumnNameLabel: '[data-cy="id-column-name-label"]', selectRowDropdown: '[data-cy="select-row-dropdown"]', getRowData: (rowNumber, columnName) => { - return `[data-cy="id-${String(rowNumber).toLowerCase().replace(/\s+/g, "-")}-column-${String(columnName).toLowerCase().replace(/\s+/g, "-")}-table-cell"]` - } -}; \ No newline at end of file + return `[data-cy="id-${String(rowNumber) + .toLowerCase() + .replace(/\s+/g, "-")}-column-${String(columnName) + .toLowerCase() + .replace(/\s+/g, "-")}-table-cell"]`; + }, +}; + +export const bulkUploadDataSelectors = { + bulkUploadDataButton: '[data-cy="bulk-upload-data-button"]', + bulkUploadbuttonText: '[data-cy="bulk-upload-button-text"]', + bulkUploadDataHeaderText: '[data-cy="bulk-upload-data-header"]', + templateHelperText: '[data-cy="helper-text-bulk-upload"]', + templateDownloadButton: '[data-cy="button-download-template"]', + bulkUploadInputField: '[data-cy="input-field-bulk-upload"]', + uploadDataButton: '[data-cy="upload-data-button"]', +}; diff --git a/cypress-tests/cypress/constants/selectors/exportImport.js b/cypress-tests/cypress/constants/selectors/exportImport.js index 7112bbfc46..5a4be317ea 100644 --- a/cypress-tests/cypress/constants/selectors/exportImport.js +++ b/cypress-tests/cypress/constants/selectors/exportImport.js @@ -39,4 +39,6 @@ export const importSelectors = { importAnApplication: '[data-cy="import-an-application"]', importOptionLabel: '[data-cy="import-option-label"]', importOptionInput: '[data-cy="import-option-input"]', + importAppTitle: '[data-cy="import-app-title"]', + importAppButton: '[data-cy="import-app"]', }; diff --git a/cypress-tests/cypress/constants/texts/button.js b/cypress-tests/cypress/constants/texts/button.js index 9d2b3e4942..bc1aebacd0 100644 --- a/cypress-tests/cypress/constants/texts/button.js +++ b/cypress-tests/cypress/constants/texts/button.js @@ -1,8 +1,8 @@ export const buttonText = { defaultWidgetText: "Button", defaultWidgetName: "button1", - buttonTextLabel: "Button Text", - loadingState: "Loading State", + buttonTextLabel: "Button text", + loadingState: "Loading state", buttonDocumentationLink: "Read documentation for Button", backgroundColor: "Background Color", textColor: "Text color", diff --git a/cypress-tests/cypress/constants/texts/common.js b/cypress-tests/cypress/constants/texts/common.js index 9e2604e1eb..1faff84416 100644 --- a/cypress-tests/cypress/constants/texts/common.js +++ b/cypress-tests/cypress/constants/texts/common.js @@ -196,13 +196,13 @@ export const commonWidgetText = { parameterShowOnMobile: "Show on mobile", parameterVisibility: "Visibility", parameterDisable: "Disable", - parameterBorderRadius: "Border Radius", + parameterBorderRadius: "Border radius", borderRadiusInput: ["{{", "20}}"], parameterOptionLabels: "Option labels", - parameterBoxShadow: "Box Shadow", + parameterBoxShadow: "Box shadow", boxShadowDefaultValue: "#00000040", parameterOptionvalues: "Option values", - boxShadowColor: "Box Shadow Color", + boxShadowColor: "Box shadow Color", boxShadowFxValue: "-5px 6px 5px 8px #ee121240", codeMirrorLabelTrue: "{{true}}", @@ -213,7 +213,7 @@ export const commonWidgetText = { addEventHandlerLink: "New event handler", inspectorComponentLabel: "components", componentValueLabel: "Value", - labelDefaultValue: "Default Value", + labelDefaultValue: "Default value", parameterLabel: "Label", labelMinimumValue: "Minimum value", labelMaximumValue: "Maximum value", diff --git a/cypress-tests/cypress/constants/texts/database.js b/cypress-tests/cypress/constants/texts/database.js index 5e8f64b93d..4eb735dd0c 100644 --- a/cypress-tests/cypress/constants/texts/database.js +++ b/cypress-tests/cypress/constants/texts/database.js @@ -81,3 +81,9 @@ export const editRowText = { selectRowToEditText: "Select a row to edit", rowEditedSuccessfullyToast: "Row edited successfully", }; + +export const bulkUploadDataText = { + bulkUploadbuttonText: "Bulk upload data", + templateHelperText: + "Download the template to add your data or format your file in the same as the template. ToolJet won’t be able to recognise files in any other format.", +}; diff --git a/cypress-tests/cypress/constants/texts/datePicker.js b/cypress-tests/cypress/constants/texts/datePicker.js index be2ea22cdc..3234f21a0f 100644 --- a/cypress-tests/cypress/constants/texts/datePicker.js +++ b/cypress-tests/cypress/constants/texts/datePicker.js @@ -7,7 +7,7 @@ export const datePickerText = { }, datepicker1: "datepicker1", - labelDefaultValue: "Default Value", + labelDefaultValue: "Default value", labelformat: "Format", labelEnableDateSection: "Enable date selection?", labelEnableTimeSection: "Enable time selection?", diff --git a/cypress-tests/cypress/constants/texts/multiselect.js b/cypress-tests/cypress/constants/texts/multiselect.js index 6d277e80bb..cf64cd67ec 100644 --- a/cypress-tests/cypress/constants/texts/multiselect.js +++ b/cypress-tests/cypress/constants/texts/multiselect.js @@ -5,5 +5,5 @@ export const multiselectText = { noEventsMessage: "No event handlers", dropdwonOptionSelectAll: "Select All", - enableSelectAllOptions: "Enable select All option", + enableSelectAllOptions: "Enable select all option", }; diff --git a/cypress-tests/cypress/e2e/database/database.cy.js b/cypress-tests/cypress/e2e/database/database.cy.js index 174f8c0c8b..b5bb20ee96 100644 --- a/cypress-tests/cypress/e2e/database/database.cy.js +++ b/cypress-tests/cypress/e2e/database/database.cy.js @@ -1,4 +1,8 @@ -import { filterSelectors, sortSelectors } from "Selectors/database"; +import { + databaseSelectors, + filterSelectors, + sortSelectors, +} from "Selectors/database"; import { databaseText, filterText, sortText } from "Texts/database"; import { navigateToDatabase } from "Support/utils/common"; import { @@ -15,6 +19,8 @@ import { deleteRowAndVerify, editRowWithInvalidData, editRowAndVerify, + exportTableAndVerify, + bulkUploadDataTemplateDownloadAndVerify, } from "Support/utils/database"; import { fake } from "Fixtures/fake"; import { randomNumber } from "Support/utils/commonWidget"; @@ -75,7 +81,9 @@ describe("Database Functionality", () => { let column1 = columnDetails(); let column2 = columnDetails(); navigateToDatabase(); - verifyAllElementsOfPage(); + cy.get(databaseSelectors.allTablesSection).should("be.visible"); + cy.get(databaseSelectors.allTableSubheader).should("be.visible"); + cy.get(databaseSelectors.addTableButton).should("be.visible"); createTableAndVerifyToastMessage(data.tableName1, false); createTableAndVerifyToastMessage( data.tableName2, @@ -85,6 +93,7 @@ describe("Database Functionality", () => { true, [column1.defaultValueVarchar, column1.defaultValueInt] ); + verifyAllElementsOfPage(); }); it("Verify all operations of table", () => { const data = {}; @@ -183,5 +192,15 @@ describe("Database Functionality", () => { [databaseText.idColumnName, column1.name, column2.name], [row4.varcharData, row4.intData] ); + exportTableAndVerify(data.tableName, [ + databaseText.idColumnName, + column1.name, + column2.name, + ]); + bulkUploadDataTemplateDownloadAndVerify(data.tableName, [ + databaseText.idColumnName, + column1.name, + column2.name, + ]); }); }); diff --git a/cypress-tests/cypress/e2e/editor/data-source/mysqlHappyPath.cy.js b/cypress-tests/cypress/e2e/editor/data-source/mysqlHappyPath.cy.js index f8ad149d8b..230baf3a29 100644 --- a/cypress-tests/cypress/e2e/editor/data-source/mysqlHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/editor/data-source/mysqlHappyPath.cy.js @@ -138,7 +138,7 @@ describe("Data sources MySql", () => { fillDataSourceTextField( postgreSqlText.labelDbName, postgreSqlText.placeholderNameOfDB, - "testdb" + "testdv" ); fillDataSourceTextField( postgreSqlText.labelUserName, @@ -205,7 +205,7 @@ describe("Data sources MySql", () => { fillConnectionForm({ Host: Cypress.env("mysql_host"), Port: Cypress.env("mysql_port"), - "Database Name": "testdb", + "Database Name": "testdv", Username: Cypress.env("mysql_user"), Password: Cypress.env("mysql_password"), }); @@ -383,7 +383,7 @@ describe("Data sources MySql", () => { fillConnectionForm({ Host: Cypress.env("mysql_host"), Port: Cypress.env("mysql_port"), - "Database Name": "testdb", + "Database Name": "testdv", Username: Cypress.env("mysql_user"), Password: Cypress.env("mysql_password"), }); @@ -416,7 +416,7 @@ describe("Data sources MySql", () => { cy.get(".p-3").should( "have.text", - `[{"Tables_in_testdb (${dbName})":"${dbName}"}]` + `[{"Tables_in_testdv (${dbName})":"${dbName}"}]` ); // addQuery( @@ -458,7 +458,7 @@ describe("Data sources MySql", () => { fillConnectionForm({ Host: Cypress.env("mysql_host"), Port: "3318", - "Database Name": "testdb", + "Database Name": "testdv", Username: Cypress.env("mysql_user"), Password: Cypress.env("mysql_password"), }); diff --git a/cypress-tests/cypress/e2e/editor/inspectorHappypath.cy.js b/cypress-tests/cypress/e2e/editor/inspectorHappypath.cy.js index 0008418468..f4ec0c005a 100644 --- a/cypress-tests/cypress/e2e/editor/inspectorHappypath.cy.js +++ b/cypress-tests/cypress/e2e/editor/inspectorHappypath.cy.js @@ -82,9 +82,14 @@ describe("Editor- Inspector", () => { cy.get('[data-cy="switch-page-label-and-input"] > .select-search') .click() .type("home{enter}"); + cy.get('[data-cy="button-add-query-param"]').click(); - cy.wait(1000); - cy.get('[data-cy="button-add-query-param"]').click(); + cy.wait(3000); + cy.get("body").then(($body) => { + if ($body.find('[data-cy="query-param-key-input-field"]').length == 0) { + cy.get('[data-cy="button-add-query-param"]').click(); + } + }); addSupportCSAData("query-param-key", "key"); addSupportCSAData("query-param-value", "value"); diff --git a/cypress-tests/cypress/e2e/editor/widget/modalHappyPath.cy.js b/cypress-tests/cypress/e2e/editor/widget/modalHappyPath.cy.js index 96cf357270..47d0af1451 100644 --- a/cypress-tests/cypress/e2e/editor/widget/modalHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/editor/widget/modalHappyPath.cy.js @@ -326,7 +326,7 @@ describe("Modal", () => { ); cy.get("#inspector-tab-properties").click(); - typeOnFx("Loading State", "{{components.toggleswitch3.value"); + typeOnFx("Loading state", "{{components.toggleswitch3.value"); cy.get("[data-cy='modal-header']").realClick(); typeOnFx("Hide title bar", "{{components.toggleswitch4.value"); diff --git a/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js b/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js index f08a07b469..df5950f0c0 100644 --- a/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js +++ b/cypress-tests/cypress/e2e/editor/widget/tableRegression.cy.js @@ -262,7 +262,7 @@ describe("Table", () => { cy.get('[data-cy="label-action-button-text"]').verifyVisibleElement( "have.text", - "Button Text" + "Button text" ); cy.get('[data-cy="action-button-text-input-field"]').type( "{selectAll}{backspace}FakeName1" @@ -273,7 +273,7 @@ describe("Table", () => { ); cy.get('[data-cy="label-action-button-position"]').verifyVisibleElement( "have.text", - "Button Position" + "Button position" ); // dropdown_type cy.forceClickOnCanvas(); cy.waitForAutoSave(); @@ -351,7 +351,7 @@ describe("Table", () => { cy.get('[data-index="0"]>.select-search-option:eq(1)').realClick(); verifyAndEnterColumnOptionInput("key", "name"); verifyAndEnterColumnOptionInput("Text color", "red"); - verifyAndEnterColumnOptionInput("Cell Background Color", "yellow"); + verifyAndEnterColumnOptionInput("Cell Background color", "yellow"); cy.get( '[data-cy="input-and-label-cell-background-color"] > .form-label' ).click(); @@ -647,7 +647,7 @@ describe("Table", () => { // cy.get("[data-cy='border-radius-fx-button']:eq(1)").click(); verifyAndModifyParameter( - "Action Button Radius", + "Action button radius", commonWidgetText.borderRadiusInput ); @@ -668,7 +668,7 @@ describe("Table", () => { cy.get(commonWidgetSelector.buttonStylesEditorSideBar).click(); verifyAndModifyParameter( - "Border Radius", + "Border radius", commonWidgetText.borderRadiusInput ); cy.get(commonWidgetSelector.buttonCloseEditorSideBar).click(); @@ -844,7 +844,7 @@ describe("Table", () => { // cy.get('[data-cy="show-search-box-toggle-button"]').click(); // verifyAndModifyToggleFx("Server-side search", " ", true); - verifyAndModifyToggleFx("Loading State", "{{false}}", true); + verifyAndModifyToggleFx("Loading state", "{{false}}", true); }); it("should verify download", () => { diff --git a/cypress-tests/cypress/e2e/exportImport/import.cy.js b/cypress-tests/cypress/e2e/exportImport/import.cy.js index dab1aebc91..0239820b01 100644 --- a/cypress-tests/cypress/e2e/exportImport/import.cy.js +++ b/cypress-tests/cypress/e2e/exportImport/import.cy.js @@ -66,8 +66,8 @@ describe("App Import Functionality", () => { cy.get(importSelectors.importOptionInput).eq(0).selectFile(appFile, { force: true, }); - cy.get('[data-cy="import-app-title"]').should("be.visible"); - cy.get('[data-cy="Import app"]').click(); + cy.get(importSelectors.importAppTitle).should("be.visible"); + cy.get(importSelectors.importAppButton).click(); cy.get(".go3958317564") .should("be.visible") .and("have.text", importText.appImportedToastMessage); @@ -116,8 +116,8 @@ describe("App Import Functionality", () => { force: true, }); - cy.get('[data-cy="import-app-title"]').should("be.visible"); - cy.get('[data-cy="Import app"]').click(); + cy.get(importSelectors.importAppTitle).should("be.visible"); + cy.get(importSelectors.importAppButton).click(); cy.get(".go3958317564") .should("be.visible") .and("have.text", importText.appImportedToastMessage); @@ -188,8 +188,8 @@ describe("App Import Functionality", () => { force: true, } ); - cy.get('[data-cy="import-app-title"]').should("be.visible"); - cy.get('[data-cy="Import app"]').click(); + cy.get(importSelectors.importAppTitle).should("be.visible"); + cy.get(importSelectors.importAppButton).click(); cy.get(".go3958317564") .should("be.visible") .and("have.text", importText.appImportedToastMessage); diff --git a/cypress-tests/cypress/support/utils/database.js b/cypress-tests/cypress/support/utils/database.js index b573ccce6a..60d8d7a2f2 100644 --- a/cypress-tests/cypress/support/utils/database.js +++ b/cypress-tests/cypress/support/utils/database.js @@ -1,3 +1,4 @@ +import { deleteDownloadsFolder } from "Support/utils/common"; import { databaseSelectors, createNewColumnSelectors, @@ -5,12 +6,14 @@ import { filterSelectors, sortSelectors, editRowSelectors, + bulkUploadDataSelectors, } from "Selectors/database"; import { databaseText, createNewColumnText, createNewRowText, editRowText, + bulkUploadDataText, } from "Texts/database"; import { commonSelectors } from "Selectors/common"; import { commonText } from "Texts/common"; @@ -28,6 +31,10 @@ export const verifyAllElementsOfPage = () => { //cy.get(databaseSelectors.searchTableInputField).should("be.visible"); cy.get(databaseSelectors.allTablesSection).should("be.visible"); cy.get(databaseSelectors.allTableSubheader).should("be.visible"); + cy.get(createNewColumnSelectors.addNewColumnButton).should("be.visible"); + cy.get(createNewRowSelectors.addNewRowButton).should("be.visible"); + cy.get(editRowSelectors.editRowbutton).should("be.visible"); + cy.get(bulkUploadDataSelectors.bulkUploadDataButton).should("be.visible"); }; export const navigateToTable = (tableName) => { cy.get(databaseSelectors.currentTable(tableName)) @@ -74,16 +81,22 @@ export const createTableAndVerifyToastMessage = ( databaseText.noRecordsText ); }; -export const editTableNameAndVerifyToastMessage = (tableName, newTableName) => { +export const selectTableOperationOption = (tableName, operationOption) => { + navigateToTable(tableName); cy.get(databaseSelectors.currentTable(tableName)) .find(databaseSelectors.tableKebabIcon) .invoke("show") .trigger("mouseover") + .trigger("mouseover") .trigger("mousemove") .trigger("mousedown") .trigger("mouseup") .click(); - cy.get(databaseSelectors.tableEditOption).click(); + cy.get(operationOption).click(); + cy.wait(500); +}; +export const editTableNameAndVerifyToastMessage = (tableName, newTableName) => { + selectTableOperationOption(tableName, databaseSelectors.tableEditOption); cy.get(databaseSelectors.editTableHeader).verifyVisibleElement( "have.text", databaseText.editTableHeader @@ -111,18 +124,11 @@ export const editTableNameAndVerifyToastMessage = (tableName, newTableName) => { ); }; export const deleteTableAndVerifyToastMessage = (tableName) => { - cy.get(databaseSelectors.currentTable(tableName)) - .find(databaseSelectors.tableKebabIcon) - .invoke("show") - .trigger("mouseover") - .trigger("mousemove") - .trigger("mousedown") - .trigger("mouseup") - .click(); - cy.get(databaseSelectors.tableDeleteOption).click(); + selectTableOperationOption(tableName, databaseSelectors.tableDeleteOption); // cy.on('window:confirm', (ConfirmAlertText) => { // expect(ConfirmAlertText).to.contains(`Are you sure you want to delete the table "${tableName}"?`); // }); + cy.wait(500); cy.verifyToastMessage( commonSelectors.toastMessage, databaseText.tableDeletedSuccessfullyToast(tableName) @@ -491,7 +497,6 @@ export const editRowAndVerify = ( ); verifyRowData(rowNumber, columnName, rowFieldData); }; - export const editRowWithInvalidData = ( tableName, rowNumber, @@ -540,3 +545,73 @@ export const editRowWithInvalidData = ( ); cy.get(commonSelectors.buttonSelector(commonText.cancelButton)).click(); }; +export const exportTableAndVerify = (tableName, columnName) => { + deleteDownloadsFolder(); + cy.reload(); + selectTableOperationOption(tableName, databaseSelectors.tableExportOption); + verifyDownloadedTableSchema(tableName, columnName); + cy.exec("cd ./cypress/downloads/ && rm -rf *"); +}; +export const verifyDownloadedTableSchema = (tableName, columnName) => { + cy.exec("ls ./cypress/downloads/").then((result) => { + const downloadedExportTableFileName = result.stdout.split("\n")[0]; + let exportedTableFilePath = `cypress/downloads/${downloadedExportTableFileName}`; + cy.readFile(exportedTableFilePath).then((table) => { + let exportedTableData = table; + expect(downloadedExportTableFileName).to.contain.string( + tableName.toLowerCase() + ); + for (let i = 0; i <= columnName.length - 1; i++) { + cy.get(databaseSelectors.columnHeader(columnName[i])).each(($el) => { + cy.wrap($el) + .should("be.visible") + .and( + "contain.text", + exportedTableData.tooljet_database[0].schema.columns[ + i + ].column_name.toLowerCase() + ); + }); + } + }); + }); +}; +export const bulkUploadDataTemplateDownloadAndVerify = ( + tableName, + columnName +) => { + deleteDownloadsFolder(); + cy.reload(); + cy.intercept("GET", "api/tooljet_db/organizations/**").as("dbLoad"); + navigateToTable(tableName); + cy.wait(1000); + cy.get(bulkUploadDataSelectors.bulkUploadbuttonText).verifyVisibleElement( + "have.text", + bulkUploadDataText.bulkUploadbuttonText + ); + cy.get(bulkUploadDataSelectors.bulkUploadDataButton) + .should("be.visible") + .click(); + cy.get(bulkUploadDataSelectors.bulkUploadDataHeaderText).verifyVisibleElement( + "have.text", + bulkUploadDataText.bulkUploadbuttonText + ); + cy.get(bulkUploadDataSelectors.templateHelperText).verifyVisibleElement( + "have.text", + bulkUploadDataText.templateHelperText + ); + cy.get(bulkUploadDataSelectors.templateDownloadButton) + .should("be.visible") + .click(); + cy.readFile(`cypress/downloads/${tableName}.csv`, "utf-8").then((table) => { + let exportedTableData = table.split(","); + cy.log(exportedTableData); + for (let i = 0; i <= columnName.length - 1; i++) { + cy.get(databaseSelectors.columnHeader(columnName[i])).each(($el) => { + cy.wrap($el) + .should("be.visible") + .and("contain.text", exportedTableData[i].toLowerCase()); + }); + } + }); +}; diff --git a/docs/docs/data-sources/restapi.md b/docs/docs/data-sources/restapi.md index 04d8057d36..179dfbd11b 100644 --- a/docs/docs/data-sources/restapi.md +++ b/docs/docs/data-sources/restapi.md @@ -1,6 +1,6 @@ --- id: restapi -title: REST API +title: REST API --- ToolJet can establish a connection with any available REST API endpoint and create queries to interact with it. @@ -74,9 +74,19 @@ Whenever a request is made to the REST API, a **tj-x-forwarded-for** header is a +## Request types + +The plugin will send a **JSON** formatted body by default. If a file object from a [`FilePicker` widget](/docs/widgets/file-picker) is set as a value, the body is automatically converted to be sent as a `multipart/form-data` request. + +
+ +ToolJet - Data source - REST API + +
+ ## Response types -REST APIs can return data in a variety of formats, including **JSON** and **Base64**. JSON is a common format used for data exchange in REST APIs, while Base64 is often used for encoding binary data, such as images or video, within a JSON response. +REST APIs can return data in a variety of formats, including **JSON** and **Base64**. JSON is a common format used for data exchange in REST APIs, while Base64 is often used for encoding binary data, such as images or video, within a JSON response. When the response `content-type` is **image**, the response will be a `base64` string. ### Example JSON response diff --git a/docs/static/img/datasource-reference/rest-api/multipart-form-data.png b/docs/static/img/datasource-reference/rest-api/multipart-form-data.png new file mode 100644 index 0000000000..da0bd3dbc0 Binary files /dev/null and b/docs/static/img/datasource-reference/rest-api/multipart-form-data.png differ diff --git a/frontend/.version b/frontend/.version index 23a93836ae..5c18f9195b 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.24.5 +2.25.0 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d8c2c5848d..c64b251548 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20295,6 +20295,7 @@ "version": "1.0.0", "dependencies": { "@tooljet-plugins/common": "file:../common", + "form-data": "^4.0.0", "got": "^11.8.6", "react": "^17.0.2", "rimraf": "^3.0.2", @@ -20369,9 +20370,9 @@ "version": "1.0.0", "dependencies": { "@tooljet-plugins/common": "file:../common", - "@types/snowflake-sdk": "^1.6.12", + "@types/snowflake-sdk": "^1.6.17", "react": "^17.0.2", - "snowflake-sdk": "^1.6.23" + "snowflake-sdk": "^1.9.1" } }, "../plugins/packages/stripe": { @@ -68031,6 +68032,7 @@ "version": "file:../plugins/packages/restapi", "requires": { "@tooljet-plugins/common": "file:../common", + "form-data": "^4.0.0", "got": "^11.8.6", "react": "^17.0.2", "rimraf": "^3.0.2", @@ -68087,9 +68089,9 @@ "version": "file:../plugins/packages/snowflake", "requires": { "@tooljet-plugins/common": "file:../common", - "@types/snowflake-sdk": "^1.6.12", + "@types/snowflake-sdk": "^1.6.17", "react": "^17.0.2", - "snowflake-sdk": "^1.6.23" + "snowflake-sdk": "^1.9.1" } }, "@tooljet-plugins/stripe": { diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx index 41d39663f0..90d708ab71 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx @@ -22,6 +22,8 @@ const DropDownSelect = ({ emptyError, shouldCenterAlignText = false, showPlaceHolder = false, + highlightSelected = true, + buttonClasses = '', }) => { const popoverId = useRef(`dd-select-${uuidv4()}`); const popoverBtnId = useRef(`dd-select-btn-${uuidv4()}`); @@ -124,11 +126,12 @@ const DropDownSelect = ({ onAdd={onAdd} addBtnLabel={addBtnLabel} emptyError={emptyError} + highlightSelected={highlightSelected} /> } > - +
-
+ ); }; diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx index 0b742dc9bd..0769e43eb6 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx @@ -88,13 +88,22 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => { )} - - -
Join
+ + +
+ Join +
- + {index ? ( { value={leftTableList.find((val) => val?.value === leftFieldTable)} /> ) : ( -
{baseTableDetails?.table_name ?? ''}
+
+ {baseTableDetails?.table_name ?? ''} +
)} - + { { }} /> ))} - + + 1 @@ -295,6 +314,7 @@ const JoinOn = ({ > {index == 1 && ( )} - {index == 0 &&
On
} + {index == 0 && ( +
+ On +
+ )} {index > 1 && ( -
+
{groupOperator}
)} - + - + {/* {operator}
+
+ {operator} +
{index > 0 && ( - + )} diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx index 4239e7ef0a..dd8413a475 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx @@ -90,12 +90,22 @@ export default function JoinSelect({ darkMode }) { const respectiveTableSelectedOptions = joinSelectOptions.filter((val) => val?.table === table); const respectiveTableOptions = tableOptions[table] ?? []; return ( - - -
{findTableDetails(table)?.table_name ?? ''}
+ + +
+ {findTableDetails(table)?.table_name ?? ''} +
- + { const tableDetails = options?.table ? findTableDetails(options?.table) : ''; return ( - - + + -
+
setJoinOrderByOptions(joinOrderByOptions.filter((opt, idx) => idx !== i))} > @@ -137,7 +142,7 @@ export default function JoinSort({ darkMode }) { }) )} {/* Dynamically render below Row */} - + setJoinOrderByOptions([...joinOrderByOptions, {}])}> diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx index 0e80238287..8c40537af3 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx @@ -90,8 +90,8 @@ const SelectTableMenu = ({ darkMode }) => {
{/* Join Section */}
- -
+ +
{joins.map((join, joinIndex) => ( {
{/* Filter Section */}
- -
+ +
{/* Sort Section */}
- -
+ +
{/* Limit Section */}
- -
+ +
{
{/* Offset Section */}
- -
+ +
{
{/* Select Section */}
- -
+ +
@@ -222,6 +222,7 @@ const RenderFilterSection = ({ darkMode }) => { }; } else { editedFilterCondition = { + operator: 'AND', ...conditions, conditionsList: [...conditionsList, { ...emptyConditionTemplate }], }; @@ -364,10 +365,11 @@ const RenderFilterSection = ({ darkMode }) => { const { operator = '', leftField = {}, rightField = {} } = conditionDetail; const LeftSideTableDetails = leftField?.table ? findTableDetails(leftField?.table) : ''; return ( - - + + {index === 1 && ( updateOperatorForConditions(change?.value)} options={groupOperators} @@ -375,11 +377,32 @@ const RenderFilterSection = ({ darkMode }) => { value={groupOperators.find((op) => op.value === conditions.operator)} /> )} - {index === 0 &&
Where
} - {index > 1 &&
{conditions?.operator}
} + {index === 0 && ( +
+ Where +
+ )} + {index > 1 && ( +
+ {conditions?.operator} +
+ )} - + updateFilterConditionEntry('Column', index, { @@ -399,8 +422,9 @@ const RenderFilterSection = ({ darkMode }) => { darkMode={darkMode} /> - + updateFilterConditionEntry('Operator', index, { operator: change?.value })} value={filterOperatorOptions.find((op) => op.value === operator)} @@ -409,9 +433,10 @@ const RenderFilterSection = ({ darkMode }) => { /> -
+
{operator === 'IS' ? ( updateFilterConditionEntry('Value', index, { value: change?.value, isLeftSideCondition: false }) @@ -429,9 +454,9 @@ const RenderFilterSection = ({ darkMode }) => { : JSON.stringify(rightField?.value) : rightField?.value } - className="codehinter-plugins" + className="border border-end-0 fs-12 tjdb-codehinter" theme={darkMode ? 'monokai' : 'default'} - height={'28px'} + height={'30px'} placeholder="Value" onChange={(newValue) => updateFilterConditionEntry('Value', index, { value: newValue, isLeftSideCondition: false }) @@ -439,10 +464,14 @@ const RenderFilterSection = ({ darkMode }) => { /> )}
+ removeFilterConditionEntry(index)} > @@ -470,7 +499,7 @@ const RenderFilterSection = ({ darkMode }) => { )} {filterComponents} - + addNewFilterConditionEntry()}> diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/ListRows.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/ListRows.jsx index 4944a64ae8..b86c00f8f0 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/ListRows.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/ListRows.jsx @@ -9,7 +9,8 @@ import { isOperatorOptions } from './util'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; export const ListRows = React.memo(({ darkMode }) => { - const { columns, listRowsOptions, limitOptionChanged, handleOptionsChange } = useContext(TooljetDatabaseContext); + const { columns, listRowsOptions, limitOptionChanged, handleOptionsChange, offsetOptionChanged } = + useContext(TooljetDatabaseContext); function handleWhereFiltersChange(filters) { handleOptionsChange('where_filters', filters); @@ -155,7 +156,7 @@ export const ListRows = React.memo(({ darkMode }) => {
{/* Limit */} -
+
@@ -170,6 +171,22 @@ export const ListRows = React.memo(({ darkMode }) => { />
+ {/* Offset */} +
+ +
+ offsetOptionChanged(newValue)} + /> +
+
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/SelectBox.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/SelectBox.jsx index 96d7d5c090..61e538f91c 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/SelectBox.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/SelectBox.jsx @@ -19,6 +19,7 @@ function DataSourceSelect({ addBtnLabel, selected, emptyError, + highlightSelected, }) { const handleChangeDataSource = (source) => { onSelect && onSelect(source); @@ -88,7 +89,7 @@ function DataSourceSelect({ /> ))} {children} - {props.isSelected && ( + {props.isSelected && highlightSelected && ( ({ ...prev, limit: value })); }; + const offsetOptionChanged = (value) => { + setListRowsOptions((prev) => ({ ...prev, offset: value })); + }; + const deleteOperationLimitOptionChanged = (limit) => { setDeleteRowsOptions((prev) => ({ ...prev, limit: limit })); }; @@ -290,6 +294,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay listRowsOptions, setListRowsOptions, limitOptionChanged, + offsetOptionChanged, handleOptionsChange, deleteRowsOptions, handleDeleteRowsOptionsChange, @@ -467,9 +472,10 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay {/* table name dropdown */}
- -
+ +
- -
+ +
{ +export const hasNullValueInFilters = (queryOptions, operation) => { const filters = get(queryOptions, `${operation}.where_filters`); if (filters) { const filterKeys = Object.keys(filters); diff --git a/frontend/src/HomePage/ExportAppModal.jsx b/frontend/src/HomePage/ExportAppModal.jsx index d523f1d04b..2e33eddaff 100644 --- a/frontend/src/HomePage/ExportAppModal.jsx +++ b/frontend/src/HomePage/ExportAppModal.jsx @@ -9,6 +9,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam const currentVersion = app?.editing_version; const [versions, setVersions] = useState(undefined); const [tables, setTables] = useState(undefined); + const [allTables, setAllTables] = useState(undefined); const [versionId, setVersionId] = useState(currentVersion?.id); const [exportTjDb, setExportTjDb] = useState(true); @@ -27,9 +28,33 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam } async function fetchAppTables() { try { - const fetchTables = await appsService.getTables(app.id); + const fetchTables = await appsService.getTables(app.id); // this is used to get all tables const { tables } = fetchTables; - setTables(tables); + const tbl = await appsService.getAppByVersion(app.id, versionId); // this is used to get particular App by version + const { dataQueries } = tbl; + const extractedIdData = []; + dataQueries.forEach((item) => { + if (item.kind === 'tooljetdb') { + const joinOptions = item.options?.join_table?.joins ?? []; + (joinOptions || []).forEach((join) => { + const { table, conditions } = join; + if (table) extractedIdData.push(table); + conditions?.conditionsList?.forEach((condition) => { + const { leftField, rightField } = condition; + if (leftField?.table) { + extractedIdData.push(leftField?.table); + } + if (rightField?.table) { + extractedIdData.push(rightField?.table); + } + }); + }); + } + }); + const uniqueSet = new Set(extractedIdData); + const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item })); + setTables(selectedVersiontable); + setAllTables(tables); } catch (error) { toast.error('Could not fetch the tables.', { position: 'top-center', @@ -40,9 +65,9 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam fetchAppVersions(); fetchAppTables(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [versionId]); - const exportApp = (app, versionId, exportTjDb, tables) => { + const exportApp = (app, versionId, exportTjDb, exportTables) => { const appOpts = { app: [ { @@ -51,10 +76,11 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam }, ], }; + const requestBody = { ...appOpts, - ...(exportTjDb && { tooljet_database: tables }), - organization_id: app.organization_id ?? app.organizationId, + ...(exportTjDb && { tooljet_database: exportTables }), + organization_id: app.organization_id, }; appsService @@ -164,7 +190,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam className="import-export-footer-btns" variant="tertiary" data-cy="export-all-button" - onClick={() => exportApp(app, null, exportTjDb, tables)} + onClick={() => exportApp(app, null, exportTjDb, allTables)} > Export All diff --git a/frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx b/frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx index 346cc365ec..7a506fb832 100644 --- a/frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx +++ b/frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx @@ -64,9 +64,10 @@ function BulkUploadDrawer({ @@ -79,7 +80,7 @@ function BulkUploadDrawer({ >
-

+

Bulk upload data

@@ -127,7 +128,7 @@ function BulkUploadDrawer({ 0 || errors.server.length > 0} - data-cy={`save-changes-button`} + data-cy={`upload-data-button`} onClick={handleBulkUpload} fill="#fff" leftIcon="floppydisk" diff --git a/frontend/src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx b/frontend/src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx index 564cd10359..3892bcea50 100644 --- a/frontend/src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx +++ b/frontend/src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx @@ -13,6 +13,7 @@ const EditRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) => {