mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Merge branch 'appbuilder/sprint-9' into feat/dropdown-option-sort
This commit is contained in:
commit
fa61418e29
243 changed files with 7362 additions and 1436 deletions
2
.github/workflows/cypress-appbuilder.yml
vendored
2
.github/workflows/cypress-appbuilder.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
4
.github/workflows/cypress-platform.yml
vendored
4
.github/workflows/cypress-platform.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
4
.github/workflows/render-preview-deploy.yml
vendored
4
.github/workflows/render-preview-deploy.yml
vendored
|
|
@ -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')
|
||||
|
|
|
|||
4
.gitmodules
vendored
4
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
|||
40
cypress-tests/cypress/constants/selectors/Plugins.js
Normal file
40
cypress-tests/cypress/constants/selectors/Plugins.js
Normal file
|
|
@ -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"]',
|
||||
};
|
||||
|
|
@ -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"]',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"]`;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"]',
|
||||
};
|
||||
|
|
|
|||
6
cypress-tests/cypress/constants/texts/airTable.js
Normal file
6
cypress-tests/cypress/constants/texts/airTable.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export const airtableText = {
|
||||
airtable: "Airtable",
|
||||
cypressairtable: "cypress-Airtable",
|
||||
ApiKey: "Personal access token",
|
||||
apikeyPlaceholder: "**************",
|
||||
};
|
||||
8
cypress-tests/cypress/constants/texts/amazonAthena.js
Normal file
8
cypress-tests/cypress/constants/texts/amazonAthena.js
Normal file
|
|
@ -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:"**************",
|
||||
};
|
||||
8
cypress-tests/cypress/constants/texts/amazonSes.js
Normal file
8
cypress-tests/cypress/constants/texts/amazonSes.js
Normal file
|
|
@ -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:"**************",
|
||||
};
|
||||
12
cypress-tests/cypress/constants/texts/appwrite.js
Normal file
12
cypress-tests/cypress/constants/texts/appwrite.js
Normal file
|
|
@ -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",
|
||||
};
|
||||
6
cypress-tests/cypress/constants/texts/baseRow.js
Normal file
6
cypress-tests/cypress/constants/texts/baseRow.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export const baseRowText = {
|
||||
baserow: "baserow",
|
||||
cypressBaseRow: "cypress-baserow",
|
||||
lableApiToken: "API token",
|
||||
placeholderApiToken:"**************",
|
||||
};
|
||||
14
cypress-tests/cypress/constants/texts/minio.js
Normal file
14
cypress-tests/cypress/constants/texts/minio.js
Normal file
|
|
@ -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`,
|
||||
};
|
||||
11
cypress-tests/cypress/constants/texts/twilio.js
Normal file
11
cypress-tests/cypress/constants/texts/twilio.js
Normal file
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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]", "");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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 = () => { };
|
||||
export const addListviewToVerifyData = () => {};
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ const WidgetIcon = (props) => {
|
|||
case 'map':
|
||||
return <Map {...props} />;
|
||||
case 'modal':
|
||||
case 'modallegacy':
|
||||
return <Modal {...props} />;
|
||||
case 'multiselect':
|
||||
case 'multiselectv2':
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...this.props} />}></Route>
|
||||
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...mergedProps} />}></Route>
|
||||
<Route path="settings/*" element={<InstanceSettings {...this.props} />}></Route>
|
||||
<Route path="/:workspaceId/settings/*" element={<Settings {...this.props} />}></Route>
|
||||
|
||||
|
|
@ -270,7 +274,7 @@ class AppComponent extends React.Component {
|
|||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
{getDataSourcesRoutes(this.props)}
|
||||
{getDataSourcesRoutes(mergedProps)}
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:id/versions/:versionId/:pageHandle?"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ import { shallow } from 'zustand/shallow';
|
|||
import './configHandle.scss';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { findHighestLevelofSelection } from '../Grid/gridUtils';
|
||||
import SolidIcon from '@/_ui/Icon/solidIcons/index';
|
||||
|
||||
const CONFIG_HANDLE_HEIGHT = 20;
|
||||
const BUFFER_HEIGHT = 1;
|
||||
|
||||
export const ConfigHandle = ({
|
||||
id,
|
||||
position,
|
||||
widgetTop,
|
||||
widgetHeight,
|
||||
setSelectedComponentAsModal = () => 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 (
|
||||
<div
|
||||
className={`config-handle ${customClassName}`}
|
||||
widget-id={id}
|
||||
style={{
|
||||
top: position === 'top' ? '-20px' : widgetTop + widgetHeight - (widgetTop < 10 ? 15 : 10),
|
||||
visibility:
|
||||
showHandle && (!isMultipleComponentsSelected || (componentType === 'Modal' && isModalOpen))
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
top:
|
||||
componentType === 'Modal' && isModalOpen
|
||||
? '0px'
|
||||
: position === 'top'
|
||||
? '-20px'
|
||||
: `${height - (CONFIG_HANDLE_HEIGHT + BUFFER_HEIGHT)}px`,
|
||||
visibility: _showHandle ? 'visible' : 'hidden',
|
||||
left: '-1px',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
|
|
@ -51,7 +74,10 @@ export const ConfigHandle = ({
|
|||
>
|
||||
<span
|
||||
style={{
|
||||
background: componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
background:
|
||||
visibility === false ? '#c6cad0' : componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
border: position === 'bottom' ? '1px solid white' : 'none',
|
||||
color: visibility === false && 'var(--text-placeholder)',
|
||||
}}
|
||||
className="badge handle-content"
|
||||
>
|
||||
|
|
@ -65,17 +91,30 @@ export const ConfigHandle = ({
|
|||
data-cy={`${componentName?.toLowerCase()}-config-handle`}
|
||||
className="text-truncate"
|
||||
>
|
||||
<img
|
||||
style={{ cursor: 'pointer', marginRight: '5px', verticalAlign: 'middle' }}
|
||||
src="assets/images/icons/settings.svg"
|
||||
width="12"
|
||||
height="12"
|
||||
draggable="false"
|
||||
/>
|
||||
{/* Settings Icon */}
|
||||
<span style={{ cursor: 'pointer', marginRight: '5px' }}>
|
||||
<SolidIcon
|
||||
name="settings"
|
||||
width="12"
|
||||
height="12"
|
||||
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
|
||||
/>
|
||||
</span>
|
||||
<span>{componentName}</span>
|
||||
{/* Divider */}
|
||||
<hr
|
||||
style={{
|
||||
marginLeft: '10px',
|
||||
height: '12px',
|
||||
width: '2px',
|
||||
backgroundColor: visibility === false ? 'var(--text-placeholder)' : '#fff',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Delete Button */}
|
||||
{!isMultipleComponentsSelected && !shouldFreeze && (
|
||||
<div className="delete-part">
|
||||
<div>
|
||||
<img
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
src="assets/images/icons/inspect.svg"
|
||||
|
|
@ -87,19 +126,20 @@ export const ConfigHandle = ({
|
|||
data-cy={`${componentName.toLowerCase()}-inspect-button`}
|
||||
className="config-handle-inspect"
|
||||
/>
|
||||
<img
|
||||
<span
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
src="assets/images/icons/trash-light.svg"
|
||||
width="12"
|
||||
role="button"
|
||||
height="12"
|
||||
draggable="false"
|
||||
onClick={() => {
|
||||
deleteComponents([id]);
|
||||
}}
|
||||
data-cy={`${componentName.toLowerCase()}-delete-button`}
|
||||
className="delete-icon"
|
||||
/>
|
||||
>
|
||||
<SolidIcon
|
||||
name="trash"
|
||||
width="12"
|
||||
height="12"
|
||||
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
<div
|
||||
className={cx('container-fluid rm-container p-0', {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,6 @@
|
|||
.target, .nested-target {
|
||||
position: absolute;
|
||||
/* width: 100px;
|
||||
height: 100px; */
|
||||
/* top: 150px;
|
||||
left: 100px; */
|
||||
/* line-height: 100px; */
|
||||
/* text-align: center; */
|
||||
/* background: #ee8; */
|
||||
/* color: #333; */
|
||||
/* font-weight: bold; */
|
||||
box-sizing: border-box;
|
||||
/* transition: transform 0.1s; */
|
||||
/* z-index: 3001; */
|
||||
}
|
||||
|
||||
.target.hovered{
|
||||
|
|
@ -76,43 +65,6 @@
|
|||
background: #8DA4EF !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Hides all the control lines*/
|
||||
/* .moveable-line {
|
||||
color: transparent !important;
|
||||
--moveable-color: transparent !important;
|
||||
}
|
||||
|
||||
.moveable-control {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.target {
|
||||
outline: 1px solid #4af;
|
||||
} */
|
||||
|
||||
|
||||
.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover {
|
||||
outline: 1px solid #4af;
|
||||
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;
|
||||
}
|
||||
|
||||
.active-target, .resizing-target {
|
||||
outline: 1px solid #4af !important;
|
||||
/* z-index: 1000000 !important; */
|
||||
}
|
||||
|
||||
.moveable-control-box:not([data-able-groupable]) .moveable-control-box:not(:hover) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
@ -141,10 +93,6 @@
|
|||
height: 0px !important;
|
||||
}
|
||||
|
||||
.resizing-target * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.moveable-control {
|
||||
width: 8px !important;
|
||||
|
|
@ -186,5 +134,43 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.moveable-guideline {
|
||||
background: #97AEFC !important;
|
||||
opacity: 0.8;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.moveable-guideline.moveable-horizontal {
|
||||
height: 1px !important;
|
||||
width: 100% !important;
|
||||
background: #97AEFC !important;
|
||||
left: 0 !important;
|
||||
|
||||
}
|
||||
|
||||
.moveable-guideline.moveable-vertical {
|
||||
width: 1px !important;
|
||||
height: 100% !important;
|
||||
background: #97AEFC !important;
|
||||
top: 0 !important;
|
||||
|
||||
}
|
||||
|
||||
.moveable-guideline-group {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.dragging-component-canvas {
|
||||
outline: 1px solid var(--border-accent-strong) !important;
|
||||
outline-offset: 0px; /* Creates space between element and outline */
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
.non-dragging-component {
|
||||
outline: 1px dotted var(--border-accent-weak) !important;
|
||||
outline-offset: 0px; /* Creates space between element and outline */
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* */
|
||||
|
|
@ -17,9 +17,15 @@ import {
|
|||
hasParentWithClass,
|
||||
getPositionForGroupDrag,
|
||||
adjustWidth,
|
||||
hideGridLines,
|
||||
showGridLines,
|
||||
handleActivateTargets,
|
||||
handleDeactivateTargets,
|
||||
handleActivateNonDraggingComponents,
|
||||
} from './gridUtils';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import './Grid.css';
|
||||
import { NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
|
||||
|
|
@ -29,6 +35,7 @@ const RESIZABLE_CONFIG = {
|
|||
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
||||
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
||||
};
|
||||
export const GRID_HEIGHT = 10;
|
||||
|
||||
export default function Grid({ gridWidth, currentLayout }) {
|
||||
const lastDraggedEventsRef = useRef(null);
|
||||
|
|
@ -51,6 +58,49 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const canvasWidth = NO_OF_GRIDS * gridWidth;
|
||||
const getHoveredComponentForGrid = useStore((state) => 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
|
|
|||
266
frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
Normal file
266
frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
Normal file
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 = ({
|
|||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: resolvedStyles?.padding == 'none' ? '0px' : '2px', //chart and image has a padding property other than container padding
|
||||
padding: resolvedStyles?.padding == 'none' ? '0px' : `${BOX_PADDING}px`, //chart and image has a padding property other than container padding
|
||||
}}
|
||||
role={'Box'}
|
||||
className={inCanvas ? `_tooljet-${component?.component} _tooljet-${component?.name}` : ''} //required for custom CSS
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const WidgetWrapper = memo(
|
|||
);
|
||||
const layoutData = useStore((state) => 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' && (
|
||||
<ConfigHandle
|
||||
id={id}
|
||||
position={layoutData.top < 15 ? 'bottom' : 'top'}
|
||||
widgetTop={layoutData.top}
|
||||
widgetHeight={layoutData.height}
|
||||
showHandle={isWidgetActive}
|
||||
componentType={componentType}
|
||||
visibility={visibility}
|
||||
subContainerIndex={subContainerIndex}
|
||||
/>
|
||||
)}
|
||||
<RenderWidget
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
export const NO_OF_GRIDS = 43;
|
||||
|
||||
export const GRID_HEIGHT = 10;
|
||||
|
||||
export const CANVAS_WIDTHS = Object.freeze({
|
||||
deviceWindowWidth: 450,
|
||||
leftSideBarWidth: 48,
|
||||
rightSideBarWidth: 300,
|
||||
});
|
||||
|
||||
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container'];
|
||||
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container', 'ModalV2'];
|
||||
|
||||
export const DEFAULT_CANVAS_WIDTH = 1292;
|
||||
|
||||
|
|
@ -15,3 +17,9 @@ export const APP_HEADER_HEIGHT = 47;
|
|||
export const LEFT_SIDEBAR_WIDTH = 348; // exclusive of border
|
||||
|
||||
export const SUBCONTAINER_WIDGETS = ['Container', 'Tabs', 'Listview', 'Kanban', 'Form'];
|
||||
|
||||
export const CONTAINER_FORM_CANVAS_PADDING = 7;
|
||||
|
||||
export const SUBCONTAINER_CANVAS_BORDER_WIDTH = 1;
|
||||
|
||||
export const BOX_PADDING = 2;
|
||||
|
|
|
|||
|
|
@ -89,7 +89,8 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
|
|||
const defaultChildren = deepClone(parentMeta)['defaultChildren'];
|
||||
|
||||
defaultChildren.forEach((child) => {
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<Popover
|
||||
|
|
@ -423,10 +419,12 @@ const PreviewContainer = ({
|
|||
<>
|
||||
{!isPortalOpen && (
|
||||
<Overlay
|
||||
placement="left"
|
||||
placement={previewPlacement || 'left'}
|
||||
{...(previewRef?.current ? { target: previewRef.current } : {})}
|
||||
show={showPreview}
|
||||
rootClose
|
||||
shouldUpdatePosition={true}
|
||||
container={document.body}
|
||||
popperConfig={{
|
||||
modifiers: [
|
||||
{
|
||||
|
|
@ -441,6 +439,7 @@ const PreviewContainer = ({
|
|||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
enabled: true,
|
||||
boundary: 'viewport',
|
||||
altAxis: true,
|
||||
tether: false,
|
||||
|
|
@ -449,10 +448,17 @@ const PreviewContainer = ({
|
|||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [33, 15],
|
||||
offset: [0, 3],
|
||||
},
|
||||
},
|
||||
],
|
||||
onFirstUpdate: (state) => {
|
||||
// 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)}
|
||||
|
|
|
|||
|
|
@ -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 = () => (
|
||||
<div className="search-replace-inputs">
|
||||
<InputComponent
|
||||
leadingIcon="search01"
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && findNext(view)}
|
||||
onFocus={() => setShortcutEnabled(true)}
|
||||
onBlur={() => setShortcutEnabled(false)}
|
||||
placeholder="Find"
|
||||
size="small"
|
||||
value={searchText}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -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 = ({
|
|||
<div
|
||||
ref={currentEditorHeightRef}
|
||||
className={`cm-codehinter ${darkMode && 'cm-codehinter-dark-themed'} ${disabled ? 'disabled-cursor' : ''}`}
|
||||
data-cy={`${cyLabel}-input-field`}
|
||||
data-cy={`${cyLabel.replace(/_/g, '-')}-input-field`}
|
||||
>
|
||||
{/* sticky element to position the preview box correctly on top without flowing out of container */}
|
||||
<div
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
ref={previewRef}
|
||||
></div>
|
||||
{usePortalEditor && (
|
||||
<CodeHinter.PopupIcon
|
||||
callback={handleTogglePopupExapand}
|
||||
|
|
@ -372,39 +390,55 @@ const EditorInput = ({
|
|||
callgpt={null}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<CodeMirror
|
||||
value={currentValue}
|
||||
placeholder={placeholder}
|
||||
height={isInsideQueryPane ? '100%' : showLineNumbers ? '400px' : '100%'}
|
||||
width="100%"
|
||||
extensions={[
|
||||
javascript({ jsx: lang === 'jsx' }),
|
||||
autoCompleteConfig,
|
||||
keymap.of([...customKeyMaps]),
|
||||
customTabKeymap,
|
||||
]}
|
||||
onChange={(val) => {
|
||||
setFirstTimeFocus(false);
|
||||
handleOnChange(val);
|
||||
onInputChange && onInputChange(val);
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
basicSetup={{
|
||||
lineNumbers: showLineNumbers,
|
||||
syntaxHighlighting: true,
|
||||
bracketMatching: true,
|
||||
foldGutter: false,
|
||||
highlightActiveLine: false,
|
||||
autocompletion: true,
|
||||
completionKeymap: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
onMouseDown={() => handleFocus()}
|
||||
onBlur={() => handleOnBlur()}
|
||||
className={customClassNames}
|
||||
theme={theme}
|
||||
indentWithTab={false}
|
||||
readOnly={disabled}
|
||||
/>
|
||||
className="check-here"
|
||||
ref={previewRef}
|
||||
>
|
||||
<CodeMirror
|
||||
value={currentValue}
|
||||
placeholder={placeholder}
|
||||
height={isInsideQueryPane ? '100%' : showLineNumbers ? '400px' : '100%'}
|
||||
width="100%"
|
||||
extensions={
|
||||
showSuggestions
|
||||
? [
|
||||
javascript({ jsx: lang === 'jsx' }),
|
||||
autoCompleteConfig,
|
||||
keymap.of([...customKeyMaps]),
|
||||
customTabKeymap,
|
||||
]
|
||||
: [javascript({ jsx: lang === 'jsx' })]
|
||||
}
|
||||
onChange={(val) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</CodeHinter.Portal>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<div className={`${errorState && 'tjdb-hinter-error'}`} data-cy={`${cyLabel}-input-field`}>
|
||||
<div className={`${errorState && 'tjdb-hinter-error'} h-100`} data-cy={`${cyLabel}-input-field`}>
|
||||
<CodeMirror
|
||||
value={currentValue}
|
||||
placeholder={placeholder}
|
||||
height={isOpen ? '350px' : '32px'}
|
||||
height={isOpen ? '32px' : '32px'}
|
||||
maxHeight={'350px'}
|
||||
width="100%"
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -644,11 +644,24 @@
|
|||
}
|
||||
|
||||
.cm-searchMatch {
|
||||
background-color: #F9E71A !important;
|
||||
|
||||
.cm-selectionMatch {
|
||||
background-color: #F9E71A !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-searchMatch.cm-searchMatch-selected {
|
||||
background-color: #F28F2D;
|
||||
background-color: #F28F2D !important;
|
||||
}
|
||||
|
||||
|
||||
.cm-custom-singleline-completion-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tjdb-hinter-portal{
|
||||
.cm-theme{
|
||||
height: 100% ;
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ export const BaseLeftSidebar = ({
|
|||
resetUnreadErrorCount,
|
||||
toggleLeftSidebar,
|
||||
isSidebarOpen,
|
||||
isDraggingQueryPane,
|
||||
] = useStore(
|
||||
(state) => [
|
||||
state.isLeftSideBarPinned,
|
||||
|
|
@ -46,6 +47,7 @@ export const BaseLeftSidebar = ({
|
|||
state.debugger.resetUnreadErrorCount,
|
||||
state.toggleLeftSidebar,
|
||||
state.isSidebarOpen,
|
||||
state.queryPanel.isDraggingQueryPane,
|
||||
],
|
||||
shallow
|
||||
);
|
||||
|
|
@ -68,11 +70,15 @@ export const BaseLeftSidebar = ({
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPopoverContentHeight(
|
||||
((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100
|
||||
);
|
||||
if (!isDraggingQueryPane) {
|
||||
setPopoverContentHeight(
|
||||
((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100
|
||||
);
|
||||
} else {
|
||||
setPopoverContentHeight(100);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryPanelHeight]);
|
||||
}, [queryPanelHeight, isDraggingQueryPane]);
|
||||
|
||||
const renderPopoverContent = () => {
|
||||
if (selectedSidebarItem === null || !isSidebarOpen) return null;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<div>
|
||||
<DataSourceIcon source={source} height={16} />{' '}
|
||||
<span data-cy={`ds-${source.name.toLowerCase()}`} className="ms-1 small" style={{ fontSize: '13px' }}>
|
||||
{source.name}
|
||||
{defaultSources[cleanWord(source.name)].name}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -178,6 +178,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
|
|||
}
|
||||
};
|
||||
|
||||
function cleanWord(word) {
|
||||
return word.replace(/default/g, '');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -45,15 +45,15 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
const queryName = selectedQuery?.name ?? '';
|
||||
const sourcecomponentName = selectedDataSource?.kind?.charAt(0).toUpperCase() + selectedDataSource?.kind?.slice(1);
|
||||
|
||||
const ElementToRender = selectedDataSource?.pluginId ? source : allSources[sourcecomponentName];
|
||||
const ElementToRender = selectedDataSource?.plugin_id ? source : allSources[sourcecomponentName];
|
||||
const defaultOptions = useRef({});
|
||||
|
||||
const isFreezed = useStore((state) => state.getShouldFreeze());
|
||||
|
||||
useEffect(() => {
|
||||
setDataSourceMeta(
|
||||
selectedQuery?.pluginId
|
||||
? selectedQuery?.manifestFile?.data?.source
|
||||
selectedQuery?.plugin_id
|
||||
? selectedQuery?.manifest_file?.data?.source
|
||||
: DataSourceTypes.find((source) => source.kind === selectedQuery?.kind)
|
||||
);
|
||||
setSelectedQueryId(selectedQuery?.id);
|
||||
|
|
@ -188,7 +188,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
<ElementToRender
|
||||
renderCopilot={renderCopilot}
|
||||
key={selectedQuery?.id}
|
||||
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
|
||||
pluginSchema={selectedDataSource?.plugin?.operations_file?.data}
|
||||
selectedDataSource={selectedDataSource}
|
||||
options={selectedQuery?.options}
|
||||
optionsChanged={optionsChanged}
|
||||
|
|
@ -281,7 +281,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE;
|
||||
const docLink = isSampleDb
|
||||
? 'https://docs.tooljet.com/docs/data-sources/sample-data-sources'
|
||||
: selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
|
||||
: selectedDataSource?.plugin_id && selectedDataSource.plugin_id.trim() !== ''
|
||||
? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
|
||||
: `https://docs.tooljet.com/docs/data-sources/${selectedDataSource?.kind}`;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
|
|||
const [codeEditorKey, setCodeEditorKey] = useState(uuidv4());
|
||||
const [state, setState] = useState({
|
||||
...defaultValue,
|
||||
[options.transformationLanguage ?? 'javascript']: options?.transformation,
|
||||
...(options?.transformation ? { [options.transformationLanguage ?? 'javascript']: options?.transformation } : {}),
|
||||
...options?.transformations,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -119,7 +120,6 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
|
|||
useEffect(() => {
|
||||
if (lang !== (options.transformationLanguage ?? 'javascript')) {
|
||||
changeOption('transformationLanguage', lang);
|
||||
changeOption('transformation', state[lang]);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lang]);
|
||||
|
|
@ -127,20 +127,24 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
|
|||
useEffect(() => {
|
||||
if (prevQueryId.current === queryId) {
|
||||
lang !== (options.transformationLanguage ?? 'javascript') && changeOption('transformationLanguage', lang);
|
||||
setState({ ...state, [lang]: options.transformation ?? state[lang] ?? defaultValue[lang] });
|
||||
setState((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
...(options?.transformation
|
||||
? { [options.transformationLanguage ?? 'javascript']: options?.transformation }
|
||||
: {}),
|
||||
...options?.transformations,
|
||||
};
|
||||
});
|
||||
}
|
||||
prevQueryId.current = queryId;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(options.transformation)]);
|
||||
}, [JSON.stringify(options?.transformation || {}), JSON.stringify(options.transformations)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedQueryId !== queryId) {
|
||||
const nonLangdefaultCode = getNonActiveTransformations(options?.transformationLanguage ?? 'javascript');
|
||||
const finalState = _.merge(
|
||||
{},
|
||||
{ [options?.transformationLanguage ?? lang]: options.transformation ?? defaultValue[lang] },
|
||||
nonLangdefaultCode
|
||||
);
|
||||
const olderTransformation = options?.transformation ? { [lang]: options?.transformation } : {};
|
||||
const finalState = _.merge({}, defaultValue, olderTransformation, options?.transformations);
|
||||
|
||||
setState(finalState);
|
||||
}
|
||||
|
|
@ -206,8 +210,6 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
|
|||
activeKey={lang}
|
||||
onSelect={(value) => {
|
||||
setLang(value);
|
||||
changeOption('transformationLanguage', value);
|
||||
changeOption('transformation', state[value]);
|
||||
}}
|
||||
defaultActiveKey="javascript"
|
||||
>
|
||||
|
|
@ -250,7 +252,7 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
|
|||
height={400}
|
||||
className="query-hinter"
|
||||
onChange={(value) => {
|
||||
changeOption('transformation', value);
|
||||
changeOption('transformations', { ...state, [lang]: value });
|
||||
}}
|
||||
renderCopilot={renderCopilot}
|
||||
componentName={`transformation`}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,31 @@
|
|||
import React from 'react';
|
||||
import OverflowTooltip from '@/_components/OverflowTooltip';
|
||||
|
||||
export const BaseUrl = ({ dataSourceURL, theme }) => {
|
||||
export const BaseUrl = ({ dataSourceURL, theme, className = 'col-auto', style = {} }) => {
|
||||
return (
|
||||
<span
|
||||
className="col-auto"
|
||||
htmlFor=""
|
||||
className={`${className} base-url-container`}
|
||||
style={{
|
||||
padding: '5px',
|
||||
border: theme === 'default' ? '1px solid rgb(217 220 222)' : '1px solid white',
|
||||
borderRightWidth: 0,
|
||||
background: theme === 'default' ? 'rgb(246 247 251)' : '#20211e',
|
||||
color: theme === 'default' ? '#9ca1a6' : '#9e9e9e',
|
||||
height: '32px',
|
||||
borderRadius: '6px 0 0 6px',
|
||||
display: 'flex',
|
||||
transition: 'height 0.2s ease',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{dataSourceURL}
|
||||
<OverflowTooltip
|
||||
text={dataSourceURL}
|
||||
width="559px"
|
||||
whiteSpace="normal"
|
||||
placement="auto"
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
{dataSourceURL}
|
||||
</OverflowTooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default ({
|
|||
return (
|
||||
<>
|
||||
<div className="row-container query-manager-border-color" key={index}>
|
||||
<div className="fields-container mb-1">
|
||||
<div className="fields-container mb-1 restapi-key-value">
|
||||
<div className="field col-4 rounded-start rest-api-codehinter-key-field">
|
||||
<CodeHinter
|
||||
type="basic"
|
||||
|
|
@ -52,7 +52,7 @@ export default ({
|
|||
/>
|
||||
</div>
|
||||
<button
|
||||
className={`d-flex justify-content-center align-items-center delete-field-option bg-transparent border-0 rounded-0 border-top border-bottom border-end rounded-end ${
|
||||
className={`d-flex justify-content-center align-items-center delete-field-option bg-transparent border-0 rounded-0 border-top border-bottom border-end rounded-end qm-delete-btn ${
|
||||
darkMode ? 'delete-field-option-dark' : ''
|
||||
}`}
|
||||
role="button"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ class Restapi extends React.Component {
|
|||
|
||||
this.state = {
|
||||
options,
|
||||
codeHinterHeight: 32, // Default height
|
||||
};
|
||||
this.codeHinterRef = React.createRef();
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
|
@ -40,21 +43,95 @@ class Restapi extends React.Component {
|
|||
},
|
||||
});
|
||||
}
|
||||
// Setup resize observer if it's not already set up
|
||||
if (this.codeHinterRef.current && !this.resizeObserver) {
|
||||
this.setupResizeObserver();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
try {
|
||||
if (isEmpty(this.state.options['headers'])) {
|
||||
this.addNewKeyValuePair('headers');
|
||||
}
|
||||
if (isEmpty(this.state.options['cookies'])) {
|
||||
this.addNewKeyValuePair('cookies');
|
||||
}
|
||||
if (isEmpty(this.state.options['method'])) {
|
||||
changeOption(this, 'method', 'get');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (isEmpty(this.state.options['url_params'])) {
|
||||
this.addNewKeyValuePair('url_params');
|
||||
}
|
||||
}, 1000);
|
||||
setTimeout(() => {
|
||||
if (isEmpty(this.state.options['body'])) {
|
||||
this.addNewKeyValuePair('body');
|
||||
}
|
||||
}, 1000);
|
||||
setTimeout(() => {
|
||||
this.initizalizeRetryNetworkErrorsToggle();
|
||||
}, 1000);
|
||||
|
||||
this.setupResizeObserver();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
setupResizeObserver() {
|
||||
if (!this.codeHinterRef.current) return;
|
||||
|
||||
// Try to find the editor element, checking multiple possible selectors
|
||||
const findEditorElement = () => {
|
||||
const element =
|
||||
this.codeHinterRef.current.querySelector('.cm-editor') ||
|
||||
this.codeHinterRef.current.querySelector('.codehinter-input') ||
|
||||
this.codeHinterRef.current.querySelector('.code-hinter-wrapper');
|
||||
return element;
|
||||
};
|
||||
|
||||
// Initial attempt to find editor
|
||||
let editorElement = findEditorElement();
|
||||
|
||||
// If not found immediately, try again after a short delay
|
||||
if (!editorElement) {
|
||||
setTimeout(() => {
|
||||
editorElement = findEditorElement();
|
||||
if (editorElement) {
|
||||
this.setupObserverForElement(editorElement);
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupObserverForElement(editorElement);
|
||||
}
|
||||
|
||||
setupObserverForElement(element) {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
const height = Math.max(32, Math.min(entry.contentRect.height, 220));
|
||||
if (height !== this.state.codeHinterHeight) {
|
||||
this.setState({ codeHinterHeight: height });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
initizalizeRetryNetworkErrorsToggle = () => {
|
||||
const isRetryNetworkErrorToggleUnused = this.props.options.retry_network_errors === null;
|
||||
if (isRetryNetworkErrorToggleUnused) {
|
||||
|
|
@ -212,13 +289,30 @@ class Restapi extends React.Component {
|
|||
useCustomStyles={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={`field w-100 rest-methods-url`}>
|
||||
<div
|
||||
className={`field rest-methods-url ${dataSourceURL && 'data-source-exists'}`}
|
||||
style={{ width: 'calc(100% - 214px)' }}
|
||||
>
|
||||
<div className="font-weight-medium color-slate12">URL</div>
|
||||
<div className="d-flex">
|
||||
<div className="d-flex h-100 w-100">
|
||||
{dataSourceURL && (
|
||||
<BaseUrl theme={this.props.darkMode ? 'monokai' : 'default'} dataSourceURL={dataSourceURL} />
|
||||
<BaseUrl
|
||||
theme={this.props.darkMode ? 'monokai' : 'default'}
|
||||
dataSourceURL={dataSourceURL}
|
||||
style={{
|
||||
overflowWrap: 'anywhere',
|
||||
maxWidth: '40%',
|
||||
width: 'fit-content',
|
||||
height: `${this.state.codeHinterHeight}px`,
|
||||
minHeight: '32px',
|
||||
maxHeight: '220px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className={`flex-grow-1 rest-api-url-codehinter ${dataSourceURL ? 'url-input-group' : ''}`}>
|
||||
<div
|
||||
ref={this.codeHinterRef}
|
||||
className={` flex-grow-1 rest-api-url-codehinter ${dataSourceURL ? 'url-input-group' : ''}`}
|
||||
>
|
||||
<CodeHinter
|
||||
type="basic"
|
||||
initialValue={options.url}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const BulkUploadPrimaryKey = () => {
|
|||
>
|
||||
<input
|
||||
type="text"
|
||||
value={bulkUpdatePrimaryKey?.primary_key?.join() || ''}
|
||||
value={bulkUpdatePrimaryKey?.primary_key?.join(', ') || ''}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
|
@ -53,7 +53,7 @@ export const BulkUploadPrimaryKey = () => {
|
|||
<div className="field flex-grow-1 minw-400-w-400">
|
||||
<CodeHinter
|
||||
type="basic"
|
||||
initialValue={bulkUpdatePrimaryKey?.rows_update ?? {}}
|
||||
initialValue={`{{${JSON.stringify(bulkUpdatePrimaryKey?.rows_update ?? [])}}}`}
|
||||
className="codehinter-plugins"
|
||||
placeholder="{{ [ { 'column1': 'value', ... } ] }}"
|
||||
onChange={(newValue) => {
|
||||
|
|
|
|||
|
|
@ -214,4 +214,9 @@
|
|||
.input-value-padding {
|
||||
box-sizing: border-box;
|
||||
padding-right: 30px !important;
|
||||
}
|
||||
|
||||
.react-datepicker__navigation{
|
||||
overflow: visible !important;
|
||||
height: inherit !important;
|
||||
}
|
||||
|
|
@ -298,7 +298,7 @@ const DropDownSelect = ({
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`col-auto ${buttonClasses}`} id={popoverBtnId.current}>
|
||||
<div className={`col-auto ${buttonClasses} h-100`} id={popoverBtnId.current}>
|
||||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
|
|
@ -322,6 +322,8 @@ const DropDownSelect = ({
|
|||
},
|
||||
'gap-0',
|
||||
'w-100',
|
||||
'h-100',
|
||||
'align-items-start',
|
||||
'rounded-0',
|
||||
'position-relative',
|
||||
'font-weight-normal',
|
||||
|
|
|
|||
|
|
@ -645,10 +645,9 @@ const JoinOn = ({
|
|||
</div>
|
||||
{index > 0 && (
|
||||
<ButtonSolid
|
||||
customStyles={{ height: '30px' }}
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0 border border-start-0 rounded-end"
|
||||
className="px-1 rounded-0 border border-start-0 rounded-en qm-delete-btn"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
|
|
|
|||
|
|
@ -340,10 +340,7 @@ const JsonBfieldsForSelect = ({ selectedJsonbColumns, handleJSonChange, table })
|
|||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0 border rounded-end"
|
||||
customStyles={{
|
||||
height: '30px',
|
||||
}}
|
||||
className="px-1 rounded-0 border rounded-end qm-delete-btn"
|
||||
onClick={() => handleRemove(colDetails.id, colDetails.name, colDetails.table)}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
|
|
|
|||
|
|
@ -164,10 +164,7 @@ export default function JoinSort({ darkMode }) {
|
|||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0 border rounded-end"
|
||||
customStyles={{
|
||||
height: '30px',
|
||||
}}
|
||||
className="px-1 rounded-0 border rounded-end qm-delete-btn"
|
||||
onClick={() => setJoinOrderByOptions(joinOrderByOptions.filter((opt, idx) => idx !== i))}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
|
|
|
|||
|
|
@ -535,12 +535,11 @@ const RenderFilterSection = ({ darkMode }) => {
|
|||
|
||||
<ButtonSolid
|
||||
customStyles={{
|
||||
height: '30px',
|
||||
maxWidth: '30px',
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0 border rounded-end col-2"
|
||||
className="px-1 rounded-0 border rounded-end col-2 qm-delete-btn"
|
||||
onClick={() => removeFilterConditionEntry(index)}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
|
|
|
|||
|
|
@ -54,10 +54,7 @@ const RenderColumnUI = ({
|
|||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0 border rounded-end"
|
||||
customStyles={{
|
||||
height: '30px',
|
||||
}}
|
||||
className="px-1 rounded-0 border rounded-end qm-delete-btn"
|
||||
onClick={() => removeColumnOptionsPair(id)}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
|
|
|
|||
|
|
@ -117,10 +117,7 @@ const RenderFilterSectionUI = ({
|
|||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0 border rounded-end"
|
||||
customStyles={{
|
||||
height: '30px',
|
||||
}}
|
||||
className="px-1 rounded-0 border rounded-end qm-delete-btn"
|
||||
onClick={() => removeFilterConditionPair(id)}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
|
|
|
|||
|
|
@ -86,10 +86,7 @@ const RenderSortUI = ({
|
|||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0 border rounded-end"
|
||||
customStyles={{
|
||||
height: '30px',
|
||||
}}
|
||||
className="px-1 rounded-0 border rounded-end qm-delete-btn"
|
||||
onClick={() => removeSortConditionPair(id)}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
|
|
|
|||
|
|
@ -677,6 +677,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
}}
|
||||
componentName="TooljetDatabase"
|
||||
delayOnChange={false}
|
||||
className="w-100"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 && <CustomDragLayer size={size} />}
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 1000,
|
||||
left: canvasBounds?.left || 0,
|
||||
top: canvasBounds?.top || 0,
|
||||
height: `${height}px`,
|
||||
width: `${width}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
|
||||
transform: `translate(${x}px, ${y}px)`,
|
||||
background: '#D9E2FC',
|
||||
opacity: '0.7',
|
||||
height: '100%',
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
|
||||
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const SHOW_ADDITIONAL_ACTIONS = [
|
|||
'Button',
|
||||
'RichTextEditor',
|
||||
'Image',
|
||||
'ModalV2',
|
||||
];
|
||||
const PROPERTIES_VS_ACCORDION_TITLE = {
|
||||
Text: 'Data',
|
||||
|
|
@ -34,6 +35,7 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
|
|||
Button: 'Data',
|
||||
Image: 'Data',
|
||||
Container: 'Data',
|
||||
ModalV2: 'Data',
|
||||
};
|
||||
|
||||
export const DefaultComponent = ({ componentMeta, darkMode, ...restProps }) => {
|
||||
|
|
@ -151,7 +153,8 @@ export const baseComponentProperties = (
|
|||
'properties',
|
||||
currentState,
|
||||
allComponents,
|
||||
darkMode
|
||||
darkMode,
|
||||
''
|
||||
)
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 <Accordion items={accordionItems} />;
|
||||
|
|
@ -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')}`,
|
||||
|
|
|
|||
|
|
@ -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 <Accordion items={accordionItems} />;
|
||||
};
|
||||
|
|
@ -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 }) {
|
|||
</div>
|
||||
<div className="field mb-2" data-cy={`input-and-label-column-name`}>
|
||||
<CodeHinter
|
||||
initialValue={isMultiSelect ? `{{${markedAsDefault.includes(item.value)}}}` : item?.default?.value}
|
||||
initialValue={isMultiSelect ? `{{${markedAsDefault.includes(item?.value)}}}` : item?.default?.value}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
|
|
@ -392,7 +390,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
|
|||
<div className="w-100" {...droppableProps} ref={innerRef}>
|
||||
{options?.map((item, index) => {
|
||||
return (
|
||||
<Draggable key={item.value} draggableId={item.value} index={index}>
|
||||
<Draggable key={item?.value} draggableId={item?.value} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
key={index}
|
||||
|
|
@ -412,8 +410,13 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
|
|||
}
|
||||
}}
|
||||
overlay={_renderOverlay(item, index)}
|
||||
onToggle={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
document.activeElement?.blur(); // Manually trigger blur when popover closes
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div key={item.value}>
|
||||
<div key={item?.value}>
|
||||
<ListGroup.Item
|
||||
style={{ marginBottom: '8px', backgroundColor: 'var(--slate3)' }}
|
||||
onMouseEnter={() => setHoveredOptionIndex(index)}
|
||||
|
|
@ -425,7 +428,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
|
|||
<SortableList.DragHandle show />
|
||||
</div>
|
||||
<div className="col text-truncate cursor-pointer" style={{ padding: '0px' }}>
|
||||
{getResolvedValue(item.label)}
|
||||
{getResolvedValue(item?.label)}
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
{index === hoveredOptionIndex && (
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{column.columnType === 'json' && (
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Indent"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={onColumnItemChange}
|
||||
property="jsonIndentation"
|
||||
props={column}
|
||||
component={component}
|
||||
paramMeta={{ type: 'toggle', displayName: 'Indent' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
|
|
|
|||
|
|
@ -122,9 +122,18 @@ export const StylesTabElements = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{['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' && (
|
||||
<div data-cy={`input-and-label-text-color`} className="field px-3">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<List>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={`field ${options.className}`} style={{ marginBottom: '8px' }}>
|
||||
<CodeEditor
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||
import { DefaultComponent } from './Components/DefaultComponent';
|
||||
import { FilePicker } from './Components/FilePicker';
|
||||
import { Modal } from './Components/Modal';
|
||||
import { ModalV2 } from './Components/ModalV2';
|
||||
import { CustomComponent } from './Components/CustomComponent';
|
||||
import { Icon } from './Components/Icon';
|
||||
import useFocus from '@/_hooks/use-focus';
|
||||
|
|
@ -78,6 +79,7 @@ const NEW_REVAMPED_COMPONENTS = [
|
|||
'Icon',
|
||||
'Image',
|
||||
'Container',
|
||||
'ModalV2',
|
||||
];
|
||||
|
||||
export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => {
|
||||
|
|
@ -703,6 +705,9 @@ const GetAccordion = React.memo(
|
|||
case 'FilePicker':
|
||||
return <FilePicker {...restProps} />;
|
||||
|
||||
case 'ModalV2':
|
||||
return <ModalV2 {...restProps} />;
|
||||
|
||||
case 'Modal':
|
||||
return <Modal {...restProps} />;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}}' },
|
||||
|
|
|
|||
|
|
@ -311,7 +311,6 @@ export const dropdownV2Config = {
|
|||
],
|
||||
},
|
||||
label: { value: 'Select' },
|
||||
value: { value: '{{"2"}}' },
|
||||
optionsLoadingState: { value: '{{false}}' },
|
||||
sort: { value: 'asc' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const modalConfig = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
name: 'ModalLegacy',
|
||||
displayName: 'Modal (Legacy)',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'Modal',
|
||||
defaultSize: {
|
||||
|
|
|
|||
277
frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
Normal file
277
frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
Normal file
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -13,6 +13,7 @@ export const tabsConfig = {
|
|||
top: 60,
|
||||
left: 17,
|
||||
height: 100,
|
||||
width: 7,
|
||||
},
|
||||
tab: 0,
|
||||
properties: ['source'],
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue