diff --git a/.github/workflows/cypress-appbuilder.yml b/.github/workflows/cypress-appbuilder.yml index 47c7bda175..bb1bc569c0 100644 --- a/.github/workflows/cypress-appbuilder.yml +++ b/.github/workflows/cypress-appbuilder.yml @@ -52,7 +52,7 @@ jobs: run: | git submodule update --init --recursive git submodule foreach --recursive ' - git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout modularisation/v3' + git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main' - name: Set up Docker diff --git a/.github/workflows/cypress-platform.yml b/.github/workflows/cypress-platform.yml index 0d15c01f9c..d136991e38 100644 --- a/.github/workflows/cypress-platform.yml +++ b/.github/workflows/cypress-platform.yml @@ -50,7 +50,7 @@ jobs: run: | git submodule update --init --recursive git submodule foreach --recursive ' - git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout modularisation/v3' + git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main' - name: Set up Docker uses: docker-practice/actions-setup-docker@master @@ -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/.github/workflows/render-preview-deploy.yml b/.github/workflows/render-preview-deploy.yml index a025290d39..ead9ba50bf 100644 --- a/.github/workflows/render-preview-deploy.yml +++ b/.github/workflows/render-preview-deploy.yml @@ -215,7 +215,7 @@ jobs: - name: Delete service run: | export SERVICE_ID=$(curl --request GET \ - --url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ + --url 'https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ --header 'accept: application/json' \ --header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \ jq -r '.[0].service.id') @@ -583,7 +583,7 @@ jobs: - name: Delete service run: | export SERVICE_ID=$(curl --request GET \ - --url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ + --url 'https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ --header 'accept: application/json' \ --header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \ jq -r '.[0].service.id') diff --git a/.gitmodules b/.gitmodules index 428ef18262..f58077ac3f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,8 +1,8 @@ [submodule "frontend/ee"] path = frontend/ee url = https://github.com/ToolJet/ee-frontend.git - branch = modularisation/v3 + branch = main [submodule "server/ee"] path = server/ee url = https://github.com/ToolJet/ee-server.git - branch = modularisation/v3 + branch = main 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/cloud/cloud-server.Dockerfile b/docker/cloud/cloud-server.Dockerfile index a808506ffb..cc9fd4fce3 100644 --- a/docker/cloud/cloud-server.Dockerfile +++ b/docker/cloud/cloud-server.Dockerfile @@ -10,7 +10,7 @@ RUN mkdir -p /app WORKDIR /app ARG CUSTOM_GITHUB_TOKEN -ARG BRANCH_NAME=modularisation/v3 +ARG BRANCH_NAME=main # Clone and checkout the frontend repository RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" diff --git a/docker/ee/ee-production.Dockerfile b/docker/ee/ee-production.Dockerfile index 31d6511be4..b69458daa1 100644 --- a/docker/ee/ee-production.Dockerfile +++ b/docker/ee/ee-production.Dockerfile @@ -11,7 +11,7 @@ WORKDIR /app # Set GitHub token and branch as build arguments ARG CUSTOM_GITHUB_TOKEN -ARG BRANCH_NAME=modularisation/v3 +ARG BRANCH_NAME=main # Clone and checkout the frontend repository RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" @@ -21,7 +21,7 @@ RUN git config --global http.postBuffer 524288000 RUN git clone https://github.com/ToolJet/ToolJet.git . # The branch name needs to be changed the branch with modularisation in CE repo -RUN git checkout modularisation/v3 +RUN git checkout main RUN git submodule update --init --recursive @@ -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/assets/images/icons/widgets/index.jsx b/frontend/assets/images/icons/widgets/index.jsx index 058bf838e1..7ecb678d1b 100644 --- a/frontend/assets/images/icons/widgets/index.jsx +++ b/frontend/assets/images/icons/widgets/index.jsx @@ -137,6 +137,7 @@ const WidgetIcon = (props) => { case 'map': return ; case 'modal': + case 'modallegacy': return ; case 'multiselect': case 'multiselectv2': diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index 5fcedc2237..00fde7c9f2 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -131,9 +131,13 @@ class AppComponent extends React.Component { } return ''; }; - render() { const { updateAvailable, darkMode, isEditorOrViewer } = this.state; + const mergedProps = { + ...this.props, + switchDarkMode: this.switchDarkMode, + darkMode: darkMode, + }; let toastOptions = { style: { wordBreak: 'break-all', @@ -256,7 +260,7 @@ class AppComponent extends React.Component { } /> )} - }> + }> }> }> @@ -270,7 +274,7 @@ class AppComponent extends React.Component { } /> - {getDataSourcesRoutes(this.props)} + {getDataSourcesRoutes(mergedProps)} null, //! Only Modal widget passes this uses props down. All other widgets use selecto lib @@ -13,6 +17,8 @@ export const ConfigHandle = ({ customClassName = '', showHandle, componentType, + visibility, + subContainerIndex, }) => { const shouldFreeze = useStore((state) => state.getShouldFreeze()); const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow); @@ -26,18 +32,35 @@ export const ConfigHandle = ({ (state) => componentType === 'Tabs' && state.getExposedValueOfComponent(id)?.currentTab, shallow ); + const position = widgetTop < 15 ? 'bottom' : 'top'; const setComponentToInspect = useStore((state) => state.setComponentToInspect); + const isModal = componentType === 'Modal' || componentType === 'ModalV2'; + const _showHandle = useStore((state) => { + const isWidgetHovered = state.getHoveredComponentForGrid() === id || state.hoveredComponentBoundaryId === id; + const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== ''; + // If one component is hovered and one is selected, show the handle for the hovered component + return ( + (subContainerIndex === 0 || subContainerIndex === null) && + (isWidgetHovered || + (showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered)) + ); + }, shallow); + + let height = visibility === false ? 10 : widgetHeight; + return (
{ @@ -51,7 +74,10 @@ export const ConfigHandle = ({ > @@ -65,17 +91,30 @@ export const ConfigHandle = ({ data-cy={`${componentName?.toLowerCase()}-config-handle`} className="text-truncate" > - + {/* Settings Icon */} + + + {componentName} + {/* Divider */} +
+ {/* Delete Button */} {!isMultipleComponentsSelected && !shouldFreeze && ( -
+
- { deleteComponents([id]); }} data-cy={`${componentName.toLowerCase()}-delete-button`} - className="delete-icon" - /> + > + +
)} diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss index 7f20210c10..5cb1b94268 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss @@ -31,22 +31,7 @@ .badge { font-size: 9px; border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - - .delete-part { - margin-left: 10px; - float: right; - } - - .delete-part::before { - height: 12px; - display: inline-block; - width: 2px; - background-color: rgba(255, 255, 255, 0.8); - opacity: 0.5; - content: ""; - vertical-align: middle; - } + border-bottom-right-radius: 0 } } @@ -65,9 +50,3 @@ } } } - -.main-editor-canvas .widget-target:hover > .config-handle { - visibility: visible !important; -} - - diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index fed1a3db34..e622e1a2cd 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -6,7 +6,15 @@ import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { useDrop } from 'react-dnd'; import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils'; -import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants'; +import { + CANVAS_WIDTHS, + NO_OF_GRIDS, + WIDGETS_WITH_DEFAULT_CHILDREN, + GRID_HEIGHT, + CONTAINER_FORM_CANVAS_PADDING, + SUBCONTAINER_CANVAS_BORDER_WIDTH, + BOX_PADDING, +} from './appCanvasConstants'; import { useGridStore } from '@/_stores/gridStore'; import NoComponentCanvasContainer from './NoComponentCanvasContainer'; import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants'; @@ -35,10 +43,10 @@ export const Container = React.memo( canvasMaxWidth, isViewerSidebarPinned, pageSidebarStyle, + componentType, }) => { const realCanvasRef = useRef(null); const components = useStore((state) => state.getContainerChildrenMapping(id), shallow); - const componentType = useStore((state) => state.getComponentTypeFromId(id), shallow); const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow); const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow); const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow); @@ -56,6 +64,11 @@ export const Container = React.memo( const [{ isOverCurrent }, drop] = useDrop({ accept: 'box', + hover: (item) => { + item.canvasRef = realCanvasRef?.current; + item.canvasId = id; + item.canvasWidth = getContainerCanvasWidth(); + }, drop: async ({ componentType }, monitor) => { const didDrop = monitor.didDrop(); if (didDrop) return; @@ -89,14 +102,19 @@ export const Container = React.memo( function getContainerCanvasWidth() { if (canvasWidth !== undefined) { if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2; - return canvasWidth; + if (id === 'canvas') return canvasWidth; + if (componentType === 'Container' || componentType === 'Form') { + return ( + canvasWidth - (2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING) + ); + } + return canvasWidth - 2; // Need to update this 2 to correct value for other subcontainers } return realCanvasRef?.current?.offsetWidth; } const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS; - useEffect(() => { - useGridStore.getState().actions.setSubContainerWidths(id, (getContainerCanvasWidth() - 2) / NO_OF_GRIDS); + useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS); // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasWidth, listViewMode, columns]); @@ -137,8 +155,7 @@ export const Container = React.memo( }} style={{ height: id === 'canvas' ? `${canvasHeight}` : '100%', - // backgroundSize: '25.3953px 10px', - backgroundSize: `${gridWidth}px 10px`, + backgroundSize: `${gridWidth}px ${GRID_HEIGHT}px`, backgroundColor: currentMode === 'view' ? computeViewerBackgroundColor(darkMode, canvasBgColor) @@ -169,6 +186,7 @@ export const Container = React.memo( data-parentId={id} canvas-height={canvasHeight} onClick={handleCanvasClick} + component-type={componentType} >
state.getHoveredComponentForGrid, shallow); const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow); + const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS); + const draggingComponentId = useStore((state) => state.draggingComponentId, shallow); + const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); + const [dragParentId, setDragParentId] = useState(null); + const [elementGuidelines, setElementGuidelines] = useState([]); + const componentsSnappedTo = useRef(null); + const prevDragParentId = useRef(null); + const newDragParentId = useRef(null); + const [isGroupDragging, setIsGroupDragging] = useState(false); + + useEffect(() => { + const selectedSet = new Set(selectedComponents); + const draggingOrResizingId = draggingComponentId || resizingComponentId; + const isGrouped = findHighestLevelofSelection().length > 1; + const firstSelectedParent = + selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null; + const selectedParent = dragParentId || firstSelectedParent; + + const guidelines = boxList + .filter((box) => { + const isVisible = + getResolvedValue(box?.component?.definition?.properties?.visibility?.value) || + getResolvedValue(box?.component?.definition?.styles?.visibility?.value); + + // Early return for non-visible elements + if (!isVisible) return false; + + if (isGrouped) { + // If component is selected, don't show its guidelines + if (selectedSet.has(box.id)) return false; + return selectedParent ? box.parent === selectedParent : !box.parent; + } + + if (draggingOrResizingId) { + if (box.id === draggingOrResizingId) return false; + return dragParentId ? box.parent === dragParentId : !box.parent; + } + + return true; + }) + .map((box) => `.ele-${box.id}`); + setElementGuidelines(guidelines); + }, [boxList, dragParentId, draggingComponentId, resizingComponentId, selectedComponents, getResolvedValue]); useEffect(() => { setBoxList( @@ -94,7 +144,7 @@ export default function Grid({ gridWidth, currentLayout }) { boxList.forEach(({ id, height, width, x, y, gw }) => { const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth; let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth); - y = Math.round(y / 10) * 10; + y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT; gw = gw ? gw : gridWidth; const parent = transformedBoxes[id]?.component?.parent; @@ -117,7 +167,7 @@ export default function Grid({ gridWidth, currentLayout }) { } setComponentLayout({ [id]: { - height: height ? height : 10, + height: height ? height : GRID_HEIGHT, width: newWidth ? newWidth : 1, top: y, left: Math.round(x / gw), @@ -319,7 +369,7 @@ export default function Grid({ gridWidth, currentLayout }) { } // Round y position - y = Math.max(0, Math.round(y / 10) * 10); + y = Math.max(0, Math.round(y / GRID_HEIGHT) * GRID_HEIGHT); // Adjust height for certain parent components if (parent) { const parentElem = document.getElementById(`canvas-${parent}`); @@ -354,17 +404,16 @@ export default function Grid({ gridWidth, currentLayout }) { ); // Add event listeners for config handle visibility when hovering over widget boundary + // This is needed even though we have hovered widget state because when hovered on boundary, + // the hovered widget state is empty, hence created a separate state for boundary React.useEffect(() => { const moveableBox = document.querySelector(`.moveable-control-box`); const showConfigHandle = (e) => { const targetId = e.target.offsetParent.getAttribute('target-id'); - const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`); - configHandle.classList.add('config-handle-visible'); + useStore.getState().setHoveredComponentBoundaryId(targetId); }; - const hideConfigHandle = (e) => { - const targetId = e.target.offsetParent.getAttribute('target-id'); - const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`); - configHandle.classList.remove('config-handle-visible'); + const hideConfigHandle = () => { + useStore.getState().setHoveredComponentBoundaryId(''); }; if (moveableBox) { moveableBox.addEventListener('mouseover', showConfigHandle); @@ -376,49 +425,10 @@ export default function Grid({ gridWidth, currentLayout }) { }; }, []); - const handleDragGridLinesVisibility = (e, events = []) => { - const { clientX, clientY } = e; - if (!document.elementFromPoint(clientX, clientY)) return; - - const targetElems = document.elementsFromPoint(clientX, clientY); - const draggedOverElements = targetElems.filter( - (ele) => - !events.some((event) => event.target.id === ele.id) && - (ele.classList.contains('target') || ele.classList.contains('real-canvas')) - ); - const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target')); - const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas')); - const appCanvas = document.getElementById('real-canvas'); - - // Show grid line for main canvas - draggedOverContainer?.classList.remove('hide-grid'); - draggedOverContainer?.classList.add('show-grid'); - - // Remove 'show-grid' class from all sub-canvases - const canvasElms = document.getElementsByClassName('sub-canvas'); - Array.from(canvasElms).forEach((element) => { - element.classList.remove('show-grid'); - element.classList.add('hide-grid'); - }); - - // Determine the potential new parent - const parentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id; - - // Show grid for the appropriate canvas - if (parentId) { - const newParentCanvas = document.getElementById('canvas-' + parentId); - if (newParentCanvas) { - appCanvas?.classList?.remove('show-grid'); - newParentCanvas?.classList.remove('hide-grid'); - newParentCanvas?.classList.add('show-grid'); - } - } - - useGridStore.getState().actions.setDragTarget(parentId); - }; - const handleDragGroupEnd = (e) => { try { + hideGridLines(); + setIsGroupDragging(false); const { events, clientX, clientY } = e; const initialParent = events[0].target.closest('.real-canvas'); // Get potential new parent using same logic as onDragEnd @@ -477,7 +487,7 @@ export default function Grid({ gridWidth, currentLayout }) { // Apply transform to return to original position ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${ - Math.round(_top / 10) * 10 + Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT }px)`; } }); @@ -514,7 +524,7 @@ export default function Grid({ gridWidth, currentLayout }) { // Apply grid snapping and bounds const snappedX = Math.round(posX / _gridWidth) * _gridWidth; - const snappedY = Math.round(posY / 10) * 10; + const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT; ev.target.style.transform = `translate(${snappedX}px, ${snappedY}px)`; return { @@ -531,6 +541,18 @@ export default function Grid({ gridWidth, currentLayout }) { } }; + React.useEffect(() => { + const components = Array.from(document.querySelectorAll('.active-target')).filter( + (component) => !selectedComponents.includes(component.getAttribute('widgetid')) + ); + const draggingOrResizing = draggingComponentId || resizingComponentId; + if (!draggingOrResizing && components.length > 0) { + for (const component of components) { + component?.classList?.remove('active-target'); + } + } + }, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]); + if (mode !== 'edit') return null; return ( @@ -557,11 +579,11 @@ export default function Grid({ gridWidth, currentLayout }) { let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; if (currentWidget.component?.parent) { document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid'); - useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent); + setDragParentId(currentWidget.component?.parent); } else { document.getElementById('real-canvas').classList.add('show-grid'); } - + handleActivateTargets(currentWidget.component?.parent); const currentWidth = currentWidget.width * _gridWidth; const diffWidth = e.width - currentWidth; const diffHeight = e.height - currentWidget.height; @@ -584,9 +606,6 @@ export default function Grid({ gridWidth, currentLayout }) { const maxLeft = containerWidth - e.target.clientWidth; const maxWidthHit = transformX < 0 || transformX >= maxLeft; const maxHeightHit = transformY < 0 || transformY >= maxY; - transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY; - transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX; - if (!maxWidthHit || e.width < e.target.clientWidth) { e.target.style.width = `${e.width}px`; } @@ -612,12 +631,13 @@ export default function Grid({ gridWidth, currentLayout }) { // When clicked on widget boundary/resizer, select the component setSelectedComponents([e.target.id]); } - + showGridLines(); if (!isComponentVisible(e.target.id)) { return false; } + handleActivateNonDraggingComponents(); useGridStore.getState().actions.setResizingComponentId(e.target.id); - e.setMin([gridWidth, 10]); + e.setMin([gridWidth, GRID_HEIGHT]); }} onResizeEnd={(e) => { try { @@ -625,11 +645,10 @@ export default function Grid({ gridWidth, currentLayout }) { const currentWidget = boxList.find(({ id }) => { return id === e.target.id; }); - document.getElementById('real-canvas')?.classList.remove('show-grid'); - document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid'); + hideGridLines(); let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth; - const height = Math.round(e?.lastEvent?.height / 10) * 10; + const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT; const currentWidth = currentWidget.width * _gridWidth; const diffWidth = e.lastEvent?.width - currentWidth; @@ -654,19 +673,17 @@ export default function Grid({ gridWidth, currentLayout }) { const maxLeft = containerWidth - e.target.clientWidth; const maxWidthHit = transformX < 0 || transformX >= maxLeft; const maxHeightHit = transformY < 0 || transformY >= maxY; - transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY; - transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX; - const roundedTransformY = Math.round(transformY / 10) * 10; - transformY = transformY % 10 === 5 ? roundedTransformY - 10 : roundedTransformY; + const roundedTransformY = Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT; + transformY = transformY % GRID_HEIGHT === 5 ? roundedTransformY - GRID_HEIGHT : roundedTransformY; e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${ - Math.round(transformY / 10) * 10 + Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT }px)`; if (!maxWidthHit || e.width < e.target.clientWidth) { e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`; } if (!maxHeightHit || e.height < e.target.clientHeight) { - e.target.style.height = `${Math.round(e.lastEvent.height / 10) * 10}px`; + e.target.style.height = `${Math.round(e.lastEvent.height / GRID_HEIGHT) * GRID_HEIGHT}px`; } const resizeData = { id: e.target.id, @@ -682,18 +699,19 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('ResizeEnd error ->', error); } - useGridStore.getState().actions.setDragTarget(); + handleDeactivateTargets(); + setDragParentId(null); toggleCanvasUpdater(); }} onResizeGroupStart={({ events }) => { - const parentElm = events[0].target.closest('.real-canvas'); - parentElm.classList.add('show-grid'); + showGridLines(); + handleActivateNonDraggingComponents(); }} onResizeGroup={({ events }) => { const parentElm = events[0].target.closest('.real-canvas'); const parentWidth = parentElm?.clientWidth; const parentHeight = parentElm?.clientHeight; - + handleActivateTargets(parentElm?.id?.replace('canvas-', '')); const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight); events.forEach((ev) => { ev.target.style.width = `${ev.width}px`; @@ -710,8 +728,7 @@ export default function Grid({ gridWidth, currentLayout }) { const { events } = e; const newBoxs = []; - const parentElm = events[0].target.closest('.real-canvas'); - parentElm.classList.remove('show-grid'); + hideGridLines(); // TODO: Logic needs to be relooked post go live P2 groupResizeDataRef.current.forEach((ev) => { @@ -722,9 +739,9 @@ export default function Grid({ gridWidth, currentLayout }) { let width = Math.round(ev.width / _gridWidth) * _gridWidth; width = width < _gridWidth ? _gridWidth : width; let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth; - let posY = Math.round(ev.drag.translate[1] / 10) * 10; - let height = Math.round(ev.height / 10) * 10; - height = height < 10 ? 10 : height; + let posY = Math.round(ev.drag.translate[1] / GRID_HEIGHT) * GRID_HEIGHT; + let height = Math.round(ev.height / GRID_HEIGHT) * GRID_HEIGHT; + height = height < GRID_HEIGHT ? GRID_HEIGHT : height; ev.target.style.width = `${width}px`; ev.target.style.height = `${height}px`; @@ -752,7 +769,7 @@ export default function Grid({ gridWidth, currentLayout }) { let posX = currentWidget?.layouts[currentLayout].left * _gridWidth; let posY = currentWidget?.layouts[currentLayout].top; let height = currentWidget?.layouts[currentLayout].height; - height = height < 10 ? 10 : height; + height = height < GRID_HEIGHT ? GRID_HEIGHT : height; ev.target.style.width = `${width}px`; ev.target.style.height = `${height}px`; ev.target.style.transform = `translate(${posX}px, ${posY}px)`; @@ -763,11 +780,18 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('Error resizing group', error); } + handleDeactivateTargets(); toggleCanvasUpdater(); }} checkInput onDragStart={(e) => { + // This is to prevent parent component from being dragged and the stop the propagation of the event + if (getHoveredComponentForGrid() !== e.target.id) { + return false; + } + newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent; e?.moveable?.controlBox?.removeAttribute('data-off-screen'); + const box = boxList.find((box) => box.id === e.target.id); // Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS if (SUBCONTAINER_WIDGETS.includes(box?.component?.component) && e.inputEvent.shiftKey) { @@ -779,7 +803,7 @@ export default function Grid({ gridWidth, currentLayout }) { // to handle their own interactions like column resizing or card dragging let isDragOnInnerElement = false; - /* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works. + /* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works. Also user dont need to drag an calender from using popup */ if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) { return false; @@ -801,7 +825,6 @@ export default function Grid({ gridWidth, currentLayout }) { container.contains(e.inputEvent.target) ); } - if (['RangeSlider', 'BoundedBox'].includes(box?.component?.component) || isDragOnInnerElement) { const targetElems = document.elementsFromPoint(e.clientX, e.clientY); const isHandle = targetElems.find((ele) => ele.classList.contains('handle-content')); @@ -809,152 +832,112 @@ export default function Grid({ gridWidth, currentLayout }) { return false; } } - // This is to prevent parent component from being dragged and the stop the propagation of the event - if (getHoveredComponentForGrid() !== e.target.id) { - return false; - } + handleActivateNonDraggingComponents(); }} onDragEnd={(e) => { + handleDeactivateTargets(); try { if (isDraggingRef.current) { - useGridStore.getState().actions.setDraggingComponentId(null); + useStore.getState().setDraggingComponentId(null); isDraggingRef.current = false; } + prevDragParentId.current = null; + newDragParentId.current = null; + setDragParentId(null); - if (!e.lastEvent) { - return; + if (!e.lastEvent) return; + + // Build the drag context from the event + const dragContext = dragContextBuilder({ event: e, widgets: boxList }); + const { target, source, dragged } = dragContext; + + const targetSlotId = target?.slotId; + const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth; + + // const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || []; + // const draggedWidgetType = dragged.widgetType; + const isParentChangeAllowed = dragContext.isDroppable; + + // Compute new position + let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged); + + const isModalToCanvas = source.isModal && target.slotId === 'real-canvas'; + + if (isParentChangeAllowed && !isModalToCanvas) { + const parent = target.slotId === 'real-canvas' ? null : target.slotId; + // Special case for Modal; If source widget is modal, prevent drops to canvas + handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]); + } else { + const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth; + + left = dragged.left * sourcegridWidth; + top = dragged.top; + + !isModalToCanvas ?? + toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`); } - let draggedOverElemId = boxList.find((box) => box.id === e.target.id)?.parent; - let draggedOverElemIdType; - const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent); - let draggedOverElem; - if (document.elementFromPoint(e.clientX, e.clientY) && parentComponent?.component?.component !== 'Modal') { - const targetElems = document.elementsFromPoint(e.clientX, e.clientY); - draggedOverElem = targetElems.find((ele) => { - const isOwnChild = e.target.contains(ele); // if the hovered element is a child of actual draged element its not considered - if (isOwnChild) return false; + // Apply transform for smooth transition + e.target.style.transform = `translate(${left}px, ${top}px)`; - let isDroppable = ele.id !== e.target.id && ele.classList.contains('drag-container-parent'); - if (isDroppable) { - let widgetId = ele?.getAttribute('component-id') || ele.id; - let widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component; - if (!widgetType) { - widgetId = widgetId.split('-').slice(0, -1).join('-'); - widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component; - } - if ( - !['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes( - widgetType - ) - ) { - isDroppable = false; - } - } - return isDroppable; - }); - draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id; - draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type'); - } - - const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth; - const currentParentId = boxList.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent; - let left = e.lastEvent?.translate[0]; - let top = e.lastEvent?.translate[1]; - if ( - ['Listview', 'Kanban', 'Container'].includes( - boxList.find((box) => box.id === draggedOverElemId)?.component?.component - ) - ) { - const elemContainer = e.target.closest('.real-canvas'); - const containerHeight = elemContainer.clientHeight; - const maxY = containerHeight - e.target.clientHeight; - top = top > maxY ? maxY : top; - } - - const currentWidget = boxList.find(({ id }) => id === e.target.id)?.component?.component; - const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId; - draggedOverElemIdType = getComponentTypeFromId(parentId); - const parentWidget = draggedOverElemIdType === 'Kanban' ? 'Kanban_card' : draggedOverElemIdType; - const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || []; - const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget); - if (draggedOverElemId !== currentParentId) { - if (isParentChangeAllowed) { - const draggedOverWidget = boxList.find((box) => box.id === draggedOverElemId); - - let parentWidgetType = boxList.find((box) => box.id === draggedOverElemId)?.component?.component; - // @TODO - When dropping back to container from canvas, the boxList doesn't have canvas header, - // boxList will return null. But we need to tell getMouseDistanceFromParentDiv parentWidgetType is container - // As container id is like 'canvas-2375e23765e-123234' - if (parentId && !parentWidgetType && draggedOverElemId.includes('-header')) { - parentWidgetType = 'Container'; - } - - let { left: _left, top: _top } = getMouseDistanceFromParentDiv( - e, - draggedOverWidget?.component?.component === 'Kanban' ? draggedOverElem : draggedOverElemId, - parentWidgetType - ); - left = _left; - top = _top; - } else { - const currBox = boxList.find((l) => l.id === e.target.id); - left = currBox.left * gridWidth; - top = currBox.top; - toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`); - } - } - - e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${ - Math.round(top / 10) * 10 - }px)`; - if (draggedOverElemId === currentParentId || isParentChangeAllowed) { - handleDragEnd([ - { - id: e.target.id, - x: left, - y: Math.round(top / 10) * 10, - parent: isParentChangeAllowed ? draggedOverElemId : undefined, - }, - ]); - } - const box = boxList.find((box) => box.id === e.target.id); - // - setTimeout(() => setSelectedComponents([box.id])); + // Select the dragged component after drop + setTimeout(() => setSelectedComponents([dragged.id])); } catch (error) { - console.log('draggedOverElemId->error', error); + console.error('Error in onDragEnd:', error); } - // Hide all sub-canvases - var canvasElms = document.getElementsByClassName('sub-canvas'); - var elementsArray = Array.from(canvasElms); - elementsArray.forEach(function (element) { - element.classList.remove('show-grid'); - element.classList.add('hide-grid'); - }); - document.getElementById('real-canvas')?.classList.remove('show-grid'); + setCanvasBounds({ ...CANVAS_BOUNDS }); + hideGridLines(); toggleCanvasUpdater(); }} onDrag={(e) => { // Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again if (!isDraggingRef.current) { - useGridStore.getState().actions.setDraggingComponentId(e.target.id); + useStore.getState().setDraggingComponentId(e.target.id); + showGridLines(); isDraggingRef.current = true; } - const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent); + const currentWidget = boxList.find((box) => box.id === e.target.id); + const currentParentId = + currentWidget?.component?.parent === null ? 'canvas' : currentWidget?.component?.parent; + const _gridWidth = useGridStore.getState().subContainerWidths[dragParentId] || gridWidth; + const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current; - let top = e.translate[1]; - let left = e.translate[0]; + // Snap to grid + let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth; + let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT; + + // This logic is to handle the case when the dragged element is over a new canvas + if (_dragParentId !== currentParentId) { + left = e.translate[0]; + top = e.translate[1]; + } // Special case for Modal - if (parentComponent?.component?.component === 'Modal') { - const elemContainer = e.target.closest('.real-canvas'); - const containerHeight = elemContainer.clientHeight; - const containerWidth = elemContainer.clientWidth; - const maxY = containerHeight - e.target.clientHeight; - const maxLeft = containerWidth - e.target.clientWidth; + const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent; + const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId; + const parentComponent = boxList.find((box) => box.id === parentId); + const parentWidgetType = parentComponent?.component?.component; + const isOnHeaderOrFooter = oldParentId + ? oldParentId.includes('-header') || oldParentId.includes('-footer') + : false; + const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter; + const isParentNewModal = parentComponent?.component?.component === 'ModalV2'; + const isParentLegacyModal = parentComponent?.component?.component === 'Modal'; + const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot; - top = top < 0 ? 0 : top > maxY ? maxY : top; - left = left < 0 ? 0 : left > maxLeft ? maxLeft : left; + if (isParentModal) { + const modalContainer = e.target.closest('.tj-modal-widget-content'); + const mainCanvas = document.getElementById('real-canvas'); + + const mainRect = mainCanvas.getBoundingClientRect(); + const modalRect = modalContainer.getBoundingClientRect(); + const relativePosition = { + top: modalRect.top - mainRect.top, + right: mainRect.right - modalRect.right + modalContainer.offsetWidth, + bottom: modalRect.height + (modalRect.top - mainRect.top), + left: modalRect.left - mainRect.left, + }; + setCanvasBounds({ ...relativePosition }); } e.target.style.transform = `translate(${left}px, ${top}px)`; @@ -963,8 +946,33 @@ export default function Grid({ gridWidth, currentLayout }) { `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` ); - handleDragGridLinesVisibility(e, [{ target: e.target }]); + // This block is to show grid lines on the canvas when the dragged element is over a new canvas + if (document.elementFromPoint(e.clientX, e.clientY)) { + const targetElems = document.elementsFromPoint(e.clientX, e.clientY); + const draggedOverElements = targetElems.filter( + (ele) => + (ele.id !== e.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas') + ); + const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target')); + const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas')); + // Determine potential new parent + let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id; + + if (newParentId === e.target.id) { + newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent; + } else if (parentComponent?.component?.component === 'Modal') { + // Never update parentId for Modal + newParentId = parentComponent?.id; + } + + if (newParentId !== prevDragParentId.current) { + setDragParentId(newParentId === 'canvas' ? null : newParentId); + newDragParentId.current = newParentId === 'canvas' ? null : newParentId; + prevDragParentId.current = newParentId; + handleActivateTargets(newParentId); + } + } // Postion ghost element exactly as same at dragged element if (document.getElementById(`moveable-drag-ghost`)) { document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`; @@ -979,31 +987,29 @@ export default function Grid({ gridWidth, currentLayout }) { parentElm?.classList?.add('show-grid'); } - handleDragGridLinesVisibility(ev, events); - events.forEach((ev) => { - let left = ev.translate[0]; - let top = ev.translate[1]; + const currentWidget = boxList.find(({ id }) => id === ev.target.id); + const _gridWidth = + useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth; + + let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth; + let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT; ev.target.style.transform = `translate(${left}px, ${top}px)`; }); + handleActivateTargets(parentElm?.id?.replace('canvas-', '')); updateNewPosition(events); }} onDragGroupStart={({ events }) => { - const parentElm = events[0]?.target?.closest('.real-canvas'); - parentElm?.classList?.add('show-grid'); + showGridLines(); + setIsGroupDragging(true); + handleActivateNonDraggingComponents(); }} onDragGroupEnd={(e) => { handleDragGroupEnd(e); + handleDeactivateTargets(); toggleCanvasUpdater(); }} - //snap settgins - snappable={true} - snapThreshold={10} - isDisplaySnapDigit={false} - bounds={CANVAS_BOUNDS} - displayAroundControls={true} - controlPadding={20} onClickGroup={(e) => { const targetId = e.inputEvent.target.id || e.inputEvent.target.closest('.moveable-box')?.getAttribute('widgetid'); @@ -1019,6 +1025,43 @@ export default function Grid({ gridWidth, currentLayout }) { } } }} + //snap settgins + snappable={true} + snapGap={false} + isDisplaySnapDigit={false} + snapThreshold={GRID_HEIGHT} + bounds={canvasBounds} + // Guidelines configuration + elementGuidelines={elementGuidelines} + snapDirections={{ + top: true, + right: true, + bottom: true, + left: true, + center: false, + middle: false, + }} + elementSnapDirections={{ + top: true, + left: true, + bottom: true, + right: true, + center: false, + middle: false, + }} + onSnap={(e) => { + const components = e.elements; + if (isArray(componentsSnappedTo.current)) { + for (const component of componentsSnappedTo.current) { + component?.element?.classList?.remove('active-target'); + } + } + componentsSnappedTo.current = components; + for (const component of components) { + component.element.classList.add('active-target'); + } + }} + snapGridAll={true} /> ); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index 817f9a5ca9..da179bc11d 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -1,7 +1,7 @@ import { useGridStore } from '@/_stores/gridStore'; import { isEmpty } from 'lodash'; import useStore from '@/AppBuilder/_stores/store'; - +import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils'; export function correctBounds(layout, bounds) { layout = scaleLayouts(layout); const collidesWith = []; @@ -291,6 +291,7 @@ export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) { ? document.getElementById(id) : id : document.getElementsByClassName('real-canvas')[0]; + parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id); if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') { parentDiv = document.getElementById('canvas-' + id); } @@ -391,3 +392,99 @@ export function hasParentWithClass(child, className) { return false; } + +export function showGridLines() { + var canvasElms = document.getElementsByClassName('sub-canvas'); + var elementsArray = Array.from(canvasElms); + elementsArray.forEach(function (element) { + element.classList.remove('hide-grid'); + element.classList.add('show-grid'); + }); + document.getElementById('real-canvas')?.classList.remove('hide-grid'); + document.getElementById('real-canvas')?.classList.add('show-grid'); +} + +export function hideGridLines() { + var canvasElms = document.getElementsByClassName('sub-canvas'); + var elementsArray = Array.from(canvasElms); + elementsArray.forEach(function (element) { + element.classList.remove('show-grid'); + element.classList.add('hide-grid'); + }); + document.getElementById('real-canvas')?.classList.remove('show-grid'); + document.getElementById('real-canvas')?.classList.add('hide-grid'); +} + +// Track previously active elements for efficient cleanup +let previousActiveWidgets = null; +let previousActiveCanvas = null; + +export const handleActivateNonDraggingComponents = () => { + // Only add non-dragging class to visible components in viewport + document.querySelectorAll('.moveable-box:not(.active-target)').forEach((component) => { + // Check if element is visible in viewport + const rect = component.getBoundingClientRect(); + const isVisible = + rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; + + if (isVisible) { + component.classList.add('non-dragging-component'); + } + }); +}; + +export const handleActivateTargets = (parentId) => { + const WIDGETS_WITH_CANVAS_OUTLINE = ['Container', 'Modal', 'Form', 'Listview', 'Kanban']; + + const newParentType = document.getElementById('canvas-' + parentId)?.getAttribute('component-type'); + let _parentId = parentId; + if (newParentType === 'Tabs') { + _parentId = getTabId(parentId); + } else if (WIDGETS_WITH_CANVAS_OUTLINE.includes(newParentType)) { + _parentId = getSubContainerIdWithSlots(parentId); + } + + // Clean up previous active elements + if (previousActiveWidgets) { + previousActiveWidgets.classList.remove('dragging-component-canvas'); + previousActiveWidgets = null; + } + + if (previousActiveCanvas) { + previousActiveCanvas.classList.remove('dragging-component-canvas'); + previousActiveCanvas = null; + } + + const parentComponent = document.getElementById(_parentId); + if (!parentComponent) return; + + if (WIDGETS_WITH_CANVAS_OUTLINE?.includes(newParentType)) { + // If it's multiple canvas in single widget, highlight the specific canvas + const canvasElm = document.getElementById('canvas-' + parentId); + if (canvasElm) { + canvasElm.classList.add('dragging-component-canvas'); + previousActiveCanvas = canvasElm; + } + } else { + // Otherwise highlight the component box + parentComponent.classList.remove('non-dragging-component'); + parentComponent.classList.add('dragging-component-canvas'); + previousActiveWidgets = parentComponent; + } +}; + +export const handleDeactivateTargets = () => { + if (previousActiveWidgets) { + previousActiveWidgets.classList.remove('dragging-component-canvas'); + previousActiveWidgets = null; + } + + if (previousActiveCanvas) { + previousActiveCanvas.classList.remove('dragging-component-canvas'); + previousActiveCanvas = null; + } + + document.querySelectorAll('.non-dragging-component').forEach((component) => { + component.classList.remove('non-dragging-component'); + }); +}; diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js new file mode 100644 index 0000000000..a9405d043e --- /dev/null +++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js @@ -0,0 +1,266 @@ +/** + * Drag Context Breakdown: + * + * This object encapsulates all relevant details about a drag event, + * grouping the **source (where the widget came from)** and **target (where it's being dropped)**. + * + * Core Concepts: + * - `draggedWidget` → The widget being dragged (`e.target`). + * - `sourceSlot` → The original parent container of `draggedWidget`. + * - This could be a **header, footer, or a sub-container (like a container body)**. + * - `targetSlot` → The new parent container where `draggedWidget` is dropped. + * - `sourceWidget` → The **widget that owns** `sourceSlot` (its direct parent). + * - `targetWidget` → The **widget that owns** `targetSlot` (its direct parent). + * + * These entities are structured into a **contextual grouping**, allowing for easy access: + * + * { + * source: { + * widget: sourceWidget, // The original widget that holds the source slot. + * slot: sourceSlot, // The slot where the widget was initially located. + * id: sourceWidget.id, // Unique identifier of the source widget. + * slotId: sourceSlot.id, // Unique identifier of the source slot. + * + * isModal: computed function, // Checks if sourceWidget is a Modal. + * slotType: computed function, // Determines if the slot is a header, footer, or body. + * widgetType: computed function, // Returns the type of the widget (e.g., Table, Form, etc.). + * }, + * + * target: { + * widget: targetWidget, // The new widget where the dragged widget is being placed. + * slot: targetSlot, // The slot inside `targetWidget` where the drop is happening. + * id: targetWidget.id, // Unique identifier of the target widget. + * slotId: targetSlot.id, // Unique identifier of the target slot. + * + * isModal: computed function, // Checks if targetWidget is a Modal. + * slotType: computed function, // Determines if the slot is a header, footer, or body. + * widgetType: computed function, // Returns the type of the target widget. + * } + * } + * + * Additional Checks: + * - `isSourceModal` → **Is the source inside a modal?** + * - `isTargetModal` → **Is the target inside a modal?** + * - `isDraggingToModalSlots` → **Is the widget being dragged into a modal slot (header/footer)?** + * - `targetSlotType` → **Determines whether the drop is happening in a header, footer, or body.** + * + * Why This Matters? + * - This structure helps **validate and restrict movements**, ensuring widgets follow UI constraints. + * - Prevents invalid drops (e.g., putting a button inside a Table component). + * - Enables **modular and flexible** widget movement across different UI sections. + */ +import { getMouseDistanceFromParentDiv } from '../gridUtils'; +import { + RESTRICTED_WIDGETS_CONFIG, + RESTRICTED_WIDGET_SLOTS_CONFIG, +} from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig'; + +const CANVAS_ID = 'canvas'; +const REAL_CANVAS_ID = 'real-canvas'; + +/** + * Represents the widget being dragged. + * + * This class encapsulates all necessary information about the dragged widget, + * including its type, position, and whether it is allowed to move into certain areas. + */ +export class DragEntity { + constructor(widget) { + this.widget = widget; // The widget object being dragged + this.id = widget?.id || null; // Unique ID of the dragged widget + this.left = widget.left; // Initial X position (relative to grid) + this.top = widget.top; // Initial Y position (relative to grid) + } + + get widgetType() { + return this.widget?.component?.component || null; + } +} + +/** + * Defines a **droppable area** in the canvas. + * + * A droppable area is a container that can accept dragged widgets. + * This class helps determine if a slot is valid and handles various properties like modals. + */ +export class DropAreaEntity { + static dropAreaWidgets = ['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'ModalV2', 'Listview', 'Container', 'Table']; + + constructor(widget, slotId) { + this.widget = widget; // The widget that owns this slot + this.id = widget?.id || CANVAS_ID; // ID of the widget + this.slotId = slotId || REAL_CANVAS_ID; // ID of the slot where the widget is located + } + + // Checks if the widget is a modal + get isModal() { + return ['Modal', 'ModalV2'].includes(this.widget?.component?.component); + } + + // Checks if the widget is the new version of modal + get isNewModal() { + return this.widget?.component?.component === 'ModalV2'; + } + + // Checks if the widget is the legacy modal + get isLegacyModal() { + return this.widget?.component?.component === 'Modal'; + } + + // Determines if the slot belongs to a modal's header/footer + get isInModalSlot() { + return this.isNewModal && this.isOnCustomSlot; + } + + // Identifies if the slot is a custom slot (e.g., modal header/footer) + get isOnCustomSlot() { + return this.slotId.includes('-header') || this.slotId.includes('-footer'); + } + + // Determines if the slot is a valid drop target + get isDroppable() { + return DropAreaEntity.dropAreaWidgets.includes(this.widgetType); + } + + // Returns the type of slot (header, footer, body, etc.) + get slotType() { + return this.slotId ? this.slotId.split('-').pop() : CANVAS_ID; + } + + // Returns the type of the widget inside the slot + get widgetType() { + return this.widget?.component?.component || CANVAS_ID; + } +} + +/** + * Represents the **dragging context**, encapsulating information + * about the source, target, and the dragged widget. + * + * This helps determine: + * - Whether the move is valid + * - Where the widget should be placed + * - Any restrictions based on parent-child relationships + */ +export class DragContext { + constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets }) { + const sourceWidgetId = sourceSlotId?.slice(0, 36); + const sourceWidget = getWidgetById(widgets, sourceWidgetId); + + const targetWidgetId = targetSlotId?.slice(0, 36); + const targetWidget = getWidgetById(widgets, targetWidgetId); + + const draggedWidget = getWidgetById(widgets, draggedWidgetId); + + this.source = new DropAreaEntity(sourceWidget, sourceSlotId); + this.target = new DropAreaEntity(targetWidget, targetSlotId); + this.dragged = new DragEntity(draggedWidget); + this.widgets = widgets; + } + + /** + * Updates the **target slot** dynamically as the drag event progresses. + */ + updateTarget(targetSlotId) { + const targetWidgetId = targetSlotId?.slice(0, 36); + const targetWidget = getWidgetById(this.widgets, targetWidgetId); + this.target = new DropAreaEntity(targetWidget, targetSlotId); + } + + get isDroppable() { + const { dragged, target } = this; + + const restrictedWidgetsOnTarget = RESTRICTED_WIDGETS_CONFIG?.[target.widgetType] || []; + const restrictedWidgetsOnTargetSlot = RESTRICTED_WIDGET_SLOTS_CONFIG?.[target.slotType] || []; + + const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot]; + return !restrictedWidgets.includes(dragged.widgetType); + ß; + } +} + +/** + * Constructs the **dragging context** by gathering all relevant details from the event. + */ +export function dragContextBuilder({ event, widgets }) { + const draggedWidgetId = event.target.id; + const draggedWidget = getWidgetById(widgets, draggedWidgetId); + const sourceSlotId = draggedWidget.parent; + + // Initialize drag context + const context = new DragContext({ widgets, draggedWidgetId, sourceSlotId, targetSlotId: sourceSlotId }); + + // Determine the potential drop target + const targetSlotId = getDroppableSlotIdOnScreen(event, widgets); + context.updateTarget(targetSlotId); + + return context; +} + +/** + * Given an event, finds the **nearest valid droppable slot**. + */ +export const getDroppableSlotIdOnScreen = (event, widgets) => { + const [slotId] = document + .elementsFromPoint(event.clientX, event.clientY) + .filter( + (ele) => + !event.target.contains(ele) && ele.id !== event.target.id && ele.classList.contains('drag-container-parent') + ) + .map((ele) => extractSlotId(ele)) + .filter((slotId) => { + const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID; + return DropAreaEntity.dropAreaWidgets.includes(widgetType); + }); + + return slotId; +}; + +/** + * Finds a widget by its ID. + */ +export function getWidgetById(boxList, targetId) { + return boxList.find((box) => box.id === targetId) ?? null; +} + +/** + * Extracts the **slot ID** from a given DOM element. + */ +const extractSlotId = (element) => { + return element?.getAttribute('component-id') || element.id.replace(/^canvas-/, ''); +}; + +/** + * Computes the final (left, top) position for a dragged widget based on grid snapping and drop conditions. + * + * @param {Object} event - Drag event object containing movement data. + * @param {DropAreaEntity} target - The target drop area entity (where widget is dropped). + * @param {boolean} isParentChangeAllowed - Whether the widget can move to the target. + * @param {number} gridWidth - The width of the grid for alignment. + * @param {DragEntity} dragged - The entity being dragged. + * @returns {Object} { left, top } - The computed position. + */ +export const getAdjustedDropPosition = (event, target, isParentChangeAllowed, gridWidth, dragged) => { + let left = event.lastEvent?.translate[0]; + let top = event.lastEvent?.translate[1]; + + if (isParentChangeAllowed) { + // Compute the relative position inside the new container + const { left: adjustedLeft, top: adjustedTop } = getMouseDistanceFromParentDiv( + event, + target.slotId, + target.widgetType + ); + + return { + left: Math.round(adjustedLeft / gridWidth) * gridWidth, // Snap to the nearest grid column + top: Math.round(adjustedTop / 10) * 10, // Snap to the nearest 10px + }; + } + + // If movement is restricted, revert to original position + return { + left: dragged.left * gridWidth, + top: dragged.top, + }; +}; diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 427cced97b..b26586dc08 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -6,7 +6,7 @@ import { OverlayTrigger } from 'react-bootstrap'; import { renderTooltip } from '@/_helpers/appUtils'; import { useTranslation } from 'react-i18next'; import ErrorBoundary from '@/_ui/ErrorBoundary'; - +import { BOX_PADDING } from './appCanvasConstants'; const shouldAddBoxShadowAndVisibility = [ 'Table', 'TextInput', @@ -164,7 +164,7 @@ const RenderWidget = ({
state.getComponentDefinition(id)?.layouts?.[currentLayout], shallow); const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow); - const isDragging = useGridStore((state) => state.draggingComponentId === id); + const isDragging = useStore((state) => state.draggingComponentId === id); const isResizing = useGridStore((state) => state.resizingComponentId === id); const componentType = useStore((state) => state.getComponentDefinition(id)?.component?.component, shallow); const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow); @@ -52,7 +52,9 @@ const WidgetWrapper = memo( height: visibility === false ? '10px' : `${height}px`, transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`, WebkitFontSmoothing: 'antialiased', + border: visibility === false ? `1px solid var(--border-default)` : 'none', }; + if (!componentType) return null; return ( <> @@ -67,8 +69,8 @@ const WidgetWrapper = memo( data-id={`${id}`} id={id} widgetid={id} + component-type={componentType} style={{ - // transform: `translate(332px, -134px)`, // zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null, ...styles, }} @@ -84,11 +86,12 @@ const WidgetWrapper = memo( {mode == 'edit' && ( )} { - const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child; + const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles, slotName } = + child; const componentMeta = deepClone(componentTypes.find((component) => component.component === componentName)); const componentData = JSON.parse(JSON.stringify(componentMeta)); @@ -139,7 +140,12 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou } const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop'; - const _parent = getParentComponentIdByType(child, parentMeta.component, parentId); + const _parent = getParentComponentIdByType({ + child, + parentComponent: parentMeta.component, + parentId, + slotName, + }); const newChildComponent = { id: uuidv4(), @@ -199,7 +205,9 @@ export const getAllChildComponents = (allComponents, parentId) => { allComponents[parentId]?.component?.component === 'Tabs' || allComponents[parentId]?.component?.component === 'Calendar' || allComponents[parentId]?.component?.component === 'Kanban' || - allComponents[parentId]?.component?.component === 'Container'; + allComponents[parentId]?.component?.component === 'Container' || + allComponents[parentId]?.component?.component === 'Form' || + allComponents[parentId]?.component?.component === 'ModalV2'; if (componentParentId && isParentTabORCalendar) { let childComponent = deepClone(allComponents[componentId]); @@ -240,6 +248,12 @@ const getSelectedText = () => { // 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(); @@ -249,7 +263,6 @@ export const copyComponents = ({ isCut = false, isCloning = false }) => { const parentComponentId = isChildOfTabsOrCalendar(selectedComponent, allComponents) ? selectedComponent.component.parent.split('-').slice(0, -1).join('-') : selectedComponent?.component?.parent; - if (parentComponentId) { // Check if the parent component is also selected const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId); @@ -320,7 +333,9 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI return ( parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar' || - parentComponent.component.component === 'Container' + parentComponent.component.component === 'Container' || + parentComponent.component.component === 'Form' || + parentComponent.component.component === 'ModalV2' ); } @@ -483,11 +498,14 @@ export function pasteComponents(targetParentId, copiedComponentObj) { // Prevent pasting if the parent subcontainer was deleted during a cut operation if ( targetParentId && + // Check if targetParentId is deleted from the components !Object.keys(components).find( (key) => targetParentId === key || (components?.[key]?.component.component === 'Tabs' && - targetParentId?.split('-')?.slice(0, -1)?.join('-') === key) + targetParentId?.split('-')?.slice(0, -1)?.join('-') === key) || + (['Container', 'Form', 'Modal'].includes(components?.[key]?.component.component) && + ['header', 'footer'].some((section) => targetParentId.includes(section))) ) ) { return; @@ -655,10 +673,42 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => { return canvasBgColor; }; -export const getParentComponentIdByType = (child, parentComponent, parentId) => { +export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName }) => { const { tab } = child; if (parentComponent === 'Tabs') return `${parentId}-${tab}`; - else if (parentComponent === 'Container') return `${parentId}-header`; + else if ( + slotName && + (parentComponent === 'Form' || parentComponent === 'Container' || parentComponent === 'ModalV2') + ) { + return `${parentId}-${slotName}`; + } return parentId; }; + +export const getParentWidgetFromId = (parentType, parentId) => { + const isAddingToSlot = parentId?.includes('-header') || parentId?.includes('-footer'); + + if (parentType === 'ModalV2' && isAddingToSlot) { + return 'ModalSlot'; + } else if (parentType === 'Kanban') { + return 'Kanban_card'; + } + return parentType; +}; + +export const getTabId = (parentId) => { + return parentId.split('-').slice(0, -1).join('-'); +}; + +export const getSubContainerIdWithSlots = (parentId) => { + let cleanParentId = parentId; + if (parentId) { + if (parentId.includes('header')) { + cleanParentId = parentId.replace('-header', ''); + } else if (parentId.includes('footer')) { + cleanParentId = parentId.replace('-footer', ''); + } + } + return cleanParentId; +}; diff --git a/frontend/src/AppBuilder/AppCanvas/selecto.scss b/frontend/src/AppBuilder/AppCanvas/selecto.scss index 9ca8a37f41..5602b35d5a 100644 --- a/frontend/src/AppBuilder/AppCanvas/selecto.scss +++ b/frontend/src/AppBuilder/AppCanvas/selecto.scss @@ -3,15 +3,18 @@ } .main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover { - z-index: 4 !important; -} - -.main-editor-canvas .widget-target:has(.nested-target:hover):hover { - outline: 0px solid #4af; -} - -.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { outline: 1px solid #4af; z-index: 4 !important; } +.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { + // outline: 1px solid #4af; + z-index: 4 !important; +} + +// .main-editor-canvas .widget-target:hover { +// outline: 1px solid #4af; +// } + + + diff --git a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx index b9fb85fcae..099cd3763a 100644 --- a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx +++ b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx @@ -5,19 +5,13 @@ const MIN_TABLE_ROW_HEIGHT_DEFAULT = 45; const TableRowHeightInput = ({ value, onChange, cyLabel, staticText, styleDefinition }) => { const [inputValue, setInputValue] = useState(value); + const minValue = + styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT; + useEffect(() => { setInputValue(value < minValue ? minValue : value); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, styleDefinition.cellSize?.value]); - useEffect(() => { - onChange( - styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const minValue = - styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT; const handleBlur = () => { const newValue = Math.max(inputValue, minValue); 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/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx index 033d266e03..f95baaa328 100644 --- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx @@ -300,10 +300,10 @@ const MultiLineCodeEditor = (props) => { editable={editable} //for transformations in query manager onCreateEditor={(view) => setEditorView(view)} onUpdate={(view) => { - const icon = document.querySelector('.codehinter-search-btn-wrapper'); + const icon = document.querySelector('.codehinter-search-btn'); if (searchPanelOpen(view.state)) { - icon.style.top = '44px'; - } else icon.style.top = '0px'; + icon.style.display = 'none'; + } else icon.style.display = 'block'; }} />
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/SearchBox.jsx b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx index 2b807f718b..28f7451b95 100644 --- a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx @@ -1,3 +1,4 @@ +/* eslint-disable import/no-unresolved */ import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { @@ -9,12 +10,13 @@ import { replaceNext, replaceAll, openSearchPanel, - // eslint-disable-next-line import/no-unresolved } from '@codemirror/search'; import './SearchBox.scss'; import InputComponent from '@/components/ui/Input/Index.jsx'; import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx'; import { ToolTip } from '@/_components/ToolTip'; +import { SelectionRange } from '@codemirror/state'; +import { useHotkeys } from 'react-hotkeys-hook'; export const handleSearchPanel = (view) => { const dom = document.createElement('div'); @@ -35,6 +37,11 @@ function SearchPanel({ view }) { replace: replaceTerm, }); view.dispatch({ effects: setSearchQuery.of(query) }); + + const currentPos = view.state.selection.main.head; + view.dispatch({ + selection: SelectionRange.create(currentPos, currentPos), + }); }; useEffect(() => { @@ -44,12 +51,28 @@ function SearchPanel({ view }) { return () => clearTimeout(handler); }, [searchText, replaceText]); + const [shortcutEnabled, setShortcutEnabled] = useState(false); + + // Shortcuts for search input field + useHotkeys( + ['shift+enter', 'enter'], + (event, handler) => { + if (handler.shift && handler.keys[0] === 'enter') findPrevious(view); + else if (handler.keys[0] === 'enter') findNext(view); + }, + { + enabled: shortcutEnabled, + enableOnFormTags: true, + } + ); + const displaySearchField = () => (
setSearchText(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && findNext(view)} + onFocus={() => setShortcutEnabled(true)} + onBlur={() => setShortcutEnabled(false)} placeholder="Find" size="small" value={searchText} 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/TJDBHinter.jsx b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx index 4edd8a0fda..dab57d0f20 100644 --- a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx +++ b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx @@ -56,8 +56,8 @@ const TJDBCodeEditor = (props) => { const handleOnChange = (value) => { if (value === '') { - setErrorState(true); - setError('JSON cannot be empty'); + setErrorState(false); + setError(null); setCurrentValue(value); return; } @@ -167,18 +167,18 @@ const TJDBCodeEditor = (props) => { componentName={componentName} key={componentName} forceUpdate={forceUpdate} - optionalProps={{ styles: { height: 300 }, cls: '' }} + optionalProps={{ styles: { height: 300 }, cls: 'tjdb-hinter-portal' }} darkMode={darkMode} selectors={{ className: 'preview-block-portal tjdb-portal-codehinter' }} dragResizePortal={true} callgpt={null} > -
+
[ 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/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx index dc4b2cf7af..3adca4be98 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx @@ -88,9 +88,19 @@ const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]); - const handleNodeExpansion = (path) => { + const handleNodeExpansion = (path, data, currentNode) => { if (pathToBeInspected && path?.length > 0) { - return pathToBeInspected.includes(path[path.length - 1]); + const shouldExpand = pathToBeInspected.includes(path[path.length - 1]); + + // Scroll to the component in the inspector + if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) { + const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + + return shouldExpand; } else return false; }; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js index 7067cd540d..937fe45b2c 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js @@ -9,6 +9,7 @@ const useCallbackActions = () => { const currentPageComponents = useStore((state) => state?.getCurrentPageComponents(), shallow); const shouldFreeze = useStore((state) => state.getShouldFreeze()); const runQuery = useStore((state) => state.queryPanel.runQuery); + const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll); const handleRemoveComponent = (component) => { deleteComponents([component.id]); @@ -30,30 +31,22 @@ const useCallbackActions = () => { return toast.success('Copied to the clipboard', { position: 'top-center' }); }; - const autoScrollTo = (id) => { - setSelectedComponents([id]); - const target = document.getElementById(id); - target.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }; - const handleAutoScrollToComponent = (data) => { - const currentPageComponents = useStore.getState().getCurrentPageComponents(); - const component = currentPageComponents?.[data.id]; - - let parentId = component?.component?.parent; - if (parentId) { - const regex = /-\d+$/; - if (regex.test(parentId)) { - parentId = parentId.replace(regex, ''); // To get parentId without tab index if parent type is Tab - } - const parentType = currentPageComponents?.[parentId]?.component?.component; - if (parentType && (parentType === 'Modal' || parentType === 'Tabs')) { - autoScrollTo(parentId); // To scroll to parent component if parent type is Modal or Tabs - return; - } + const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(data.id); + if (!isAccessible) { + if (isOnCanvas) { + toast.success( + `This component can't be opened because it's on the main canvas. Close ${computedComponentId} and click "Go to component" to view it there` + ); + } else + toast.success( + `This component can't be opened because it's inside ${computedComponentId}. Open ${computedComponentId} and click "Go to component"to view it.` + ); + return; } - - autoScrollTo(data.id); + setSelectedComponents([computedComponentId]); + const target = document.getElementById(computedComponentId); + target.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }; const callbackActions = [ diff --git a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx index 0cdcdb844b..f0a3bbe811 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx @@ -7,7 +7,7 @@ import { getWorkspaceId, decodeEntities } from '@/_helpers/utils'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore'; import { useDataQueriesActions } from '@/_stores/dataQueriesStore'; -import { staticDataSources as staticDatasources } from '../constants'; +import { defaultSources, staticDataSources as staticDatasources } from '../constants'; import { useQueryPanelActions } from '@/_stores/queryPanelStore'; import Search from '@/_ui/Icon/solidIcons/Search'; import { Tooltip } from 'react-tooltip'; @@ -135,7 +135,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
{' '} - {source.name} + {defaultSources[cleanWord(source.name)].name}
), @@ -178,6 +178,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc } }; + function cleanWord(word) { + return word.replace(/default/g, ''); + } + return (
{
{ diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss index 95d2ab56e0..23d1c7f7cf 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss @@ -214,4 +214,9 @@ .input-value-padding { box-sizing: border-box; padding-right: 30px !important; +} + +.react-datepicker__navigation{ + overflow: visible !important; + height: inherit !important; } \ No newline at end of file diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx index 3b32706f67..20f499fbe8 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx @@ -298,7 +298,7 @@ const DropDownSelect = ({

) : ( -
+
{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/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx index e3f31c974d..e873228888 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx @@ -677,6 +677,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay }} componentName="TooljetDatabase" delayOnChange={false} + className="w-100" />
)} 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/ComponentsManagerTab/ComponentsManagerTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx index ef70343140..2ad7977496 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx @@ -133,7 +133,7 @@ export const ComponentsManagerTab = ({ darkMode }) => { 'StarRating', ]; const integrationItems = ['Map']; - const layoutItems = ['Container', 'Listview', 'Tabs', 'Modal']; + const layoutItems = ['Container', 'Listview', 'Tabs', 'ModalV2']; filteredComponents.forEach((f) => { if (commonItems.includes(f)) commonSection.items.push(f); diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx index 7450011b93..77274cd658 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx @@ -2,12 +2,14 @@ import React, { useEffect } from 'react'; import { WidgetBox } from '../WidgetBox'; import { useDrag, useDragLayer } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; +import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils'; +import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants'; export const DragLayer = ({ index, component }) => { const [{ isDragging }, drag, preview] = useDrag( () => ({ type: 'box', - item: { componentType: component.component }, + item: { componentType: component.component, component }, collect: (monitor) => ({ isDragging: monitor.isDragging() }), }), [component.component] @@ -18,7 +20,6 @@ export const DragLayer = ({ index, component }) => { }, []); const size = component.defaultSize || { width: 30, height: 40 }; - return ( <> {isDragging && } @@ -30,32 +31,39 @@ export const DragLayer = ({ index, component }) => { }; const CustomDragLayer = ({ size }) => { - const { currentOffset } = useDragLayer((monitor) => ({ + const { currentOffset, item } = useDragLayer((monitor) => ({ currentOffset: monitor.getSourceClientOffset(), + item: monitor.getItem(), })); if (!currentOffset) return null; - const canvasWidth = document.getElementsByClassName('real-canvas')[0]?.getBoundingClientRect()?.width; - + const canvasWidth = item?.canvasWidth; + const canvasBounds = item?.canvasRef?.getBoundingClientRect(); const height = size.height; - const width = (canvasWidth * size.width) / 43; + const width = (canvasWidth * size.width) / NO_OF_GRIDS; + + // Calculate position relative to the current canvas (parent or child) + const left = currentOffset.x - (canvasBounds?.left || 0); + const top = currentOffset.y - (canvasBounds?.top || 0); + + const [x, y] = snapToGrid(canvasWidth, left, top); return (
{ @@ -151,7 +153,8 @@ export const baseComponentProperties = ( 'properties', currentState, allComponents, - darkMode + darkMode, + '' ) ), }); diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx index d4676ad4b6..b39924854e 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx @@ -19,16 +19,34 @@ export const Form = ({ allComponents, pages, }) => { - const properties = Object.keys(componentMeta.properties); + const tempComponentMeta = deepClone(componentMeta); + + let properties = []; + let additionalActions = []; + let dataProperties = []; + const events = Object.keys(componentMeta.events); const validations = Object.keys(componentMeta.validation || {}); - const tempComponentMeta = deepClone(componentMeta); + + for (const [key] of Object.entries(componentMeta?.properties)) { + if (componentMeta?.properties[key]?.section === 'additionalActions') { + additionalActions.push(key); + } else if (componentMeta?.properties[key]?.accordian === 'Data') { + dataProperties.push(key); + } else { + properties.push(key); + } + } const { id } = component; const newOptions = [{ name: 'None', value: 'none' }]; - Object.entries(allComponents).forEach(([componentId, component]) => { - if (component.component.parent === id && component?.component?.component === 'Button') { - newOptions.push({ name: component.component.name, value: componentId }); + Object.entries(allComponents).forEach(([componentId, _component]) => { + const validParent = + _component.component.parent === id || + _component.component.parent === `${id}-footer` || + _component.component.parent === `${id}-header`; + if (validParent && _component?.component?.component === 'Button') { + newOptions.push({ name: _component.component.name, value: componentId }); } }); @@ -48,7 +66,8 @@ export const Form = ({ allComponents, validations, darkMode, - pages + pages, + additionalActions ); return ; @@ -68,7 +87,8 @@ export const baseComponentProperties = ( allComponents, validations, darkMode, - pages + pages, + additionalActions ) => { let items = []; if (properties.length > 0) { @@ -90,6 +110,24 @@ export const baseComponentProperties = ( }); } + items.push({ + title: 'Additional actions', + isOpen: true, + children: additionalActions?.map((property) => + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + property, + 'properties', + currentState, + allComponents, + darkMode + ) + ), + }); + if (events.length > 0) { items.push({ title: `${i18next.t('widget.common.events', 'Events')}`, diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx new file mode 100644 index 0000000000..4131217386 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import Accordion from '@/_ui/Accordion'; +import { renderElement } from '../Utils'; +import { baseComponentProperties } from './DefaultComponent'; +import { resolveReferences } from '@/_helpers/utils'; + +const INDEX_OF_TRIGGER = 2; + +export const ModalV2 = ({ componentMeta, darkMode, ...restProps }) => { + const { + layoutPropertyChanged, + component, + paramUpdated, + dataQueries, + currentState, + eventsChanged, + apps, + allComponents, + } = restProps; + + let properties = []; + let additionalActions = []; + let dataProperties = []; + + const events = Object.keys(componentMeta.events); + const validations = Object.keys(componentMeta.validation || {}); + + for (const [key] of Object.entries(componentMeta?.properties)) { + if (componentMeta?.properties[key]?.section === 'additionalActions') { + additionalActions.push(key); + } else if (componentMeta?.properties[key]?.accordian === 'Data') { + dataProperties.push(key); + } else { + properties.push(key); + } + } + + const renderCustomElement = (param, paramType = 'properties') => { + return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState); + }; + const conditionalAccordionItems = (component) => { + const useDefaultButton = resolveReferences( + component.component.definition.properties.useDefaultButton?.value ?? false + ); + const accordionItems = []; + let renderOptions = []; + const options = ['visibility', 'disabledTrigger', 'useDefaultButton']; + + options.map((option) => renderOptions.push(renderCustomElement(option))); + + const conditionalOptions = [{ name: 'triggerButtonLabel', condition: useDefaultButton }]; + + conditionalOptions.map(({ name, condition }) => { + if (condition) renderOptions.push(renderCustomElement(name)); + }); + + accordionItems.push({ + title: 'Trigger', + children: renderOptions, + }); + + return accordionItems; + }; + + if (component.component.definition.properties.size.value === 'fullscreen') { + component.component.properties.modalHeight = { + ...component.component.properties.modalHeight, + isHidden: true, + }; + } + + if (component.component.definition.properties.showHeader.value === '{{false}}') { + component.component.properties.headerHeight = { + ...component.component.properties.headerHeight, + isHidden: true, + }; + } + + if (component.component.definition.properties.showFooter.value === '{{false}}') { + component.component.properties.footerHeight = { + ...component.component.properties.footerHeight, + isHidden: true, + }; + } + + const accordionItems = baseComponentProperties( + dataProperties, + events, + component, + componentMeta, + layoutPropertyChanged, + paramUpdated, + dataQueries, + currentState, + eventsChanged, + apps, + allComponents, + validations, + darkMode, + [], + additionalActions + ); + + const [optionsItems] = conditionalAccordionItems(component); + + // Insert the Trigger option as the third item + accordionItems.splice(INDEX_OF_TRIGGER, 0, optionsItems); + + return ; +}; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx index a99ce25ac6..095deeccaa 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx @@ -41,7 +41,6 @@ export function Select({ componentMeta, darkMode, ...restProps }) { if (!Array.isArray(optionsValue)) { optionsValue = Object.values(optionsValue); } - const valuesToResolve = ['label', 'value']; let options = []; if (isDynamicOptionsEnabled || typeof optionsValue === 'string') { @@ -216,7 +215,6 @@ export function Select({ componentMeta, darkMode, ...restProps }) { }); updateOptions(_options); setMarkedAsDefault(_value); - paramUpdated({ name: 'value' }, 'value', _value, 'properties'); } }; @@ -315,7 +313,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)} @@ -425,7 +428,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/ColumnManager/PropertiesTabElements.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx index 27ba100f52..2e975109d2 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx @@ -52,6 +52,7 @@ export const PropertiesTabElements = ({ { label: 'Boolean', value: 'boolean' }, { label: 'Image', value: 'image' }, { label: 'Link', value: 'link' }, + { label: 'JSON', value: 'json' }, // Following column types are deprecated { label: 'Default', value: 'default' }, { label: 'Dropdown', value: 'dropdown' }, @@ -266,6 +267,24 @@ export const PropertiesTabElements = ({ )}
)} + {column.columnType === 'json' && ( +
+
+ +
+
+ )}
)} - {['string', 'default', undefined, 'number', 'boolean', 'select', 'text', 'newMultiSelect', 'datepicker'].includes( - column.columnType - ) && ( + {[ + 'string', + 'default', + undefined, + 'number', + 'json', + 'boolean', + 'select', + 'text', + 'newMultiSelect', + 'datepicker', + ].includes(column.columnType) && ( <> {column.columnType !== 'boolean' && (
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx index e559369396..c3fb47d612 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx @@ -50,6 +50,8 @@ export const ProgramaticallyHandleProperties = ({ return props?.parseInUnixTimestamp; case 'isDateSelectionEnabled': return props?.isDateSelectionEnabled; + case 'jsonIndentation': + return props?.jsonIndentation; default: return; } @@ -81,6 +83,9 @@ export const ProgramaticallyHandleProperties = ({ if (property === 'linkColor') { return definitionObj?.value ?? '#1B1F24'; } + if (property === 'jsonIndentation') { + return definitionObj?.value ?? `{{true}}`; + } return definitionObj?.value ?? `{{false}}`; }; @@ -111,7 +116,9 @@ export const ProgramaticallyHandleProperties = ({ const fxActiveFieldsPropExists = props?.hasOwnProperty('fxActiveFields') ?? false; //to support backward compatibility, when fxActive is true for a particular column, we are passing all possible combinations which should render codehinter const fxActive = - props?.fxActive && resolveReferences(props.fxActive) ? ['isEditable', 'columnVisibility', 'linkTarget'] : []; + props?.fxActive && resolveReferences(props.fxActive) + ? ['isEditable', 'columnVisibility', 'jsonIndentation', 'linkTarget'] + : []; const checkFxActiveFieldIsArrray = (fxActiveFieldsProperty) => { // adding error handling mechanism for fxActiveFieldsProperty , if props.fxActiveFields is array , then return props.fxActiveFields or else return [], this will make sure, fxActiveFields wil always be array diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx index 5a2175cfe1..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 }); + }} >
@@ -624,6 +629,8 @@ class TableComponent extends React.Component { return 'Select'; case 'newMultiSelect': return 'Multiselect'; + case 'json': + return 'JSON'; default: capitalize(text ?? ''); } @@ -647,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/RightSideBar/Inspector/Elements/Code.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx index 7ee512ee20..c74e023c0b 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx @@ -19,6 +19,7 @@ export const Code = ({ accordian, placeholder, validationFn, + isHidden = false, }) => { const currentState = useCurrentState(); @@ -43,6 +44,7 @@ export const Code = ({ onChange({ name: 'iconVisibility' }, 'value', value, 'styles'); } + if (isHidden) return null; return (
{ @@ -703,6 +705,9 @@ const GetAccordion = React.memo( case 'FilePicker': return ; + case 'ModalV2': + return ; + case 'Modal': return ; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js index 0ddda9f572..62ee032172 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js @@ -50,7 +50,8 @@ export function renderCustomStyles( componentConfig.component == 'MultiselectV2' || componentConfig.component == 'RadioButtonV2' || componentConfig.component == 'Button' || - componentConfig.component == 'Image' + componentConfig.component == 'Image' || + componentConfig.component == 'ModalV2' ) { const paramTypeConfig = componentMeta[paramType] || {}; const paramConfig = paramTypeConfig[param] || {}; @@ -131,6 +132,7 @@ export function renderElement( const paramTypeDefinition = componentDefinition[paramType] || {}; const definition = paramTypeDefinition[param] || {}; const meta = componentMeta[paramType][param]; + const isHidden = component.component.properties[param]?.isHidden ?? false; if ( componentConfig.component == 'DropDown' || @@ -170,6 +172,7 @@ export function renderElement( component={component} placeholder={placeholder} validationFn={validationFn} + isHidden={isHidden} /> ); } diff --git a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx index 1226bfba3e..69ded14971 100644 --- a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx +++ b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx @@ -2,7 +2,7 @@ import React from 'react'; import WidgetIcon from '@/../assets/images/icons/widgets'; import { useTranslation } from 'react-i18next'; -const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker']; +const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal']; const NEW_WIDGETS = [ 'ToggleSwitchV2', 'DropdownV2', @@ -12,6 +12,7 @@ const NEW_WIDGETS = [ 'DaterangePicker', 'DatePickerV2', 'TimePicker', + 'ModalV2', ]; export const WidgetBox = ({ component, darkMode }) => { diff --git a/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js index 4ad52da372..6933f4a5a5 100644 --- a/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js +++ b/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js @@ -4,7 +4,14 @@ export const RESTRICTED_WIDGETS_CONFIG = { Calendar: ['Calendar', 'Kanban'], Container: ['Calendar', 'Kanban'], Modal: ['Calendar', 'Kanban'], + ModalV2: ['Calendar', 'Kanban'], + ModalSlot: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'], Tabs: ['Calendar', 'Kanban'], Kanban_popout: ['Calendar', 'Kanban'], Listview: ['Calendar', 'Kanban'], }; + +export const RESTRICTED_WIDGET_SLOTS_CONFIG = { + header: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'], + footer: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'], +}; diff --git a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js index 13832857fb..be2855c476 100644 --- a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js +++ b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js @@ -3,6 +3,7 @@ import { tableConfig, chartConfig, modalConfig, + modalV2Config, formConfig, textinputConfig, numberinputConfig, @@ -64,6 +65,7 @@ export const widgets = [ buttonConfig, chartConfig, modalConfig, + modalV2Config, formConfig, textinputConfig, numberinputConfig, 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/dropdownV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js index b4672c6afe..308aff1f36 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js @@ -311,7 +311,6 @@ export const dropdownV2Config = { ], }, label: { value: 'Select' }, - value: { value: '{{"2"}}' }, optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select an option' }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index 0e9f5f4ce3..2d8eb7f0a8 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -4,9 +4,40 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 330, + height: 480, }, defaultChildren: [ + { + componentName: 'Text', + slotName: 'header', + layout: { + top: 10, + left: 1, + height: 40, + }, + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor'], + defaultValue: { + text: 'Form title', + textSize: 20, + textColor: '#000', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 12, + left: 32, + height: 36, + }, + properties: ['text'], + defaultValue: { + text: 'Button2', + padding: 'none', + }, + }, { componentName: 'Text', layout: { @@ -225,6 +256,7 @@ export const formConfig = { loadingState: { type: 'toggle', displayName: 'Loading state', + section: 'additionalActions', validation: { schema: { type: 'boolean' }, defaultValue: false, @@ -242,12 +274,64 @@ export const formConfig = { value: true, }, }, + showHeader: { type: 'toggle', displayName: 'Header' }, + showFooter: { type: 'toggle', displayName: 'Footer' }, + visibility: { + type: 'toggle', + displayName: 'Visibility', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + }, + disabledState: { + type: 'toggle', + displayName: 'Disable', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + }, }, events: { onSubmit: { displayName: 'On submit' }, onInvalid: { displayName: 'On invalid' }, }, styles: { + headerBackgroundColor: { + type: 'color', + displayName: 'Header background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + footerBackgroundColor: { + type: 'color', + displayName: 'Footer background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + headerHeight: { + type: 'code', + displayName: 'Header height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, + footerHeight: { + type: 'code', + displayName: 'Footer height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, backgroundColor: { type: 'color', displayName: 'Background color', @@ -274,26 +358,13 @@ export const formConfig = { defaultValue: '#fff', }, }, - visibility: { - type: 'toggle', - displayName: 'Visibility', - validation: { - schema: { type: 'boolean' }, - defaultValue: true, - }, - }, - disabledState: { - type: 'toggle', - displayName: 'Disable', - validation: { - schema: { type: 'boolean' }, - defaultValue: false, - }, - }, }, exposedVariables: { data: {}, isValid: true, + isVisible: true, + isDisabled: false, + isLoading: false, }, actions: [ { @@ -304,6 +375,21 @@ export const formConfig = { handle: 'resetForm', displayName: 'Reset Form', }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'setDisable', + displayName: 'Set Disable', + params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set Loading', + params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }], + }, ], definition: { others: { @@ -317,15 +403,18 @@ export const formConfig = { value: "{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}", }, - buttonToSubmit: { value: '{{"none"}}' }, + showHeader: { value: '{{false}}' }, + showFooter: { value: '{{false}}' }, + visibility: { value: '{{true}}' }, + disabledState: { value: '{{false}}' }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - visibility: { value: '{{true}}' }, - disabledState: { value: '{{false}}' }, + headerHeight: { value: '60px' }, + footerHeight: { value: '60px' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/index.js b/frontend/src/AppBuilder/WidgetManager/widgets/index.js index 2540cdeeef..ce0e73fdf5 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/index.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/index.js @@ -2,6 +2,7 @@ import { buttonConfig } from './button'; import { tableConfig } from './table'; import { chartConfig } from './chart'; import { modalConfig } from './modal'; +import { modalV2Config } from './modalV2'; import { formConfig } from './form'; import { textinputConfig } from './textinput'; import { numberinputConfig } from './numberinput'; @@ -62,7 +63,8 @@ export { buttonConfig, tableConfig, chartConfig, - modalConfig, + modalConfig, //Deprecated + modalV2Config, formConfig, textinputConfig, numberinputConfig, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js index a813bb5a0b..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', @@ -48,8 +49,12 @@ export const listviewConfig = { data: { type: 'code', displayName: 'List data', - validation: { - schema: { type: 'array', element: { type: 'object' } }, + schema: { + type: 'union', + schemas: [ + { type: 'array', element: { type: 'object' } }, + { type: 'array', element: { type: 'string' } }, + ], defaultValue: "[{text: 'Sample text 1'}]", }, }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js index 42740ad9c1..8f0c34b566 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js @@ -1,6 +1,6 @@ export const modalConfig = { - name: 'Modal', - displayName: 'Modal', + name: 'ModalLegacy', + displayName: 'Modal (Legacy)', description: 'Show pop-up windows', component: 'Modal', defaultSize: { diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js new file mode 100644 index 0000000000..e7e96c4398 --- /dev/null +++ b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js @@ -0,0 +1,277 @@ +export const modalV2Config = { + name: 'Modal', + displayName: 'Modal', + description: 'Show pop-up windows', + component: 'ModalV2', + defaultSize: { + width: 10, + height: 34, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + loadingState: { + type: 'toggle', + displayName: 'Loading state', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, + visibility: { + type: 'toggle', + displayName: 'Modal trigger visibility', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + }, + disabledTrigger: { + type: 'toggle', + displayName: 'Disable modal trigger', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + }, + disabledModal: { + type: 'toggle', + displayName: 'Disable modal window', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, + useDefaultButton: { + type: 'toggle', + displayName: 'Use default trigger button', + validation: { + schema: { + type: 'boolean', + }, + defaultValue: true, + }, + }, + triggerButtonLabel: { + type: 'code', + displayName: 'Trigger button label', + validation: { + schema: { + type: 'string', + }, + defaultValue: 'Launch Modal', + }, + }, + + // Data Accordion + showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' }, + showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' }, + + size: { + type: 'select', + displayName: 'Width', + accordian: 'Data', + options: [ + { name: 'small', value: 'sm' }, + { name: 'medium', value: 'lg' }, + { name: 'large', value: 'xl' }, + { name: 'fullscreen', value: 'fullscreen' }, + ], + validation: { + schema: { type: 'string' }, + defaultValue: 'lg', + }, + }, + modalHeight: { + type: 'numberInput', + displayName: 'Height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 }, + }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' }, + closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' }, + hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' }, + }, + events: { + onOpen: { displayName: 'On open' }, + onClose: { displayName: 'On close' }, + }, + defaultChildren: [ + { + componentName: 'Text', + slotName: 'header', + layout: { + top: 21, + left: 1, + height: 40, + }, + displayName: 'ModalHeaderTitle', + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor'], + defaultValue: { + text: 'Modal title', + textSize: 20, + textColor: '#000', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 24, + left: 22, + height: 36, + }, + displayName: 'ModalFooterCancel', + properties: ['text'], + styles: ['type', 'borderColor', 'padding'], + defaultValue: { + text: 'Button1', + type: 'outline', + borderColor: '#CCD1D5', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 24, + left: 32, + height: 36, + }, + displayName: 'ModalFooterConfirm', + properties: ['text'], + defaultValue: { + text: 'Button2', + padding: 'none', + }, + }, + ], + styles: { + headerBackgroundColor: { + type: 'color', + displayName: 'Header background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + footerBackgroundColor: { + type: 'color', + displayName: 'Footer background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + bodyBackgroundColor: { + type: 'color', + displayName: 'Body background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + triggerButtonBackgroundColor: { + type: 'color', + displayName: 'Trigger button background color', + validation: { + schema: { type: 'string' }, + defaultValue: false, + }, + }, + triggerButtonTextColor: { + type: 'color', + displayName: 'Trigger button text color', + validation: { + schema: { type: 'string' }, + defaultValue: false, + }, + }, + }, + exposedVariables: { + show: false, + isDisabledModal: false, + isDisabledTrigger: false, + isVisible: true, + isLoading: false, + }, + actions: [ + { + handle: 'open', + displayName: 'Open', + }, + { + handle: 'close', + displayName: 'Close', + }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'setDisableTrigger', + displayName: 'Set disable trigger', + params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setDisableModal', + displayName: 'Set disable modal', + params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set loading', + params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + ], + definition: { + others: { + showOnDesktop: { value: '{{true}}' }, + showOnMobile: { value: '{{false}}' }, + }, + properties: { + loadingState: { value: `{{false}}` }, + visibility: { value: '{{true}}' }, + disabledTrigger: { value: '{{false}}' }, + disabledModal: { value: '{{false}}' }, + useDefaultButton: { value: `{{true}}` }, + triggerButtonLabel: { value: `Launch Modal` }, + size: { value: 'lg' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, + hideCloseButton: { value: '{{false}}' }, + hideOnEsc: { value: '{{true}}' }, + closeOnClickingOutside: { value: '{{false}}' }, + modalHeight: { value: 400 }, + headerHeight: { value: 80 }, + footerHeight: { value: 80 }, + }, + events: [], + styles: { + headerBackgroundColor: { value: '#ffffffff' }, + footerBackgroundColor: { value: '#ffffffff' }, + bodyBackgroundColor: { value: '#ffffffff' }, + triggerButtonBackgroundColor: { value: '#4D72FA' }, + triggerButtonTextColor: { value: '#ffffffff' }, + }, + }, +}; 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/Widgets/Container.jsx b/frontend/src/AppBuilder/Widgets/Container.jsx deleted file mode 100644 index 3ccedd869b..0000000000 --- a/frontend/src/AppBuilder/Widgets/Container.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useMemo } from 'react'; -import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container'; -import Spinner from '@/_ui/Spinner'; -import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables'; - -export const Container = ({ - id, - properties, - styles, - darkMode, - height, - width, - setExposedVariables, - setExposedVariable, -}) => { - const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles; - - const { isDisabled, isVisible, isLoading } = useExposeState( - properties.loadingState, - properties.visibility, - properties.disabledState, - setExposedVariables, - setExposedVariable - ); - - const contentBgColor = useMemo(() => { - return { - backgroundColor: - ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor, - }; - }, [styles.backgroundColor, darkMode]); - - const headerBgColor = useMemo(() => { - return { - backgroundColor: - ['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode - ? '#232E3C' - : styles.headerBackgroundColor, - }; - }, [styles.headerBackgroundColor, darkMode]); - - const computedStyles = { - backgroundColor: contentBgColor.backgroundColor, - borderRadius: borderRadius ? parseFloat(borderRadius) : 0, - border: `1px solid ${borderColor}`, - height, - display: isVisible ? 'flex' : 'none', - overflow: 'hidden auto', - position: 'relative', - boxShadow, - }; - - const computedHeaderStyles = { - ...headerBgColor, - height: `${headerHeight}px`, - flexShrink: 0, - flexGrow: 0, - borderBottom: `1px solid var(--border-weak)`, - }; - - const computedContentStyles = { - ...contentBgColor, - flex: 1, - overflow: 'auto', - }; - - return ( -
- {properties.showHeader && ( - - )} - {isLoading ? ( -
- -
- ) : ( - - )} -
- ); -}; diff --git a/frontend/src/AppBuilder/Widgets/Container/Container.jsx b/frontend/src/AppBuilder/Widgets/Container/Container.jsx new file mode 100644 index 0000000000..4978427370 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Container/Container.jsx @@ -0,0 +1,119 @@ +import React, { useMemo } from 'react'; +import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container'; +import Spinner from '@/_ui/Spinner'; +import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables'; +import { shallow } from 'zustand/shallow'; +import { + CONTAINER_FORM_CANVAS_PADDING, + SUBCONTAINER_CANVAS_BORDER_WIDTH, +} from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import useStore from '@/AppBuilder/_stores/store'; +import './container.scss'; + +export const Container = ({ + id, + properties, + styles, + darkMode, + height, + width, + setExposedVariables, + setExposedVariable, +}) => { + const { isDisabled, isVisible, isLoading } = useExposeState( + properties.loadingState, + properties.visibility, + properties.disabledState, + setExposedVariables, + setExposedVariable + ); + + const isWidgetInContainerDragging = useStore( + (state) => state.containerChildrenMapping?.[id]?.includes(state?.draggingComponentId), + shallow + ); + + const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles; + const contentBgColor = useMemo(() => { + return { + backgroundColor: + ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor, + }; + }, [styles.backgroundColor, darkMode]); + + const headerBgColor = useMemo(() => { + return { + backgroundColor: + ['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode + ? '#232E3C' + : styles.headerBackgroundColor, + }; + }, [styles.headerBackgroundColor, darkMode]); + + const computedStyles = { + backgroundColor: contentBgColor.backgroundColor, + borderRadius: borderRadius ? parseFloat(borderRadius) : 0, + border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`, + height, + display: isVisible ? 'flex' : 'none', + flexDirection: 'column', + position: 'relative', + boxShadow, + }; + + const containerHeaderStyles = { + flexShrink: 0, + padding: `${CONTAINER_FORM_CANVAS_PADDING}px ${CONTAINER_FORM_CANVAS_PADDING}px 3px ${CONTAINER_FORM_CANVAS_PADDING}px`, + ...headerBgColor, + }; + + const containerContentStyles = { + overflow: 'hidden auto', + display: 'flex', + height: '100%', + padding: `${CONTAINER_FORM_CANVAS_PADDING}px`, + }; + + return ( +
+ {isLoading ? ( + + ) : ( + <> + {properties.showHeader && ( +
+ +
+ )} +
+ +
+ + )} +
+ ); +}; diff --git a/frontend/src/AppBuilder/Widgets/Container/container.scss b/frontend/src/AppBuilder/Widgets/Container/container.scss new file mode 100644 index 0000000000..323f5e8c9a --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Container/container.scss @@ -0,0 +1,13 @@ +.wj-container-header { + position: relative; + &::after { + content: ''; + position: absolute; + bottom: 0; + left: -7px; + right: -7px; + height: 1px; + background-color: var(--border-weak); + } + } + \ No newline at end of file diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 674d707f03..afeb4cf844 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -1,17 +1,25 @@ -import React, { useRef, useState, useEffect, useMemo } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; // eslint-disable-next-line import/no-unresolved -import { diff } from 'deep-object-diff'; import _, { debounce, omit } from 'lodash'; -import { Box } from '@/Editor/Box'; import { generateUIComponents } from './FormUtils'; import { useMounted } from '@/_hooks/use-mount'; import { onComponentClick, removeFunctionObjects } from '@/_helpers/appUtils'; -import { useAppInfo } from '@/_stores/appDataStore'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; import RenderSchema from './RenderSchema'; import useStore from '@/AppBuilder/_stores/store'; +import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables'; import { shallow } from 'zustand/shallow'; +import { + CONTAINER_FORM_CANVAS_PADDING, + SUBCONTAINER_CANVAS_BORDER_WIDTH, +} from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import './form.scss'; + +const getCanvasHeight = (height) => { + const parsedHeight = height.includes('px') ? parseInt(height, 10) : height; + return Math.ceil(parsedHeight); +}; export const Form = function Form(props) { const { @@ -29,27 +37,71 @@ export const Form = function Form(props) { dataCy, } = props; const childComponents = useStore((state) => state.getChildComponents(id), shallow); - const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles; - const { buttonToSubmit, loadingState, advanced, JSONSchema } = properties; + const { + borderRadius, + borderColor, + boxShadow, + headerHeight, + footerHeight, + footerBackgroundColor, + headerBackgroundColor, + } = styles; + const { + buttonToSubmit, + loadingState, + advanced, + JSONSchema, + showHeader = false, + showFooter = false, + visibility, + disabledState, + } = properties; + const { isDisabled, isVisible, isLoading } = useExposeState( + properties.loadingState, + properties.visibility, + properties.disabledState, + setExposedVariables, + setExposedVariable + ); const backgroundColor = ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor; const computedStyles = { backgroundColor, borderRadius: borderRadius ? parseFloat(borderRadius) : 0, - border: `1px solid ${borderColor}`, + border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`, height, - display: visibility ? 'flex' : 'none', + display: isVisible ? 'flex' : 'none', position: 'relative', - overflow: 'hidden auto', boxShadow, + flexDirection: 'column', }; - const childIdNameMap = useMemo(() => { - return Object.keys(childComponents).reduce((acc, id) => { - const component = childComponents[id]?.component?.component; - return { ...acc, [id]: component?.name }; - }, {}); - }, [childComponents]); + const formHeader = { + flexShrink: 0, + paddingBottom: '3px', + paddingTop: '7px', + paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + backgroundColor: + ['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor, + }; + + const formContent = { + overflow: 'hidden auto', + display: 'flex', + height: '100%', + paddingTop: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingBottom: showFooter ? '3px' : '7px', + paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + }; + + const formFooter = { + flexShrink: 0, + padding: `${CONTAINER_FORM_CANVAS_PADDING}px`, + backgroundColor: + ['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor, + }; const parentRef = useRef(null); const childDataRef = useRef({}); @@ -58,6 +110,8 @@ export const Form = function Form(props) { const [isValid, setValidation] = useState(true); const [uiComponents, setUIComponents] = useState([]); const mounted = useMounted(); + const canvasHeaderHeight = getCanvasHeight(headerHeight) / 10; + const canvasFooterHeight = getCanvasHeight(footerHeight) / 10; useEffect(() => { const exposedVariables = { @@ -155,7 +209,7 @@ export const Form = function Form(props) { }; setExposedVariables(exposedVariables); setValidation(childValidation); - }, [childrenData, advanced, JSON.stringify(childIdNameMap)]); + }, [childrenData, advanced]); useEffect(() => { document.addEventListener('submitForm', handleFormSubmission); @@ -245,105 +299,113 @@ export const Form = function Form(props) { if (e.target.className === 'real-canvas') onComponentClick(id, component); }} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked > - {loadingState ? ( -
-
-
-
-
- ) : ( -
- {!advanced && ( -
- - {/* - */} -
+ {showHeader && ( +
+ + {isDisabled && ( +
{}} + onDrop={(e) => e.stopPropagation()} + /> )} - {advanced && - uiComponents?.map((item, index) => { - return ( -
-
- +
+ )} +
+ {isLoading ? ( +
+
+
+ ) : ( +
+ {!advanced && ( +
+ +
+ )} + {advanced && + uiComponents?.map((item, index) => { + return ( +
+
+ +
- {/* */} -
- ); - })} -
+ ); + })} + + )} +
+ {showFooter && ( +
+ + {isDisabled && ( + )} ); diff --git a/frontend/src/AppBuilder/Widgets/Form/form.scss b/frontend/src/AppBuilder/Widgets/Form/form.scss new file mode 100644 index 0000000000..530e837eb2 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Form/form.scss @@ -0,0 +1,40 @@ +.wj-form-header { + position: relative; + &::after { + content: ""; + position: absolute; + bottom: 0; + left: -7px; + right: -7px; + height: 1px; + background-color: var(--border-weak); + } +} + +.wj-form-footer { + position: relative; + &::after { + content: ""; + position: absolute; + top: 0; + left: -7px; + right: -7px; + height: 1px; + background-color: var(--border-weak); + } +} + +.tj-form-disabled-overlay { + /* TODO: Make slot overlays common */ + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.8); + z-index: 1; + margin: 0; + + box-sizing: content-box; + padding: 4px 0; +} diff --git a/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx b/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx index 34efc57221..8aa7b578ac 100644 --- a/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx +++ b/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx @@ -56,6 +56,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { const [containers, setContainers] = useState([]); const [showModal, setShowModal] = useState(false); + const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas); const [activeId, setActiveId] = useState(null); const cardMovementRef = useRef(null); const shouldUpdateData = useRef(false); @@ -117,6 +118,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { } /**** End - Logic to reduce the zIndex of modal control box ****/ } + setModalOpenOnCanvas(`${id}-modal`, showModal); }, [showModal]); useEffect(() => { @@ -410,6 +412,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { width: `${(Number(cardWidth) || 300) + 48}px`, }} kanbanProps={kanbanProps} + componentType="Kanban" > {items[columnId] && ( diff --git a/frontend/src/AppBuilder/Widgets/Listview.jsx b/frontend/src/AppBuilder/Widgets/Listview.jsx index 5ec5abbb17..285f9e5bf9 100644 --- a/frontend/src/AppBuilder/Widgets/Listview.jsx +++ b/frontend/src/AppBuilder/Widgets/Listview.jsx @@ -12,11 +12,8 @@ import { shallow } from 'zustand/shallow'; export const Listview = function Listview({ id, - component, width, height, - containerProps, - removeComponent, properties, styles, fireEvent, @@ -270,38 +267,8 @@ export const Listview = function Listview({ columns={positiveColumns} listViewMode={mode} darkMode={darkMode} + componentType="Listview" /> - {/* { - const changedData = { [component.name]: { [optionName]: value } }; - const existingDataAtIndex = prevData[index] ?? {}; - const newDataAtIndex = { - ...prevData[index], - [component.name]: { - ...existingDataAtIndex[component.name], - ...changedData[component.name], - id: componentId, - }, - }; - const newChildrenData = { ...prevData, [index]: newDataAtIndex }; - return { ...prevData, ...newChildrenData }; - }); - }} - /> */}
))}
diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx index 5543ce4ee0..e0f099205f 100644 --- a/frontend/src/AppBuilder/Widgets/Modal.jsx +++ b/frontend/src/AppBuilder/Widgets/Modal.jsx @@ -49,6 +49,7 @@ export const Modal = function Modal({ const size = properties.size ?? 'lg'; const [modalWidth, setModalWidth] = useState(); const mode = useStore((state) => state.currentMode, shallow); + const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas); /**** Start - Logic to reset the zIndex of modal control box ****/ useEffect(() => { @@ -63,6 +64,7 @@ export const Modal = function Modal({ useGridStore.getState().actions.setOpenModalWidgetId(null); } } + setModalOpenOnCanvas(id, showModal); }, [showModal, id, mode]); /**** End - Logic to reset the zIndex of modal control box ****/ diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx new file mode 100644 index 0000000000..e25027ce33 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { default as BootstrapModal } from 'react-bootstrap/Modal'; +import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; +import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils'; + +export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode, width, footerHeight, onClick }) => { + const canvasFooterHeight = getCanvasHeight(footerHeight); + return ( + + + {isDisabled && ( + ); diff --git a/frontend/src/AppBuilder/_helpers/editorHelpers.js b/frontend/src/AppBuilder/_helpers/editorHelpers.js index 9abeb9a56b..5e817c7f7e 100644 --- a/frontend/src/AppBuilder/_helpers/editorHelpers.js +++ b/frontend/src/AppBuilder/_helpers/editorHelpers.js @@ -59,12 +59,13 @@ import { BoundedBox } from '@/Editor/Components/BoundedBox/BoundedBox'; import { isPDFSupported } from '@/_helpers/appUtils'; import { resolveWidgetFieldValue } from '@/_helpers/utils'; import { useEditorStore } from '@/_stores/editorStore'; -import { Container } from '@/AppBuilder/Widgets/Container'; +import { Container } from '@/AppBuilder/Widgets/Container/Container'; import { Listview } from '@/AppBuilder/Widgets/Listview'; import { Tabs } from '@/AppBuilder/Widgets/Tabs'; import { Kanban } from '@/AppBuilder/Widgets/Kanban/Kanban'; import { Form } from '@/AppBuilder/Widgets/Form/Form'; import { Modal } from '@/AppBuilder/Widgets/Modal'; +import { ModalV2 } from '@/AppBuilder/Widgets/ModalV2/ModalV2'; import { Calendar } from '@/AppBuilder/Widgets/Calendar/Calendar'; // import './requestIdleCallbackPolyfill'; @@ -106,6 +107,7 @@ export const AllComponents = { Multiselect, MultiselectV2, Modal, + ModalV2, Chart, Map: MapComponent, QrScanner, diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index a8fac45c48..1b664cfa21 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -329,6 +329,11 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v setCurrentPageHandle(startingPage.handle); updateFeatureAccess(); setCurrentPageId(startingPage.id, moduleId); + setResolvedPageConstants({ + id: startingPage?.id, + handle: startingPage?.handle, + name: startingPage?.name, + }); setComponentNameIdMapping(moduleId); updateEventsField('events', appData.events); setCurrentVersionId(appData.editing_version?.id || appData.current_version_id); diff --git a/frontend/src/AppBuilder/_stores/slices/appSlice.js b/frontend/src/AppBuilder/_stores/slices/appSlice.js index 0132b78d8b..39cc7a4986 100644 --- a/frontend/src/AppBuilder/_stores/slices/appSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/appSlice.js @@ -7,6 +7,7 @@ import { getWorkspaceId } from '@/_helpers/utils'; import { navigate } from '@/AppBuilder/_utils/misc'; import queryString from 'query-string'; import { replaceEntityReferencesWithIds } from '../utils'; +import _ from 'lodash'; const initialState = { app: {}, @@ -124,10 +125,14 @@ export const createAppSlice = (set, get) => ({ setComponentNameIdMapping('canvas'); setQueryMapping('canvas'); + const isLicenseValid = + !_.get(license, 'featureAccess.licenseStatus.isExpired', true) && + _.get(license, 'featureAccess.licenseStatus.isLicenseValid', false); + const appId = get().app.appId; const filteredQueryParams = queryParams.filter(([key, value]) => { if (!value) return false; - if (key === 'env' && !license.isLicenseValid()) return false; + if (key === 'env' && isLicenseValid) return false; return true; }); diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index 1f6ce18405..8f325dd614 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -10,7 +10,11 @@ import { import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; import { cloneDeep, merge, set as lodashSet } from 'lodash'; -import { computeComponentName, getAllChildComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils'; +import { + computeComponentName, + getAllChildComponents, + getParentWidgetFromId, +} from '@/AppBuilder/AppCanvas/appCanvasUtils'; import { pageConfig } from '@/AppBuilder/RightSideBar/PageSettingsTab/pageConfig'; import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants'; import { DEFAULT_COMPONENT_STRUCTURE } from './resolvedSlice'; @@ -40,6 +44,7 @@ const initialState = { currentPageHandle: null, showWidgetDeleteConfirmation: false, focusedParentId: null, + modalsOpenOnCanvas: [], }; export const createComponentsSlice = (set, get) => ({ @@ -502,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`, @@ -761,7 +766,7 @@ export const createComponentsSlice = (set, get) => ({ const { getComponentTypeFromId } = get(); const transformedParentId = parentId?.length > 36 ? parentId.slice(0, 36) : parentId; let parentType = getComponentTypeFromId(transformedParentId, moduleId); - const parentWidget = parentType === 'Kanban' ? 'Kanban_card' : parentType; + const parentWidget = getParentWidgetFromId(parentType, parentId); const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || []; const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget); if (!isParentChangeAllowed) @@ -1742,7 +1747,10 @@ export const createComponentsSlice = (set, get) => ({ getCustomResolvableReference: (value, parentId, moduleId) => { const { getParentComponentType } = get(); const parentComponentType = getParentComponentType(parentId, moduleId); - if (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) { + if ( + (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) || + value === '{{listItem}}' + ) { return { entityType: 'components', entityNameOrId: parentId, entityKey: 'listItem' }; } else if ( parentComponentType === 'Kanban' && @@ -1860,4 +1868,17 @@ export const createComponentsSlice = (set, get) => ({ const currentPage = getCurrentPage(moduleId); return currentPage?.autoComputeLayout; }, + setModalOpenOnCanvas: (modalId, isOpen) => { + const { modalsOpenOnCanvas } = get(); + let newModalOpenOnCanvas = []; + + if (isOpen) { + newModalOpenOnCanvas = [...modalsOpenOnCanvas, modalId]; + } else { + newModalOpenOnCanvas = modalsOpenOnCanvas.filter((id) => id !== modalId); + } + set((state) => { + state.modalsOpenOnCanvas = newModalOpenOnCanvas; + }); + }, }); diff --git a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js index 0d444272a3..1edd3994c5 100644 --- a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js @@ -136,7 +136,6 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({ updateVersionNameAction: async (appId, versionId, versionName, onSuccess, onFailure) => { try { await appVersionService.save(appId, versionId, { name: versionName }); - console.log('happening'); set((state) => { if (state.selectedVersion && state.selectedVersion.id === versionId) { @@ -177,7 +176,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({ appVersionsLazyLoaded: false, selectedEnvironment: response.editorEnvironment, appVersionEnvironment: response.appVersionEnvironment, - environments: response.environments, + environments: response?.environments?.length ? response.environments : get().environments, }; if (state.selectedVersion?.id === versionId) { @@ -241,7 +240,6 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({ useStore.getState()?.license?.featureAccess ), }; - console.log({ environment, get: get().appVersionEnvironment }); const versionIsAvailableInEnvironment = environment?.priority <= get().currentAppVersionEnvironment?.priority; if (!versionIsAvailableInEnvironment) { @@ -252,7 +250,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({ }); selectedVersion = response.editorVersion; const appVersionEnvironment = get().environments.find( - (environment) => environment.id === selectedVersion.current_environment_id + (environment) => environment.id === selectedVersion.currentEnvironmentId ); //TODO: need to check if this is needed 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/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js index cc80a9dbf7..642266a32b 100644 --- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js @@ -3,9 +3,11 @@ import { debounce } from 'lodash'; const initialState = { hoveredComponentForGrid: '', + hoveredComponentBoundaryId: '', triggerCanvasUpdater: false, lastCanvasIdClick: '', lastCanvasClickPosition: null, + draggingComponentId: null, }; export const createGridSlice = (set, get) => ({ @@ -13,11 +15,14 @@ export const createGridSlice = (set, get) => ({ setHoveredComponentForGrid: (id) => set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }), getHoveredComponentForGrid: () => get().hoveredComponentForGrid, + setHoveredComponentBoundaryId: (id) => + set(() => ({ hoveredComponentBoundaryId: id }), false, { type: 'setHoveredComponentBoundaryId', id }), toggleCanvasUpdater: () => set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }), debouncedToggleCanvasUpdater: debounce(() => { get().toggleCanvasUpdater(); }, 200), + setDraggingComponentId: (id) => set(() => ({ draggingComponentId: id })), moveComponentPosition: (direction) => { const { setComponentLayout, currentLayout, getSelectedComponentsDefinition, debouncedToggleCanvasUpdater } = get(); let layouts = {}; diff --git a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js index 98decac629..367ca4cf0c 100644 --- a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js @@ -37,4 +37,85 @@ export const createLeftSideBarSlice = (set, get) => ({ toggleLeftSidebar(true); } }, + getComponentIdToAutoScroll: (componentId) => { + const { getCurrentPageComponents, getAllExposedValues, modalsOpenOnCanvas } = get(); + const currentPageComponents = getCurrentPageComponents(); + + let targetComponentId = componentId; + let current = componentId; + const visited = new Set(); + let isInsideOpenModal = false; + + // Bubble up to the outermost parent to find the target component + // eslint-disable-next-line no-constant-condition + while (true) { + if (visited.has(current)) break; + visited.add(current); + + const parentId = currentPageComponents?.[current]?.component?.parent; + if (!parentId) break; + + let isComponentVisibleInParent = true; + let nextPossibleCandidate = parentId; + + // If the component exists inside a tab component + const regForTabs = /-(?!\d{12}$)\d+$/; // Parent id for tabs follow the format 'id-index' and index is not UUIDv4 id segment + if (regForTabs.test(parentId)) { + const reg = /-(\d+)$/; + const tabIndex = Number(parentId.match(reg)[1]); // Tab index inside which the component exists + + const tabId = parentId.replace(regForTabs, ''); // Extract tab id from parent id + + const { currentTab } = getAllExposedValues().components?.[tabId] || {}; + const activeTabIndex = Number(currentTab); + + nextPossibleCandidate = tabId; + if (tabIndex !== activeTabIndex) { + isComponentVisibleInParent = false; + } + } + + // If the component exists inside a modal component + if (currentPageComponents?.[parentId]?.component?.component === 'Modal') { + nextPossibleCandidate = parentId; + if (!modalsOpenOnCanvas.includes(parentId)) { + isComponentVisibleInParent = false; + } + } + + // If the component exists inside the kanban component's modal + if (parentId.endsWith('-modal')) { + nextPossibleCandidate = parentId.replace(/-modal$/, ''); // Extract kanban id from parent id + if (!modalsOpenOnCanvas.includes(parentId)) { + isComponentVisibleInParent = false; + } + } + + // If the open modal contains the component + if (modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] === parentId) { + isInsideOpenModal = true; + } + + if (!isComponentVisibleInParent) { + targetComponentId = nextPossibleCandidate; + } + current = nextPossibleCandidate; + } + + if (modalsOpenOnCanvas.length > 0 && !isInsideOpenModal) { + const targetId = visited.size === 1 ? modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] : current; + const componentName = currentPageComponents?.[targetId]?.component?.name; + + return { + isAccessible: false, + computedComponentId: componentName, + isOnCanvas: visited.size === 1, + }; + } + + return { + isAccessible: true, + computedComponentId: targetComponentId, + }; + }, }); 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/CustomComponent/CustomComponent.jsx b/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx index 5bfe9818e5..4d4b7a2e48 100644 --- a/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx +++ b/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx @@ -42,7 +42,7 @@ export const CustomComponent = (props) => { setCustomProps({ ...customPropRef.current, ...e.data.updatedObj }); } else if (e.data.message === 'RUN_QUERY') { const options = { - parameters: e.data.parameters, + parameters: JSON.parse(e.data.parameters), queryName: e.data.queryName, }; onEvent('onTrigger', [], options); diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index ce286d7fb8..216e8caa12 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -61,7 +61,6 @@ export const DropdownV2 = ({ }) => { const { label, - value, advanced, schema, placeholder, @@ -90,7 +89,7 @@ export const DropdownV2 = ({ padding, } = styles; const isInitialRender = useRef(true); - const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value)); + const [currentValue, setCurrentValue] = useState(() => findDefaultItem(schema)); const isMandatory = validation?.mandatory ?? false; const options = properties?.options; const [validationStatus, setValidationStatus] = useState(validate(currentValue)); @@ -170,18 +169,9 @@ export const DropdownV2 = ({ }; useEffect(() => { - if (advanced) { - setInputValue(findDefaultItem(schema)); - } + setInputValue(findDefaultItem(advanced ? schema : options)); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [advanced, JSON.stringify(schema)]); - - useEffect(() => { - if (!advanced) { - setInputValue(value); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [advanced, value]); + }, [advanced, JSON.stringify(schema), JSON.stringify(options)]); useEffect(() => { if (visibility !== properties.visibility) setVisibility(properties.visibility); @@ -448,6 +438,7 @@ export const DropdownV2 = ({ onChange={(selectedOption, actionProps) => { if (actionProps.action === 'clear') { setInputValue(null); + fireEvent('onSelect'); } if (actionProps.action === 'select-option') { setInputValue(selectedOption.value); 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/ControlledComponentToRender.jsx b/frontend/src/Editor/ControlledComponentToRender.jsx index 4765d0d69f..54a451188b 100644 --- a/frontend/src/Editor/ControlledComponentToRender.jsx +++ b/frontend/src/Editor/ControlledComponentToRender.jsx @@ -10,7 +10,7 @@ function deepEqualityCheckusingLoDash(obj1, obj2) { export const shouldUpdate = (prevProps, nextProps) => { const listToRender = getComponentsToRenders(); // evaluate change in exposedVariables only for Modal component, because open/close in modal relies on exposedVariables - const compareExposedVariables = nextProps.componentName === 'Modal'; + const compareExposedVariables = nextProps.componentName === 'Modal' || nextProps.componentName === 'ModalV2'; let needToRender = false; diff --git a/frontend/src/Editor/DraggableBox.jsx b/frontend/src/Editor/DraggableBox.jsx index ae2740bc84..3cdad272b7 100644 --- a/frontend/src/Editor/DraggableBox.jsx +++ b/frontend/src/Editor/DraggableBox.jsx @@ -10,7 +10,7 @@ import { resolveWidgetFieldValue } from '@/_helpers/utils'; import ErrorBoundary from './ErrorBoundary'; import { useEditorStore } from '@/_stores/editorStore'; import { shallow } from 'zustand/shallow'; -import { useNoOfGrid, useGridStore } from '@/_stores/gridStore'; +import { useGridStore } from '@/_stores/gridStore'; import WidgetBox from './WidgetBox'; import * as Sentry from '@sentry/react'; import { findHighestLevelofSelection } from './DragContainer'; @@ -61,7 +61,7 @@ const DraggableBox = React.memo( }) => { const isResizing = useGridStore((state) => state.resizingComponentId === id); const [canDrag, setCanDrag] = useState(true); - const noOfGrid = useNoOfGrid(); + const noOfGrid = 43; const { currentLayout, setHoveredComponent, diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx index 66548cbef8..aba2ca6af1 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx @@ -5,6 +5,7 @@ import JSONTreeViewer from '@/_ui/JSONTreeViewer'; import cx from 'classnames'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import useStore from '@/AppBuilder/_stores/store'; +import { toast } from 'react-hot-toast'; function Logs({ logProps, idx }) { const [open, setOpen] = React.useState(false); @@ -52,10 +53,19 @@ function Logs({ logProps, idx }) { } }; + const copyToClipboard = (data) => { + const stringified = JSON.stringify(data, null, 2).replace(/\\/g, ''); + navigator.clipboard.writeText(stringified); + return toast.success('Value copied to clipboard', { position: 'top-center' }); + }; + const callbackActions = [ { for: 'all', - actions: [{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }], + actions: [ + { name: 'Copy value', dispatchAction: copyToClipboard, icon: false }, + { name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }, + ], enableForAllChildren: true, enableFor1stLevelChildren: true, }, diff --git a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx index 7e8865cdfe..d1afd378f4 100644 --- a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx +++ b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx @@ -7,7 +7,6 @@ import { getWorkspaceId, decodeEntities } from '@/_helpers/utils'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore'; import { useDataQueriesActions } from '@/_stores/dataQueriesStore'; -import { staticDataSources as staticDatasources } from '../constants'; import { useQueryPanelActions } from '@/_stores/queryPanelStore'; import Search from '@/_ui/Icon/solidIcons/Search'; import { Tooltip } from 'react-tooltip'; @@ -16,7 +15,7 @@ import { canCreateDataSource } from '@/_helpers'; import './../queryManager.theme.scss'; import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; -function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, defaultDataSources }) { +function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, staticDataSources }) { const dataSources = useDataSources(); const globalDataSources = useGlobalDataSources(); const sampleDataSource = useSampleDataSource(); @@ -33,11 +32,6 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc closePopup(); }; - const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true'; - const staticDataSources = workflowsEnabled - ? staticDatasources - : staticDatasources.filter((ds) => ds?.kind !== 'workflows'); - useEffect(() => { const shouldAddSampleDataSource = !!sampleDataSource; const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter( @@ -148,7 +142,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
), isDisabled: true, - options: defaultDataSources?.map((source) => ({ + options: staticDataSources?.map((source) => ({ label: (
{' '} diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx index 40818a3bb9..7a4a0b0bce 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx @@ -163,7 +163,7 @@ export const DateTimePicker = ({
Save Changes
-
+
Esc
Discard Changes
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss index 95d2ab56e0..55d0e7f3ed 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss @@ -214,4 +214,24 @@ .input-value-padding { box-sizing: border-box; padding-right: 30px !important; +} + +.datepicker-widget.theme-tjdb{ + .react-datepicker__navigation{ + overflow: visible !important; + height: inherit !important; + } +} + +.esc-btn-datepicker{ + height: 18px ; + align-items: center; +} + +.tjdb-td-wrapper{ + .react-datepicker-time__input{ + input{ + line-height: normal !important; + } + } } \ No newline at end of file diff --git a/frontend/src/Editor/SubContainer.jsx b/frontend/src/Editor/SubContainer.jsx index 01335af55a..fc78875819 100644 --- a/frontend/src/Editor/SubContainer.jsx +++ b/frontend/src/Editor/SubContainer.jsx @@ -23,7 +23,7 @@ import { useEditorStore } from '@/_stores/editorStore'; // eslint-disable-next-line import/no-unresolved import { diff } from 'deep-object-diff'; -import { useGridStore, useResizingComponentId } from '@/_stores/gridStore'; +import { useGridStore } from '@/_stores/gridStore'; import GhostWidget from './GhostWidget'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; @@ -68,7 +68,7 @@ export const SubContainer = ({ shallow ); - const resizingComponentId = useResizingComponentId(); + const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); const noOfGrids = 43; const { isGridActive } = useGridStore((state) => ({ isGridActive: state.activeGrid === parent }), shallow); diff --git a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js index b4672c6afe..308aff1f36 100644 --- a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js +++ b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js @@ -311,7 +311,6 @@ export const dropdownV2Config = { ], }, label: { value: 'Select' }, - value: { value: '{{"2"}}' }, optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select an option' }, diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js index ac82fcb171..2d8eb7f0a8 100644 --- a/frontend/src/Editor/WidgetManager/configs/form.js +++ b/frontend/src/Editor/WidgetManager/configs/form.js @@ -4,9 +4,40 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 330, + height: 480, }, defaultChildren: [ + { + componentName: 'Text', + slotName: 'header', + layout: { + top: 10, + left: 1, + height: 40, + }, + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor'], + defaultValue: { + text: 'Form title', + textSize: 20, + textColor: '#000', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 12, + left: 32, + height: 36, + }, + properties: ['text'], + defaultValue: { + text: 'Button2', + padding: 'none', + }, + }, { componentName: 'Text', layout: { @@ -225,6 +256,7 @@ export const formConfig = { loadingState: { type: 'toggle', displayName: 'Loading state', + section: 'additionalActions', validation: { schema: { type: 'boolean' }, defaultValue: false, @@ -242,12 +274,64 @@ export const formConfig = { value: true, }, }, + showHeader: { type: 'toggle', displayName: 'Header' }, + showFooter: { type: 'toggle', displayName: 'Footer' }, + visibility: { + type: 'toggle', + displayName: 'Visibility', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + }, + disabledState: { + type: 'toggle', + displayName: 'Disable', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + }, }, events: { onSubmit: { displayName: 'On submit' }, onInvalid: { displayName: 'On invalid' }, }, styles: { + headerBackgroundColor: { + type: 'color', + displayName: 'Header background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + footerBackgroundColor: { + type: 'color', + displayName: 'Footer background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + headerHeight: { + type: 'code', + displayName: 'Header height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, + footerHeight: { + type: 'code', + displayName: 'Footer height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, backgroundColor: { type: 'color', displayName: 'Background color', @@ -274,26 +358,13 @@ export const formConfig = { defaultValue: '#fff', }, }, - visibility: { - type: 'toggle', - displayName: 'Visibility', - validation: { - schema: { type: 'boolean' }, - defaultValue: true, - }, - }, - disabledState: { - type: 'toggle', - displayName: 'Disable', - validation: { - schema: { type: 'boolean' }, - defaultValue: false, - }, - }, }, exposedVariables: { data: {}, isValid: true, + isVisible: true, + isDisabled: false, + isLoading: false, }, actions: [ { @@ -304,6 +375,21 @@ export const formConfig = { handle: 'resetForm', displayName: 'Reset Form', }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'setDisable', + displayName: 'Set Disable', + params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set Loading', + params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }], + }, ], definition: { others: { @@ -317,14 +403,18 @@ export const formConfig = { value: "{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}", }, + showHeader: { value: '{{false}}' }, + showFooter: { value: '{{false}}' }, + visibility: { value: '{{true}}' }, + disabledState: { value: '{{false}}' }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - visibility: { value: '{{true}}' }, - disabledState: { value: '{{false}}' }, + headerHeight: { value: '60px' }, + footerHeight: { value: '60px' }, }, }, }; diff --git a/frontend/src/Editor/WidgetManager/configs/index.js b/frontend/src/Editor/WidgetManager/configs/index.js index d73cd8934f..93e45fd06c 100644 --- a/frontend/src/Editor/WidgetManager/configs/index.js +++ b/frontend/src/Editor/WidgetManager/configs/index.js @@ -2,6 +2,7 @@ import { buttonConfig } from './button'; import { tableConfig } from './table'; import { chartConfig } from './chart'; import { modalConfig } from './modal'; +import { modalV2Config } from './modalV2'; import { formConfig } from './form'; import { textinputConfig } from './textinput'; import { numberinputConfig } from './numberinput'; @@ -59,7 +60,8 @@ export { buttonConfig, tableConfig, chartConfig, - modalConfig, + modalConfig, //!Depreciated + modalV2Config, formConfig, textinputConfig, numberinputConfig, diff --git a/frontend/src/Editor/WidgetManager/configs/listview.js b/frontend/src/Editor/WidgetManager/configs/listview.js index a813bb5a0b..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', @@ -49,7 +50,13 @@ export const listviewConfig = { type: 'code', displayName: 'List data', validation: { - schema: { type: 'array', element: { type: 'object' } }, + schema: { + type: 'union', + schemas: [ + { type: 'array', element: { type: 'object' } }, + { type: 'array', element: { type: 'string' } }, + ], + }, defaultValue: "[{text: 'Sample text 1'}]", }, }, diff --git a/frontend/src/Editor/WidgetManager/configs/modal.js b/frontend/src/Editor/WidgetManager/configs/modal.js index 60f791831e..7716ac8e2c 100644 --- a/frontend/src/Editor/WidgetManager/configs/modal.js +++ b/frontend/src/Editor/WidgetManager/configs/modal.js @@ -1,6 +1,6 @@ export const modalConfig = { - name: 'Modal', - displayName: 'Modal', + name: 'ModalLegacy', + displayName: 'Modal (Legacy)', description: 'Show pop-up windows', component: 'Modal', defaultSize: { diff --git a/frontend/src/Editor/WidgetManager/configs/modalV2.js b/frontend/src/Editor/WidgetManager/configs/modalV2.js new file mode 100644 index 0000000000..e7e96c4398 --- /dev/null +++ b/frontend/src/Editor/WidgetManager/configs/modalV2.js @@ -0,0 +1,277 @@ +export const modalV2Config = { + name: 'Modal', + displayName: 'Modal', + description: 'Show pop-up windows', + component: 'ModalV2', + defaultSize: { + width: 10, + height: 34, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + loadingState: { + type: 'toggle', + displayName: 'Loading state', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, + visibility: { + type: 'toggle', + displayName: 'Modal trigger visibility', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + }, + disabledTrigger: { + type: 'toggle', + displayName: 'Disable modal trigger', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + }, + disabledModal: { + type: 'toggle', + displayName: 'Disable modal window', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, + useDefaultButton: { + type: 'toggle', + displayName: 'Use default trigger button', + validation: { + schema: { + type: 'boolean', + }, + defaultValue: true, + }, + }, + triggerButtonLabel: { + type: 'code', + displayName: 'Trigger button label', + validation: { + schema: { + type: 'string', + }, + defaultValue: 'Launch Modal', + }, + }, + + // Data Accordion + showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' }, + showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' }, + + size: { + type: 'select', + displayName: 'Width', + accordian: 'Data', + options: [ + { name: 'small', value: 'sm' }, + { name: 'medium', value: 'lg' }, + { name: 'large', value: 'xl' }, + { name: 'fullscreen', value: 'fullscreen' }, + ], + validation: { + schema: { type: 'string' }, + defaultValue: 'lg', + }, + }, + modalHeight: { + type: 'numberInput', + displayName: 'Height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 }, + }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' }, + closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' }, + hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' }, + }, + events: { + onOpen: { displayName: 'On open' }, + onClose: { displayName: 'On close' }, + }, + defaultChildren: [ + { + componentName: 'Text', + slotName: 'header', + layout: { + top: 21, + left: 1, + height: 40, + }, + displayName: 'ModalHeaderTitle', + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor'], + defaultValue: { + text: 'Modal title', + textSize: 20, + textColor: '#000', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 24, + left: 22, + height: 36, + }, + displayName: 'ModalFooterCancel', + properties: ['text'], + styles: ['type', 'borderColor', 'padding'], + defaultValue: { + text: 'Button1', + type: 'outline', + borderColor: '#CCD1D5', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 24, + left: 32, + height: 36, + }, + displayName: 'ModalFooterConfirm', + properties: ['text'], + defaultValue: { + text: 'Button2', + padding: 'none', + }, + }, + ], + styles: { + headerBackgroundColor: { + type: 'color', + displayName: 'Header background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + footerBackgroundColor: { + type: 'color', + displayName: 'Footer background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + bodyBackgroundColor: { + type: 'color', + displayName: 'Body background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + triggerButtonBackgroundColor: { + type: 'color', + displayName: 'Trigger button background color', + validation: { + schema: { type: 'string' }, + defaultValue: false, + }, + }, + triggerButtonTextColor: { + type: 'color', + displayName: 'Trigger button text color', + validation: { + schema: { type: 'string' }, + defaultValue: false, + }, + }, + }, + exposedVariables: { + show: false, + isDisabledModal: false, + isDisabledTrigger: false, + isVisible: true, + isLoading: false, + }, + actions: [ + { + handle: 'open', + displayName: 'Open', + }, + { + handle: 'close', + displayName: 'Close', + }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'setDisableTrigger', + displayName: 'Set disable trigger', + params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setDisableModal', + displayName: 'Set disable modal', + params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set loading', + params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + ], + definition: { + others: { + showOnDesktop: { value: '{{true}}' }, + showOnMobile: { value: '{{false}}' }, + }, + properties: { + loadingState: { value: `{{false}}` }, + visibility: { value: '{{true}}' }, + disabledTrigger: { value: '{{false}}' }, + disabledModal: { value: '{{false}}' }, + useDefaultButton: { value: `{{true}}` }, + triggerButtonLabel: { value: `Launch Modal` }, + size: { value: 'lg' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, + hideCloseButton: { value: '{{false}}' }, + hideOnEsc: { value: '{{true}}' }, + closeOnClickingOutside: { value: '{{false}}' }, + modalHeight: { value: 400 }, + headerHeight: { value: 80 }, + footerHeight: { value: 80 }, + }, + events: [], + styles: { + headerBackgroundColor: { value: '#ffffffff' }, + footerBackgroundColor: { value: '#ffffffff' }, + bodyBackgroundColor: { value: '#ffffffff' }, + triggerButtonBackgroundColor: { value: '#4D72FA' }, + triggerButtonTextColor: { value: '#ffffffff' }, + }, + }, +}; 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/Editor/WidgetManager/constants.js b/frontend/src/Editor/WidgetManager/constants.js index 35ea9d5252..8c593455e1 100644 --- a/frontend/src/Editor/WidgetManager/constants.js +++ b/frontend/src/Editor/WidgetManager/constants.js @@ -1 +1,7 @@ -export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy', 'RadioButtonLegacy']; +export const LEGACY_ITEMS = [ + 'ToggleSwitchLegacy', + 'DropdownLegacy', + 'MultiselectLegacy', + 'RadioButtonLegacy', + 'ModalLegacy', +]; diff --git a/frontend/src/Editor/WidgetManager/widgetConfig.js b/frontend/src/Editor/WidgetManager/widgetConfig.js index 30bfdfc7f2..2b03f5c3f5 100644 --- a/frontend/src/Editor/WidgetManager/widgetConfig.js +++ b/frontend/src/Editor/WidgetManager/widgetConfig.js @@ -3,6 +3,7 @@ import { tableConfig, chartConfig, modalConfig, + modalV2Config, formConfig, textinputConfig, numberinputConfig, @@ -62,6 +63,7 @@ export const widgets = [ buttonConfig, chartConfig, modalConfig, + modalV2Config, formConfig, textinputConfig, numberinputConfig, diff --git a/frontend/src/TooljetDatabase/Filter/index.jsx b/frontend/src/TooljetDatabase/Filter/index.jsx index 84b0110fe4..6c8f7c811d 100644 --- a/frontend/src/TooljetDatabase/Filter/index.jsx +++ b/frontend/src/TooljetDatabase/Filter/index.jsx @@ -251,11 +251,7 @@ const Filter = ({ } />
- {filterCount > 0 ? ( - {pluralize(validFilterCountRef.current, 'filter')} - ) : ( -
  Filter
- )} + {filterCount > 0 ? {pluralize(filterCount, 'filter')} :
  Filter
}
{/* {areFiltersApplied && ( ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')} 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 ({ />