diff --git a/.github/workflows/cypress-platform.yml b/.github/workflows/cypress-platform.yml index 112a10c199..d136991e38 100644 --- a/.github/workflows/cypress-platform.yml +++ b/.github/workflows/cypress-platform.yml @@ -79,7 +79,7 @@ jobs: - name: Set up environment variables run: | - echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'EE' || 'CE' }}" >> .env + echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'ee' || 'ce' }}" >> .env echo "TOOLJET_HOST=http://localhost:8082" >> .env echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env diff --git a/cypress-tests/cypress/commands/commands.js b/cypress-tests/cypress/commands/commands.js index 0f9a6c9996..02933afd7a 100644 --- a/cypress-tests/cypress/commands/commands.js +++ b/cypress-tests/cypress/commands/commands.js @@ -116,8 +116,10 @@ Cypress.Commands.add( }); const splitIntoFlatArray = (value) => { - const regex = /(\{|\}|\(|\)|\[|\]|,|:|;|=>|'[^']*'|[a-zA-Z0-9._]+|\s+)/g; + const regex = + /(\{|\}|\(|\)|\[|\]|,|:|;|=>|'[^']*'|"[^"]*"|[a-zA-Z0-9._-]+|\s+)/g; let prefix = ""; + return ( value.match(regex)?.reduce((acc, part) => { if (part === "{{" || part === "((") { @@ -132,6 +134,10 @@ Cypress.Commands.add( acc.push(prefix + " "); } else if (part === ":") { acc.push(prefix + ":"); + } else if (part === '"') { + acc.push(prefix + '"'); + } else if (part.includes("-")) { + acc.push(prefix + part); // Ensure hyphen is included } else { acc.push(prefix + part); prefix = ""; @@ -142,13 +148,11 @@ Cypress.Commands.add( }; if (Array.isArray(value)) { - cy.wrap(subject) - .last() - .realType(value, { - parseSpecialCharSequences: false, - delay: 0, - force: true, - }); + cy.wrap(subject).last().realType(value, { + parseSpecialCharSequences: false, + delay: 0, + force: true, + }); } else { splitIntoFlatArray(value).forEach((i) => { cy.wrap(subject) @@ -228,9 +232,9 @@ Cypress.Commands.add( .invoke("text") .then((text) => { cy.wrap(subject).realType(createBackspaceText(text)), - { - delay: 0, - }; + { + delay: 0, + }; }); } ); diff --git a/cypress-tests/cypress/constants/selectors/Plugins.js b/cypress-tests/cypress/constants/selectors/Plugins.js new file mode 100644 index 0000000000..04481d09fa --- /dev/null +++ b/cypress-tests/cypress/constants/selectors/Plugins.js @@ -0,0 +1,40 @@ +export const pluginSelectors = { + regionField: '[data-cy="region-section"] .react-select__control', + regionFieldValue: '[data-cy="region-section"] .react-select__single-value', + amazonsesAccesKey: '[data-cy="access-key-text-field"]', + operationDropdown: '[data-cy="operation-select-dropdown"]', + sendEmailInputField: '[data-cy="send-mail-to-input-field"]', + ccEmailInputField: '[data-cy="cc-to-input-field"]', + bccEmailInputField: '[data-cy="bcc-to-input-field"]', + sendEmailFromInputField: '[data-cy="send-mail-from-input-field"]', + emailSubjetInputField: '[data-cy="subject-input-field"]', + emailbodyInputField: '[data-cy="body-input-field"]', + amazonAthenaDbName: '[data-cy="database-text-field"]', +}; + +export const baserowSelectors = { + hostField: '[data-cy="host-select-dropdown"]', + baserowApiKey: '[data-cy="api-token-text-field"]', + table: '[data-cy="table-id-input-field"]', + rowIdinputfield: '[data-cy="row-id-input-field"]', +}; + +export const appWriteSelectors = { + projectID: '[data-cy="project-id-text-field"]', + collectionId: '[data-cy="collectionid-input-field"]', + documentId: '[data-cy="documentid-input-field"]', + bodyInput: '[data-cy="body-input-field"]', +}; + +export const twilioSelectors = { + toNumberInputField: '[data-cy="to-number-input-field"]', + bodyInput: '[data-cy="body-input-field"]', +}; + +export const minioSelectors = { + sslToggle: 'data-cy="ssl-enabled-toggle-input"', + bucketNameInputField: '[data-cy="bucket-input-field"]', + objectNameInputField: '[data-cy="objectname-input-field"]', + contentTypeInputField: '[data-cy="contenttype-input-field"]', + dataInput: '[data-cy="data-input-field"]', +}; diff --git a/cypress-tests/cypress/constants/selectors/awss3.js b/cypress-tests/cypress/constants/selectors/awss3.js index 59bdebacff..81f1a6efe8 100644 --- a/cypress-tests/cypress/constants/selectors/awss3.js +++ b/cypress-tests/cypress/constants/selectors/awss3.js @@ -5,5 +5,5 @@ export const s3Selector = { regionLabel: '[data-cy="label-region"]', customEndpointLabel: '[data-cy="label-custom-endpoint"]', customEndpointInput: '[data-cy="undefined-text-field"]', - dataSourceNameInput: '[data-cy="data-source-name-input-filed"]', + dataSourceNameInput: '[data-cy="data-source-name-input-field"]', }; diff --git a/cypress-tests/cypress/constants/selectors/dataSource.js b/cypress-tests/cypress/constants/selectors/dataSource.js index bdf1677d91..8283eb2b92 100644 --- a/cypress-tests/cypress/constants/selectors/dataSource.js +++ b/cypress-tests/cypress/constants/selectors/dataSource.js @@ -14,7 +14,7 @@ export const dataSourceSelector = { dataSourceSearchInputField: '[data-cy="home-page-search-bar"]', postgresDataSource: "[data-cy='data-source-postgresql']", - dataSourceNameInputField: '[data-cy="data-source-name-input-filed"]', + dataSourceNameInputField: '[data-cy="data-source-name-input-field"]', labelHost: '[data-cy="label-host"]', labelPort: '[data-cy="label-port"]', labelSsl: '[data-cy="label-ssl"]', @@ -28,7 +28,7 @@ export const dataSourceSelector = { buttonTestConnection: '[data-cy="test-connection-button"]', connectionFailedText: '[data-cy="test-connection-failed-text"]', buttonSave: '[data-cy="db-connection-save-button"] > .tj-base-btn', - dangerAlertNotSupportSSL: '.go3958317564', + dangerAlertNotSupportSSL: ".go3958317564", passwordTextField: '[data-cy="password-text-field"]', textConnectionVerified: '[data-cy="test-connection-verified-text"]', @@ -97,11 +97,11 @@ export const dataSourceSelector = { eventQuerySelectionField: '[data-cy="query-selection-field"]', addedDsSearchIcon: '[data-cy="added-ds-search-icon"]', AddedDsSearchBar: '[data-cy="added-ds-search-bar"]', - dsNameInputField: '[data-cy="data-source-name-input-filed"]', + dsNameInputField: '[data-cy="data-source-name-input-field"]', unSavedModalTitle: '[data-cy="unsaved-changes-title"]', eventQuerySelectionField: '[data-cy="query-selection-field"]', connectionAlertText: '[data-cy="connection-alert-text"]', deleteDSButton: (datasourceName) => { - return `[data-cy="${cyParamName(datasourceName)}-delete-button"]` + return `[data-cy="${cyParamName(datasourceName)}-delete-button"]`; }, }; diff --git a/cypress-tests/cypress/constants/selectors/postgreSql.js b/cypress-tests/cypress/constants/selectors/postgreSql.js index 49e0351656..4f38357961 100644 --- a/cypress-tests/cypress/constants/selectors/postgreSql.js +++ b/cypress-tests/cypress/constants/selectors/postgreSql.js @@ -12,7 +12,7 @@ export const postgreSqlSelector = { dataSourceSearchInputField: '[data-cy="home-page-search-bar"]', postgresDataSource: "[data-cy='data-source-postgresql']", - dataSourceNameInputField: '[data-cy="data-source-name-input-filed"]', + dataSourceNameInputField: '[data-cy="data-source-name-input-field"]', labelHost: '[data-cy="label-host"]', labelPort: '[data-cy="label-port"]', labelSsl: '[data-cy="label-ssl"]', @@ -88,3 +88,11 @@ export const postgreSqlSelector = { eventQuerySelectionField: '[data-cy="query-selection-field"]', }; + +export const airTableSelector = { + operationSelectDropdown: '[data-cy="operation-select-dropdown"]', + baseIdInputField: '[data-cy="base-id-input-field"]', + tableNameInputField: '[data-cy="table-name-input-field"]', + recordIdInputField: '[data-cy="record-id-input-field"]', + bodyInputField: '[data-cy="body-input-field"]', +}; diff --git a/cypress-tests/cypress/constants/texts/airTable.js b/cypress-tests/cypress/constants/texts/airTable.js new file mode 100644 index 0000000000..44df3cf9e1 --- /dev/null +++ b/cypress-tests/cypress/constants/texts/airTable.js @@ -0,0 +1,6 @@ +export const airtableText = { + airtable: "Airtable", + cypressairtable: "cypress-Airtable", + ApiKey: "Personal access token", + apikeyPlaceholder: "**************", + }; \ No newline at end of file diff --git a/cypress-tests/cypress/constants/texts/amazonAthena.js b/cypress-tests/cypress/constants/texts/amazonAthena.js new file mode 100644 index 0000000000..794815d4a7 --- /dev/null +++ b/cypress-tests/cypress/constants/texts/amazonAthena.js @@ -0,0 +1,8 @@ +export const amazonAthenaText = { + AmazonAthena: "Amazon Athena", + cypressAmazonAthena: "cypress-Amazon Athena", + labelAccesskey: "Access key", + labelSecretKey: "Secret key", + placeholderEnteraAccessKey: "Enter access key", + placeholderSecretKey:"**************", + }; \ No newline at end of file diff --git a/cypress-tests/cypress/constants/texts/amazonSes.js b/cypress-tests/cypress/constants/texts/amazonSes.js new file mode 100644 index 0000000000..2abaed71ab --- /dev/null +++ b/cypress-tests/cypress/constants/texts/amazonSes.js @@ -0,0 +1,8 @@ +export const amazonSesText = { + AmazonSES: "Amazon SES", + cypressAmazonSES: "cypress-Amazon SES", + labelAccesskey: "Access key", + labelSecretKey: "Secret key", + placeholderAccessKey: "Enter access key", + placeholderSecretKey:"**************", + }; \ No newline at end of file diff --git a/cypress-tests/cypress/constants/texts/appwrite.js b/cypress-tests/cypress/constants/texts/appwrite.js new file mode 100644 index 0000000000..15b723d1cc --- /dev/null +++ b/cypress-tests/cypress/constants/texts/appwrite.js @@ -0,0 +1,12 @@ +export const appwriteText = { + appwrite: "Appwrite", + cypressAppwrite: "cypress-Appwrite", + host: "Host", + ProjectID: "Project ID", + DatabaseID: "Database ID", + SecretKey: "Secret Key", + SecretKeyPlaceholder: "**************", + hostPlaceholder: "Appwrite database host/endpoint", + projectIdPlaceholder: "Appwrite project id", + databaseIdPlaceholder: "Appwrite Database id", +}; diff --git a/cypress-tests/cypress/constants/texts/baseRow.js b/cypress-tests/cypress/constants/texts/baseRow.js new file mode 100644 index 0000000000..6f9958d979 --- /dev/null +++ b/cypress-tests/cypress/constants/texts/baseRow.js @@ -0,0 +1,6 @@ +export const baseRowText = { + baserow: "baserow", + cypressBaseRow: "cypress-baserow", + lableApiToken: "API token", + placeholderApiToken:"**************", + }; \ No newline at end of file diff --git a/cypress-tests/cypress/constants/texts/minio.js b/cypress-tests/cypress/constants/texts/minio.js new file mode 100644 index 0000000000..5afd1ed477 --- /dev/null +++ b/cypress-tests/cypress/constants/texts/minio.js @@ -0,0 +1,14 @@ +export const minioText = { + minio: "Minio", + cypressMinio: "cypressMinio", + hostLabel: "Host", + hostInputPlaceholder: "Enter host", + portLabel: "Port", + portPlaceholder: "Enter port", + labelAccesskey: "Access key", + labelSecretKey: "Secret key", + placeholderAccessKey: "Enter access key", + placeholderSecretKey: "**************", + bucketName: `my-second-bucket`, + objectName: `mybucket`, +}; diff --git a/cypress-tests/cypress/constants/texts/twilio.js b/cypress-tests/cypress/constants/texts/twilio.js new file mode 100644 index 0000000000..535bfb3b8a --- /dev/null +++ b/cypress-tests/cypress/constants/texts/twilio.js @@ -0,0 +1,11 @@ +export const twilioText = { + twilio: "Twilio", + cypresstwilio: "cypress-Twilio", + authTokenLabel: "Auth Token", + authTokenPlaceholder: "**************", + accountSidLabel: "Account SID", + accountSidPlaceholder: "Account SID for Twilio", + messagingSIDLabel: "Messaging Service SID", + messagingSIDPalceholder: "Messaging Service SID for Twilio", + messageText: "Sending test message to check twilio", +}; diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/addAllPluginsToApp.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/addAllPluginsToApp.cy.js new file mode 100644 index 0000000000..fa0fad800f --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/addAllPluginsToApp.cy.js @@ -0,0 +1,210 @@ +import { fake } from "Fixtures/fake"; +import { postgreSqlSelector } from "Selectors/postgreSql"; +import { postgreSqlText } from "Texts/postgreSql"; +import { commonSelectors } from "Selectors/common"; + +import { selectAndAddDataSource } from "Support/utils/postgreSql"; + +import { closeDSModal } from "Support/utils/dataSource"; + +const data = {}; +data.dsNamefake = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); +data.dsNamefake1 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); +const cyParamName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "-"); + +const dataSources = [ + "BigQuery", + "ClickHouse", + "CosmosDB", + "CouchDB", + "Databricks", + "DynamoDB", + "Elasticsearch", + "Firestore", + "InfluxDB", + "MariaDB", + "MongoDB", + "SQL Server", + "MySQL", + "Oracle DB", + "PostgreSQL", + "Redis", + "RethinkDB", + "SAP HANA", + "Snowflake", + "TypeSense", + "Airtable", + "Amazon SES", + "Appwrite", + "Amazon Athena", + "Baserow", + // "Google Sheets", need to remove + "GraphQL", + // "gRPC", need to remove + "Mailgun", + "n8n", + "Notion", + "OpenAPI", + "REST API", + "SendGrid", + // "Slack", need to remove + "SMTP", + "Stripe", + "Twilio", + "Woocommerce", + //"Zendesk", need to remove + "Azure Blob Storage", + "GCS", + "Minio", + "AWS S3", +]; + +describe("Add all Data sources to app", () => { + beforeEach(() => { + cy.apiLogin(); + cy.defaultWorkspaceLogin(); + }); + + it("Should verify global data source page", () => { + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + cy.get(postgreSqlSelector.allDatasourceLabelAndCount).should( + "have.text", + postgreSqlText.allDataSources() + ); + cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should( + "have.text", + postgreSqlText.commonlyUsed + ); + cy.get(postgreSqlSelector.cloudStorageLabelAndCount).should( + "have.text", + postgreSqlText.allCloudStorage + ); + }); + + it("Should add all data sources in data source page", () => { + dataSources.forEach((dsName) => { + cy.get(commonSelectors.globalDataSourceIcon).click(); + selectAndAddDataSource("databases", dsName, dsName); // Using the correct fake name + + // Test connection + // cy.get(postgreSqlSelector.buttonTestConnection).click(); + // cy.get(postgreSqlSelector.textConnectionVerified, { + // timeout: 10000, + // }).should("have.text", postgreSqlText.labelConnectionVerified); + + // // Save data source + // cy.get(postgreSqlSelector.buttonSave).click(); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Data Source ${dsName} saved.` + // ); + }); + }); + + it("Should add all data sources in the app", () => { + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsNamefake); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.wrap(dataSources).each((dsName) => { + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type( + `cypress-${cyParamName(dsName)}-${cyParamName(dsName)}` + ); + cy.wait(500); + + cy.contains( + `[id*="react-select-"]`, + `cypress-${cyParamName(dsName)}-${cyParamName(dsName)}` + ) + .should("be.visible") + .click(); + + cy.wait(500); + }); + }); + + it("Should install all makretplace plugins and add them into the app", () => { + const dataSourcesMarketplace = [ + "Plivo", + "GitHub", + "OpenAI", + "AWS Textract", + "HarperDB", + "AWS Redshift", + "PocketBase", + "AWS Lambda", + "Supabase", + "Engagespot", + // "Salesforce", need to remove + "Presto", + "Jira", + // "Sharepoint", need to remove + "Portkey", + "Pinecone", + "Hugging Face", + "Cohere", + "Gemini", + "Mistral", + "Anthropic", + "Qdrant", + "Weaviate DB", + ]; + + cy.get(commonSelectors.globalDataSourceIcon).click(); + + cy.window().then((win) => { + cy.stub(win, "open").callsFake((url) => { + win.location.href = url; + }); + }); + + cy.get('[data-cy="data-source-add-plugin"]').click(); + + cy.get(".marketplace-install").each(($el) => { + cy.wrap($el).click(); + cy.wait(500); + cy.get(commonSelectors.toastMessage).should("include.text", "installed"); + }); + cy.wait(1000); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + cy.get(commonSelectors.pageSectionHeader).should( + "have.text", + "Data sources" + ); + + cy.wrap(dataSourcesMarketplace).each((dsName) => { + cy.get(commonSelectors.globalDataSourceIcon).click(); + selectAndAddDataSource("databases", dsName, dsName); + cy.wait(500); + }); + + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsNamefake1); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.wrap(dataSourcesMarketplace).each((dsName) => { + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type( + `cypress-${cyParamName(dsName)}-${cyParamName(dsName)}` + ); + cy.wait(500); + + cy.contains( + `[id*="react-select-"]`, + `cypress-${cyParamName(dsName)}-${cyParamName(dsName)}` + ) + .should("be.visible") + .click(); + + cy.wait(500); + }); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTable.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTable.cy.js new file mode 100644 index 0000000000..bade9e6c5c --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTable.cy.js @@ -0,0 +1,289 @@ +import { fake } from "Fixtures/fake"; +import { postgreSqlSelector, airTableSelector } from "Selectors/postgreSql"; +import { postgreSqlText } from "Texts/postgreSql"; +import { airtableText } from "Texts/airTable"; +import { commonSelectors } from "Selectors/common"; +import { commonText } from "Texts/common"; + +import { + fillDataSourceTextField, + selectAndAddDataSource, +} from "Support/utils/postgreSql"; +import { redisText } from "Texts/redis"; + +import { + verifyCouldnotConnectWithAlert, + deleteDatasource, + closeDSModal, + addQuery, + addDsAndAddQuery, + selectDatasource, +} from "Support/utils/dataSource"; + +import { openQueryEditor } from "Support/utils/dataSource"; +import { dataSourceSelector } from "../../../../../constants/selectors/dataSource"; + +const data = {}; +data.dsName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); +data.dsName1 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); + +describe("Data source Airtable", () => { + beforeEach(() => { + cy.apiLogin(); + cy.defaultWorkspaceLogin(); + }); + + it("Should verify elements on connection AirTable form", () => { + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + cy.get(postgreSqlSelector.allDatasourceLabelAndCount).should( + "have.text", + postgreSqlText.allDataSources() + ); + cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should( + "have.text", + postgreSqlText.commonlyUsed + ); + cy.get(postgreSqlSelector.databaseLabelAndCount).should( + "have.text", + postgreSqlText.allDatabase() + ); + cy.get(postgreSqlSelector.apiLabelAndCount).should( + "have.text", + postgreSqlText.allApis + ); + cy.get(postgreSqlSelector.cloudStorageLabelAndCount).should( + "have.text", + postgreSqlText.allCloudStorage + ); + + selectAndAddDataSource("databases", airtableText.airtable, data.dsName); + + cy.get(postgreSqlSelector.buttonSave).verifyVisibleElement( + "have.text", + postgreSqlText.buttonTextSave + ); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + deleteDatasource(`cypress-${data.dsName}-airtable`); + }); + + it("Should verify the functionality of AirTable connection form.", () => { + selectAndAddDataSource("databases", airtableText.airtable, data.dsName); + + fillDataSourceTextField( + airtableText.ApiKey, + airtableText.apikeyPlaceholder, + Cypress.env("airTable_apikey") + ); + cy.get(postgreSqlSelector.buttonSave).click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + cy.get( + `[data-cy="cypress-${data.dsName}-airtable-button"]` + ).verifyVisibleElement("have.text", `cypress-${data.dsName}-airtable`); + deleteDatasource(`cypress-${data.dsName}-airtable`); + }); + + it("Should able to run the query with valid conection", () => { + const airTable_apiKey = Cypress.env("airTable_apikey"); + const airTable_baseId = Cypress.env("airtabelbaseId"); + const airTable_tableName = Cypress.env("airtable_tableName"); + const airTable_recordID = Cypress.env("airtable_recordId"); + + selectAndAddDataSource("databases", airtableText.airtable, data.dsName); + + fillDataSourceTextField( + airtableText.ApiKey, + airtableText.apikeyPlaceholder, + airTable_apiKey + ); + + cy.wait(1000); + cy.get(postgreSqlSelector.buttonSave).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + cy.get( + `[data-cy="cypress-${data.dsName}-airtable-button"]` + ).verifyVisibleElement("have.text", `cypress-${data.dsName}-airtable`); + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsName); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type(`${data.dsName}`); + cy.contains(`[id*="react-select-"]`, data.dsName).click(); + cy.get('[data-cy="query-rename-input"]').clear().type(data.dsName); + + // Verfiy List Recored operation + + cy.get(airTableSelector.operationSelectDropdown) + .click() + .type("List records{enter}"); + + cy.get(airTableSelector.baseIdInputField).clearAndTypeOnCodeMirror( + airTable_baseId + ); + + cy.get(airTableSelector.tableNameInputField).clearAndTypeOnCodeMirror( + airTable_tableName + ); + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + + // Verfiy Retrieve record operation + + cy.get(airTableSelector.operationSelectDropdown) + .click() + .type("Retrieve record{enter}"); + + cy.get(airTableSelector.baseIdInputField).clearAndTypeOnCodeMirror( + airTable_baseId + ); + cy.get(airTableSelector.tableNameInputField).clearAndTypeOnCodeMirror( + airTable_tableName + ); + + cy.get(airTableSelector.recordIdInputField).clearAndTypeOnCodeMirror( + airTable_recordID + ); + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + + // Verfiy Create record operation + + cy.get(airTableSelector.operationSelectDropdown) + .click() + .type("Create record{enter}"); + + cy.get(airTableSelector.baseIdInputField).clearAndTypeOnCodeMirror( + airTable_baseId + ); + + cy.get(airTableSelector.tableNameInputField).clearAndTypeOnCodeMirror( + airTable_tableName + ); + + cy.get(airTableSelector.bodyInputField) + .realClick() + .realType('[{"', { force: true, delay: 0 }) + .realType("fields", { force: true, delay: 0 }) + .realType('": {}', { force: true, delay: 0 }); + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + + // Verfiy Update record operation + + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type(`${data.dsName}`); + cy.contains(`[id*="react-select-"]`, data.dsName).click(); + cy.get('[data-cy="query-rename-input"]').clear().type(data.dsName1); + + cy.get(airTableSelector.operationSelectDropdown) + .click() + .type("Update record{enter}"); + + cy.get(airTableSelector.baseIdInputField).clearAndTypeOnCodeMirror( + airTable_baseId + ); + cy.get(airTableSelector.tableNameInputField).clearAndTypeOnCodeMirror( + airTable_tableName + ); + + cy.get(airTableSelector.recordIdInputField).clearAndTypeOnCodeMirror( + airTable_recordID + ); + + cy.get(airTableSelector.bodyInputField) + .realClick() + .realType("{", { force: true, delay: 0 }) + .realType("{enter}", { force: true, delay: 0 }) + .realType('"Phone Number": "555_98"', { force: true, delay: 0 }); + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName1}) completed.` + ); + + // Verify Delete record operation + + cy.get(airTableSelector.operationSelectDropdown) + .click() + .type("Delete record{enter}"); + + const recordId = Cypress._.uniqueId("recDummy_"); + + cy.request({ + method: "POST", + url: `https://api.airtable.com/v0/${airTable_baseId}/${airTable_tableName}`, + headers: { + Authorization: `Bearer ${Cypress.env("airTable_apikey")}`, + "Content-Type": "application/json", + }, + body: { + records: [ + { + fields: { + "Employee ID": "E005", + "First Name": "test", + "Last Name": "abc", + Email: "doe@example.com", + "Phone Number": "555-12", + }, + }, + ], + }, + }).then((createResponse) => { + const newRecordId = createResponse.body.records[0].id; + + cy.get(airTableSelector.operationSelectDropdown) + .click() + .type("Delete record{enter}"); + + cy.get(airTableSelector.baseIdInputField).clearAndTypeOnCodeMirror( + airTable_baseId + ); + cy.get(airTableSelector.tableNameInputField).clearAndTypeOnCodeMirror( + airTable_tableName + ); + + cy.get(airTableSelector.recordIdInputField).clearAndTypeOnCodeMirror( + newRecordId + ); + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName1}) completed.` + ); + }); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/amazonAthena.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/amazonAthena.cy.js new file mode 100644 index 0000000000..41bf4183b8 --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/amazonAthena.cy.js @@ -0,0 +1,207 @@ +import { fake } from "Fixtures/fake"; +import { postgreSqlSelector } from "Selectors/postgreSql"; +import { pluginSelectors } from "Selectors/plugins"; +import { postgreSqlText } from "Texts/postgreSql"; +import { amazonSesText } from "Texts/amazonSes"; +import { amazonAthenaText } from "Texts/amazonAthena"; +import { commonSelectors } from "Selectors/common"; +import { commonText } from "Texts/common"; + +import { + fillDataSourceTextField, + selectAndAddDataSource, +} from "Support/utils/postgreSql"; + +import { + verifyCouldnotConnectWithAlert, + deleteDatasource, + closeDSModal, + addQuery, + addDsAndAddQuery, +} from "Support/utils/dataSource"; + +import { openQueryEditor } from "Support/utils/dataSource"; +import { dataSourceSelector } from "../../../../../constants/selectors/dataSource"; + +const data = {}; +data.dsName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); + +describe("Data source amazon athena", () => { + beforeEach(() => { + cy.apiLogin(); + cy.defaultWorkspaceLogin(); + cy.intercept("POST", "/api/data_queries").as("createQuery"); + }); + + it("Should verify elements on amazon athena connection form", () => { + const Accesskey = Cypress.env("amazonathena_accessKey"); + const Secretkey = Cypress.env("amazonathena_secretKey"); + const DbName = Cypress.env("amazonathena_DbName"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + cy.get(postgreSqlSelector.allDatasourceLabelAndCount).should( + "have.text", + postgreSqlText.allDataSources() + ); + cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should( + "have.text", + postgreSqlText.commonlyUsed + ); + cy.get(postgreSqlSelector.databaseLabelAndCount).should( + "have.text", + postgreSqlText.allDatabase() + ); + cy.get(postgreSqlSelector.apiLabelAndCount).should( + "have.text", + postgreSqlText.allApis + ); + cy.get(postgreSqlSelector.cloudStorageLabelAndCount).should( + "have.text", + postgreSqlText.allCloudStorage + ); + + selectAndAddDataSource( + "databases", + amazonAthenaText.AmazonAthena, + data.dsName + ); + + cy.get(pluginSelectors.amazonAthenaDbName).click().type(DbName); + + cy.get(pluginSelectors.amazonsesAccesKey).click().type(" "); + + fillDataSourceTextField( + amazonSesText.labelSecretKey, + amazonAthenaText.placeholderSecretKey, + Secretkey + ); + + cy.get(".react-select__dropdown-indicator").eq(1).click(); + cy.get(".react-select__option").contains("US West (N. California)").click(); + + cy.get(postgreSqlSelector.buttonTestConnection) + .verifyVisibleElement( + "have.text", + postgreSqlText.buttonTextTestConnection + ) + .click(); + cy.get(postgreSqlSelector.connectionFailedText).verifyVisibleElement( + "have.text", + postgreSqlText.couldNotConnect + ); + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-Amazon-Athena`); + }); + + it("Should verify the functionality of amazon athena connection form.", () => { + const Accesskey = Cypress.env("amazonathena_accessKey"); + const Secretkey = Cypress.env("amazonathena_secretKey"); + const DbName = Cypress.env("amazonathena_DbName"); + selectAndAddDataSource( + "databases", + amazonAthenaText.AmazonAthena, + data.dsName + ); + + cy.get(pluginSelectors.amazonAthenaDbName).click().type(DbName); + + cy.get(pluginSelectors.amazonsesAccesKey).click().type(Accesskey); + + fillDataSourceTextField( + amazonSesText.labelSecretKey, + amazonAthenaText.placeholderSecretKey, + Secretkey + ); + + cy.get(".react-select__dropdown-indicator").eq(1).click(); + cy.get(".react-select__option").contains("US West (N. California)").click(); + + cy.get(postgreSqlSelector.buttonTestConnection).click(); + cy.get(postgreSqlSelector.textConnectionVerified, { + timeout: 10000, + }).should("have.text", postgreSqlText.labelConnectionVerified); + cy.get(postgreSqlSelector.buttonSave).click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-amazon-Athena`); + }); + + it("Should able to run the query with valid conection", () => { + const Accesskey = Cypress.env("amazonathena_accessKey"); + const Secretkey = Cypress.env("amazonathena_secretKey"); + const DbName = Cypress.env("amazonathena_DbName"); + selectAndAddDataSource( + "databases", + amazonAthenaText.AmazonAthena, + data.dsName + ); + + cy.get(pluginSelectors.amazonAthenaDbName).click().type(DbName); + + fillDataSourceTextField( + amazonAthenaText.labelAccesskey, + amazonAthenaText.placeholderEnteraAccessKey, + Cypress.env("amazonathena_accessKey") + ); + fillDataSourceTextField( + amazonAthenaText.labelSecretKey, + amazonAthenaText.placeholderSecretKey, + Cypress.env("amazonathena_secretKey") + ); + + cy.get(".react-select__dropdown-indicator").eq(1).click(); + cy.get(".react-select__option").contains("US West (N. California)").click(); + + cy.get(postgreSqlSelector.buttonTestConnection).click(); + cy.get(postgreSqlSelector.textConnectionVerified, { + timeout: 10000, + }).should("have.text", postgreSqlText.labelConnectionVerified); + + cy.get(postgreSqlSelector.buttonSave).click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + cy.get( + `[data-cy="cypress-${data.dsName}-amazon-athena-button"]` + ).verifyVisibleElement("have.text", `cypress-${data.dsName}-amazon-athena`); + cy.wait(1000); + + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsName); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type(`${data.dsName}`); + cy.contains(`[id*="react-select-"]`, data.dsName).click(); + cy.get('[data-cy="query-rename-input"]').clear().type(data.dsName); + + cy.get('[data-cy="query-input-field"]').clearAndTypeOnCodeMirror( + "SHOW DATABASES;" + ); + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/amazonses.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/amazonses.cy.js new file mode 100644 index 0000000000..d1ec035a69 --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/amazonses.cy.js @@ -0,0 +1,204 @@ +import { fake } from "Fixtures/fake"; +import { postgreSqlSelector } from "Selectors/postgreSql"; +import { pluginSelectors } from "Selectors/plugins"; +import { postgreSqlText } from "Texts/postgreSql"; +import { amazonSesText } from "Texts/amazonSes"; +import { commonSelectors } from "Selectors/common"; +import { commonText } from "Texts/common"; + +import { + fillDataSourceTextField, + selectAndAddDataSource, +} from "Support/utils/postgreSql"; + +import { + verifyCouldnotConnectWithAlert, + deleteDatasource, + closeDSModal, + addQuery, + addDsAndAddQuery, +} from "Support/utils/dataSource"; + +import { openQueryEditor } from "Support/utils/dataSource"; +import { dataSourceSelector } from "../../../../../constants/selectors/dataSource"; + +const data = {}; +data.dsName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); + +describe("Data source amazon ses", () => { + beforeEach(() => { + cy.apiLogin(); + cy.defaultWorkspaceLogin(); + cy.intercept("POST", "/api/data_queries").as("createQuery"); + }); + + it("Should verify elements on amazonses connection form", () => { + const Accesskey = Cypress.env("amazonSes_accessKey"); + const Secretkey = Cypress.env("amazonSes_secretKey"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + cy.get(postgreSqlSelector.allDatasourceLabelAndCount).should( + "have.text", + postgreSqlText.allDataSources() + ); + cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should( + "have.text", + postgreSqlText.commonlyUsed + ); + cy.get(postgreSqlSelector.databaseLabelAndCount).should( + "have.text", + postgreSqlText.allDatabase() + ); + cy.get(postgreSqlSelector.apiLabelAndCount).should( + "have.text", + postgreSqlText.allApis + ); + cy.get(postgreSqlSelector.cloudStorageLabelAndCount).should( + "have.text", + postgreSqlText.allCloudStorage + ); + + selectAndAddDataSource("databases", amazonSesText.AmazonSES, data.dsName); + + cy.get(".react-select__dropdown-indicator").eq(1).click(); + cy.get(".react-select__option").contains("US West (N. California)").click(); + + cy.get(pluginSelectors.amazonsesAccesKey).click().type(Accesskey); + + fillDataSourceTextField( + amazonSesText.labelSecretKey, + "**************", + Secretkey + ); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-Amazon-ses`); + }); + + it("Should verify the functionality of amazonses connection form.", () => { + selectAndAddDataSource("databases", amazonSesText.AmazonSES, data.dsName); + + cy.get(".react-select__dropdown-indicator").eq(1).click(); + cy.get(".react-select__option").contains("US West (N. California)").click(); + + fillDataSourceTextField( + amazonSesText.labelAccesskey, + amazonSesText.placeholderAccessKey, + Cypress.env("amazonSes_accessKey") + ); + fillDataSourceTextField( + amazonSesText.labelSecretKey, + amazonSesText.placeholderSecretKey, + Cypress.env("amazonSes_secretKey") + ); + + cy.get(postgreSqlSelector.buttonSave).click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + cy.get( + `[data-cy="cypress-${data.dsName}-amazon-ses-button"]` + ).verifyVisibleElement("have.text", `cypress-${data.dsName}-amazon-ses`); + + deleteDatasource(`cypress-${data.dsName}-amazon-ses`); + }); + + it("Should able to run the query with valid conection", () => { + const email = "adish" + "@" + "tooljet.com"; + selectAndAddDataSource("databases", amazonSesText.AmazonSES, data.dsName); + + cy.get(".react-select__dropdown-indicator").eq(1).click(); + cy.get(".react-select__option").contains("US West (N. California)").click(); + + fillDataSourceTextField( + amazonSesText.labelAccesskey, + amazonSesText.placeholderAccessKey, + Cypress.env("amazonSes_accessKey") + ); + fillDataSourceTextField( + amazonSesText.labelSecretKey, + amazonSesText.placeholderSecretKey, + Cypress.env("amazonSes_secretKey") + ); + + cy.get(postgreSqlSelector.buttonSave).click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + cy.get( + `[data-cy="cypress-${data.dsName}-amazon-ses-button"]` + ).verifyVisibleElement("have.text", `cypress-${data.dsName}-amazon-ses`); + cy.wait(1000); + + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsName); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type(`${data.dsName}`); + cy.contains(`[id*="react-select-"]`, data.dsName).click(); + cy.get('[data-cy="query-rename-input"]').clear().type(data.dsName); + + cy.get(pluginSelectors.operationDropdown) + .click() + .type("Email service{enter}"); + + cy.wait(500); + + cy.get(pluginSelectors.sendEmailInputField) + .realClick() + .realType('{{["', { force: true, delay: 0 }) + .realType("mekhla@tooljet.com", { force: true, delay: 0 }); + + cy.get(pluginSelectors.ccEmailInputField) + .realClick() + .realType('{{["', { force: true, delay: 0 }) + .realType("mani@tooljet.com", { force: true, delay: 0 }); + + cy.get(pluginSelectors.bccEmailInputField) + .realClick() + .realType('{{["', { force: true, delay: 0 }) + .realType("midhun@tooljet.com", { force: true, delay: 0 }); + + cy.get(pluginSelectors.sendEmailFromInputField) + .realClick() + .realType("adish", { force: true, delay: 0 }) + .realType("@", { force: true, delay: 0 }) + .realType("tooljet.com", { force: true, delay: 0 }); + + cy.get(pluginSelectors.emailSubjetInputField).clearAndTypeOnCodeMirror( + "Testmail for amazon ses" + ); + + cy.get(pluginSelectors.emailbodyInputField).clearAndTypeOnCodeMirror( + "Body text for amazon ses" + ); + + cy.wait(1000); + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/appWrite.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/appWrite.cy.js new file mode 100644 index 0000000000..aeeb12529d --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/appWrite.cy.js @@ -0,0 +1,308 @@ +import { fake } from "Fixtures/fake"; +import { postgreSqlSelector } from "Selectors/postgreSql"; +import { postgreSqlText } from "Texts/postgreSql"; +import { appwriteText } from "Texts/appWrite"; +import { appWriteSelectors } from "Selectors/Plugins"; +import { commonSelectors } from "Selectors/common"; +import { commonText } from "Texts/common"; + +import { + fillDataSourceTextField, + selectAndAddDataSource, +} from "Support/utils/postgreSql"; + +import { + verifyCouldnotConnectWithAlert, + deleteDatasource, + closeDSModal, + addQuery, + addDsAndAddQuery, +} from "Support/utils/dataSource"; + +import { openQueryEditor } from "Support/utils/dataSource"; +import { dataSourceSelector } from "../../../../../constants/selectors/dataSource"; + +const data = {}; +data.dsName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); + +describe("Data source AppWrite", () => { + beforeEach(() => { + cy.apiLogin(); + cy.defaultWorkspaceLogin(); + cy.intercept("POST", "/api/data_queries").as("createQuery"); + }); + + it("Should verify elements on appwrite connection form", () => { + const Host = Cypress.env("appwrite_host"); + const ProjectID = Cypress.env("appwrite_projectID"); + const DatabaseID = Cypress.env("appwrite_databaseID"); + const SecretKey = Cypress.env("appwrite_secretkey"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should( + "have.text", + postgreSqlText.commonlyUsed + ); + + cy.get(postgreSqlSelector.apiLabelAndCount).should( + "have.text", + postgreSqlText.allApis + ); + cy.get(postgreSqlSelector.cloudStorageLabelAndCount).should( + "have.text", + postgreSqlText.allCloudStorage + ); + + selectAndAddDataSource("databases", appwriteText.appwrite, data.dsName); + + fillDataSourceTextField( + appwriteText.host, + appwriteText.hostPlaceholder, + Host + ); + + fillDataSourceTextField( + appwriteText.ProjectID, + appwriteText.projectIdPlaceholder, + ProjectID + ); + + fillDataSourceTextField( + appwriteText.DatabaseID, + appwriteText.databaseIdPlaceholder, + DatabaseID + ); + + fillDataSourceTextField( + appwriteText.SecretKey, + appwriteText.SecretKeyPlaceholder, + SecretKey + ); + + cy.get(postgreSqlSelector.buttonTestConnection).click(); + cy.get(postgreSqlSelector.textConnectionVerified, { + timeout: 10000, + }).should("have.text", postgreSqlText.labelConnectionVerified); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-Appwrite`); + }); + + it("Should verify the functionality of appwrite connection form.", () => { + const Host = Cypress.env("appwrite_host"); + const ProjectID = Cypress.env("appwrite_projectID"); + const DatabaseID = Cypress.env("appwrite_databaseID"); + const SecretKey = Cypress.env("appwrite_secretkey"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + selectAndAddDataSource("databases", appwriteText.appwrite, data.dsName); + + fillDataSourceTextField( + appwriteText.host, + appwriteText.hostPlaceholder, + Host + ); + + fillDataSourceTextField( + appwriteText.ProjectID, + appwriteText.projectIdPlaceholder, + ProjectID + ); + + fillDataSourceTextField( + appwriteText.DatabaseID, + appwriteText.databaseIdPlaceholder, + DatabaseID + ); + + fillDataSourceTextField( + appwriteText.SecretKey, + appwriteText.SecretKeyPlaceholder, + SecretKey + ); + + cy.get(postgreSqlSelector.buttonTestConnection).click(); + cy.get(postgreSqlSelector.textConnectionVerified, { + timeout: 10000, + }).should("have.text", postgreSqlText.labelConnectionVerified); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + deleteDatasource(`cypress-${data.dsName}-Appwrite`); + }); + + it("Should be able to run the query with a valid connection", () => { + const Host = Cypress.env("appwrite_host"); + const ProjectID = Cypress.env("appwrite_projectID"); + const DatabaseID = Cypress.env("appwrite_databaseID"); + const SecretKey = Cypress.env("appwrite_secretkey"); + const CollectionID = Cypress.env("appwrite_collectionID"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + selectAndAddDataSource("databases", appwriteText.appwrite, data.dsName); + + fillDataSourceTextField( + appwriteText.host, + appwriteText.hostPlaceholder, + Host + ); + fillDataSourceTextField( + appwriteText.ProjectID, + appwriteText.projectIdPlaceholder, + ProjectID + ); + fillDataSourceTextField( + appwriteText.DatabaseID, + appwriteText.databaseIdPlaceholder, + DatabaseID + ); + fillDataSourceTextField( + appwriteText.SecretKey, + appwriteText.SecretKeyPlaceholder, + SecretKey + ); + + cy.get(postgreSqlSelector.buttonTestConnection).click(); + + cy.get(postgreSqlSelector.textConnectionVerified, { + timeout: 10000, + }).should("have.text", postgreSqlText.labelConnectionVerified); + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + cy.get( + `[data-cy="cypress-${data.dsName}-appwrite-button"]` + ).verifyVisibleElement("have.text", `cypress-${data.dsName}-appwrite`); + cy.wait(1000); + + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsName); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type(`${data.dsName}`); + cy.contains(`[id*="react-select-"]`, data.dsName).click(); + cy.get('[data-cy="query-rename-input"]').clear().type(data.dsName); + + // Create API document for delete operation + + cy.request({ + method: "POST", + url: `https://cloud.appwrite.io/v1/databases/${DatabaseID}/collections/${CollectionID}/documents`, + headers: { + "X-Appwrite-Project": ProjectID, + "X-Appwrite-Key": SecretKey, + "Content-Type": "application/json", + }, + body: { + documentId: "unique()", + data: { + User_name: "test", + User_ID: 30, + }, + permissions: ['read("any")'], + }, + }).then((response) => { + expect(response.status).to.eq(201); + cy.wrap(response.body.$id).as("documentId"); + }); + + // Verify all operations + const operations = [ + "List documents", + "Get document", + "Add Document to Collection", + "Update document", + "Delete document", + ]; + + cy.get("@documentId").then((documentId) => { + operations.forEach((operation) => { + cy.get(".react-select__input") + .eq(1) + .type(`${operation}{enter}`, { force: true }); + + if (operation === "Get document") { + cy.get(appWriteSelectors.collectionId).clearAndTypeOnCodeMirror( + CollectionID + ); + + cy.get(appWriteSelectors.documentId).clearAndTypeOnCodeMirror( + Cypress.env("appwrite_documentID") + ); + } + + if (operation === "Add Document to Collection") { + cy.get(appWriteSelectors.collectionId).clearAndTypeOnCodeMirror( + CollectionID + ); + cy.get(appWriteSelectors.bodyInput).clearAndTypeOnCodeMirror( + '{"User_name": "John Updated", "User_ID": 35}' + ); + } + + if (operation === "Update document") { + cy.get(appWriteSelectors.collectionId).clearAndTypeOnCodeMirror( + CollectionID + ); + + cy.get(appWriteSelectors.documentId).clearAndTypeOnCodeMirror( + Cypress.env("appwrite_documentID") + ); + cy.get(appWriteSelectors.bodyInput).clearAndTypeOnCodeMirror( + '{"User_name": "John Updated", "User_ID": 35}' + ); + } + + if (operation === "List documents") { + cy.get(appWriteSelectors.collectionId).clearAndTypeOnCodeMirror( + CollectionID + ); + } + + if (operation === "Delete document") { + cy.get(appWriteSelectors.collectionId).clearAndTypeOnCodeMirror( + CollectionID + ); + cy.get(appWriteSelectors.documentId).clearAndTypeOnCodeMirror( + documentId + ); + } + cy.get(dataSourceSelector.queryPreviewButton).click(); + + // Verify toast message + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + }); + }); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/baseRow.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/baseRow.cy.js new file mode 100644 index 0000000000..4c0532a9ca --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/baseRow.cy.js @@ -0,0 +1,219 @@ +import { fake } from "Fixtures/fake"; +import { postgreSqlSelector } from "Selectors/postgreSql"; +import { pluginSelectors, baserowSelectors } from "Selectors/plugins"; +import { postgreSqlText } from "Texts/postgreSql"; +import { amazonSesText } from "Texts/amazonSes"; +import { baseRowText } from "Texts/baseRow"; +import { amazonAthenaText } from "Texts/amazonAthena"; +import { commonSelectors } from "Selectors/common"; +import { commonText } from "Texts/common"; + +import { + fillDataSourceTextField, + selectAndAddDataSource, +} from "Support/utils/postgreSql"; + +import { + verifyCouldnotConnectWithAlert, + deleteDatasource, + closeDSModal, + addQuery, + addDsAndAddQuery, +} from "Support/utils/dataSource"; + +import { openQueryEditor } from "Support/utils/dataSource"; +import { dataSourceSelector } from "../../../../../constants/selectors/dataSource"; + +const data = {}; +data.dsName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); + +describe("Data source baserow", () => { + beforeEach(() => { + cy.apiLogin(); + cy.defaultWorkspaceLogin(); + cy.intercept("POST", "/api/data_queries").as("createQuery"); + }); + + it("Should verify elements on baserow connection form", () => { + const Apikey = Cypress.env("baserow_apikey"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + cy.get(postgreSqlSelector.allDatasourceLabelAndCount).should( + "have.text", + postgreSqlText.allDataSources() + ); + cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should( + "have.text", + postgreSqlText.commonlyUsed + ); + cy.get(postgreSqlSelector.databaseLabelAndCount).should( + "have.text", + postgreSqlText.allDatabase() + ); + cy.get(postgreSqlSelector.apiLabelAndCount).should( + "have.text", + postgreSqlText.allApis + ); + cy.get(postgreSqlSelector.cloudStorageLabelAndCount).should( + "have.text", + postgreSqlText.allCloudStorage + ); + + selectAndAddDataSource("databases", baseRowText.baserow, data.dsName); + + fillDataSourceTextField( + baseRowText.lableApiToken, + baseRowText.placeholderApiToken, + Apikey + ); + + cy.get(".react-select__control").eq(1).click(); + + cy.get(".react-select__option").contains("Baserow Cloud").click(); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-baserow`); + }); + + it("Should verify the functionality of baserow connection form.", () => { + const Apikey = Cypress.env("baserow_apikey"); + + selectAndAddDataSource("databases", baseRowText.baserow, data.dsName); + + fillDataSourceTextField( + baseRowText.lableApiToken, + baseRowText.placeholderApiToken, + Apikey + ); + + cy.get(".react-select__control").eq(1).click(); + + cy.get(".react-select__option").contains("Baserow Cloud").click(); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + deleteDatasource(`cypress-${data.dsName}-baserow`); + }); + + it("Should be able to run the query with a valid connection", () => { + const baserowTableID = Cypress.env("baserow_tableid"); + const baserowRowID = Cypress.env("baserow_rowid"); + const Apikey = Cypress.env("baserow_apikey"); + + selectAndAddDataSource("databases", baseRowText.baserow, data.dsName); + + fillDataSourceTextField( + baseRowText.lableApiToken, + baseRowText.placeholderApiToken, + Apikey + ); + + cy.get(".react-select__control").eq(1).click(); + cy.get(".react-select__option").contains("Baserow Cloud").click(); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + cy.get( + `[data-cy="cypress-${data.dsName}-baserow-button"]` + ).verifyVisibleElement("have.text", `cypress-${data.dsName}-baserow`); + cy.wait(1000); + + cy.log("Baserow Table ID:", baserowTableID); + cy.log("Row ID:", baserowRowID); + cy.log("API Key:", Apikey); + + if (!baserowTableID || !Apikey) { + throw new Error("Missing required environment variables!"); + } + + cy.request({ + method: "POST", + url: `https://api.baserow.io/api/database/rows/table/${baserowTableID}/`, + headers: { Authorization: `Token ${Apikey}` }, + body: { + field_1: "Sample Data", + field_2: "Another Value", + }, + }).then((response) => { + expect(response.status).to.eq(200); + const rowId = response.body.id; + + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsName); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type(`${data.dsName}`); + cy.contains(`[id*="react-select-"]`, data.dsName).click(); + cy.get('[data-cy="query-rename-input"]').clear().type(data.dsName); + + // Verify delete operation (Need to uncomment after bug fixes) + + // cy.get('[data-cy="operation-select-dropdown"]').click(); + // cy.get(".react-select__option").contains("Delete row").click(); + + // cy.get(baserowSelectors.baserowTabelId).clearAndTypeOnCodeMirror(baserowTableID); + // cy.get(baserowSelectors.rowIdinputfield).clearAndTypeOnCodeMirror(rowId.toString()); + + // cy.get(dataSourceSelector.queryPreviewButton).click(); + // cy.verifyToastMessage(commonSelectors.toastMessage, `Query (${data.dsName}) completed.`); + }); + + // Verify other operations + const operations = [ + "List fields", + "List rows", + "Get row", + "Create row", + "Update row", + "Move row", + ]; + + operations.forEach((operation) => { + cy.get(pluginSelectors.operationDropdown).click(); + cy.get(".react-select__option").contains(operation).click(); + + cy.get(baserowSelectors.table).clearAndTypeOnCodeMirror(baserowTableID); + + if (operation === "Get row") { + cy.get(baserowSelectors.rowIdinputfield).clearAndTypeOnCodeMirror( + baserowRowID + ); + } + if (operation === "Move row") { + cy.get('[data-cy="before-id-input-field"]').clearAndTypeOnCodeMirror( + "1" + ); + } + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + }); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/minio.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/minio.cy.js new file mode 100644 index 0000000000..e25111b9b6 --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/minio.cy.js @@ -0,0 +1,278 @@ +import { fake } from "Fixtures/fake"; +import { postgreSqlSelector } from "Selectors/postgreSql"; +import { postgreSqlText } from "Texts/postgreSql"; +import { minioText } from "Texts/minio"; +import { minioSelectors } from "Selectors/Plugins"; +import { commonSelectors } from "Selectors/common"; +import { commonText } from "Texts/common"; + +import { + fillDataSourceTextField, + selectAndAddDataSource, +} from "Support/utils/postgreSql"; + +import { + verifyCouldnotConnectWithAlert, + deleteDatasource, + closeDSModal, + addQuery, + addDsAndAddQuery, +} from "Support/utils/dataSource"; + +import { openQueryEditor } from "Support/utils/dataSource"; +import { dataSourceSelector } from "../../../../../constants/selectors/dataSource"; + +const data = {}; +data.dsName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); + +describe("Data source minio", () => { + beforeEach(() => { + cy.apiLogin(); + cy.defaultWorkspaceLogin(); + }); + + it("Should verify elements on minio connection form", () => { + const Host = Cypress.env("minio_host"); + const Port = Cypress.env("minio_port"); + const AccessKey = Cypress.env("minio_accesskey"); + const SecretKey = Cypress.env("minio_secretkey"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should( + "have.text", + postgreSqlText.commonlyUsed + ); + + cy.get(postgreSqlSelector.apiLabelAndCount).should( + "have.text", + postgreSqlText.allApis + ); + + cy.get(postgreSqlSelector.cloudStorageLabelAndCount).should( + "have.text", + postgreSqlText.allCloudStorage + ); + + selectAndAddDataSource("databases", minioText.minio, data.dsName); + + fillDataSourceTextField( + minioText.hostLabel, + minioText.hostInputPlaceholder, + Host + ); + + fillDataSourceTextField( + minioText.portLabel, + minioText.portPlaceholder, + Port + ); + + cy.get(`[${minioSelectors.sslToggle}]`).click(); + + fillDataSourceTextField( + minioText.labelAccesskey, + minioText.placeholderAccessKey, + AccessKey + ); + + fillDataSourceTextField( + minioText.labelSecretKey, + minioText.placeholderSecretKey, + SecretKey + ); + + cy.get(postgreSqlSelector.buttonTestConnection).click(); + cy.get(postgreSqlSelector.textConnectionVerified, { + timeout: 10000, + }).should("have.text", postgreSqlText.labelConnectionVerified); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-minio`); + }); + + it("Should verify functionality of minio connection form", () => { + const Host = Cypress.env("minio_host"); + const Port = Cypress.env("minio_port"); + const AccessKey = Cypress.env("minio_accesskey"); + const SecretKey = Cypress.env("minio_secretkey"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + selectAndAddDataSource("databases", minioText.minio, data.dsName); + + fillDataSourceTextField( + minioText.hostLabel, + minioText.hostInputPlaceholder, + Host + ); + + fillDataSourceTextField( + minioText.portLabel, + minioText.portPlaceholder, + Port + ); + + cy.get(`[${minioSelectors.sslToggle}]`).click(); + + fillDataSourceTextField( + minioText.labelAccesskey, + minioText.placeholderAccessKey, + AccessKey + ); + + fillDataSourceTextField( + minioText.labelSecretKey, + minioText.placeholderSecretKey, + SecretKey + ); + + cy.get(postgreSqlSelector.buttonTestConnection).click(); + cy.get(postgreSqlSelector.textConnectionVerified, { + timeout: 10000, + }).should("have.text", postgreSqlText.labelConnectionVerified); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-minio`); + }); + + it("Should be able to run the query with a valid connection", () => { + const Host = Cypress.env("minio_host"); + const Port = Cypress.env("minio_port"); + const AccessKey = Cypress.env("minio_accesskey"); + const SecretKey = Cypress.env("minio_secretkey"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + selectAndAddDataSource("databases", minioText.minio, data.dsName); + + fillDataSourceTextField( + minioText.hostLabel, + minioText.hostInputPlaceholder, + Host + ); + + fillDataSourceTextField( + minioText.portLabel, + minioText.portPlaceholder, + Port + ); + + cy.get(`[${minioSelectors.sslToggle}]`).click(); + + fillDataSourceTextField( + minioText.labelAccesskey, + minioText.placeholderAccessKey, + AccessKey + ); + + fillDataSourceTextField( + minioText.labelSecretKey, + minioText.placeholderSecretKey, + SecretKey + ); + + cy.get(postgreSqlSelector.buttonTestConnection).click(); + cy.get(postgreSqlSelector.textConnectionVerified, { + timeout: 10000, + }).should("have.text", postgreSqlText.labelConnectionVerified); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsName); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type(`${data.dsName}`); + cy.contains(`[id*="react-select-"]`, data.dsName).click(); + cy.get('[data-cy="query-rename-input"]').clear().type(data.dsName); + + const operationsMinio = [ + "List buckets", + "Put object", + "List objects in a bucket", + "Read object", + "Presigned url for download", + "Presigned url for upload", + "Remove object", + ]; + + operationsMinio.forEach((operation) => { + cy.get(".react-select__input") + .eq(1) + .type(`${operation}{enter}`, { force: true }); + + if (operation === "List objects in a bucket") { + cy.get(minioSelectors.bucketNameInputField).clearAndTypeOnCodeMirror( + minioText.bucketName + ); + } + if (operation === "Read object" || operation === "Remove object") { + cy.get(minioSelectors.bucketNameInputField).clearAndTypeOnCodeMirror( + minioText.bucketName + ); + cy.get(minioSelectors.objectNameInputField).clearAndTypeOnCodeMirror( + minioText.objectName + ); + } + if (operation === "Put object") { + cy.get(minioSelectors.bucketNameInputField).clearAndTypeOnCodeMirror( + minioText.bucketName + ); + cy.get(minioSelectors.objectNameInputField).clearAndTypeOnCodeMirror( + minioText.objectName + ); + cy.get(minioSelectors.contentTypeInputField).clearAndTypeOnCodeMirror( + '"string"' + ); + cy.get(minioSelectors.dataInput).clearAndTypeOnCodeMirror(`test`); + } + if ( + operation === "Presigned url for download" || + operation === "Presigned url for upload" + ) { + cy.get(minioSelectors.bucketNameInputField).clearAndTypeOnCodeMirror( + minioText.bucketName + ); + cy.get(minioSelectors.objectNameInputField).clearAndTypeOnCodeMirror( + minioText.objectName + ); + } + + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + }); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/oracleDbHappyPath.cy.skip.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/oracleDbHappyPath.cy.skip.js index c42bda600b..e7ae4f296b 100644 --- a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/oracleDbHappyPath.cy.skip.js +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/oracleDbHappyPath.cy.skip.js @@ -123,7 +123,7 @@ describe("Data sources", () => { ); cy.clearAndType( - '[data-cy="data-source-name-input-filed"]', + '[data-cy="data-source-name-input-field"]', postgreSqlText.psqlName ); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/s3HappyPath.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/s3HappyPath.cy.js index 3e8a243c53..7a128c1470 100644 --- a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/s3HappyPath.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/s3HappyPath.cy.js @@ -19,7 +19,8 @@ const data = {}; describe("Data sources AWS S3", () => { beforeEach(() => { - cy.appUILogin(); + cy.apiLogin(); + cy.defaultWorkspaceLogin(); data.dataSourceName = fake.lastName .toLowerCase() .replaceAll("[^A-Za-z]", ""); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/sapHanaHappyPath.cy.skip.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/sapHanaHappyPath.cy.skip.js index 813b2663c9..c240286274 100644 --- a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/sapHanaHappyPath.cy.skip.js +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/sapHanaHappyPath.cy.skip.js @@ -108,7 +108,7 @@ describe("Data sources", () => { selectAndAddDataSource(postgreSqlText.postgreSQL); cy.clearAndType( - '[data-cy="data-source-name-input-filed"]', + '[data-cy="data-source-name-input-field"]', postgreSqlText.psqlName ); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/twilio.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/twilio.cy.js new file mode 100644 index 0000000000..824c8d3bbc --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/twilio.cy.js @@ -0,0 +1,189 @@ +import { fake } from "Fixtures/fake"; +import { postgreSqlSelector } from "Selectors/postgreSql"; +import { postgreSqlText } from "Texts/postgreSql"; +import { twilioText } from "Texts/twilio"; +import { twilioSelectors } from "Selectors/Plugins"; +import { commonSelectors } from "Selectors/common"; +import { commonText } from "Texts/common"; + +import { + fillDataSourceTextField, + selectAndAddDataSource, +} from "Support/utils/postgreSql"; + +import { + verifyCouldnotConnectWithAlert, + deleteDatasource, + closeDSModal, + addQuery, + addDsAndAddQuery, +} from "Support/utils/dataSource"; + +import { openQueryEditor } from "Support/utils/dataSource"; +import { dataSourceSelector } from "../../../../../constants/selectors/dataSource"; +import { pluginSelectors } from "Selectors/plugins"; + +const data = {}; +data.dsName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); + +describe("Data source Twilio", () => { + beforeEach(() => { + cy.apiLogin(); + cy.defaultWorkspaceLogin(); + }); + + it("Should verify elements on Twilio connection form", () => { + const AuthToken = Cypress.env("twilio_auth_token"); + const AccountSID = Cypress.env("twilio_account_SID"); + const MessageSID = Cypress.env("twilio_messaging_service_SID"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should( + "have.text", + postgreSqlText.commonlyUsed + ); + + cy.get(postgreSqlSelector.apiLabelAndCount).should( + "have.text", + postgreSqlText.allApis + ); + cy.get(postgreSqlSelector.cloudStorageLabelAndCount).should( + "have.text", + postgreSqlText.allCloudStorage + ); + + selectAndAddDataSource("databases", twilioText.twilio, data.dsName); + + fillDataSourceTextField( + twilioText.authTokenLabel, + twilioText.authTokenPlaceholder, + AuthToken + ); + + fillDataSourceTextField( + twilioText.accountSidLabel, + twilioText.accountSidPlaceholder, + AccountSID + ); + + fillDataSourceTextField( + twilioText.messagingSIDLabel, + twilioText.messagingSIDPalceholder, + MessageSID + ); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-twilio`); + }); + + it("Should verify functionality of Twilio connection form", () => { + const AuthToken = Cypress.env("twilio_auth_token"); + const AccountSID = Cypress.env("twilio_account_SID"); + const MessageSID = Cypress.env("twilio_messaging_service_SID"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + selectAndAddDataSource("databases", twilioText.twilio, data.dsName); + + fillDataSourceTextField( + twilioText.authTokenLabel, + twilioText.authTokenPlaceholder, + AuthToken + ); + + fillDataSourceTextField( + twilioText.accountSidLabel, + twilioText.accountSidPlaceholder, + AccountSID + ); + + fillDataSourceTextField( + twilioText.messagingSIDLabel, + twilioText.messagingSIDPalceholder, + MessageSID + ); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + deleteDatasource(`cypress-${data.dsName}-twilio`); + }); + + it("Should be able to run the query with a valid connection", () => { + const AuthToken = Cypress.env("twilio_auth_token"); + const AccountSID = Cypress.env("twilio_account_SID"); + const MessageSID = Cypress.env("twilio_messaging_service_SID"); + const MessageNumber = Cypress.env("twilio_message_number"); + + cy.get(commonSelectors.globalDataSourceIcon).click(); + closeDSModal(); + + selectAndAddDataSource("databases", twilioText.twilio, data.dsName); + + fillDataSourceTextField( + twilioText.authTokenLabel, + twilioText.authTokenPlaceholder, + AuthToken + ); + + fillDataSourceTextField( + twilioText.accountSidLabel, + twilioText.accountSidPlaceholder, + AccountSID + ); + + fillDataSourceTextField( + twilioText.messagingSIDLabel, + twilioText.messagingSIDPalceholder, + MessageSID + ); + + cy.get(postgreSqlSelector.buttonSave) + .verifyVisibleElement("have.text", postgreSqlText.buttonTextSave) + .click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + postgreSqlText.toastDSSaved + ); + + cy.get(commonSelectors.dashboardIcon).click(); + cy.get(commonSelectors.appCreateButton).click(); + cy.get(commonSelectors.appNameInput).click().type(data.dsName); + cy.get(commonSelectors.createAppButton).click(); + cy.skipWalkthrough(); + + cy.get('[data-cy="show-ds-popover-button"]').click(); + cy.get(".css-4e90k9").type(`${data.dsName}`); + cy.contains(`[id*="react-select-"]`, data.dsName).click(); + cy.get('[data-cy="query-rename-input"]').clear().type(data.dsName); + + cy.get(pluginSelectors.operationDropdown).click().type("Send SMS{enter}"); + + cy.get(twilioSelectors.toNumberInputField).clearAndTypeOnCodeMirror( + MessageNumber + ); + cy.get(twilioSelectors.bodyInput).clearAndTypeOnCodeMirror( + twilioText.messageText + ); + cy.get(dataSourceSelector.queryPreviewButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + `Query (${data.dsName}) completed.` + ); + }); +}); diff --git a/cypress-tests/cypress/support/utils/mongoDB.js b/cypress-tests/cypress/support/utils/mongoDB.js index 0ca418db9d..61107f639e 100644 --- a/cypress-tests/cypress/support/utils/mongoDB.js +++ b/cypress-tests/cypress/support/utils/mongoDB.js @@ -10,7 +10,7 @@ export const connectMongo = () => { selectAndAddDataSource(mongoDbText.mongoDb); cy.clearAndType( - '[data-cy="data-source-name-input-filed"]', + '[data-cy="data-source-name-input-field"]', mongoDbText.cypressMongoDb ); diff --git a/cypress-tests/cypress/support/utils/multipage.js b/cypress-tests/cypress/support/utils/multipage.js index 8821054af0..299986f47c 100644 --- a/cypress-tests/cypress/support/utils/multipage.js +++ b/cypress-tests/cypress/support/utils/multipage.js @@ -3,7 +3,7 @@ import { commonSelectors } from "../../constants/selectors/common"; export const searchPage = (pageName) => { cy.get('[title="Search"]').click(); - cy.get('[data-cy="search-input-filed"]').type(pageName); + cy.get('[data-cy="search-input-field"]').type(pageName); }; export const clearSearch = () => { diff --git a/cypress-tests/cypress/support/utils/postgreSql.js b/cypress-tests/cypress/support/utils/postgreSql.js index fc3f257627..72c4ce9cbc 100644 --- a/cypress-tests/cypress/support/utils/postgreSql.js +++ b/cypress-tests/cypress/support/utils/postgreSql.js @@ -48,27 +48,25 @@ export const selectAndAddDataSource = ( dataSourceName ) => { cy.get(commonSelectors.globalDataSourceIcon).click(); - cy.wait(1000) + cy.wait(1000); cy.get(`[data-cy="${cyParamName(dscategory)}-datasource-button"]`).click(); - cy.wait(500) + cy.wait(500); cy.get(postgreSqlSelector.dataSourceSearchInputField).type(dataSource); - cy.get(`[data-cy="data-source-${(dataSource).toLowerCase()}"]`) + cy.get(`[data-cy="data-source-${dataSource.toLowerCase()}"]`) .parent() .within(() => { cy.get( - `[data-cy="data-source-${( - dataSource - ).toLowerCase()}"]>>>.datasource-card-title` + `[data-cy="data-source-${dataSource.toLowerCase()}"]>>>.datasource-card-title` ).realHover("mouse"); cy.get( `[data-cy="${cyParamName(dataSource).toLowerCase()}-add-button"]` ).click(); }); - cy.wait(1000) - cy.get(postgreSqlSelector.buttonSave).should("be.disabled") + cy.wait(1000); + cy.get(postgreSqlSelector.buttonSave).should("be.disabled"); cy.clearAndType( - '[data-cy="data-source-name-input-filed"]', + '[data-cy="data-source-name-input-field"]', cyParamName(`cypress-${dataSourceName}-${dataSource}`) ); cy.get(postgreSqlSelector.buttonSave).click(); @@ -184,4 +182,4 @@ export const addWidgetsToAddUser = () => { addEventHandlerToRunQuery("add_data_using_widgets"); }; -export const addListviewToVerifyData = () => { }; \ No newline at end of file +export const addListviewToVerifyData = () => {}; diff --git a/docker/ee/ee-production.Dockerfile b/docker/ee/ee-production.Dockerfile index 230a2f8ebb..b69458daa1 100644 --- a/docker/ee/ee-production.Dockerfile +++ b/docker/ee/ee-production.Dockerfile @@ -62,8 +62,40 @@ FROM debian:11 RUN apt-get update -yq \ && apt-get install curl gnupg zip -yq \ && apt-get install -yq build-essential \ + && apt -y install redis \ && apt-get clean -y +# Install required dependencies for downloading and extracting files +RUN apt-get update && apt-get install -y \ + curl tar xz-utils postgresql postgresql-contrib postgresql-client && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install PostgREST from official Docker image +COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin + +RUN apt-get update && apt-get install -y supervisor + +# Create supervisord configuration file +RUN echo "[supervisord]\n" \ + "nodaemon=true\n" \ + "\n" \ + "[program:postgrest]\n" \ + "command=/bin/postgrest \n" \ + "autostart=true\n" \ + "autorestart=true\n" \ + "stdout_logfile=/dev/stdout\n" \ + "stderr_logfile=/dev/stderr\n" \ + "stdout_logfile_maxbytes=0\n" \ + "stderr_logfile_maxbytes=0\n" \ + "\n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf + +# Create a wrapper for PostgREST to prefix its logs +RUN mv /bin/postgrest /bin/postgrest-original && \ + echo '#!/bin/bash\n\ +exec /bin/postgrest-original "$@" 2>&1 | sed "s/^/[PostgREST] /"\n\ +' > /bin/postgrest && \ + chmod +x /bin/postgrest + RUN curl -O https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz \ && tar -xf node-v18.18.2-linux-x64.tar.xz \ @@ -152,6 +184,25 @@ RUN mkdir -p /tmp/.npm/npm-cache/_logs \ && chmod g+s /tmp/.npm/npm-cache/_logs \ && chmod -R g=u /tmp/.npm/npm-cache/_logs +# Create Redis data, log, and configuration directories +RUN mkdir -p /var/lib/redis /var/log/redis /etc/redis \ + && chown -R appuser:0 /var/lib/redis /var/log/redis /etc/redis \ + && chmod g+s /var/lib/redis /var/log/redis /etc/redis \ + && chmod -R g=u /var/lib/redis /var/log/redis /etc/redis + +# Set permissions for PostgREST binary +RUN chown appuser:0 /bin/postgrest && chmod u+x /bin/postgrest && chmod g=u /bin/postgrest + +RUN touch /tmp/postgrest.conf \ + && chown appuser:0 /tmp/postgrest.conf \ + && chmod 640 /tmp/postgrest.conf + +# Create PostgREST data, log, and configuration directories +RUN mkdir -p /var/lib/postgrest /var/log/postgrest /etc/postgrest \ + && chown -R appuser:0 /var/lib/postgrest /var/log/postgrest /etc/postgrest \ + && chmod g+s /var/lib/postgrest /var/log/postgrest /etc/postgrest \ + && chmod -R g=u /var/lib/postgrest /var/log/postgrest /etc/postgrest + ENV HOME=/home/appuser # Switch back to appuser diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index 4c558f3cc2..aaedf9deb9 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -18,6 +18,7 @@ export const ConfigHandle = ({ showHandle, componentType, visibility, + subContainerIndex, }) => { const shouldFreeze = useStore((state) => state.getShouldFreeze()); const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow); @@ -40,10 +41,12 @@ export const ConfigHandle = ({ const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== ''; // If one component is hovered and one is selected, show the handle for the hovered component return ( - isWidgetHovered || - (showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered) + (subContainerIndex === 0 || subContainerIndex === null) && + (isWidgetHovered || + (showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered)) ); }, shallow); + let height = visibility === false ? 10 : widgetHeight; return ( diff --git a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx index 0f1cb3359f..0384d4cc6e 100644 --- a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx +++ b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx @@ -91,6 +91,7 @@ const WidgetWrapper = memo( showHandle={isWidgetActive} componentType={componentType} visibility={visibility} + subContainerIndex={subContainerIndex} /> )} { // TODO: Move this function to componentSlice export const copyComponents = ({ isCut = false, isCloning = false }) => { + const selectedText = window.getSelection()?.toString().trim(); + if (selectedText) { + navigator.clipboard.writeText(selectedText); + return; + } + const selectedComponents = useStore.getState().getSelectedComponentsDefinition(); if (selectedComponents.length < 1) return getSelectedText(); const allComponents = useStore.getState().getCurrentPageComponents(); diff --git a/frontend/src/AppBuilder/CodeBuilder/utils.js b/frontend/src/AppBuilder/CodeBuilder/utils.js index 7a75bdb3b8..be3674aa1f 100644 --- a/frontend/src/AppBuilder/CodeBuilder/utils.js +++ b/frontend/src/AppBuilder/CodeBuilder/utils.js @@ -74,6 +74,7 @@ export function getSuggestionKeys(refState) { 'setVariable', 'getVariable', 'unSetVariable', + 'unsetAllVariables', 'showAlert', 'logout', 'showModal', @@ -85,6 +86,7 @@ export function getSuggestionKeys(refState) { 'setPageVariable', 'getPageVariable', 'unsetPageVariable', + 'unsetAllPageVariables', 'switchPage', ]; diff --git a/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx b/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx index 2429973c25..6c28bdbb21 100644 --- a/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx +++ b/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx @@ -294,13 +294,9 @@ const PreviewContainer = ({ ...restProps }) => { const { validationSchema, isWorkspaceVariable, errorStateActive, previewPlacement, validationFn } = restProps; - const [errorMessage, setErrorMessage] = useState(''); - const typeofError = getCurrentNodeType(errorMessage); - const errorMsg = typeofError === 'Array' ? errorMessage[0] : errorMessage; - const darkMode = localStorage.getItem('darkMode') === 'true'; const popover = ( {!isPortalOpen && ( { + // Force position update on first render + // This is done to avoid scroll issue + if (state.elements.popper) { + state.elements.popper.style.position = 'fixed'; + } + }, }} > {(props) => React.cloneElement(popover, props)} diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index 1243f26f43..7f8765e287 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -1,5 +1,5 @@ /* eslint-disable import/no-unresolved */ -import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { PreviewBox } from './PreviewBox'; import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip'; import { useTranslation } from 'react-i18next'; @@ -31,6 +31,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r const [currentValue, setCurrentValue] = useState(''); const [errorStateActive, setErrorStateActive] = useState(false); const [cursorInsidePreview, setCursorInsidePreview] = useState(false); + const [showSuggestions, setShowSuggestions] = useState(true); const validationFn = restProps?.validationFn; const componentDefinition = useStore((state) => state.getComponentDefinition(componentId), shallow); const parentId = componentDefinition?.component?.parent; @@ -38,6 +39,30 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r const customVariables = customResolvables?.[parentId]?.[0] || {}; + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.intersectionRatio < 1) { + setShowPreview(false); + setShowSuggestions(false); + } else { + setShowSuggestions(true); + } + }, + { root: null, threshold: [1] } // Fires when any part of the element is out of view + ); + + if (wrapperRef.current) { + observer.observe(wrapperRef.current); + } + + return () => { + if (wrapperRef.current) { + observer.unobserve(wrapperRef.current); + } + }; + }, []); + const isPreviewFocused = useRef(false); const wrapperRef = useRef(null); @@ -136,6 +161,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r componentName={componentName} setShowPreview={setShowPreview} showPreview={showPreview} + showSuggestions={showSuggestions} {...restProps} /> @@ -168,6 +194,7 @@ const EditorInput = ({ previewRef, setShowPreview, onInputChange, + showSuggestions, }) => { const getSuggestions = useStore((state) => state.getSuggestions, shallow); function autoCompleteExtensionConfig(context) { @@ -223,7 +250,7 @@ const EditorInput = ({ defaultKeymap: true, positionInfo: () => { return { - class: 'cm-completionInfo-top cm-custom-completion-info', + class: 'cm-completionInfo-top cm-custom-completion-info cm-custom-singleline-completion-info', }; }, maxRenderedOptions: 10, @@ -286,7 +313,7 @@ const EditorInput = ({ const isInsideQueryPane = !!currentEditorHeightRef?.current?.closest('.query-details'); const showLineNumbers = lang == 'jsx' || type === 'extendedSingleLine' || false; - const customClassNames = cx('codehinter-input', { + const customClassNames = cx('codehinter-input single-line-codehinter-input', { 'border-danger': error, focused: isFocused, 'focus-box-shadow-active': firstTimeFocus, @@ -336,18 +363,9 @@ const EditorInput = ({
{/* sticky element to position the preview box correctly on top without flowing out of container */} -
{usePortalEditor && ( - { - setFirstTimeFocus(false); - handleOnChange(val); - onInputChange && onInputChange(val); +
handleFocus()} - onBlur={() => handleOnBlur()} - className={customClassNames} - theme={theme} - indentWithTab={false} - readOnly={disabled} - /> + className="check-here" + ref={previewRef} + > + { + setFirstTimeFocus(false); + handleOnChange(val); + onInputChange && onInputChange(val); + }} + basicSetup={{ + lineNumbers: showLineNumbers, + syntaxHighlighting: true, + bracketMatching: true, + foldGutter: false, + highlightActiveLine: false, + autocompletion: showSuggestions, + completionKeymap: true, + searchKeymap: false, + }} + onMouseDown={() => handleFocus()} + onBlur={() => handleOnBlur()} + className={customClassNames} + theme={theme} + indentWithTab={false} + readOnly={disabled} + /> +
diff --git a/frontend/src/AppBuilder/CodeEditor/styles.scss b/frontend/src/AppBuilder/CodeEditor/styles.scss index 5c66bc37ee..d7f715fb3b 100644 --- a/frontend/src/AppBuilder/CodeEditor/styles.scss +++ b/frontend/src/AppBuilder/CodeEditor/styles.scss @@ -655,6 +655,11 @@ background-color: #F28F2D !important; } + +.cm-custom-singleline-completion-info { + display: none; +} + .tjdb-hinter-portal{ .cm-theme{ height: 100% ; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx index 4146c4a28e..aaf12714b9 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx @@ -34,6 +34,7 @@ export const BaseLeftSidebar = ({ resetUnreadErrorCount, toggleLeftSidebar, isSidebarOpen, + isDraggingQueryPane, ] = useStore( (state) => [ state.isLeftSideBarPinned, @@ -46,6 +47,7 @@ export const BaseLeftSidebar = ({ state.debugger.resetUnreadErrorCount, state.toggleLeftSidebar, state.isSidebarOpen, + state.queryPanel.isDraggingQueryPane, ], shallow ); @@ -68,11 +70,15 @@ export const BaseLeftSidebar = ({ }; useEffect(() => { - setPopoverContentHeight( - ((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100 - ); + if (!isDraggingQueryPane) { + setPopoverContentHeight( + ((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100 + ); + } else { + setPopoverContentHeight(100); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [queryPanelHeight]); + }, [queryPanelHeight, isDraggingQueryPane]); const renderPopoverContent = () => { if (selectedSidebarItem === null || !isSidebarOpen) return null; diff --git a/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx b/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx index 52178043cf..2234fa8e50 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx @@ -108,7 +108,8 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende const [codeEditorKey, setCodeEditorKey] = useState(uuidv4()); const [state, setState] = useState({ ...defaultValue, - [options.transformationLanguage ?? 'javascript']: options?.transformation, + ...(options?.transformation ? { [options.transformationLanguage ?? 'javascript']: options?.transformation } : {}), + ...options?.transformations, }); const { t } = useTranslation(); @@ -119,7 +120,6 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende useEffect(() => { if (lang !== (options.transformationLanguage ?? 'javascript')) { changeOption('transformationLanguage', lang); - changeOption('transformation', state[lang]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [lang]); @@ -127,20 +127,24 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende useEffect(() => { if (prevQueryId.current === queryId) { lang !== (options.transformationLanguage ?? 'javascript') && changeOption('transformationLanguage', lang); - setState({ ...state, [lang]: options.transformation ?? state[lang] ?? defaultValue[lang] }); + setState((prevState) => { + return { + ...prevState, + ...(options?.transformation + ? { [options.transformationLanguage ?? 'javascript']: options?.transformation } + : {}), + ...options?.transformations, + }; + }); } prevQueryId.current = queryId; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(options.transformation)]); + }, [JSON.stringify(options?.transformation || {}), JSON.stringify(options.transformations)]); useEffect(() => { if (selectedQueryId !== queryId) { - const nonLangdefaultCode = getNonActiveTransformations(options?.transformationLanguage ?? 'javascript'); - const finalState = _.merge( - {}, - { [options?.transformationLanguage ?? lang]: options.transformation ?? defaultValue[lang] }, - nonLangdefaultCode - ); + const olderTransformation = options?.transformation ? { [lang]: options?.transformation } : {}; + const finalState = _.merge({}, defaultValue, olderTransformation, options?.transformations); setState(finalState); } @@ -206,8 +210,6 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende activeKey={lang} onSelect={(value) => { setLang(value); - changeOption('transformationLanguage', value); - changeOption('transformation', state[value]); }} defaultActiveKey="javascript" > @@ -250,7 +252,7 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende height={400} className="query-hinter" onChange={(value) => { - changeOption('transformation', value); + changeOption('transformations', { ...state, [lang]: value }); }} renderCopilot={renderCopilot} componentName={`transformation`} diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/TabContent.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/TabContent.jsx index cf47ded427..ee55937b1c 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/TabContent.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/TabContent.jsx @@ -30,7 +30,7 @@ export default ({ return ( <>
-
+
) : ( -
+
{index > 0 && ( diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx index 4734addb26..863c826b8a 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx @@ -340,10 +340,7 @@ const JsonBfieldsForSelect = ({ selectedJsonbColumns, handleJSonChange, table }) handleRemove(colDetails.id, colDetails.name, colDetails.table)} > diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSort.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSort.jsx index 2d5a2de518..9e129e9eb4 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSort.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSort.jsx @@ -164,10 +164,7 @@ export default function JoinSort({ darkMode }) { setJoinOrderByOptions(joinOrderByOptions.filter((opt, idx) => idx !== i))} > diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx index eba36f37a3..cf14448967 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx @@ -535,12 +535,11 @@ const RenderFilterSection = ({ darkMode }) => { removeFilterConditionEntry(index)} > diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderColumnUI.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderColumnUI.jsx index 1766691d66..89323c1c3c 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderColumnUI.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderColumnUI.jsx @@ -54,10 +54,7 @@ const RenderColumnUI = ({ removeColumnOptionsPair(id)} > diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderFilterSectionUI.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderFilterSectionUI.jsx index 983019697a..cd7f94abf3 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderFilterSectionUI.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderFilterSectionUI.jsx @@ -117,10 +117,7 @@ const RenderFilterSectionUI = ({ removeFilterConditionPair(id)} > diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderSortUI.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderSortUI.jsx index b5021974ba..a349b9e5ed 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderSortUI.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderSortUI.jsx @@ -86,10 +86,7 @@ const RenderSortUI = ({ removeSortConditionPair(id)} > diff --git a/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx b/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx index 827efe6d33..ea8623b0c1 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx @@ -185,6 +185,7 @@ export const QueryPanel = ({ darkMode }) => { id="query-manager" style={{ height: `calc(100% - ${isExpanded ? height : 100}%)`, + maxHeight: '93.5%', cursor: isDraggingQueryPane || isTopOfQueryPanel ? 'row-resize' : 'default', ...(!isExpanded && { border: 'none', diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx index 370aaf87ac..12d4d68102 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx @@ -295,7 +295,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
{options?.map((item, index) => { return ( - + {(provided, snapshot) => (
{ + if (!isOpen) { + document.activeElement?.blur(); // Manually trigger blur when popover closes + } + }} > -
+
setHoveredOptionIndex(index)} @@ -399,7 +404,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
- {getResolvedValue(item.label)} + {getResolvedValue(item?.label)}
{index === hoveredOptionIndex && ( diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx index b11a675e80..3aca83b046 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx @@ -177,7 +177,7 @@ class TableComponent extends React.Component { style={{ width: '280px', maxHeight: resolveReferences(column.isEditable) ? '100vh' : 'inherit', - overflowY: 'auto', + // overflowY: 'auto', zIndex: '9999', }} > @@ -327,7 +327,12 @@ class TableComponent extends React.Component { placement="left" rootClose={this.state.actionPopOverRootClose} overlay={this.actionPopOver(action, index)} - onToggle={(showing) => this.setState({ showPopOver: showing })} + onToggle={(showing) => { + if (!showing) { + document.activeElement?.blur(); // Manually trigger blur when popover closes + } + this.setState({ showPopOver: showing }); + }} >
@@ -649,6 +654,7 @@ class TableComponent extends React.Component { if (show) { this.handleToggleColumnPopover(index); } else { + document.activeElement?.blur(); // Manually trigger blur when popover closes this.handleToggleColumnPopover(null); } }} diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js index c0fa889dd5..ab3eb40c2c 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js @@ -127,6 +127,13 @@ export const buttonGroupConfig = { exposedVariables: { selected: [1], }, + actions: [ + { + handle: 'setSelected', + displayName: 'Select option', + params: [{ handle: 'selected', displayName: 'Value' }], + }, + ], definition: { others: { showOnDesktop: { value: '{{true}}' }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js index fec2e812b4..62b55a7fea 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js @@ -13,6 +13,7 @@ export const listviewConfig = { top: 15, left: 3, height: 100, + width: 7, }, properties: ['source'], accessorKey: 'imageURL', diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js b/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js index a397979a3e..0ed1e2a320 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js @@ -13,6 +13,7 @@ export const tabsConfig = { top: 60, left: 17, height: 100, + width: 7, }, tab: 0, properties: ['source'], diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index c4448a3bbf..8f325dd614 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -507,7 +507,7 @@ export const createComponentsSlice = (set, get) => ({ const resolvedMandatory = getResolvedValue(mandatory, customResolveObjects) || false; - if (resolvedMandatory == true && !widgetValue) { + if (resolvedMandatory == true && !widgetValue && widgetValue !== 0) { return { isValid: false, validationError: `Field cannot be empty`, diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 948ac39b39..995050d023 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -690,6 +690,12 @@ export const createEventsSlice = (set, get) => ({ return getVariable(key); } + case 'unset-all-custom-variables': { + const { unsetAllVariables } = get(); + unsetAllVariables(); + return Promise.resolve(); + } + case 'unset-custom-variable': { const { unsetVariable } = get(); const key = getResolvedValue(event.key, customVariables); @@ -746,6 +752,12 @@ export const createEventsSlice = (set, get) => ({ return getPageVariable(key); } + case 'unset-all-page-variables': { + const { unsetAllPageVariables } = get(); + unsetAllPageVariables(); + return Promise.resolve(); + } + case 'unset-page-variable': { const { unsetPageVariable } = get(); const key = getResolvedValue(event.key, customVariables); @@ -953,6 +965,13 @@ export const createEventsSlice = (set, get) => ({ } }; + const unsetAllVariables = () => { + const event = { + actionId: 'unset-all-custom-variables', + }; + return executeAction(event, mode, {}); + }; + const unSetVariable = (key = '') => { if (key) { const event = { @@ -1066,6 +1085,13 @@ export const createEventsSlice = (set, get) => ({ return executeAction(event, mode, {}); }; + const unsetAllPageVariables = () => { + const event = { + actionId: 'unset-all-page-variables', + }; + return executeAction(event, mode, {}); + }; + const unsetPageVariable = (key = '') => { const event = { actionId: 'unset-page-variable', @@ -1133,6 +1159,7 @@ export const createEventsSlice = (set, get) => ({ runQuery, setVariable, getVariable, + unsetAllVariables, unSetVariable, showAlert, logout, @@ -1144,6 +1171,7 @@ export const createEventsSlice = (set, get) => ({ generateFile, setPageVariable, getPageVariable, + unsetAllPageVariables, unsetPageVariable, switchPage, logInfo, diff --git a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js index ebbd524fd1..b76c8b072d 100644 --- a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js @@ -140,6 +140,21 @@ export const createResolvedSlice = (set, get) => ({ get().updateDependencyValues(`variables.${key}`); }, + unsetAllVariables: (moduleId = 'canvas') => { + const variables = get().resolvedStore.modules[moduleId].exposedValues.variables; + set( + (state) => { + state.resolvedStore.modules[moduleId].exposedValues.variables = {}; + }, + false, + 'unsetAllVariables' + ); + Object.keys(variables).forEach((key) => { + get().removeNode(`variables.${key}`); + get().updateDependencyValues(`variables.${key}`); + }); + }, + // page.variables setPageVariable: (key, value, moduleId = 'canvas') => { set( @@ -167,6 +182,21 @@ export const createResolvedSlice = (set, get) => ({ get().updateDependencyValues(`page.variables.${key}`); }, + unsetAllPageVariables: (moduleId = 'canvas') => { + const pageVariables = get().resolvedStore.modules[moduleId].exposedValues.page.variables; + set( + (state) => { + state.resolvedStore.modules[moduleId].exposedValues.page.variables = {}; + }, + false, + 'unsetAllPageVariables' + ); + Object.keys(pageVariables).forEach((key) => { + get().removeNode(`page.variables.${key}`); + get().updateDependencyValues(`page.variables.${key}`); + }); + }, + setResolvedQuery: (queryId, details, moduleId = 'canvas') => { set( (state) => { diff --git a/frontend/src/AppBuilder/_stores/utils.js b/frontend/src/AppBuilder/_stores/utils.js index bb08c620da..33e50eb9cc 100644 --- a/frontend/src/AppBuilder/_stores/utils.js +++ b/frontend/src/AppBuilder/_stores/utils.js @@ -473,6 +473,7 @@ export function createReferencesLookup(currentState, forQueryParams = false, ini const actions = [ 'runQuery', 'setVariable', + 'unsetAllVariables', 'unSetVariable', 'showAlert', 'logout', @@ -483,6 +484,7 @@ export function createReferencesLookup(currentState, forQueryParams = false, ini 'goToApp', 'generateFile', 'setPageVariable', + 'unsetAllPageVariables', 'unsetPageVariable', 'switchPage', 'logInfo', diff --git a/frontend/src/Editor/ActionTypes.js b/frontend/src/Editor/ActionTypes.js index 01b0a34759..0bad71b3ac 100644 --- a/frontend/src/Editor/ActionTypes.js +++ b/frontend/src/Editor/ActionTypes.js @@ -78,6 +78,10 @@ export const ActionTypes = [ { name: 'value', type: 'code', default: '' }, ], }, + { + name: 'Unset all variables', + id: 'unset-all-custom-variables', + }, { name: 'Unset variable', id: 'unset-custom-variable', @@ -96,6 +100,10 @@ export const ActionTypes = [ { name: 'value', type: 'code', default: '' }, ], }, + { + name: 'Unset all page variables', + id: 'unset-all-page-variables', + }, { name: 'Unset page variable', id: 'unset-page-variable', @@ -104,6 +112,7 @@ export const ActionTypes = [ { name: 'value', type: 'code', default: '' }, ], }, + { name: 'Control component', id: 'control-component', diff --git a/frontend/src/Editor/Components/ButtonGroup.jsx b/frontend/src/Editor/Components/ButtonGroup.jsx index 7364348271..67618a61ab 100644 --- a/frontend/src/Editor/Components/ButtonGroup.jsx +++ b/frontend/src/Editor/Components/ButtonGroup.jsx @@ -66,6 +66,34 @@ export const ButtonGroup = function Button({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [multiSelection]); + const setSelected = (selected) => { + if (multiSelection) { + if (Array.isArray(selected)) { + const filteredItems = selected.filter((item) => values.includes(item)); + setDefaultActive(filteredItems); + setExposedVariable('selected', filteredItems.join(',')); + } else if ((typeof selected === 'string' || typeof selected === 'number') && values.includes(selected)) { + setDefaultActive([selected]); + setExposedVariable('selected', String(selected)); + } + } else { + if (Array.isArray(selected)) { + const filteredItems = selected.filter((item) => values.includes(item)); + if (filteredItems?.length >= 1) { + setDefaultActive([filteredItems[0]]); + setExposedVariable('selected', String(filteredItems[0])); + } + } else if ((typeof selected === 'string' || typeof selected === 'number') && values.includes(selected)) { + setDefaultActive([selected]); + setExposedVariable('selected', String(selected)); + } + } + }; + + useEffect(() => { + setExposedVariable('setSelected', setSelected); + }, [multiSelection, values]); + const buttonClick = (index) => { if (defaultActive?.includes(values[index]) && multiSelection) { const copyDefaultActive = [...defaultActive]; diff --git a/frontend/src/Editor/Components/TextInput.jsx b/frontend/src/Editor/Components/TextInput.jsx index 29fbaaa625..ce482947a9 100644 --- a/frontend/src/Editor/Components/TextInput.jsx +++ b/frontend/src/Editor/Components/TextInput.jsx @@ -236,8 +236,6 @@ export const TextInput = function TextInput({ value: properties.value, isMandatory: isMandatory, isLoading: loading, - isVisible: visibility, - isDisabled: disable, }; setExposedVariables(exposedVariables); @@ -245,6 +243,17 @@ export const TextInput = function TextInput({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + // Fix for "visibility is not defined" in production because there's a naming conflict in the code. + // The issue is in the exposedVariables object where we had both a function named visibility and a property isVisible that depends on the state variable with the same name. + const exposedVariables = { + isVisible: visibility, + isDisabled: disable, + }; + setExposedVariables(exposedVariables); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const setInputValue = (value) => { setValue(value); setExposedVariable('value', value); diff --git a/frontend/src/Editor/WidgetManager/configs/listview.js b/frontend/src/Editor/WidgetManager/configs/listview.js index 25da8f73c6..86825142eb 100644 --- a/frontend/src/Editor/WidgetManager/configs/listview.js +++ b/frontend/src/Editor/WidgetManager/configs/listview.js @@ -13,6 +13,7 @@ export const listviewConfig = { top: 15, left: 3, height: 100, + width: 7, }, properties: ['source'], accessorKey: 'imageURL', diff --git a/frontend/src/Editor/WidgetManager/configs/tabs.js b/frontend/src/Editor/WidgetManager/configs/tabs.js index a397979a3e..0ed1e2a320 100644 --- a/frontend/src/Editor/WidgetManager/configs/tabs.js +++ b/frontend/src/Editor/WidgetManager/configs/tabs.js @@ -13,6 +13,7 @@ export const tabsConfig = { top: 60, left: 17, height: 100, + width: 7, }, tab: 0, properties: ['source'], diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index 141bd5c927..0f5db30e9b 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -572,6 +572,7 @@ const DynamicForm = ({ 'd-flex': isHorizontalLayout, 'dynamic-form-row': isHorizontalLayout, })} + data-cy={`${key.replace(/_/g, '-')}-section`} key={key} > {!isSpecificComponent && ( @@ -628,6 +629,7 @@ const DynamicForm = ({ {...getElementProps(obj[key])} {...computedProps[propertyKey]} data-cy={`${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}-text-field`} + dataCy={obj[key].key.replace(/_/g, '-')} //to be removed after whole ui is same isHorizontalLayout={isHorizontalLayout} /> @@ -669,7 +671,7 @@ const DynamicForm = ({ )} -
+
keyValuePairValueChanged(e.target.value, 0, index)} @@ -47,6 +50,7 @@ export default ({ />