Merge branch 'release/marketplace_1.3' into feature/restapi-ssl-options

This commit is contained in:
Akshay Sasidharan 2023-08-24 21:50:51 +05:30
commit 7cb2100a2e
2344 changed files with 306393 additions and 14185 deletions

View file

@ -76,4 +76,4 @@ SSO_DISABLE_SIGNUPS=
ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS= ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=
#session expiry in minutes #session expiry in minutes
USER_SESSION_EXPIRY=2880 USER_SESSION_EXPIRY=

View file

@ -10,24 +10,236 @@ env:
PR_NUMBER: ${{ github.event.number }} PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }} BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
cancel-in-progress: true
jobs: jobs:
Run-Cypress: Cypress-App-Builder:
if: ${{ github.event.action == 'labeled' && (github.event.label.name == 'run-cypress' || github.event.label.name == 'run-cypress-app-builder'|| github.event.label.name == 'run-cypress-workspace')}}
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-app-builder' }}
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.3.0
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
- name: Run PosgtreSQL Database Docker Container
run: |
sudo docker network create tooljet
sudo docker run -d --name postgres --network tooljet -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_PORT=5432 -d postgres:13
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Install and build dependencies
run: |
npm cache clean --force
npm install
npm install --prefix server
npm install --prefix frontend
npm run build:plugins
- name: Set up environment variables
run: |
echo "TOOLJET_HOST=http://localhost:8082" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=localhost" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=localhost" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=localhost:3001" >> .env
- name: Set up database
run: |
npm run --prefix server db:create
npm run --prefix server db:reset
npm run --prefix server db:seed
- name: sleep 5
run: sleep 5
- name: Run PostgREST Docker Container
run: |
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
-e PGRST_DB_URI="postgres://postgres:postgres@postgres:5432/tooljet" -e PGRST_DB_ANON_ROLE="postgres" -e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
postgrest/postgrest:v10.1.1.20221215
- name: Run plugins compilation in watch mode
run: cd plugins && npm start &
- name: Run the server
run: cd server && npm run start:dev &
- name: Run the client
run: cd frontend && npm start &
- name: Wait for the server to be ready
run: |
timeout 1500 bash -c '
until curl --silent --fail http://localhost:8082; do
sleep 5
done'
- name: docker logs
run: sudo docker logs postgrest
- name: Create Cypress environment file
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: App builder
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:8082"
config-file: cypress-app-builder.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v3
if: always()
with:
name: screenshots
path: cypress-tests/cypress/screenshots
Cypress-Platform:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-workspace' }}
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.3.0
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
- name: Run PosgtreSQL Database Docker Container
run: |
sudo docker network create tooljet
sudo docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_PORT=5432 -d postgres:13
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Install and build dependencies
run: |
npm cache clean --force
npm install
npm install --prefix server
npm install --prefix frontend
npm run build:plugins
- name: Set up environment variables
run: |
echo "TOOLJET_HOST=http://localhost:8082" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=localhost" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=localhost" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=localhost:3001" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
- name: Set up database
run: |
npm run --prefix server db:create
npm run --prefix server db:reset
npm run --prefix server db:seed
- name: sleep 5
run: sleep 5
- name: Run PostgREST Docker Container
run: |
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
-e PGRST_DB_URI="postgres://postgres:postgres@postgres:5432/tooljet" -e PGRST_DB_ANON_ROLE="postgres" -e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
postgrest/postgrest:v10.1.1.20221215
- name: Run plugins compilation in watch mode
run: cd plugins && npm start &
- name: Run the server
run: cd server && npm run start:dev &
- name: Run the client
run: cd frontend && npm start &
- name: Wait for the server to be ready
run: |
timeout 1500 bash -c '
until curl --silent --fail http://localhost:8082; do
sleep 5
done'
- name: docker logs
run: sudo docker logs postgrest
- name: Create Cypress environment file
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Platform
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:8082"
config-file: cypress-workspace.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v3
if: always()
with:
name: screenshots
path: cypress-tests/cypress/screenshots
Cypress-Marketplace:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-marketplace' }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
- name: Setup Node.js 18.3.0 - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: 18.3.0 node-version: 18.3.0
@ -44,43 +256,14 @@ jobs:
with: with:
name: "cypress.env.json" name: "cypress.env.json"
json: ${{ secrets.CYPRESS_SECRETS }} json: ${{ secrets.CYPRESS_SECRETS }}
dir: './cypress-tests' dir: "./cypress-tests"
- name: Update "database" value in cypress.env.json - name: Marketplace
run: |
# Set the new value for the "database" field
new_database_value="${{ env.PR_NUMBER }}_cypress"
# Read the content of cypress.env.json and update the "database" field using jq
cat ./cypress-tests/cypress.env.json | jq --arg new_value "$new_database_value" '.app_db.database = $new_value' > tmp_cypress.env.json
# Move the updated file back to the original location
mv tmp_cypress.env.json ./cypress-tests/cypress.env.json
- name: Cypress Integration Test
if: ${{ github.event.label.name == 'run-cypress' }}
uses: cypress-io/github-action@v5 uses: cypress-io/github-action@v5
with: with:
working-directory: ./cypress-tests working-directory: ./cypress-tests
config: "baseUrl=https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com" config: "baseUrl=https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
config-file: cypress-run.config.js config-file: cypress-marketplace.config.js
- name: Cypress Integration Test (App Builder)
if: ${{ github.event.label.name == 'run-cypress-app-builder' }}
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
config-file: cypress-app-builder.config.js
- name: Cypress Integration Test (Workspace)
if: ${{ github.event.label.name == 'run-cypress-workspace' }}
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
config-file: cypress-workspace.config.js
- name: Capture Screenshots - name: Capture Screenshots
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
@ -88,5 +271,3 @@ jobs:
with: with:
name: screenshots name: screenshots
path: cypress-tests/cypress/screenshots path: cypress-tests/cypress/screenshots

View file

@ -3,21 +3,18 @@ name: Tooljet release docker images build
on: on:
release: release:
types: [published] types: [published]
branches:
- main
workflow_dispatch: workflow_dispatch:
inputs: inputs:
job-to-run: job-to-run:
description: Enter the job name (tooljet-ce/tooljet-server-ce/try-tooljet) description: Enter the job name (tooljet-ce/tooljet-server-ce/try-tooljet)
options: ['tooljet-ce', 'try-tooljet', 'tooljet-server-ce'] options: ["tooljet-ce", "try-tooljet", "tooljet-server-ce"]
required: true required: true
image: image:
description: 'Enter the latest image tag' description: "Enter the latest image tag"
required: true required: true
jobs: jobs:
build-tooljet-ce-image: build-tooljet-ce-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: "${{ github.event.release }}" if: "${{ github.event.release }}"
@ -71,7 +68,6 @@ jobs:
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
try-tooljet-image-build: try-tooljet-image-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-tooljet-ce-image needs: build-tooljet-ce-image
@ -142,7 +138,6 @@ jobs:
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
building-tooljet-server-ce-image: building-tooljet-server-ce-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: "${{ github.event.release }}" if: "${{ github.event.release }}"
@ -189,15 +184,14 @@ jobs:
- name: Send Slack Notification - name: Send Slack Notification
run: | run: |
if [[ "${{ job.status }}" == "success" ]]; then if [[ "${{ job.status }}" == "success" ]]; then
message="Job '${{ env.JOB_NAME }}' succeeded! tooljet/tooljet-ce:${{ github.event.release.tag_name }}" message="Job '${{ env.JOB_NAME }}' succeeded! tooljet/tooljet-server-ce:${{ github.event.release.tag_name }}"
else else
message="Job '${{ env.JOB_NAME }}' failed! tooljet/tooljet-ce:${{ github.event.release.tag_name }}" message="Job '${{ env.JOB_NAME }}' failed! tooljet/tooljet-server-ce:${{ github.event.release.tag_name }}"
fi fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
#Below code helps to trigger the workflow separately
#Below code helps to trigger the workflow separately
tooljet-ce: tooljet-ce:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -297,14 +291,13 @@ jobs:
- name: Send Slack Notification - name: Send Slack Notification
run: | run: |
if [[ "${{ job.status }}" == "success" ]]; then if [[ "${{ job.status }}" == "success" ]]; then
message="Job '${{ env.JOB_NAME }}' succeeded! tooljet/tooljet-ce:${{ github.event.release.tag_name }}" message="Job '${{ env.JOB_NAME }}' succeeded! tooljet/tooljet-server-ce:${{ github.event.release.tag_name }}"
else else
message="Job '${{ env.JOB_NAME }}' failed! tooljet/tooljet-ce:${{ github.event.release.tag_name }}" message="Job '${{ env.JOB_NAME }}' failed! tooljet/tooljet-server-ce:${{ github.event.release.tag_name }}"
fi fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
try-tooljet: try-tooljet:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.job-to-run == 'try-tooljet' }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.job-to-run == 'try-tooljet' }}

View file

@ -1 +1 @@
2.9.4 2.14.0

View file

@ -76,10 +76,6 @@ module.exports = defineConfig({
experimentalRunAllSpecs: true, experimentalRunAllSpecs: true,
baseUrl: "http://localhost:8082", baseUrl: "http://localhost:8082",
specPattern: [ specPattern: [
"cypress/e2e/editor/app-version/version.cy.js",
"cypress/e2e/exportImport/export.cy.js",
"cypress/e2e/exportImport/import.cy.js",
"cypress/e2e/database/database.cy.js",
"cypress/e2e/editor/widget/*.cy.js", "cypress/e2e/editor/widget/*.cy.js",
"cypress/e2e/editor/multipage/*.cy.js", "cypress/e2e/editor/multipage/*.cy.js",
"cypress/e2e/editor/globalSetingsHappyPath.cy.js", "cypress/e2e/editor/globalSetingsHappyPath.cy.js",

View file

@ -0,0 +1,95 @@
const { defineConfig } = require("cypress");
const { rmdir } = require("fs");
const fs = require("fs");
const XLSX = require("node-xlsx");
const pg = require("pg");
const path = require("path");
const pdf = require("pdf-parse");
module.exports = defineConfig({
execTimeout: 1800000,
defaultCommandTimeout: 30000,
requestTimeout: 10000,
pageLoadTimeout: 20000,
responseTimeout: 10000,
viewportWidth: 1440,
viewportHeight: 960,
chromeWebSecurity: false,
trashAssetsBeforeRuns: true,
e2e: {
setupNodeEvents(on, config) {
on("task", {
readPdf(pathToPdf) {
return new Promise((resolve) => {
const pdfPath = path.resolve(pathToPdf);
let dataBuffer = fs.readFileSync(pdfPath);
pdf(dataBuffer).then(function ({ text }) {
resolve(text);
});
});
},
});
on("task", {
readXlsx(filePath) {
return new Promise((resolve, reject) => {
try {
let dataBuffer = fs.readFileSync(filePath);
const jsonData = XLSX.parse(dataBuffer);
// jsonData= jsonData[0].data
resolve(jsonData[0]["data"].toString());
} catch (e) {
reject(e);
}
});
},
});
on("task", {
deleteFolder(folderName) {
return new Promise((resolve, reject) => {
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
if (err) {
console.error(err);
return reject(err);
}
resolve(null);
});
});
},
});
on("task", {
updateId({ dbconfig, sql }) {
const client = new pg.Pool(dbconfig);
return client.query(sql);
},
});
return require("./cypress/plugins/index.js")(on, config);
},
downloadsFolder: "cypress/downloads",
experimentalRunAllSpecs: true,
experimentalModfyObstructiveThirdPartyCode: true,
experimentalRunAllSpecs: true,
baseUrl: "http://localhost:8082",
specPattern: [
"cypress/e2e/exportImport/export.cy.js",
"cypress/e2e/exportImport/import.cy.js",
"cypress/e2e/database/database.cy.js",
"cypress/e2e/editor/data-source/*.cy.js",
],
numTestsKeptInMemory: 1,
redirectionLimit: 7,
experimentalRunAllSpecs: true,
experimentalMemoryManagement: true,
video: false,
videoUploadOnPasses: false,
retries: {
runMode: 2,
openMode: 0,
},
},
});

View file

@ -79,7 +79,8 @@ module.exports = defineConfig({
"cypress/e2e/selfHost/*.cy.js", "cypress/e2e/selfHost/*.cy.js",
"cypress/e2e/authentication/*.cy.js", "cypress/e2e/authentication/*.cy.js",
"cypress/e2e/workspace/*.cy.js", "cypress/e2e/workspace/*.cy.js",
"cypress/e2e/editor/data-source/*.cy.js", // "cypress/e2e/globalDataSources/*.cy.js",
"cypress/e2e/editor/app-version/version.cy.js"
], ],
numTestsKeptInMemory: 1, numTestsKeptInMemory: 1,
redirectionLimit: 7, redirectionLimit: 7,

View file

@ -211,6 +211,17 @@ export const commonSelectors = {
editFolderOption: (folderName) => { editFolderOption: (folderName) => {
return `[data-cy="${cyParamName(folderName)}-edit-folder-option"]`; return `[data-cy="${cyParamName(folderName)}-edit-folder-option"]`;
}, },
inspectorPinIcon: '[data-cy="null-option-icon"]',
groupInputFieldLabel: '[data-cy="label-group-input-field"]',
documentationLink: '[data-cy="read-documentation-option-button"]',
nameLabel: '[data-cy="name-label"]',
valueLabel: '[data-cy="value-label"]',
valueInputField: '[data-cy="value-input-field"]',
yesButton: '[data-cy="yes-button"]',
pagination: '[data-cy="pagination-section"]',
workspaceConstantsOption: '[data-cy="workspace-constants-list-item"]',
nameErrorText: '[data-cy="name-error-text"]',
valueErrorText: '[data-cy="value-error-text"]',
}; };
export const commonWidgetSelector = { export const commonWidgetSelector = {
@ -314,12 +325,12 @@ export const commonWidgetSelector = {
makePublicAppToggleLabel: '[data-cy="make-public-app-label"]', makePublicAppToggleLabel: '[data-cy="make-public-app-label"]',
shareableAppLink: '[data-cy="shareable-app-link-label"]', shareableAppLink: '[data-cy="shareable-app-link-label"]',
copyAppLinkButton: '[data-cy="copy-app-link-button"]', copyAppLinkButton: '[data-cy="copy-app-link-button"]',
iframeLinkLabel: '[data-cy="iframe-link-label"]', // iframeLinkLabel: '[data-cy="iframe-link-label"]',
ifameLinkCopyButton: '[data-cy="iframe-link-copy-button"]', // ifameLinkCopyButton: '[data-cy="iframe-link-copy-button"]',
}, },
makePublicAppToggle: '[data-cy="make-public-app-toggle"]', makePublicAppToggle: '[data-cy="make-public-app-toggle"]',
appLink: '[data-cy="app-link"]', appLink: '[data-cy="app-link"]',
appNameSlugInput: '[data-cy="app-name-slug-input"]', appNameSlugInput: '[data-cy="app-name-slug-input"]',
iframeLink: '[data-cy="iframe-link"]', // iframeLink: '[data-cy="iframe-link"]',
modalCloseButton: '[data-cy="modal-close-button"]', modalCloseButton: '[data-cy="modal-close-button"]',
}; };

View file

@ -0,0 +1,89 @@
export const dataSourceSelector = {
leftSidebarDatasourceButton: "[data-cy='left-sidebar-database-button']",
labelDataSources: "[data-cy='label-datasources']",
addDatasourceLink: "[data-cy='add-datasource-link']",
allDatasourceLabelAndCount: '[data-rr-ui-event-key="#alldatasources"]',
databaseLabelAndCount: '[data-rr-ui-event-key="#databases"]',
apiLabelAndCount: '[data-rr-ui-event-key="#apis"]',
cloudStorageLabelAndCount: '[data-rr-ui-event-key="#cloudstorage"]',
dataSourceSearchInputField: '[data-cy="datasource-search-input"]',
postgresDataSource: "[data-cy='data-source-postgresql']",
dataSourceNameInputField: '[data-cy="data-source-name-input-filed"]',
labelHost: '[data-cy="label-host"]',
labelPort: '[data-cy="label-port"]',
labelSsl: '[data-cy="label-ssl"]',
labelDbName: '[data-cy="label-database-name"]',
labelUserName: '[data-cy="label-username"]',
labelPassword: '[data-cy="label-password"]',
labelSSLCertificate: '[data-cy="ssl-certificate-dropdown-label"]',
labelIpWhitelist: '[data-cy="white-list-ip-text"]',
buttonCopyIp: '[data-cy="button-copy-ip"]',
linkReadDocumentation: '[data-cy="link-read-documentation"]',
buttonTestConnection: '[data-cy="test-connection-button"]',
connectionFailedText: '[data-cy="test-connection-failed-text"]',
buttonSave: '[data-cy="db-connection-save-button"]',
dangerAlertNotSupportSSL: ".go3958317564",
passwordTextField: '[data-cy="password-text-field"]',
textConnectionVerified: '[data-cy="test-connection-verified-text"]',
datasourceLabelOnList: '[data-cy="datasource-Label"]',
buttonAddNewQueries: '[data-cy="button-add-new-queries"]',
addQueriesCard: '[data-cy="postgresql-add-query-card"]',
headerQueryManager: '[data-cy="header-queries-on-query-manager"]',
labelNoQuery: '[data-cy="no-query-text"]',
createQueryButton: '[data-cy="query-create-and-run-button"]',
querySearchBar: '[data-cy="home-page-search-bar"]',
labelSelectDataSource: '[data-cy="label-select-datasource"]',
queryTabGeneral: '[data-cy="query-tab-general"]',
queryLabelInputField: '[data-cy="query-rename-input"]',
queryPreviewButton: '[data-cy="query-preview-button"]',
queryCreateAndRunButton: '[data-cy="query-run-button"]',
queryCreateDropdown: '[data-cy="query-create-dropdown"]',
queryCreateAndRunOption: '[data-cy="query-create-and-run-option"]',
queryCreateOption: '[data-cy="query-create-option"]',
queryInputField: '[data-cy="query-input-field"]',
labelTransformation: '[data-cy="label-query-transformation"]',
toggleTransformation: '[data-cy="transformation-toggle-switch"]',
inputFieldTransformation: '[data-cy="transformation-input-input-field"]',
headerQueryPreview: ".py-2",
previewTabJson: '[data-cy="preview-tab-json"]',
previewTabRaw: '[data-cy="preview-tab-raw"]',
operationsDropDownLabel: '[data-cy="operation-dropdown-label"]',
labelTableNameInputField: '[data-cy="label-table"]',
labelPrimaryKeyColoumn: '[data-cy="label-primary-key-column"]',
labelRecordsToUpdate: '[data-cy="label-records-to-update"]',
queryTabAdvanced: '[data-cy="query-tab-advanced"]',
labelRunQueryOnPageLoad: '[data-cy="run-on-app-load-toggle-label"]',
labelRequestConfirmationOnRun:
'[data-cy="confirmation-before-run-toggle-label"]',
labelShowNotification: '[data-cy="notification-on-success-toggle-label"]',
toggleNotification: '[data-cy="notification-on-success-toggle-switch"]',
labelSuccessMessageInput: '[data-cy="label-success-message-input"]',
notificationDurationInput: '[data-cy="label-notification-duration-input"]',
addEventHandler: '[data-cy="add-event-handler"]',
noEventHandlerMessage: '[data-cy="no-event-handler-message"]',
postgresqlQueryRunButton: '[data-cy="postgresql1-query-run-button"]',
psqlQueryLabel: '[data-cy="postgresql1-query-label"]',
psqlQueryDeleteButton: '[data-cy="postgresql1-query-delete-button"]',
deleteModalMessage: '[data-cy="modal-message"]',
deleteModalCancelButton: '[data-cy="modal-cancel-button"]',
deleteModalConfirmButton: '[data-cy="modal-confirm-button"]',
querySelectDropdown: "[data-cy='query-select-dropdown']",
opetionQuerySave: "[data-cy='query-save-option']",
dataExistanceQuery: '[data-cy="existance_of_table-query-label"]',
tableNameInputField: '[data-cy="table-input-field"]',
primaryKeyColoumnInputField: '[data-cy="primary_key_column-input-field"]',
recordsInputField: '[data-cy="records-input-field"]',
eventQuerySelectionField: '[data-cy="query-selection-field"]',
connectionAlertText: '[data-cy="connection-alert-text"]',
};

View file

@ -64,4 +64,7 @@ export const groupsSelector = {
groupPageTitle: (groupname) => { groupPageTitle: (groupname) => {
return `[data-cy="${cyParamName(groupname)}-title"]`; return `[data-cy="${cyParamName(groupname)}-title"]`;
}, },
userRow: (email) => {
return `[data-cy="${cyParamName(email)}-user-row"]`
}
}; };

View file

@ -0,0 +1,30 @@
export const cyParamName = (paramName = "") => {
return paramName.toLowerCase().replace(/\s+/g, "-");
};
export const workspaceConstantsSelectors = {
workspaceConstantsHelperText: '[data-cy="workspace-constant-helper-text"]',
emptyStateImage: '[data-cy="empty-state-image"]',
emptyStateHeader: '[data-cy="empty-state-header"]',
emptyStateText: '[data-cy="empty-state-text"]',
addNewConstantButton: '[data-cy="add-new-constant-button"]',
contantFormTitle: '[data-cy="constant-form-title"]',
addConstantButton: '[data-cy="add-constant-button"]',
envName: '[data-cy="env-name"]',
constantsTableNameHeader: '[data-cy="workspace-variable-table-name-header"]',
constantsTableValueHeader:
'[data-cy="workspace-variable-table-value-header"]',
constantName: (constName) => {
return `[data-cy="${cyParamName(constName)}-workspace-constant-name"]`;
},
constantValue: (constName) => {
return `[data-cy="${cyParamName(constName)}-workspace-constant-value"]`;
},
constEditButton: (constName) => {
return `[data-cy="${cyParamName(constName)}-edit-button"]`;
},
constDeleteButton: (constName) => {
return `[data-cy="${cyParamName(constName)}-delete-button"]`;
},
};

View file

@ -0,0 +1,6 @@
export const azureBlobStorageText = {
azureBlobStorage: "Azure Blob Storage",
connectionStringPlaceholder: "Enter connection string",
unableExtractAccountNameText:
"Unable to extract accountName with provided information.",
};

View file

@ -45,7 +45,7 @@ export const commonText = {
createFolderButton: "Create folder", createFolderButton: "Create folder",
editFolderOption: "Edit folder", editFolderOption: "Edit folder",
deleteFolderOption: "Delete folder", deleteFolderOption: "Delete folder",
updateFolderTitle: "Update folder", updateFolderTitle: "Edit folder",
updateFolderButton: "Update folder", updateFolderButton: "Update folder",
folderDeleteModalMessage: (folderName) => { folderDeleteModalMessage: (folderName) => {
`Are you sure you want to delete the folder ${folderName}? Apps within the folder will not be deleted.`; `Are you sure you want to delete the folder ${folderName}? Apps within the folder will not be deleted.`;
@ -167,9 +167,13 @@ export const commonText = {
makePublicAppToggleLabel: "Make application public?", makePublicAppToggleLabel: "Make application public?",
shareableAppLink: "Get shareable link for this application", shareableAppLink: "Get shareable link for this application",
copyAppLinkButton: "copy", copyAppLinkButton: "copy",
iframeLinkLabel: "Get embeddable link for this application", // iframeLinkLabel: "Get embeddable link for this application",
ifameLinkCopyButton: "copy", // ifameLinkCopyButton: "copy",
}, },
groupInputFieldLabel: "Select Group",
documentationLink: "Read Documentation",
constantsNameError: "Constant name should start with a letter or underscore and can only contain letters, numbers and underscores",
constantsValueError: "Value should be less than 10000 characters and cannot be empty"
}; };
export const commonWidgetText = { export const commonWidgetText = {
@ -198,7 +202,7 @@ export const commonWidgetText = {
codeMirrorInputTrue: codeMirrorInputLabel(true), codeMirrorInputTrue: codeMirrorInputLabel(true),
codeMirrorInputFalse: codeMirrorInputLabel("false"), codeMirrorInputFalse: codeMirrorInputLabel("false"),
addEventHandlerLink: "+ Add event handler", addEventHandlerLink: "Add handler",
inspectorComponentLabel: "components", inspectorComponentLabel: "components",
componentValueLabel: "Value", componentValueLabel: "Value",
labelDefaultValue: "Default Value", labelDefaultValue: "Default Value",

View file

@ -0,0 +1,71 @@
export const dataSourceText = {
labelDataSources: "Datasources",
labelAddDataSource: "+ add data source",
allDataSources: "All Datasources (41)",
allDatabase: "Databases (17)",
allApis: "APIs (20)",
allCloudStorage: "Cloud Storage (4)",
postgreSQL: "PostgreSQL",
labelHost: "Host",
labelPort: "Port",
labelSSL: "SSL",
labelDbName: "Database Name",
labelUserName: "Username",
labelPassword: "Password",
label: "Encrypted",
sslCertificate: "SSL Certificate",
whiteListIpText:
"Please white-list our IP address if the data source is not publicly accessible",
textCopy: "Copy",
readDocumentation: "Read documentation",
couldNotConnect: "could not connect",
buttonTextSave: "Save",
serverNotSuppotSsl: "The server does not support SSL connections",
psqlName: "cypress-postgresql",
labelConnectionVerified: "connection verified",
toastDSAdded: "Datasource Added",
placeholderNameOfDB: "Name of the database",
placeholderEnterHost: "Enter host",
placeholderEnterPort: "Enter port",
placeholderEnterUserName: "Enter username",
headerQueries: "Queries",
headerSelectDatasource: "Select Datasource",
noQueryText: "You haven't created queries yet.",
buttonLabelCreateQuery: "Create query",
tabGeneral: "General",
firstQueryName: "postgresql1",
buttonLabelPreview: "Preview",
buttonLabelCreateAndRun: "Create & Run",
buttonLabelCreate: "Create",
queryModeSql: "SQL mode",
queryModeGui: "GUI mode",
headerTransformations: "Enable Transformations",
json: "JSON",
raw: "Raw",
labelOperation: "Operation",
labelTable: "Table",
labelPrimaryKeyColumn: "Primary key column",
labelRecordsToUpdate: "Records to update",
toggleLabelRunOnPageLoad: "Run this query on application load?",
toggleLabelconfirmation: "Request confirmation before running query?",
toggleLabelShowNotification: "Show notification on success?",
labelSuccessMessage: "Success Message",
labelNotificatioDuration: "Notification duration (s)",
dialogueTextDelete: "Do you really want to delete this query?",
cancel: "Cancel",
yes: "Yes",
guiOptionBulkUpdate: "Bulk update using primary key",
buttonTextTestConnection: "Test Connection",
tabAdvanced: "Advanced",
labelNoEventhandler: "This query doesn't have any event handlers",
};

View file

@ -15,7 +15,7 @@ export const multipageText = {
optionEventHandler: "Event Handlers", optionEventHandler: "Event Handlers",
eventModalTitle: "Page Events", eventModalTitle: "Page Events",
labelEvents: "Events", labelEvents: "Events",
addEventHandlerLink: "+ Add event handler", addEventHandlerLink: "Add handler",
noEventHandlerInfo: "This page doesn't have any event handlers", noEventHandlerInfo: "This page doesn't have any event handlers",
optionDeletePage: "Delete Page", optionDeletePage: "Delete Page",

View file

@ -2,10 +2,10 @@ export const postgreSqlText = {
labelDataSources: "Datasources", labelDataSources: "Datasources",
labelAddDataSource: "+ add data source", labelAddDataSource: "+ add data source",
allDataSources: "All Datasources (42)", allDataSources: "All Datasources (43)",
allDatabase: "Databases (19)", allDatabase: "Databases (19)",
allApis: "APIs (20)", allApis: "APIs (20)",
allCloudStorage: "Cloud Storage (3)", allCloudStorage: "Cloud Storage (4)",
postgreSQL: "PostgreSQL", postgreSQL: "PostgreSQL",
labelHost: "Host", labelHost: "Host",

View file

@ -10,7 +10,7 @@ export const deleteVersionText = {
deleteModalText: (text) => { deleteModalText: (text) => {
return `Are you sure you want to delete this version - ${cyParamName( return `Are you sure you want to delete this version - ${cyParamName(
text text
)}`; )}?`;
}, },
deleteToastMessage: (version) => { deleteToastMessage: (version) => {
return `Version - ${cyParamName(version)} Deleted`; return `Version - ${cyParamName(version)} Deleted`;

View file

@ -0,0 +1,10 @@
export const workspaceConstantsText = {
workspaceConstantsHelperText:
"To resolve a Workspace constant use {{constants.access_token}}",
emptyStateHeader: "No Workspace constants yet",
emptyStateText:
"Use Workspace constants seamlessly in both the app builder and global data source connections across ToolJet.",
addNewConstantButton: "Create new constant",
addConstatntText: "Add new constant in production ",
constantCreatedToast: "Constant has been created",
};

View file

@ -65,7 +65,7 @@ describe("App Version Functionality", () => {
it("Verify all functionality for the app version", () => { it("Verify all functionality for the app version", () => {
navigateToAppEditor(data.appName); navigateToAppEditor(data.appName);
cy.get('[data-cy="widget-list-box-table"]').should("be.visible"); cy.get('[data-cy="widget-list-box-table"]').should("be.visible");
cy.get(".driver-close-btn").click(); cy.skipEditorPopover();
cy.dragAndDropWidget("Toggle Switch", 50, 50); cy.dragAndDropWidget("Toggle Switch", 50, 50);
verifyComponent("toggleswitch1"); verifyComponent("toggleswitch1");

View file

@ -0,0 +1,158 @@
import { fake } from "Fixtures/fake";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { azureBlobStorageText } from "Texts/azureBlobStorage";
import { mongoDbText } from "Texts/mongoDb";
import { commonSelectors } from "Selectors/common";
import {
fillDataSourceTextField,
selectDataSource,
} from "Support/utils/postgreSql";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { closeDSModal, deleteDatasource } from "Support/utils/dataSource";
const data = {};
data.lastName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.customText = fake.randomSentence;
describe("Data source Azure Blob Storage", () => {
beforeEach(() => {
cy.appUILogin();
cy.intercept("GET", "/api/v2/data_sources");
});
it("Should verify elements on Azure Blob Storage connection form", () => {
cy.get(commonSelectors.globalDataSourceIcon).click();
closeDSModal();
cy.get(commonSelectors.addNewDataSourceButton)
.verifyVisibleElement("have.text", commonText.addNewDataSourceButton)
.click();
cy.get(postgreSqlSelector.allDatasourceLabelAndCount).should(
"have.text",
postgreSqlText.allDataSources
);
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
);
cy.get(postgreSqlSelector.dataSourceSearchInputField).type(
azureBlobStorageText.azureBlobStorage
);
cy.get("[data-cy*='data-source-']")
.eq(1)
.should("contain", azureBlobStorageText.azureBlobStorage);
cy.get('[data-cy="data-source-azure blob storage"]').click();
cy.get(postgreSqlSelector.dataSourceNameInputField).should(
"have.value",
azureBlobStorageText.azureBlobStorage
);
cy.get('[data-cy="label-connection-string"]').verifyVisibleElement(
"have.text",
mongoDbText.labelConnectionString
);
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
"have.text",
postgreSqlText.whiteListIpText
);
cy.get(postgreSqlSelector.buttonCopyIp).verifyVisibleElement(
"have.text",
postgreSqlText.textCopy
);
cy.get(postgreSqlSelector.linkReadDocumentation).verifyVisibleElement(
"have.text",
postgreSqlText.readDocumentation
);
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
);
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Cannot read properties of undefined (reading 'startsWith')"
);
});
it("Should verify the functionality of Azure Blob Storage connection form.", () => {
selectDataSource(azureBlobStorageText.azureBlobStorage);
cy.clearAndType(
'[data-cy="data-source-name-input-filed"]',
`cypress-${data.lastName}-azure-blob-storage`
);
fillDataSourceTextField(
mongoDbText.labelConnectionString,
azureBlobStorageText.connectionStringPlaceholder,
data.customText,
"contain",
{ parseSpecialCharSequences: false, delay: 0 }
);
cy.get(postgreSqlSelector.buttonTestConnection)
.verifyVisibleElement(
"have.text",
postgreSqlText.buttonTextTestConnection
)
.click();
cy.get(postgreSqlSelector.connectionFailedText).verifyVisibleElement(
"have.text",
postgreSqlText.couldNotConnect
);
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
azureBlobStorageText.unableExtractAccountNameText
);
fillDataSourceTextField(
mongoDbText.labelConnectionString,
azureBlobStorageText.connectionStringPlaceholder,
Cypress.env("azure_blob_storage_connection_string"),
"contain",
{ parseSpecialCharSequences: false, delay: 0 }
);
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.toastDSAdded
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
`[data-cy="cypress-${data.lastName}-azure-blob-storage-button"]`
).verifyVisibleElement(
"have.text",
`cypress-${data.lastName}-azure-blob-storage`
);
deleteDatasource(`cypress-${data.lastName}-azure-blob-storage`);
});
});

View file

@ -173,6 +173,12 @@ describe("Data sources MySql", () => {
verifyCouldnotConnectWithAlert( verifyCouldnotConnectWithAlert(
"ER_ACCESS_DENIED_ERROR: Access denied for user 'root'@'103.171.99.42' (using password: YES)" "ER_ACCESS_DENIED_ERROR: Access denied for user 'root'@'103.171.99.42' (using password: YES)"
); );
cy.get('[data-cy="-toggle-input"]').then(($el) => {
if ($el.is(":checked")) {
cy.get('[data-cy="-toggle-input"]').uncheck();
}
});
cy.get(postgreSqlSelector.passwordTextField).should("be.visible");
cy.get(postgreSqlSelector.passwordTextField).type( cy.get(postgreSqlSelector.passwordTextField).type(
`{selectAll}{backspace}${Cypress.env("mysql_password")}`, `{selectAll}{backspace}${Cypress.env("mysql_password")}`,
{ log: false } { log: false }

View file

@ -25,6 +25,7 @@ describe("Data sources", () => {
it("Should verify elements on connection form", () => { it("Should verify elements on connection form", () => {
cy.get(commonSelectors.globalDataSourceIcon).click(); cy.get(commonSelectors.globalDataSourceIcon).click();
cy.wait(1000);
cy.get(commonSelectors.addNewDataSourceButton) cy.get(commonSelectors.addNewDataSourceButton)
.verifyVisibleElement("have.text", commonText.addNewDataSourceButton) .verifyVisibleElement("have.text", commonText.addNewDataSourceButton)
.click(); .click();
@ -134,7 +135,11 @@ describe("Data sources", () => {
postgreSqlText.placeholderEnterPort, postgreSqlText.placeholderEnterPort,
"5432" "5432"
); );
cy.get('[data-cy="-toggle-input"]').then(($el) => {
if ($el.is(":checked")) {
cy.get('[data-cy="-toggle-input"]').uncheck(); cy.get('[data-cy="-toggle-input"]').uncheck();
}
});
fillDataSourceTextField( fillDataSourceTextField(
postgreSqlText.labelDbName, postgreSqlText.labelDbName,
postgreSqlText.placeholderNameOfDB, postgreSqlText.placeholderNameOfDB,

View file

@ -118,7 +118,7 @@ describe("Data sources", () => {
); );
}); });
it("Should verify the functionality of PostgreSQL connection form.", () => { it.skip("Should verify the functionality of Snowflake connection form.", () => {
selectDataSource("Snowflake"); selectDataSource("Snowflake");
cy.clearAndType( cy.clearAndType(

View file

@ -46,10 +46,6 @@ describe("Editor- Global Settings", () => {
"have.text", "have.text",
"Max width of canvas" "Max width of canvas"
); );
cy.get('[data-cy="label-max-canvas-height"]').verifyVisibleElement(
"have.text",
"Max height of canvas"
);
cy.get('[data-cy="label-bg-canvas"]').verifyVisibleElement( cy.get('[data-cy="label-bg-canvas"]').verifyVisibleElement(
"have.text", "have.text",
"Background color of canvas" "Background color of canvas"

View file

@ -19,18 +19,22 @@ describe("Editor- Inspector", () => {
}); });
it("should verify the values of inspector", () => { it("should verify the values of inspector", () => {
const countGlobal =
Cypress.env("environment") === "Community" ? "4 entries " : "5 entries ";
const countUser =
Cypress.env("environment") === "Community" ? "4 entries " : "5 entries ";
cy.get(commonWidgetSelector.sidebarinspector).click(); cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide"); cy.get(".tooltip-inner").invoke("hide");
verifyNodeData("queries", "Object", "0 entry "); verifyNodeData("queries", "Object", "0 entry ");
verifyNodeData("components", "Object", "0 entry "); verifyNodeData("components", "Object", "0 entry ");
verifyNodeData("globals", "Object", "3 entries "); verifyNodeData("globals", "Object", countGlobal);
verifyNodeData("variables", "Object", "0 entry "); verifyNodeData("variables", "Object", "0 entry ");
verifyNodeData("page", "Object", "4 entries "); verifyNodeData("page", "Object", "4 entries ");
openNode("globals"); openNode("globals");
verifyNodeData("theme", "Object", "1 entry "); verifyNodeData("theme", "Object", "1 entry ");
verifyNodeData("urlparams", "Object", "0 entry "); verifyNodeData("urlparams", "Object", "0 entry ");
verifyNodeData("currentUser", "Object", "4 entries "); verifyNodeData("currentUser", "Object", countUser);
openNode("theme"); openNode("theme");
verifyValue("name", "String", `"light"`); verifyValue("name", "String", `"light"`);
@ -40,10 +44,28 @@ describe("Editor- Inspector", () => {
verifyValue("firstName", "String", `"The"`); verifyValue("firstName", "String", `"The"`);
verifyValue("lastName", "String", `"Developer"`); verifyValue("lastName", "String", `"Developer"`);
verifyNodeData("groups", "Array", "2 items "); verifyNodeData("groups", "Array", "2 items ");
if (Cypress.env("environment") !== "Community") {
cy.get(
'[data-cy="inspector-node-ssouserinfo"] > .node-key'
).verifyVisibleElement("have.text", "ssoUserInfo");
cy.get(
'[data-cy="inspector-node-ssouserinfo"] > .mx-2'
).verifyVisibleElement("have.text", "undefined");
openNode("theme");
openNode("environment");
verifyValue("name", "String", `"development"`);
cy.get('[data-cy="inspector-node-id"] > .node-key').verifyVisibleElement(
"have.text",
"id"
);
}
openNode("mode");
verifyValue("value", "String", `"edit"`);
openNode("groups"); openNode("groups");
verifyValue("0", "String", `"all_users"`); verifyValue("0", "String", `"all_users"`);
verifyValue("1", "String", `"admin"`); verifyValue("1", "String", `"admin"`);
verifyNodeData("constants", "Object", "0 entry ");
openNode("globals"); openNode("globals");
openNode("page"); openNode("page");

View file

@ -193,7 +193,7 @@ describe("Multipage", () => {
multipageText.labelEvents multipageText.labelEvents
); );
cy.get(multipageSelector.addEventHandlerLink).verifyVisibleElement( cy.get(multipageSelector.addEventHandlerLink).verifyVisibleElement(
"have.text", "contain.text",
multipageText.addEventHandlerLink multipageText.addEventHandlerLink
); );
cy.get(multipageSelector.noEventHandlerMessage).verifyVisibleElement( cy.get(multipageSelector.noEventHandlerMessage).verifyVisibleElement(

View file

@ -33,7 +33,7 @@ import {
} from "Support/utils/events"; } from "Support/utils/events";
import { import {
selectQuery, selectQueryFromLandingPage,
deleteQuery, deleteQuery,
query, query,
changeQueryToggles, changeQueryToggles,
@ -66,17 +66,15 @@ describe("RunJS", () => {
cy.createApp(); cy.createApp();
cy.viewport(1800, 1800); cy.viewport(1800, 1800);
cy.dragAndDropWidget("Button"); cy.dragAndDropWidget("Button");
resizeQueryPanel("50"); resizeQueryPanel("80");
}); });
it("should verify basic runjs", () => { it("should verify basic runjs", () => {
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run JavaScript code"); selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField("runjs", "return true"); addInputOnQueryField("runjs", "return true");
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("preview"); query("preview");
verifypreview("raw", "true"); verifypreview("raw", "true");
query("run"); query("run");
@ -92,7 +90,7 @@ describe("RunJS", () => {
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run JavaScript code"); selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField( addInputOnQueryField(
"runjs", "runjs",
`setTimeout(() => { `setTimeout(() => {
@ -101,7 +99,6 @@ describe("RunJS", () => {
}, [0]) ` }, [0]) `
); );
query("run"); query("run");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.get(commonWidgetSelector.sidebarinspector).click(); cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide"); cy.get(".tooltip-inner").invoke("hide");
verifyNodeData("variables", "Object", "1 entry "); verifyNodeData("variables", "Object", "1 entry ");
@ -134,12 +131,6 @@ describe("RunJS", () => {
"actions.showAlert('success', 'alert from runjs');" "actions.showAlert('success', 'alert from runjs');"
); );
query("run"); query("run");
// cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runjs1) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
cy.get(multipageSelector.sidebarPageButton).click(); cy.get(multipageSelector.sidebarPageButton).click();
@ -159,8 +150,6 @@ describe("RunJS", () => {
addInputOnQueryField("runjs", "actions.closeModal('modal1');"); addInputOnQueryField("runjs", "actions.closeModal('modal1');");
query("run"); query("run");
cy.intercept("GET", "api/data_queries?**").as("addQuery");
cy.wait("@addQuery");
cy.wait(200); cy.wait(200);
cy.notVisible('[data-cy="modal-title"]'); cy.notVisible('[data-cy="modal-title"]');
@ -169,7 +158,6 @@ describe("RunJS", () => {
"actions.copyToClipboard('data from runjs');" "actions.copyToClipboard('data from runjs');"
); );
query("run"); query("run");
cy.wait("@addQuery");
cy.window().then((win) => { cy.window().then((win) => {
win.navigator.clipboard.readText().then((text) => { win.navigator.clipboard.readText().then((text) => {
@ -181,7 +169,6 @@ describe("RunJS", () => {
"actions.setLocalStorage('localStorage','data from runjs');" "actions.setLocalStorage('localStorage','data from runjs');"
); );
query("run"); query("run");
cy.wait("@addQuery");
cy.getAllLocalStorage().then((result) => { cy.getAllLocalStorage().then((result) => {
expect(result[Cypress.config().baseUrl].localStorage).to.deep.equal( expect(result[Cypress.config().baseUrl].localStorage).to.deep.equal(
@ -194,8 +181,6 @@ describe("RunJS", () => {
"actions.generateFile('runjscsv', 'csv', [{ name: 'John', email: 'john@tooljet.com' }])" "actions.generateFile('runjscsv', 'csv', [{ name: 'John', email: 'john@tooljet.com' }])"
); );
query("run"); query("run");
cy.wait("@addQuery");
cy.wait(3000);
cy.readFile("cypress/downloads/runjscsv.csv", "utf-8") cy.readFile("cypress/downloads/runjscsv.csv", "utf-8")
.should("contain", "name,email") .should("contain", "name,email")
@ -206,11 +191,9 @@ describe("RunJS", () => {
// "actions.goToApp('111234')" // "actions.goToApp('111234')"
// ); // );
// query("run"); // query("run");
// cy.wait("@addQuery");
addInputOnQueryField("runjs", "actions.logout()"); addInputOnQueryField("runjs", "actions.logout()");
query("run"); query("run");
cy.wait("@addQuery");
cy.wait(3000);
cy.get('[data-cy="sign-in-header"]').should("be.visible"); cy.get('[data-cy="sign-in-header"]').should("be.visible");
}); });
@ -218,99 +201,123 @@ describe("RunJS", () => {
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run JavaScript code"); selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField("runjs", "return [page.handle,page.name,globals]"); addInputOnQueryField("runjs", "return [page.handle,page.name]");
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("preview"); query("preview");
verifypreview( verifypreview("raw", `["home","Home"]`);
"raw",
`["home","Home",{"theme":{"name":"light"},"urlparams":{},"currentUser":{"email":"dev@tooljet.io","firstName":"The","lastName":"Developer","groups":["all_users","admin"]}}]` addInputOnQueryField("runjs", "return globals.theme");
query("preview");
verifypreview("raw", `{"name":"light"}`);
// addInputOnQueryField("runjs", "return globals.currentUser");
// query("preview");
// verifypreview(
// "raw",
// `{"email":"dev@tooljet.io","firstName":"The","lastName":"Developer","groups":["all_users","admin"]}`
// );
addInputOnQueryField("runjs", "return globals.currentUser.email");
query("preview");
verifypreview("raw", `dev@tooljet.io`);
addInputOnQueryField("runjs", "return globals.currentUser.email");
query("preview");
verifypreview("raw", `dev@tooljet.io`);
addInputOnQueryField("runjs", "return globals.currentUser.firstName");
query("preview");
verifypreview("raw", `The`);
addInputOnQueryField("runjs", "return globals.currentUser.lastName");
query("preview");
verifypreview("raw", `Developer`);
addInputOnQueryField("runjs", "return globals.currentUser.groups");
query("preview");
verifypreview("raw", `["all_users","admin"]`);
if (Cypress.env("environment") != "Community") {
addInputOnQueryField("runjs", "return globals.environment.name");
query("preview");
verifypreview("raw", `development`);
addInputOnQueryField(
"runjs",
"return globals.currentUser.ssoUserInfo == undefined"
); );
query("preview");
verifypreview("raw", `true`);
}
addInputOnQueryField("runjs", "return globals.mode");
query("preview");
verifypreview("raw", `{"value":"edit"}`);
addInputOnQueryField("runjs", "return constants");
query("preview");
verifypreview("raw", `{}`);
}); });
it("should verify action by button", () => { it("should verify action by button", () => {
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run JavaScript code"); selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField( addInputOnQueryField(
"runjs", "runjs",
"actions.showAlert('success', 'alert from runjs');" "actions.showAlert('success', 'alert from runjs');"
); );
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("run"); query("run");
openEditorSidebar("button1");
selectEvent("On Click", "Run query", 1); selectEvent("On Click", "Run query", 1);
cy.get('[data-cy="query-selection-field"]').type("runjs1{enter}"); cy.get('[data-cy="query-selection-field"]').type("runjs1{enter}");
cy.get(commonWidgetSelector.draggableWidget("button1")).click(); cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runjs1) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
renameQueryFromEditor("newrunjs"); renameQueryFromEditor("newrunjs");
cy.wait(3000); cy.waitForAutoSave();
cy.get('[data-cy="event-handler"]').click(); cy.get('[data-cy="event-handler"]').click();
cy.get('[data-cy="query-selection-field"]').should("have.text", "newrunjs"); cy.get('[data-cy="query-selection-field"]').should("have.text", "newrunjs");
cy.get(commonWidgetSelector.draggableWidget("button1")).click(); cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (newrunjs) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
}); });
it("should verify runjs toggle options", () => { it("should verify runjs toggle options", () => {
cy.intercept("PATCH", "api/data_queries/**").as("editQuery");
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run JavaScript code"); selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField( addInputOnQueryField(
"runjs", "runjs",
"actions.showAlert('success', 'alert from runjs');" "actions.showAlert('success', 'alert from runjs');"
); );
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
changeQueryToggles("run-on-app-load"); changeQueryToggles("run-on-app-load");
query("save"); cy.wait(`@editQuery`);
cy.waitForAutoSave();
cy.reload(); cy.reload();
cy.wait(3000);
cy.verifyToastMessage( cy.verifyToastMessage(
commonSelectors.toastMessage, commonSelectors.toastMessage,
"Query (runjs1) completed." "alert from runjs",
false
); );
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
changeQueryToggles("confirmation-before-run"); changeQueryToggles("confirmation-before-run");
query("save"); cy.wait(`@editQuery`);
cy.waitForAutoSave();
cy.reload(); cy.reload();
cy.wait(3000);
cy.get('[data-cy="modal-message"]').verifyVisibleElement( cy.get('[data-cy="modal-message"]').verifyVisibleElement(
"have.text", "have.text",
"Do you want to run this query - runjs1?" "Do you want to run this query - runjs1?"
); );
cy.get('[data-cy="modal-confirm-button"]').realClick(); cy.get('[data-cy="modal-confirm-button"]').realClick();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runjs1) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
resizeQueryPanel("80");
changeQueryToggles("notification-on-success"); changeQueryToggles("notification-on-success");
cy.get('[data-cy="success-message-input-field"]').clearAndTypeOnCodeMirror( cy.get('[data-cy="success-message-input-field"]').clearAndTypeOnCodeMirror(
"Success alert" "Success alert"
); );
query("save"); cy.get('[data-cy="runjs-input-field"]').realClick();
cy.wait(1000);
cy.waitForAutoSave();
cy.reload(); cy.reload();
cy.wait(3000);
cy.get('[data-cy="modal-confirm-button"]').realClick(); cy.get('[data-cy="modal-confirm-button"]').realClick();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runjs1) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "Success alert"); cy.verifyToastMessage(commonSelectors.toastMessage, "Success alert");
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
}); });

View file

@ -33,12 +33,12 @@ import {
} from "Support/utils/events"; } from "Support/utils/events";
import { import {
selectQuery, selectQueryFromLandingPage,
deleteQuery,
query, query,
changeQueryToggles, changeQueryToggles,
renameQueryFromEditor, renameQueryFromEditor,
addInputOnQueryField, addInputOnQueryField,
waitForQueryAction,
} from "Support/utils/queries"; } from "Support/utils/queries";
import { import {
@ -66,17 +66,17 @@ describe("runpy", () => {
cy.createApp(); cy.createApp();
cy.viewport(1800, 1800); cy.viewport(1800, 1800);
cy.dragAndDropWidget("Button"); cy.dragAndDropWidget("Button");
resizeQueryPanel("50"); resizeQueryPanel("80");
cy.intercept("PATCH", "api/data_queries/**").as("editQuery");
}); });
it("should verify basic runpy", () => { it("should verify basic runpy", () => {
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run Python code"); selectQueryFromLandingPage("runpy", "Python");
addInputOnQueryField("runpy", "True"); addInputOnQueryField("runpy", "True");
query("create"); cy.waitForAutoSave();
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("preview"); query("preview");
verifypreview("raw", "true"); verifypreview("raw", "true");
query("run"); query("run");
@ -88,18 +88,18 @@ describe("runpy", () => {
verifyValue("rawData", "Boolean", "true"); verifyValue("rawData", "Boolean", "true");
}); });
it.only("should verify actions", () => { it("should verify actions", () => {
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run Python code"); selectQueryFromLandingPage("runpy", "Python");
addInputOnQueryField( addInputOnQueryField(
"runpy", "runpy",
`actions.setVariable('var', 'test') `actions.setVariable('var', 'test')
actions.setPageVariable('pageVar', 'pageTest')` actions.setPageVariable('pageVar', 'pageTest')`
); );
cy.waitForAutoSave();
query("run"); query("run");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.get(commonWidgetSelector.sidebarinspector).click(); cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide"); cy.get(".tooltip-inner").invoke("hide");
verifyNodeData("variables", "Object", "1 entry "); verifyNodeData("variables", "Object", "1 entry ");
@ -130,14 +130,12 @@ actions.unsetPageVariable('pageVar')`
"actions.showAlert('success', 'alert from runpy')" "actions.showAlert('success', 'alert from runpy')"
); );
query("run"); query("run");
// cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.verifyToastMessage( cy.verifyToastMessage(
commonSelectors.toastMessage, commonSelectors.toastMessage,
"Query (runpy1) completed." "alert from runpy",
false
); );
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
cy.get(multipageSelector.sidebarPageButton).click(); cy.get(multipageSelector.sidebarPageButton).click();
addNewPage("test_page"); addNewPage("test_page");
cy.url().should("contain", "/test-page"); cy.url().should("contain", "/test-page");
@ -147,32 +145,25 @@ actions.unsetPageVariable('pageVar')`
cy.url().should("contain", "/home"); cy.url().should("contain", "/home");
cy.get('[data-cy="real-canvas"]').click("topRight", { force: true }); cy.get('[data-cy="real-canvas"]').click("topRight", { force: true });
cy.dragAndDropWidget("Modal"); cy.dragAndDropWidget("Modal", 300, 300);
cy.waitForAutoSave(); cy.waitForAutoSave();
addInputOnQueryField("runpy", "actions.showModal('modal1')"); addInputOnQueryField("runpy", "actions.showModal('modal1')");
query("run"); query("run");
cy.closeToastMessage();
cy.get('[data-cy="modal-title"]').should("be.visible"); cy.get('[data-cy="modal-title"]').should("be.visible");
cy.get('[data-cy="runpy-input-field"]').click({ force: true }); cy.get('[data-cy="runpy-input-field"]').click({ force: true });
addInputOnQueryField("runpy", "actions.closeModal('modal1')"); addInputOnQueryField("runpy", "actions.closeModal('modal1')");
cy.wait(`@editQuery`);
cy.waitForAutoSave();
query("run"); query("run");
cy.intercept("GET", "api/data_queries?**").as("addQuery"); waitForQueryAction("run");
cy.wait("@addQuery");
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runpy1) completed."
);
cy.wait(1000);
cy.notVisible('[data-cy="modal-title"]'); cy.notVisible('[data-cy="modal-title"]');
addInputOnQueryField("runpy", "actions.copyToClipboard('data from runpy')"); addInputOnQueryField("runpy", "actions.copyToClipboard('data from runpy')");
cy.wait(`@editQuery`);
cy.waitForAutoSave();
query("run"); query("run");
cy.wait("@addQuery"); waitForQueryAction("run");
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runpy1) completed."
);
cy.window().then((win) => { cy.window().then((win) => {
win.navigator.clipboard.readText().then((text) => { win.navigator.clipboard.readText().then((text) => {
expect(text).to.eq("data from runpy"); expect(text).to.eq("data from runpy");
@ -182,13 +173,10 @@ actions.unsetPageVariable('pageVar')`
"runpy", "runpy",
"actions.setLocalStorage('localStorage','data from runpy')" "actions.setLocalStorage('localStorage','data from runpy')"
); );
cy.wait(`@editQuery`);
cy.waitForAutoSave();
query("run"); query("run");
cy.wait("@addQuery"); waitForQueryAction("run");
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runpy1) completed."
);
cy.wait(1000);
cy.getAllLocalStorage().then((result) => { cy.getAllLocalStorage().then((result) => {
expect(result[Cypress.config().baseUrl].localStorage).to.deep.equal( expect(result[Cypress.config().baseUrl].localStorage).to.deep.equal(
@ -201,7 +189,7 @@ actions.unsetPageVariable('pageVar')`
// "actions.generateFile('runpycsv', 'csv', [{ 'name': 'John', 'email': 'john@tooljet.com' }])" // "actions.generateFile('runpycsv', 'csv', [{ 'name': 'John', 'email': 'john@tooljet.com' }])"
// ); // );
// query("run"); // query("run");
// cy.wait("@addQuery");
// cy.verifyToastMessage( // cy.verifyToastMessage(
// commonSelectors.toastMessage, // commonSelectors.toastMessage,
// "Query (runpy1) completed." // "Query (runpy1) completed."
@ -218,11 +206,13 @@ actions.unsetPageVariable('pageVar')`
// "actions.goToApp('111234')" // "actions.goToApp('111234')"
// ); // );
// query("run"); // query("run");
// cy.wait("@addQuery");
addInputOnQueryField("runpy", "actions.logout()"); addInputOnQueryField("runpy", "actions.logout()");
cy.wait(`@editQuery`);
cy.wait(200);
cy.waitForAutoSave();
query("run"); query("run");
cy.wait("@addQuery"); waitForQueryAction("run");
cy.wait(3000);
cy.get('[data-cy="sign-in-header"]').should("be.visible"); cy.get('[data-cy="sign-in-header"]').should("be.visible");
}); });
@ -230,48 +220,75 @@ actions.unsetPageVariable('pageVar')`
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run Python code"); selectQueryFromLandingPage("runpy", "Python");
addInputOnQueryField("runpy", "tj_globals"); addInputOnQueryField("runpy", "tj_globals.theme");
query("create"); cy.waitForAutoSave();
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("preview"); query("preview");
verifypreview( verifypreview("raw", `{"name":"light"}`);
"raw",
`{"theme":{"name":"light"},"urlparams":{},"currentUser":{"email":"dev@tooljet.io","firstName":"The","lastName":"Developer","groups":["all_users","admin"]}}` addInputOnQueryField("runpy", "tj_globals.currentUser.email");
query("preview");
verifypreview("raw", `dev@tooljet.io`);
addInputOnQueryField("runpy", "tj_globals.currentUser.email");
query("preview");
verifypreview("raw", `dev@tooljet.io`);
addInputOnQueryField("runpy", "tj_globals.currentUser.firstName");
query("preview");
verifypreview("raw", `The`);
addInputOnQueryField("runpy", "tj_globals.currentUser.lastName");
query("preview");
verifypreview("raw", `Developer`);
addInputOnQueryField("runpy", "tj_globals.currentUser.groups");
query("preview");
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runpy1) completed."
); );
waitForQueryAction("preview");
verifypreview("raw", `["all_users","admin"]`);
if (Cypress.env("environment") != "Community") {
addInputOnQueryField("runpy", "tj_globals.environment.name");
query("preview");
verifypreview("raw", `development`);
// addInputOnQueryField( //WIP
// "runpy",
// "(tj_globals.currentUser.ssoUserInfo == undefined)"
// );
// query("preview");
// verifypreview("raw", `true`);
}
addInputOnQueryField("runpy", "tj_globals.mode.value");
query("preview");
verifypreview("raw", `edit`);
addInputOnQueryField("runpy", "constants");
query("preview");
waitForQueryAction("preview");
verifypreview("raw", `{}`);
}); });
it("should verify action by button", () => { it("should verify action by button", () => {
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run Python code"); selectQueryFromLandingPage("runpy", "Python");
addInputOnQueryField( addInputOnQueryField(
"runpy", "runpy",
"actions.showAlert('success', 'alert from runpy');" "actions.showAlert('success', 'alert from runpy');"
); );
query("create"); cy.waitForAutoSave();
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("run"); query("run");
openEditorSidebar("button1");
selectEvent("On Click", "Run query", 1); selectEvent("On Click", "Run query", 1);
cy.get('[data-cy="query-selection-field"]').type("runpy1{enter}"); cy.get('[data-cy="query-selection-field"]').type("runpy1{enter}");
cy.get(commonWidgetSelector.draggableWidget("button1")).click(); cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runpy1) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
renameQueryFromEditor("newrunpy"); renameQueryFromEditor("newrunpy");
cy.wait(3000);
cy.get('[data-cy="event-handler"]').click(); cy.get('[data-cy="event-handler"]').click();
cy.get('[data-cy="query-selection-field"]').should("have.text", "newrunpy"); cy.get('[data-cy="query-selection-field"]').should("have.text", "newrunpy");
cy.get(commonWidgetSelector.draggableWidget("button1")).click(); cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (newrunpy) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
}); });
@ -279,51 +296,48 @@ actions.unsetPageVariable('pageVar')`
const data = {}; const data = {};
data.customText = randomString(12); data.customText = randomString(12);
selectQuery("Run Python code"); selectQueryFromLandingPage("runpy", "Python");
cy.waitForAutoSave();
addInputOnQueryField( addInputOnQueryField(
"runpy", "runpy",
"actions.showAlert('success', 'alert from runpy');" "actions.showAlert('success', 'alert from runpy');"
); );
query("create"); cy.wait("@editQuery");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added"); cy.wait(200);
cy.waitForAutoSave();
changeQueryToggles("run-on-app-load"); changeQueryToggles("run-on-app-load");
query("save"); cy.wait("@editQuery");
cy.waitForAutoSave();
cy.reload(); cy.reload();
cy.wait(3000);
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runpy1) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
changeQueryToggles("confirmation-before-run"); changeQueryToggles("confirmation-before-run");
query("save"); cy.wait("@editQuery");
cy.wait(200);
cy.waitForAutoSave();
cy.reload(); cy.reload();
cy.wait(3000);
cy.get('[data-cy="modal-message"]').verifyVisibleElement( cy.get('[data-cy="modal-message"]').verifyVisibleElement(
"have.text", "have.text",
"Do you want to run this query - runpy1?" "Do you want to run this query - runpy1?"
); );
cy.get('[data-cy="modal-confirm-button"]').realClick(); cy.get('[data-cy="modal-confirm-button"]').realClick();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runpy1) completed."
);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy"); cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
changeQueryToggles("notification-on-success"); changeQueryToggles("notification-on-success");
cy.get('[data-cy="success-message-input-field"]').clearAndTypeOnCodeMirror( cy.get('[data-cy="success-message-input-field"]').clearAndTypeOnCodeMirror(
"Success alert" "Success alert"
); );
query("save"); cy.forceClickOnCanvas();
cy.wait("@editQuery");
cy.wait(200);
cy.waitForAutoSave();
cy.reload(); cy.reload();
cy.wait(3000); cy.get('[data-cy="modal-confirm-button"]', { timeout: 10000 }).realClick();
cy.get('[data-cy="modal-confirm-button"]').realClick(); cy.verifyToastMessage(commonSelectors.toastMessage, "Success alert", false);
cy.verifyToastMessage( cy.verifyToastMessage(
commonSelectors.toastMessage, commonSelectors.toastMessage,
"Query (runpy1) completed." "alert from runpy",
false
); );
cy.verifyToastMessage(commonSelectors.toastMessage, "Success alert");
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
}); });
}); });

View file

@ -300,6 +300,7 @@ describe("Editor- Test Button widget", () => {
cy.waitForAutoSave(); cy.waitForAutoSave();
cy.openInCurrentTab(commonWidgetSelector.previewButton); cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get( cy.get(
commonWidgetSelector.draggableWidget(buttonText.defaultWidgetName) commonWidgetSelector.draggableWidget(buttonText.defaultWidgetName)
@ -308,6 +309,8 @@ describe("Editor- Test Button widget", () => {
cy.get( cy.get(
commonWidgetSelector.draggableWidget(buttonText.defaultWidgetName) commonWidgetSelector.draggableWidget(buttonText.defaultWidgetName)
).click(); ).click();
cy.wait(500);
cy.verifyToastMessage(commonSelectors.toastMessage, data.alertMessage); cy.verifyToastMessage(commonSelectors.toastMessage, data.alertMessage);
cy.get(commonWidgetSelector.draggableWidget("textinput1")).should( cy.get(commonWidgetSelector.draggableWidget("textinput1")).should(
"have.value", "have.value",

View file

@ -29,6 +29,7 @@ import {
commonWidgetText, commonWidgetText,
codeMirrorInputLabel, codeMirrorInputLabel,
} from "Texts/common"; } from "Texts/common";
import { resizeQueryPanel } from "Support/utils/dataSource";
describe("Basic components", () => { describe("Basic components", () => {
const data = {}; const data = {};
@ -37,7 +38,6 @@ describe("Basic components", () => {
cy.appUILogin(); cy.appUILogin();
cy.createApp(); cy.createApp();
cy.modifyCanvasSize(900, 900); cy.modifyCanvasSize(900, 900);
cy.get('[data-tooltip-id="tooltip-for-hide-query-editor"]').click();
cy.renameApp(data.appName); cy.renameApp(data.appName);
cy.intercept("GET", "/api/comments/*").as("loadComments"); cy.intercept("GET", "/api/comments/*").as("loadComments");
}); });
@ -65,7 +65,7 @@ describe("Basic components", () => {
).should("have.text", "label"); ).should("have.text", "label");
cy.go("back"); cy.go("back");
cy.wait("@appVersion"); cy.waitForAppLoad();
deleteComponentAndVerify("toggleswitch2"); deleteComponentAndVerify("toggleswitch2");
cy.get(commonSelectors.editorPageLogo).click(); cy.get(commonSelectors.editorPageLogo).click();
@ -617,7 +617,9 @@ describe("Basic components", () => {
}); });
it("Should verify Tabs", () => { it("Should verify Tabs", () => {
cy.dragAndDropWidget("Tabs", 50, 50); cy.viewport(1200, 1300);
resizeQueryPanel("0");
cy.dragAndDropWidget("Tabs", 100, 100);
verifyComponent("tabs1"); verifyComponent("tabs1");
deleteComponentAndVerify("image1"); deleteComponentAndVerify("image1");

View file

@ -13,7 +13,7 @@ import {
import { verifyComponent } from "Support/utils/basicComponents"; import { verifyComponent } from "Support/utils/basicComponents";
import { commonWidgetText } from "Texts/common"; import { commonWidgetText } from "Texts/common";
describe("Editor- Test Button widget", () => { describe("Editor- CSA", () => {
const toolJetImage = "cypress/fixtures/Image/tooljet.png"; const toolJetImage = "cypress/fixtures/Image/tooljet.png";
beforeEach(() => { beforeEach(() => {
cy.appUILogin(); cy.appUILogin();
@ -194,7 +194,9 @@ describe("Editor- Test Button widget", () => {
}); });
it("Should verify Kanban CSA", () => { it("Should verify Kanban CSA", () => {
cy.dragAndDropWidget("Kanban", 50, 100); cy.viewport(1400, 1900);
cy.dragAndDropWidget("Kanban", 50, 400);
addDefaultEventHandler("Card updated successfully"); addDefaultEventHandler("Card updated successfully");
selectEvent( selectEvent(
"Card added", "Card added",
@ -231,7 +233,7 @@ describe("Editor- Test Button widget", () => {
.clearAndTypeOnCodeMirror("Card moved successfully"); .clearAndTypeOnCodeMirror("Card moved successfully");
cy.get('[data-cy="real-canvas"]').click("topRight", { force: true }); cy.get('[data-cy="real-canvas"]').click("topRight", { force: true });
cy.dragAndDropWidget("Button", 100, 600); cy.dragAndDropWidget("Button", 100, 200);
selectEvent("On click", "Control Component"); selectEvent("On click", "Control Component");
selectCSA("kanban1", "Add Card"); selectCSA("kanban1", "Add Card");
addSupportCSAData( addSupportCSAData(
@ -240,20 +242,20 @@ describe("Editor- Test Button widget", () => {
); );
cy.get('[data-cy="real-canvas"]').click("topRight", { force: true }); cy.get('[data-cy="real-canvas"]').click("topRight", { force: true });
cy.dragAndDropWidget("Button", 250, 600); cy.dragAndDropWidget("Button", 250, 200);
selectEvent("On click", "Control Component"); selectEvent("On click", "Control Component");
selectCSA("kanban1", "Delete Card"); selectCSA("kanban1", "Delete Card");
addSupportCSAData("Card Id", "c11"); addSupportCSAData("Card Id", "c11");
cy.get('[data-cy="real-canvas"]').click("topRight", { force: true }); cy.get('[data-cy="real-canvas"]').click("topRight", { force: true });
cy.dragAndDropWidget("Button", 350, 600); cy.dragAndDropWidget("Button", 350, 200);
selectEvent("On click", "Control Component"); selectEvent("On click", "Control Component");
selectCSA("kanban1", "Move Card"); selectCSA("kanban1", "Move Card");
addSupportCSAData("Card Id", "c1"); addSupportCSAData("Card Id", "c1");
addSupportCSAData("Destination Column Id", "r2"); addSupportCSAData("Destination Column Id", "r2");
cy.get('[data-cy="real-canvas"]').click("topRight", { force: true }); cy.get('[data-cy="real-canvas"]').click("topRight", { force: true });
cy.dragAndDropWidget("Button", 450, 600); cy.dragAndDropWidget("Button", 450, 200);
selectEvent("On click", "Control Component"); selectEvent("On click", "Control Component");
selectCSA("kanban1", "Update Card Data"); selectCSA("kanban1", "Update Card Data");
addSupportCSAData("Card Id", "c1"); addSupportCSAData("Card Id", "c1");
@ -276,9 +278,10 @@ describe("Editor- Test Button widget", () => {
commonSelectors.toastMessage, commonSelectors.toastMessage,
"Card removed successfully" "Card removed successfully"
); );
cy.get('[label="To Do"] .kanban-item [data-cy="draggable-widget-text1"]') cy.contains(
.last() '[label="To Do"] .kanban-item [data-cy="draggable-widget-text1"]',
.should("not.be.visible"); "New Card"
).should("not.exist");
cy.get('[label="To Do"] .kanban-item [data-cy="draggable-widget-text1"]') cy.get('[label="To Do"] .kanban-item [data-cy="draggable-widget-text1"]')
.first() .first()

View file

@ -37,7 +37,7 @@ describe("List view widget", () => {
cy.appUILogin(); cy.appUILogin();
cy.createApp(); cy.createApp();
cy.viewport(1200, 1200); cy.viewport(1200, 1200);
cy.dragAndDropWidget("List View", 200, 200); cy.dragAndDropWidget("List View", 50, 500);
cy.modifyCanvasSize(1200, 700); cy.modifyCanvasSize(1200, 700);
cy.intercept("PUT", "/api/apps/**").as("apps"); cy.intercept("PUT", "/api/apps/**").as("apps");
}); });

View file

@ -80,7 +80,7 @@ describe("Number Input", () => {
); );
cy.get( cy.get(
commonWidgetSelector.draggableWidget(data.widgetName) commonWidgetSelector.draggableWidget(data.widgetName)
).verifyVisibleElement("have.value", data.minimumvalue); ).verifyVisibleElement("have.value", `${data.minimumvalue}.00`);
openEditorSidebar(data.widgetName); openEditorSidebar(data.widgetName);
openAccordion(commonWidgetText.accordionProperties, [ openAccordion(commonWidgetText.accordionProperties, [
@ -90,7 +90,7 @@ describe("Number Input", () => {
]); ]);
verifyAndModifyParameter( verifyAndModifyParameter(
commonWidgetText.labelMaximumValue, commonWidgetText.labelMaximumValue,
data.maximumValue `${data.maximumValue}`
); );
cy.clearAndType( cy.clearAndType(
commonWidgetSelector.draggableWidget(data.widgetName), commonWidgetSelector.draggableWidget(data.widgetName),
@ -98,7 +98,7 @@ describe("Number Input", () => {
); );
cy.get( cy.get(
commonWidgetSelector.draggableWidget(data.widgetName) commonWidgetSelector.draggableWidget(data.widgetName)
).verifyVisibleElement("have.value", data.maximumValue); ).verifyVisibleElement("have.value", `${data.maximumValue}.00`);
openEditorSidebar(data.widgetName); openEditorSidebar(data.widgetName);
openAccordion(commonWidgetText.accordionProperties, [ openAccordion(commonWidgetText.accordionProperties, [
@ -217,11 +217,11 @@ describe("Number Input", () => {
); );
verifyAndModifyParameter( verifyAndModifyParameter(
commonWidgetText.labelMinimumValue, commonWidgetText.labelMinimumValue,
data.minimumvalue `${data.minimumvalue}`
); );
verifyAndModifyParameter( verifyAndModifyParameter(
commonWidgetText.labelMaximumValue, commonWidgetText.labelMaximumValue,
data.maximumValue `${data.maximumValue}`
); );
verifyAndModifyParameter( verifyAndModifyParameter(
commonWidgetText.labelPlaceHolder, commonWidgetText.labelPlaceHolder,
@ -270,14 +270,14 @@ describe("Number Input", () => {
); );
cy.get( cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName) commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).verifyVisibleElement("have.value", data.minimumvalue); ).verifyVisibleElement("have.value", `${data.minimumvalue}.00`);
cy.clearAndType( cy.clearAndType(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName), commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName),
randomNumber(100, 110) randomNumber(100, 110)
); );
cy.get( cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName) commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).verifyVisibleElement("have.value", data.maximumValue); ).verifyVisibleElement("have.value", `${data.maximumValue}.00`);
cy.get( cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName) commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
) )

View file

@ -51,16 +51,18 @@ import {
import { verifyNodeData, openNode, verifyValue } from "Support/utils/inspector"; import { verifyNodeData, openNode, verifyValue } from "Support/utils/inspector";
import { textInputText } from "Texts/textInput"; import { textInputText } from "Texts/textInput";
import { deleteDownloadsFolder } from "Support/utils/common"; import { deleteDownloadsFolder } from "Support/utils/common";
import { resizeQueryPanel } from "Support/utils/dataSource";
describe("Table", () => { describe("Table", () => {
beforeEach(() => { beforeEach(() => {
cy.appUILogin(); cy.appUILogin();
cy.createApp(); cy.createApp();
deleteDownloadsFolder(); deleteDownloadsFolder();
cy.viewport(1200, 1200); cy.viewport(1400, 2200);
cy.modifyCanvasSize(900, 800); cy.modifyCanvasSize(900, 800);
cy.dragAndDropWidget("Table", 50, 50); cy.dragAndDropWidget("Table", 50, 50);
cy.resizeWidget(tableText.defaultWidgetName, 750, 600); cy.resizeWidget(tableText.defaultWidgetName, 750, 600);
resizeQueryPanel("1");
cy.get(`[data-cy="allow-selection-toggle-button"]`).click({ force: true }); cy.get(`[data-cy="allow-selection-toggle-button"]`).click({ force: true });
}); });
@ -366,7 +368,7 @@ describe("Table", () => {
verifyAndEnterColumnOptionInput("Text color", "red"); verifyAndEnterColumnOptionInput("Text color", "red");
verifyAndEnterColumnOptionInput( verifyAndEnterColumnOptionInput(
"Cell Background Color", "Cell Background Color",
"{backspace}{backspace}{backspace}{backspace}{backspace}yellow" "{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}yellow"
); );
cy.get( cy.get(
'[data-cy="input-and-label-cell-background-color"] > .form-label' '[data-cy="input-and-label-cell-background-color"] > .form-label'
@ -481,14 +483,14 @@ describe("Table", () => {
cy.get('[data-cy="make-editable-toggle-button"]').click(); cy.get('[data-cy="make-editable-toggle-button"]').click();
cy.forceClickOnCanvas(); cy.forceClickOnCanvas();
cy.get(`${tableSelector.column(0)}:eq(0) .badge`) cy.get(`${tableSelector.column(1)}:eq(0) .badge`)
.eq(0) .eq(0)
.should("have.text", "Onex") .should("have.text", "Onex")
.next() .next()
.should("have.text", "Twox") .should("have.text", "Twox")
.next() .next()
.should("have.text", "Threex"); .should("have.text", "Threex");
cy.get(`${tableSelector.column(0)}:eq(0) .badge`) cy.get(`${tableSelector.column(1)}:eq(0) .badge`)
.first() .first()
.click({ force: true }) .click({ force: true })
.trigger("mouseover") .trigger("mouseover")
@ -615,9 +617,7 @@ describe("Table", () => {
cy.get('[data-cy*="-cell-0"]') cy.get('[data-cy*="-cell-0"]')
.eq(0) .eq(0)
.should("have.css", "background-color", "rgb(0, 0, 0)"); .should("have.css", "background-color", "rgb(0, 0, 0)");
cy.get( cy.get('[data-cy*="-cell-0"] > .td-container > .w-100 > .d-flex')
'[data-cy*="-cell-0"] > .td-container > :nth-child(1) > .d-flex >div'
)
.eq(0) .eq(0)
.should("have.css", "color", "rgb(255, 255, 255)"); .should("have.css", "color", "rgb(255, 255, 255)");
verifyInvalidFeedback(1, 1, "Minimum 5 characters is needed"); verifyInvalidFeedback(1, 1, "Minimum 5 characters is needed");
@ -1128,7 +1128,9 @@ describe("Table", () => {
verifyNodeData("components", "Object", "1 entry "); verifyNodeData("components", "Object", "1 entry ");
openNode("components"); openNode("components");
verifyNodeData(tableText.defaultWidgetName, "Object", "17 entries "); verifyNodeData(tableText.defaultWidgetName, "Object", "17 entries ");
cy.wait(1000);
openNode(tableText.defaultWidgetName); openNode(tableText.defaultWidgetName);
cy.wait(500);
verifyNodeData("newRows", "Array", "1 item "); verifyNodeData("newRows", "Array", "1 item ");
openNode("newRows"); openNode("newRows");
verifyNodeData("0", "Object", "3 entries "); verifyNodeData("0", "Object", "3 entries ");
@ -1148,10 +1150,12 @@ describe("Table", () => {
"Button" "Button"
); );
deleteAndVerifyColumn("name"); deleteAndVerifyColumn("name");
deleteAndVerifyColumn("email");
cy.get(tableSelector.columnHeader("actions")) cy.get(tableSelector.columnHeader("actions"))
.scrollIntoView() .scrollIntoView()
.verifyVisibleElement("have.text", "Actions"); .verifyVisibleElement("have.text", "Actions");
cy.get(`${tableSelector.column(2)} > > > button`) cy.get(`${tableSelector.column(1)} > > > button`)
.eq("0") .eq("0")
.should("have.text", "Button") .should("have.text", "Button")
.and("not.have.attr", "disabled"); .and("not.have.attr", "disabled");
@ -1165,7 +1169,7 @@ describe("Table", () => {
"have.text", "have.text",
"Actions" "Actions"
); );
cy.get(`${tableSelector.column(2)} > > > button`) cy.get(`${tableSelector.column(1)} > > > button`)
.eq("0") .eq("0")
.click(); .click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Hello world!"); cy.verifyToastMessage(commonSelectors.toastMessage, "Hello world!");
@ -1182,7 +1186,7 @@ describe("Table", () => {
cy.get(tableSelector.columnHeader("actions")) cy.get(tableSelector.columnHeader("actions"))
.scrollIntoView() .scrollIntoView()
.verifyVisibleElement("have.text", "Actions"); .verifyVisibleElement("have.text", "Actions");
cy.get(`${tableSelector.column(2)} > > > button`) cy.get(`${tableSelector.column(1)} > > > button`)
.eq("0") .eq("0")
.should("have.text", "Button") .should("have.text", "Button")
.and("have.attr", "disabled"); .and("have.attr", "disabled");
@ -1205,7 +1209,7 @@ describe("Table", () => {
cy.get(tableSelector.columnHeader("actions")) cy.get(tableSelector.columnHeader("actions"))
.scrollIntoView() .scrollIntoView()
.verifyVisibleElement("have.text", "Actions"); .verifyVisibleElement("have.text", "Actions");
cy.get(`${tableSelector.column(2)} > > > button`) cy.get(`${tableSelector.column(1)} > > > button`)
.eq("0") .eq("0")
.click(); .click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Hello world!"); cy.verifyToastMessage(commonSelectors.toastMessage, "Hello world!");
@ -1215,13 +1219,14 @@ describe("Table", () => {
cy.get(tableSelector.columnHeader("actions")) cy.get(tableSelector.columnHeader("actions"))
.scrollIntoView() .scrollIntoView()
.verifyVisibleElement("have.text", "Actions"); .verifyVisibleElement("have.text", "Actions");
cy.get(`${tableSelector.column(2)} > > > button`) cy.get(`${tableSelector.column(1)} > > > button`)
.eq("0") .eq("0")
.should("have.text", "Button") .should("have.text", "Button")
.and("have.attr", "disabled"); .and("have.attr", "disabled");
}); });
it("should verify Programatically actions on table column", () => { it("should verify Programatically actions on table column", () => {
deleteAndVerifyColumn("id");
cy.get('[data-cy="inspector-close-icon"]').click(); cy.get('[data-cy="inspector-close-icon"]').click();
cy.dragAndDropWidget("Text", 800, 200); cy.dragAndDropWidget("Text", 800, 200);
openEditorSidebar(commonWidgetText.text1); openEditorSidebar(commonWidgetText.text1);
@ -1267,17 +1272,17 @@ describe("Table", () => {
.click() .click()
.clearAndTypeOnCodeMirror(`{{components.toggleswitch1.value`); .clearAndTypeOnCodeMirror(`{{components.toggleswitch1.value`);
cy.forceClickOnCanvas(); cy.forceClickOnCanvas();
cy.get('[data-cy*="-cell-1"]').should("not.have.class", "has-text"); cy.get('[data-cy*="-cell-0"]').should("not.have.class", "has-text");
cy.get( cy.get(
'[data-cy="draggable-widget-toggleswitch1"] [type="checkbox"]' '[data-cy="draggable-widget-toggleswitch1"] [type="checkbox"]'
).click(); ).click();
cy.get('[data-cy*="-cell-1"]') cy.get('[data-cy*="-cell-0"]')
.eq(0) .eq(0)
.click() .click()
.type(`{selectAll}{backspace}Jack`); .type(`{selectAll}{backspace}Jack`);
cy.forceClickOnCanvas(); cy.forceClickOnCanvas();
cy.get('[data-cy*="-cell-1"]').should("have.class", "has-text"); cy.get('[data-cy*="-cell-0"]').should("have.class", "has-text");
cy.get('[data-cy*="-cell-1"] [type="text"]') cy.get('[data-cy*="-cell-0"] [type="text"]')
.eq(0) .eq(0)
.verifyVisibleElement("have.value", "Jack"); .verifyVisibleElement("have.value", "Jack");
}); });

View file

@ -87,11 +87,11 @@ describe("App Export Functionality", () => {
navigateToAppEditor(data.appName1); navigateToAppEditor(data.appName1);
cy.get('[data-cy="widget-list-box-table"]').should("be.visible"); cy.get('[data-cy="widget-list-box-table"]').should("be.visible");
cy.get(".driver-close-btn").click(); cy.skipEditorPopover();
cy.get(appVersionSelectors.appVersionMenuField) cy.get(appVersionSelectors.appVersionMenuField)
.should("be.visible") .should("be.visible")
.click(); .click();
createNewVersion(otherVersions = ["v2"], currentVersion = "v1"); createNewVersion((otherVersions = ["v2"]), (currentVersion = "v1"));
cy.wait(500); cy.wait(500);
cy.dragAndDropWidget("Toggle Switch", 50, 50); cy.dragAndDropWidget("Toggle Switch", 50, 50);
cy.waitForAutoSave(); cy.waitForAutoSave();

View file

@ -20,7 +20,7 @@ describe("App Import Functionality", () => {
let appData; let appData;
var data = {}; var data = {};
data.appName = `${fake.companyName}-App`; data.appName = `${fake.companyName}-App`;
data.appReName = `${fake.companyName}-App`; data.appReName = `${fake.companyName}-${fake.companyName}-App`;
let currentVersion = ""; let currentVersion = "";
let otherVersions = ""; let otherVersions = "";
const toolJetImage = "cypress/fixtures/Image/tooljet.png"; const toolJetImage = "cypress/fixtures/Image/tooljet.png";
@ -55,17 +55,18 @@ describe("App Import Functionality", () => {
}); });
cy.verifyToastMessage( cy.verifyToastMessage(
commonSelectors.toastMessage, commonSelectors.toastMessage,
importText.couldNotImportAppToastMessage importText.couldNotImportAppToastMessage,
false
); );
cy.get(importSelectors.importOptionInput).selectFile(appFile, { cy.get(importSelectors.importOptionInput).selectFile(appFile, {
force: true, force: true,
}); });
cy.get(".driver-close-btn").click();
cy.verifyToastMessage( cy.verifyToastMessage(
commonSelectors.toastMessage, commonSelectors.toastMessage,
importText.appImportedToastMessage importText.appImportedToastMessage
); );
cy.get(".driver-close-btn").click();
cy.get(commonSelectors.appNameInput).verifyVisibleElement( cy.get(commonSelectors.appNameInput).verifyVisibleElement(
"contain.value", "contain.value",
appData.name appData.name

View file

@ -0,0 +1,274 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import {
fillDataSourceTextField,
selectDataSource,
fillConnectionForm,
addQuery,
} from "Support/utils/postgreSql";
import { commonText } from "Texts/common";
import { closeDSModal, deleteDatasource } from "Support/utils/dataSource";
import { dataSourceSelector } from "Selectors/dataSource";
import { dataSourceText } from "Texts/dataSource";
import { addNewUserMW } from "Support/utils/userPermissions";
import { groupsSelector } from "Selectors/manageGroups";
import {
logout,
navigateToAppEditor,
navigateToManageGroups,
pinInspector
} from "Support/utils/common";
const data = {};
data.firstName = fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.email = fake.email.toLowerCase();
data.lastName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.appName = `${fake.companyName}-App`;
describe("Global Datasource Manager", () => {
beforeEach(() => {
cy.appUILogin();
cy.viewport(1200, 1300);
});
before(() => {
cy.appUILogin();
cy.createApp();
cy.renameApp(data.appName);
cy.dragAndDropWidget("Button", 50, 50);
cy.get(commonSelectors.editorPageLogo).click();
cy.reloadAppForTheElement(data.appName);
addNewUserMW(data.firstName, data.email);
logout();
});
it("Should verify the global data source manager UI", () => {
cy.get(commonSelectors.globalDataSourceIcon).click();
closeDSModal();
cy.get(commonSelectors.addNewDataSourceButton)
.verifyVisibleElement("have.text", commonText.addNewDataSourceButton)
.click();
cy.get(dataSourceSelector.allDatasourceLabelAndCount).should(
"have.text",
dataSourceText.allDataSources
);
cy.get(dataSourceSelector.databaseLabelAndCount).should(
"have.text",
dataSourceText.allDatabase
);
cy.get(dataSourceSelector.apiLabelAndCount).should(
"have.text",
dataSourceText.allApis
);
cy.get(dataSourceSelector.cloudStorageLabelAndCount).should(
"have.text",
dataSourceText.allCloudStorage
);
});
it("Should verify the Datasource connection and query creation using global data source", () => {
selectDataSource(dataSourceText.postgreSQL);
cy.clearAndType(
'[data-cy="data-source-name-input-filed"]',
`cypress-${data.lastName}-postgresql`
);
cy.intercept("GET", "api/v2/data_sources").as("datasource");
fillConnectionForm(
{
Host: Cypress.env("gds_pg_host"),
Port: "5432",
"Database Name": Cypress.env("gds_pg_user"),
Username: Cypress.env("gds_pg_user"),
Password: Cypress.env("gds_pg_password"),
},
".form-switch"
);
cy.wait("@datasource");
cy.get(dataSourceSelector.buttonTestConnection).click();
cy.get(dataSourceSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", dataSourceText.labelConnectionVerified);
cy.get(dataSourceSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
dataSourceText.toastDSAdded
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
`[data-cy="cypress-${data.lastName}-postgresql-button"]`
).verifyVisibleElement("have.text", `cypress-${data.lastName}-postgresql`);
cy.get(commonSelectors.dashboardIcon).click();
navigateToAppEditor(data.appName);
cy.get(
`[data-cy="cypress-${data.lastName}-postgresql-add-query-card"]`
).should("be.visible");
addQuery(
"table_preview",
`SELECT * FROM persons;`,
`cypress-${data.lastName}-postgresql`
);
cy.get('[data-cy="list-query-table_preview"]').verifyVisibleElement(
"have.text",
"table_preview"
);
pinInspector()
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get('[data-cy="inspector-node-queries"]')
.parent()
.within(() => {
cy.get("span").first().scrollIntoView().contains("queries").dblclick();
});
cy.get('[data-cy="inspector-node-table_preview"] > .node-key').click();
cy.get('[data-cy="inspector-node-data"] > .fs-9').verifyVisibleElement(
"have.text",
"4 items "
);
cy.get(dataSourceSelector.buttonAddNewQueries).click();
cy.get(
".query-datasource-card-container > .col-auto > .query-manager-btn-name"
)
.should("be.visible")
.and("have.text", "Add new global datasource");
cy.get(
".query-datasource-card-container > .col-auto > .query-manager-btn-name"
).click();
selectDataSource(dataSourceText.postgreSQL);
cy.clearAndType(
'[data-cy="data-source-name-input-filed"]',
`cypress-${data.firstName}-postgresql`
);
cy.intercept("GET", "api/v2/data_sources").as("datasource");
fillConnectionForm(
{
Host: Cypress.env("pg_host"),
Port: "5432",
"Database Name": Cypress.env("pg_user"),
Username: Cypress.env("pg_user"),
Password: Cypress.env("pg_password"),
},
".form-switch"
);
cy.wait("@datasource");
navigateToManageGroups();
cy.get(groupsSelector.appSearchBox).click();
cy.get(groupsSelector.searchBoxOptions).contains(data.appName).click();
cy.get(groupsSelector.selectAddButton).click();
cy.contains("tr", data.appName)
.parent()
.within(() => {
cy.get("td input").eq(1).check();
});
cy.verifyToastMessage(
commonSelectors.toastMessage,
"App permissions updated"
);
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.appsCreateCheck).then(($el) => {
if (!$el.is(":checked")) {
cy.get(groupsSelector.appsCreateCheck).check();
}
});
});
it("Should validate the user's global data source permissions on apps created by admin", () => {
logout();
cy.login(data.email, "password");
cy.get(commonSelectors.globalDataSourceIcon).should("not.exist");
navigateToAppEditor(data.appName);
cy.get('[data-cy="list-query-table_preview"]').verifyVisibleElement(
"have.text",
"table_preview"
);
pinInspector()
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get('[data-cy="inspector-node-queries"]')
.parent()
.within(() => {
cy.get("span").first().scrollIntoView().contains("queries").dblclick();
});
cy.get('[data-cy="inspector-node-table_preview"] > .node-key').click();
cy.get('[data-cy="inspector-node-data"] > .fs-9').verifyVisibleElement(
"have.text",
"4 items "
);
addQuery(
"student_data",
`SELECT * FROM student_data;`,
`cypress-${data.firstName}-postgresql`
);
cy.get('[data-cy="list-query-student_data"]').verifyVisibleElement(
"have.text",
"student_data"
);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get('[data-cy="inspector-node-queries"]')
.parent()
.within(() => {
cy.get("span").first().scrollIntoView().contains("queries").dblclick();
});
cy.get('[data-cy="inspector-node-student_data"] > .node-key').click();
cy.get('[data-cy="inspector-node-data"] > .fs-9').verifyVisibleElement(
"have.text",
"8 items "
);
cy.get(dataSourceSelector.buttonAddNewQueries).click();
cy.get(
".query-datasource-card-container > .col-auto > .query-manager-btn-name"
).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"You don't have access to GDS, contact your workspace admin to add datasources"
);
});
it("Should verify the query creation and scope changing functionality.", () => {
logout();
cy.login(data.email, "password");
cy.createApp();
cy.renameApp(data.appName);
cy.dragAndDropWidget("Button", 50, 50);
addQuery(
"table_preview",
`SELECT * FROM persons;`,
`cypress-${data.lastName}-postgresql`
);
cy.get('[data-cy="list-query-table_preview"]').verifyVisibleElement(
"have.text",
"table_preview"
);
pinInspector()
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get('[data-cy="inspector-node-queries"]')
.parent()
.within(() => {
cy.get("span").first().scrollIntoView().contains("queries").dblclick();
});
cy.get('[data-cy="inspector-node-table_preview"] > .node-key').click();
cy.get('[data-cy="inspector-node-data"] > .fs-9').verifyVisibleElement(
"have.text",
"4 items "
);
})
});

View file

@ -137,10 +137,11 @@ describe("Bulk user upload", () => {
force: true, force: true,
}); });
cy.get(usersSelector.buttonUploadUsers).click(); cy.get(usersSelector.buttonUploadUsers).click();
cy.wait(1000);
cy.get(".go2072408551") cy.get(".go2072408551")
.should("be.visible") .should("be.visible")
.and("have.text", "250 users are being added"); .and("have.text", "250 users are being added");
cy.wait(500); cy.wait(1000);
common.searchUser("test12@gmail.com"); common.searchUser("test12@gmail.com");
cy.contains("td", "test12@gmail.com") cy.contains("td", "test12@gmail.com")
.parent() .parent()

View file

@ -33,7 +33,7 @@ describe("dashboard", () => {
beforeEach(() => { beforeEach(() => {
cy.intercept("DELETE", "/api/folders/*").as("folderDeleted"); cy.intercept("DELETE", "/api/folders/*").as("folderDeleted");
cy.intercept("GET", "/api/apps").as("appEditor"); // cy.intercept("GET", "/api/apps").as("appEditor");
cy.intercept("GET", "/api/library_apps").as("appLibrary"); cy.intercept("GET", "/api/library_apps").as("appLibrary");
}); });
@ -53,6 +53,8 @@ describe("dashboard", () => {
cy.wait("@folders"); cy.wait("@folders");
cy.wait("@version"); cy.wait("@version");
// deleteDownloadsFolder(); // deleteDownloadsFolder();
cy.visitTheWorkspace('My workspace')
}); });
it("should verify the elements on empty dashboard", () => { it("should verify the elements on empty dashboard", () => {
@ -281,13 +283,13 @@ describe("dashboard", () => {
commonSelectors.toastMessage, commonSelectors.toastMessage,
dashboardText.appClonedToast dashboardText.appClonedToast
); );
cy.wait("@appEditor"); cy.waitForAppLoad();
cy.wait(300); cy.wait(2000);
cy.clearAndType(commonSelectors.appNameInput, data.cloneAppName); cy.clearAndType(commonSelectors.appNameInput, data.cloneAppName);
cy.dragAndDropWidget("button", 25, 25); cy.dragAndDropWidget("button", 25, 25);
cy.get(commonSelectors.editorPageLogo).click(); cy.get(commonSelectors.editorPageLogo).click();
cy.wait("@appLibrary"); cy.wait("@appLibrary");
cy.wait(500); cy.wait(1000);
cy.reloadAppForTheElement(data.cloneAppName); cy.reloadAppForTheElement(data.cloneAppName);
cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible"); cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible");
@ -340,7 +342,7 @@ describe("dashboard", () => {
cy.appUILogin(); cy.appUILogin();
cy.createApp(); cy.createApp();
cy.renameApp(data.appName); cy.renameApp(data.appName);
cy.dragAndDropWidget("Button", 50, 50); cy.dragAndDropWidget("Button", 450, 450);
cy.get(commonSelectors.editorPageLogo).click(); cy.get(commonSelectors.editorPageLogo).click();
cy.reloadAppForTheElement(data.appName); cy.reloadAppForTheElement(data.appName);
@ -350,7 +352,6 @@ describe("dashboard", () => {
); );
navigateToAppEditor(data.appName); navigateToAppEditor(data.appName);
cy.dragAndDropWidget("Button");
cy.get(commonSelectors.canvas).should("contain", "Button"); cy.get(commonSelectors.canvas).should("contain", "Button");
cy.get(commonSelectors.editorPageLogo).click(); cy.get(commonSelectors.editorPageLogo).click();
cy.wait("@appLibrary"); cy.wait("@appLibrary");
@ -414,11 +415,28 @@ describe("dashboard", () => {
).verifyVisibleElement("have.text", commonText.deleteFolderOption); ).verifyVisibleElement("have.text", commonText.deleteFolderOption);
cy.get(commonSelectors.editFolderOption(data.folderName)).click(); cy.get(commonSelectors.editFolderOption(data.folderName)).click();
verifyModal(
commonText.updateFolderTitle, cy.get(commonSelectors.modalComponent).should("be.visible");
commonText.updateFolderButton, cy.get(commonSelectors.modalTitle(commonText.updateFolderTitle))
commonSelectors.folderNameInput .should("be.visible")
.and("have.text", commonText.updateFolderTitle);
cy.get(commonSelectors.buttonSelector(commonText.closeButton)).should(
"be.visible"
); );
cy.get(commonSelectors.buttonSelector(commonText.cancelButton))
.should("be.visible")
.and("have.text", commonText.cancelButton);
cy.get(commonSelectors.buttonSelector(commonText.updateFolderButton))
.should("be.visible")
.and("have.text", "Edit folder");
cy.get(commonSelectors.folderNameInput).should("be.visible")
// verifyModal(
// commonText.updateFolderTitle,
// commonText.updateFolderButton,
// commonSelectors.folderNameInput
// );
cy.clearAndType(commonSelectors.folderNameInput, data.updatedFolderName); cy.clearAndType(commonSelectors.folderNameInput, data.updatedFolderName);
closeModal(commonText.closeButton); closeModal(commonText.closeButton);

View file

@ -7,11 +7,13 @@ import * as common from "Support/utils/common";
import { path } from "Texts/common"; import { path } from "Texts/common";
import { dashboardSelector } from "Selectors/dashboard"; import { dashboardSelector } from "Selectors/dashboard";
import { updateWorkspaceName } from "Support/utils/userPermissions"; import { updateWorkspaceName } from "Support/utils/userPermissions";
import { groupsSelector } from "Selectors/manageGroups";
import { groupsText } from "Texts/manageGroups";
const data = {}; const data = {};
data.firstName = fake.firstName; data.firstName = fake.firstName;
data.lastName = fake.lastName.replaceAll("[^A-Za-z]", "");
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""); data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
data.groupName = fake.firstName.replaceAll("[^A-Za-z]", "");
describe("Manage Users", () => { describe("Manage Users", () => {
beforeEach(() => { beforeEach(() => {
@ -24,6 +26,7 @@ describe("Manage Users", () => {
url = ""; url = "";
it("Should verify the Manage users page", () => { it("Should verify the Manage users page", () => {
common.navigateToManageUsers(); common.navigateToManageUsers();
users.manageUsersElements(); users.manageUsersElements();
cy.get(commonSelectors.cancelButton).click(); cy.get(commonSelectors.cancelButton).click();
@ -83,9 +86,7 @@ describe("Manage Users", () => {
"have.text", "have.text",
"My workspace" "My workspace"
); );
// cy.get(commonSelectors.workspaceName).click();
updateWorkspaceName(data.email); updateWorkspaceName(data.email);
cy.get(dashboardSelector.emptyPageHeader).should("be.visible");
common.logout(); common.logout();
cy.appUILogin(); cy.appUILogin();
@ -119,10 +120,12 @@ describe("Manage Users", () => {
}); });
common.logout(); common.logout();
cy.visit('/');
cy.clearAndType(commonSelectors.workEmailInputField, data.email); cy.clearAndType(commonSelectors.workEmailInputField, data.email);
cy.clearAndType(commonSelectors.passwordInputField, usersText.password); cy.clearAndType(commonSelectors.passwordInputField, usersText.password);
cy.get(commonSelectors.loginButton).click(); cy.get(commonSelectors.loginButton).click();
updateWorkspaceName(data.email);
cy.get(commonSelectors.workspaceName).click(); cy.get(commonSelectors.workspaceName).click();
cy.contains("My workspace").should("not.exist"); cy.contains("My workspace").should("not.exist");
common.logout(); common.logout();
@ -169,10 +172,7 @@ describe("Manage Users", () => {
cy.contains("td", data.email) cy.contains("td", data.email)
.parent() .parent()
.within(() => { .within(() => {
cy.get("td small").should( cy.get("td small").should("have.text", usersText.invitedStatus);
"have.text",
usersText.invitedStatus
);
}); });
common.logout(); common.logout();
cy.wait(500); cy.wait(500);
@ -195,4 +195,65 @@ describe("Manage Users", () => {
cy.get("td small").should("have.text", usersText.activeStatus); cy.get("td small").should("have.text", usersText.activeStatus);
}); });
}); });
it.skip("Should verify the user onboarding with groups", () => {
data.firstName = fake.firstName;
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
common.navigateToManageUsers();
users.fillUserInviteForm(data.firstName, data.email);
cy.get(".dropdown-heading-value > .gray").click();
cy.clearAndType(".search > input", "Test");
cy.get(".no-options").verifyVisibleElement("have.text", "No options");
users.selectUserGroup("Admin");
cy.get(".dropdown-heading-value > span").verifyVisibleElement(
"have.text",
"Admin"
);
cy.get(commonSelectors.cancelButton).click();
cy.get(usersSelector.buttonAddUsers).click();
cy.get(".dropdown-heading-value > .gray").verifyVisibleElement(
"have.text",
"Select groups to add for this user"
);
cy.get(commonSelectors.cancelButton).click();
users.inviteUserWithUserGroup(
data.firstName,
data.email,
"All users",
"Admin"
);
common.navigateToManageGroups();
cy.get(groupsSelector.groupLink("Admin")).click();
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.userRow(data.email)).should("be.visible");
data.firstName = fake.firstName;
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
cy.get(groupsSelector.createNewGroupButton).click();
cy.clearAndType(groupsSelector.groupNameInput, data.groupName);
cy.get(groupsSelector.createGroupButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.groupCreatedToast)
common.navigateToManageUsers();
users.inviteUserWithUserGroup(
data.firstName,
data.email,
"All users",
data.groupName
);
common.logout()
cy.appUILogin()
common.navigateToManageGroups();
cy.get(groupsSelector.groupLink(data.groupName)).click();
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.userRow(data.email)).should("be.visible");
});
}); });

View file

@ -35,15 +35,15 @@ describe("Profile Settings", () => {
cy.get(profileSelector.updateButton).click(); cy.get(profileSelector.updateButton).click();
cy.verifyToastMessage( cy.verifyToastMessage(
commonSelectors.toastMessage, commonSelectors.toastMessage,
profileText.nameErrorToast profileText.firstNameErrorToast
); );
cy.clearAndType(profileSelector.firstNameInput, profileText.firstName); // cy.clearAndType(profileSelector.firstNameInput, profileText.firstName);
cy.get(profileSelector.updateButton).click(); // cy.get(profileSelector.updateButton).click();
cy.verifyToastMessage( // cy.verifyToastMessage(
commonSelectors.toastMessage, // commonSelectors.toastMessage,
profileText.lastNameNameErrorToast // profileText.lastNameNameErrorToast
); // );
cy.clearAndType(profileSelector.firstNameInput, randomFirstName); cy.clearAndType(profileSelector.firstNameInput, randomFirstName);
cy.clearAndType(profileSelector.lastNameInput, randomLastName); cy.clearAndType(profileSelector.lastNameInput, randomLastName);

View file

@ -12,6 +12,7 @@ describe("App share functionality", () => {
data.lastName = fake.lastName.replaceAll("[^A-Za-z]", ""); data.lastName = fake.lastName.replaceAll("[^A-Za-z]", "");
data.email = fake.email.toLowerCase(); data.email = fake.email.toLowerCase();
const slug = data.appName.toLowerCase().replace(/\s+/g, "-"); const slug = data.appName.toLowerCase().replace(/\s+/g, "-");
const firstUserEmail = data.email
beforeEach(() => { beforeEach(() => {
cy.appUILogin(); cy.appUILogin();
}); });
@ -35,7 +36,7 @@ describe("App share functionality", () => {
cy.get(commonWidgetSelector.makePublicAppToggle).should("be.visible"); cy.get(commonWidgetSelector.makePublicAppToggle).should("be.visible");
cy.get(commonWidgetSelector.appLink).should("be.visible"); cy.get(commonWidgetSelector.appLink).should("be.visible");
cy.get(commonWidgetSelector.appNameSlugInput).should("be.visible"); cy.get(commonWidgetSelector.appNameSlugInput).should("be.visible");
cy.get(commonWidgetSelector.iframeLink).should("be.visible"); // cy.get(commonWidgetSelector.iframeLink).should("be.visible");
cy.get(commonWidgetSelector.modalCloseButton).should("be.visible"); cy.get(commonWidgetSelector.modalCloseButton).should("be.visible");
cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`); cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`);
@ -78,12 +79,6 @@ describe("App share functionality", () => {
cy.appUILogin(); cy.appUILogin();
navigateToAppEditor(data.appName); navigateToAppEditor(data.appName);
cy.wait(1000);
cy.get("body").then(($el) => {
if ($el.text().includes("Skip", { timeout: 10000 })) {
cy.get(commonSelectors.skipButton).click();
}
});
cy.get(commonWidgetSelector.shareAppButton).click(); cy.get(commonWidgetSelector.shareAppButton).click();
cy.get(commonWidgetSelector.makePublicAppToggle).uncheck(); cy.get(commonWidgetSelector.makePublicAppToggle).uncheck();
cy.get(commonWidgetSelector.modalCloseButton).click(); cy.get(commonWidgetSelector.modalCloseButton).click();
@ -99,19 +94,22 @@ describe("App share functionality", () => {
); );
}); });
it.skip("Verify app private and public app visibility for the same instance user", () => { it("Verify app private and public app visibility for the same instance user", () => {
data.firstName = fake.firstName; data.firstName = fake.firstName;
data.email = fake.email.toLowerCase(); data.email = fake.email.toLowerCase();
logout(); logout();
userSignUp(data.firstName, data.email, "Test"); userSignUp(data.firstName, data.email, "Test");
cy.visit(`/applications/${slug}`); cy.visit(`/applications/${slug}`);
cy.wait(1000);
cy.clearAndType(commonSelectors.workEmailInputField, data.email); cy.clearAndType(commonSelectors.workEmailInputField, data.email);
cy.clearAndType(commonSelectors.passwordInputField, "password"); cy.clearAndType(commonSelectors.passwordInputField, "password");
cy.get(commonSelectors.signInButton).click(); cy.get(commonSelectors.signInButton).click();
cy.wait(1000);
cy.visit("/"); cy.visit("/");
cy.wait(2000);
logout(); logout();
cy.appUILogin(); cy.appUILogin();

View file

@ -33,6 +33,7 @@ describe("User permissions", () => {
}); });
beforeEach(() => { beforeEach(() => {
cy.appUILogin(); cy.appUILogin();
cy.visitTheWorkspace("My workspace");
}); });
it("Should verify the create new app permission", () => { it("Should verify the create new app permission", () => {
@ -87,6 +88,10 @@ describe("User permissions", () => {
.within(() => { .within(() => {
cy.get("td input").eq(1).check(); cy.get("td input").eq(1).check();
}); });
cy.verifyToastMessage(
commonSelectors.toastMessage,
"App permissions updated"
);
common.logout(); common.logout();
cy.login(data.email, usersText.password); cy.login(data.email, usersText.password);
@ -203,56 +208,23 @@ describe("User permissions", () => {
it("Should verify Create/Update/Delete workspace variable permission", () => { it("Should verify Create/Update/Delete workspace variable permission", () => {
common.navigateToWorkspaceVariable(); common.navigateToWorkspaceVariable();
cy.get(workspaceVarSelectors.addNewVariableButton).should("exist"); cy.get('[data-cy="alert-info-text"]>>.text-muted').verifyVisibleElement(
"have.text",
common.logout(); "There are no Workspace variables. Workspace variables are being deprecated soon, so please use Workspace constants instead."
cy.login(data.email, usersText.password); );
common.navigateToWorkspaceVariable(); cy.get(
cy.get(workspaceVarSelectors.addNewVariableButton).should("not.exist"); '[data-cy="go-to-workspace-constants-option-button"]'
).verifyVisibleElement("have.text", "Go to workspace constants");
permissions.adminLogin();
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.workspaceVarCheckbox).check();
common.logout(); common.logout();
cy.login(data.email, usersText.password); cy.login(data.email, usersText.password);
common.navigateToWorkspaceVariable(); common.navigateToWorkspaceVariable();
cy.get(workspaceVarSelectors.addNewVariableButton).should("exist").click(); cy.get('[data-cy="alert-info-text"]>>.text-muted').verifyVisibleElement(
cy.clearAndType( "have.text",
workspaceVarSelectors.workspaceVarNameInput, "There are no Workspace variables. Workspace variables are being deprecated soon, so please use Workspace constants instead."
data.firstName
);
cy.clearAndType(
workspaceVarSelectors.workspaceVarValueInput,
common.randomValue()
);
cy.get(workspaceVarSelectors.addVariableButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
workspaceVarText.workspaceVarCreatedToast
);
cy.get(workspaceVarSelectors.workspaceVarName(data.firstName)).should(
"be.visible"
); );
cy.get( cy.get(
workspaceVarSelectors.workspaceVarEditButton(data.firstName) '[data-cy="go-to-workspace-constants-option-button"]'
).click(); ).verifyVisibleElement("have.text", "Go to workspace constants");
cy.clearAndType(workspaceVarSelectors.workspaceVarNameInput, data.lastName);
cy.get(workspaceVarSelectors.addVariableButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
workspaceVarText.workspaceVarUpdatedToast
);
cy.get(workspaceVarSelectors.workspaceVarName(data.lastName)).should(
"be.visible"
);
cy.get(
workspaceVarSelectors.workspaceVarDeleteButton(data.lastName)
).click();
cy.get(commonSelectors.buttonSelector("Yes")).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
workspaceVarText.workspaceVarDeletedToast
);
}); });
}); });

View file

@ -0,0 +1,340 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { fake } from "Fixtures/fake";
import { workspaceConstantsSelectors } from "Selectors/workspaceConstants";
import { workspaceConstantsText } from "Texts/workspaceConstants";
import { commonText, commonWidgetText } from "Texts/common";
import * as common from "Support/utils/common";
import {
contantsNameValidation,
AddNewconstants,
} from "Support/utils/workspaceConstants";
import { buttonText } from "Texts/button";
import {
verifyAndModifyParameter,
editAndVerifyWidgetName,
} from "Support/utils/commonWidget";
import { verifypreview } from "Support/utils/dataSource";
import {
selectQueryFromLandingPage,
query,
addInputOnQueryField,
} from "Support/utils/queries";
const data = {};
data.constName = fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.newConstvalue = `New ${data.constName}`;
data.constantsName = fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.constantsValue = "dJ_8Q~BcaMPd";
data.appName = `${fake.companyName}-App`;
data.slug = data.appName.toLowerCase().replace(/\s+/g, "-");
describe("Workspace constants", () => {
const envVar = Cypress.env("environment");
beforeEach(() => {
cy.appUILogin();
cy.intercept("GET", "/api/apps?page=1&folder=&searchKey=").as("homePage");
});
it("Verify workspace constants UI and CRUD operations", () => {
cy.get(commonSelectors.workspaceSettingsIcon).click();
cy.get(commonSelectors.workspaceConstantsOption)
.should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
"Workspace constants"
);
})
.click();
cy.get(commonSelectors.breadcrumbTitle).should(($el) => {
expect($el.contents().first().text().trim()).to.eq("Workspace settings");
});
cy.get(commonSelectors.breadcrumbPageTitle).verifyVisibleElement(
"have.text",
" Workspace constants"
);
cy.get(
workspaceConstantsSelectors.workspaceConstantsHelperText
).verifyVisibleElement(
"have.text",
workspaceConstantsText.workspaceConstantsHelperText
);
cy.get(commonSelectors.documentationLink).verifyVisibleElement(
"have.text",
commonText.documentationLink
);
cy.get("body").then(($body) => {
if ($body.find(workspaceConstantsSelectors.emptyStateImage).length > 0) {
cy.get(workspaceConstantsSelectors.emptyStateImage).should(
"be.visible"
);
cy.get(
workspaceConstantsSelectors.emptyStateHeader
).verifyVisibleElement(
"have.text",
workspaceConstantsText.emptyStateHeader
);
cy.get(workspaceConstantsSelectors.emptyStateText).verifyVisibleElement(
"have.text",
workspaceConstantsText.emptyStateText
);
cy.get(
workspaceConstantsSelectors.addNewConstantButton
).verifyVisibleElement(
"have.text",
workspaceConstantsText.addNewConstantButton
);
}
});
cy.get(workspaceConstantsSelectors.addNewConstantButton).click();
cy.get(workspaceConstantsSelectors.contantFormTitle).verifyVisibleElement(
"have.text",
workspaceConstantsText.addConstatntText
);
cy.get(commonSelectors.nameLabel).verifyVisibleElement("have.text", "Name");
cy.get(commonSelectors.nameInputField)
.invoke("attr", "placeholder")
.should("eq", "Enter Constant Name");
cy.get(commonSelectors.nameInputField).should("be.visible");
cy.get(commonSelectors.valueLabel).verifyVisibleElement(
"have.text",
"Value"
);
cy.get(commonSelectors.valueInputField)
.invoke("attr", "placeholder")
.should("eq", "Enter Value");
cy.get(commonSelectors.valueInputField).should("be.visible");
cy.get(commonSelectors.cancelButton).verifyVisibleElement(
"have.text",
"Cancel"
);
cy.get(workspaceConstantsSelectors.addConstantButton).verifyVisibleElement(
"have.text",
"Add constant"
);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.disabled");
contantsNameValidation(" ", commonText.constantsNameError);
contantsNameValidation("9", commonText.constantsNameError);
contantsNameValidation("%", commonText.constantsNameError);
contantsNameValidation(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`a",
"Maximum length has been reached"
);
contantsNameValidation(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`afgg",
"Constant name should be between 1 and 32 characters"
);
cy.clearAndType(commonSelectors.valueInputField, " ");
cy.get(commonSelectors.valueErrorText).verifyVisibleElement(
"have.text",
commonText.constantsValueError
);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.disabled");
cy.get(commonSelectors.cancelButton).click();
cy.get(workspaceConstantsSelectors.addNewConstantButton).click();
cy.clearAndType(commonSelectors.nameInputField, data.constName);
cy.clearAndType(commonSelectors.valueInputField, data.constName);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.enabled");
cy.get(commonSelectors.cancelButton).click();
cy.get(workspaceConstantsSelectors.constantName(data.constName)).should(
"not.exist"
);
cy.get(workspaceConstantsSelectors.addNewConstantButton).click();
cy.clearAndType(commonSelectors.nameInputField, data.constName);
cy.clearAndType(commonSelectors.valueInputField, data.constName);
cy.get(workspaceConstantsSelectors.addConstantButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
workspaceConstantsText.constantCreatedToast
);
cy.get(workspaceConstantsSelectors.addNewConstantButton).click();
contantsNameValidation(
data.constName,
"Constant with this name already exists in Production environment"
);
cy.get(commonSelectors.cancelButton).click();
cy.get(workspaceConstantsSelectors.envName).verifyVisibleElement(
"have.text",
"Production"
);
cy.get(
workspaceConstantsSelectors.addNewConstantButton
).verifyVisibleElement("have.text", "Create new constant");
cy.get(
workspaceConstantsSelectors.constantsTableNameHeader
).verifyVisibleElement("have.text", "Name");
cy.get(
workspaceConstantsSelectors.constantsTableValueHeader
).verifyVisibleElement("have.text", "Value");
cy.get(
workspaceConstantsSelectors.constantName(data.constName)
).verifyVisibleElement("have.text", data.constName);
cy.get(
workspaceConstantsSelectors.constantValue(data.constName)
).verifyVisibleElement("have.text", data.constName);
cy.get(
workspaceConstantsSelectors.constEditButton(data.constName)
).verifyVisibleElement("have.text", "Edit");
cy.get(
workspaceConstantsSelectors.constDeleteButton(data.constName)
).verifyVisibleElement("have.text", "Delete");
cy.get(commonSelectors.pagination).should("be.visible");
cy.get(workspaceConstantsSelectors.constEditButton(data.constName)).click();
cy.get(workspaceConstantsSelectors.contantFormTitle).verifyVisibleElement(
"have.text",
"Update constant in production "
);
cy.get(commonSelectors.nameLabel).verifyVisibleElement("have.text", "Name");
cy.get(commonSelectors.nameInputField).should("have.value", data.constName);
cy.get(commonSelectors.nameInputField)
.should("be.visible")
.and("be.disabled");
cy.get(commonSelectors.valueLabel).verifyVisibleElement(
"have.text",
"Value"
);
cy.get(commonSelectors.valueInputField)
.should("be.visible")
.and("have.value", data.constName);
cy.get(commonSelectors.cancelButton).verifyVisibleElement(
"have.text",
"Cancel"
);
cy.get(workspaceConstantsSelectors.addConstantButton).verifyVisibleElement(
"have.text",
"Update"
);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.disabled");
cy.clearAndType(commonSelectors.valueInputField, data.newConstvalue);
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.enabled");
cy.get(commonSelectors.cancelButton).click();
cy.get(
workspaceConstantsSelectors.constantValue(data.constName)
).verifyVisibleElement("have.text", data.constName);
cy.get(workspaceConstantsSelectors.constEditButton(data.constName)).click();
cy.clearAndType(commonSelectors.valueInputField, data.newConstvalue);
cy.get(workspaceConstantsSelectors.addConstantButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Constant updated successfully"
);
cy.get(
workspaceConstantsSelectors.constantValue(data.constName)
).verifyVisibleElement("have.text", data.newConstvalue);
cy.get(
workspaceConstantsSelectors.constDeleteButton(data.constName)
).click();
cy.get(commonSelectors.modalMessage).verifyVisibleElement(
"have.text",
`Are you sure you want to delete ${data.constName} from production?`
);
cy.get(commonSelectors.cancelButton).verifyVisibleElement(
"have.text",
"Cancel"
);
cy.get(commonSelectors.yesButton).verifyVisibleElement("have.text", "Yes");
cy.get(commonSelectors.cancelButton).click();
cy.get(
workspaceConstantsSelectors.constantValue(data.constName)
).verifyVisibleElement("have.text", data.newConstvalue);
cy.get(
workspaceConstantsSelectors.constDeleteButton(data.constName)
).click();
cy.get(commonSelectors.yesButton).click();
cy.get(workspaceConstantsSelectors.constantValue(data.constName)).should(
"not.exist"
);
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Constant deleted successfully"
);
});
it("should verify the constants resolving value on components and query", () => {
common.navigateToworkspaceConstants();
AddNewconstants(data.constantsName, data.constantsValue);
cy.get(
workspaceConstantsSelectors.constantName(data.constantsName)
).verifyVisibleElement("have.text", data.constantsName);
cy.get(
workspaceConstantsSelectors.constantValue(data.constantsName)
).verifyVisibleElement("have.text", data.constantsValue);
cy.get(commonSelectors.homePageLogo).click();
cy.wait("@homePage");
cy.createApp();
cy.renameApp(data.appName);
selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField("runjs", `return constants.${data.constantsName}`);
query("preview");
verifypreview("raw", "dJ_8Q~BcaMPd");
cy.dragAndDropWidget("Text", 550, 350);
editAndVerifyWidgetName(data.constantsName);
cy.waitForAutoSave();
verifyAndModifyParameter("Text", `{{constants.${data.constantsName}`);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
common.pinInspector();
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonWidgetSelector.inspectorNodeComponents).click();
cy.get(commonWidgetSelector.nodeComponent(data.constantsName)).click();
cy.get('[data-cy="inspector-node-text"] > .mx-2').verifyVisibleElement(
"have.text",
`"dJ_8Q~BcaMPd"`
);
cy.get('[data-cy="inspector-node-constants"] > .node-key').click();
cy.get(`[data-cy="inspector-node-${data.constantsName}"] > .node-key`)
.eq(1)
.verifyVisibleElement("have.text", data.constantsName);
cy.get(
`[data-cy="inspector-node-${data.constantsName}"] > .mx-2`
).verifyVisibleElement("have.text", `"dJ_8Q~BcaMPd"`);
cy.get('[data-cy="button-release"]').click();
cy.get('[data-cy="yes-button"]').click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${data.slug}`);
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constantsName)
).verifyVisibleElement("have.text", "dJ_8Q~BcaMPd");
cy.get('[data-cy="viewer-page-logo"]').click();
cy.wait("@homePage");
if (envVar === "Community") {
cy.visit(`/applications/${data.slug}`);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constantsName)
).verifyVisibleElement("have.text", "dJ_8Q~BcaMPd");
}
});
});

View file

@ -11,8 +11,10 @@ Cypress.Commands.add(
cy.clearAndType(commonSelectors.workEmailInputField, email); cy.clearAndType(commonSelectors.workEmailInputField, email);
cy.clearAndType(commonSelectors.passwordInputField, password); cy.clearAndType(commonSelectors.passwordInputField, password);
cy.get(commonSelectors.signInButton).click(); cy.get(commonSelectors.signInButton).click();
cy.intercept("GET", "api/library_apps").as("apps");
cy.wait("@apps");
cy.wait(4000);
cy.get(commonSelectors.homePageLogo).should("be.visible"); cy.get(commonSelectors.homePageLogo).should("be.visible");
cy.wait(2000);
} }
); );
@ -24,63 +26,20 @@ Cypress.Commands.add("forceClickOnCanvas", () => {
cy.get(commonSelectors.canvas).click("topRight", { force: true }); cy.get(commonSelectors.canvas).click("topRight", { force: true });
}); });
Cypress.Commands.add("verifyToastMessage", (selector, message) => { Cypress.Commands.add(
cy.get(selector).should("contain.text", message); "verifyToastMessage",
(selector, message, closeAction = true) => {
cy.get(selector).as("toast").should("contain.text", message);
if (closeAction) {
cy.get("body").then(($body) => { cy.get("body").then(($body) => {
if ($body.find(commonSelectors.toastCloseButton).length > 0) { if ($body.find(commonSelectors.toastCloseButton).length > 0) {
cy.closeToastMessage(); cy.closeToastMessage();
cy.wait(200); cy.wait(200);
} }
}); });
}); }
}
Cypress.Commands.add("appLogin", () => { );
cy.request({
url: "http://localhost:3000/api/authenticate",
method: "POST",
body: {
email: "dev@tooljet.io",
password: "password",
},
})
.its("body")
.then((res) =>
localStorage.setItem(
"currentUser",
JSON.stringify({
id: res.id,
auth_token: res.auth_token,
email: res.email,
first_name: res.first_name,
last_name: res.last_name,
organization_id: res.organization_id,
organization: res.organization,
admin: true,
group_permissions: [
{
id: res.id,
organization_id: res.organization_id,
group: res.group,
app_create: false,
app_delete: false,
folder_create: false,
},
{
id: res.id,
organization_id: res.organization_id,
group: res.group,
app_create: true,
app_delete: true,
folder_create: true,
},
],
app_group_permissions: [],
})
)
);
cy.visit("/");
});
Cypress.Commands.add("waitForAutoSave", () => { Cypress.Commands.add("waitForAutoSave", () => {
cy.wait(200); cy.wait(200);
@ -92,16 +51,16 @@ Cypress.Commands.add("waitForAutoSave", () => {
}); });
Cypress.Commands.add("createApp", (appName) => { Cypress.Commands.add("createApp", (appName) => {
const getAppButtonSelector = ($title) =>
$title.text().includes(commonText.introductionMessage)
? commonSelectors.emptyAppCreateButton
: commonSelectors.appCreateButton;
cy.get("body").then(($title) => { cy.get("body").then(($title) => {
if ($title.text().includes(commonText.introductionMessage)) { cy.get(getAppButtonSelector($title)).click();
cy.get(commonSelectors.emptyAppCreateButton).eq(0).click();
} else {
cy.get(commonSelectors.appCreateButton).click();
}
cy.intercept("GET", "/api/apps/**/versions").as("appVersion");
cy.wait("@appVersion", { timeout: 15000 });
cy.skipEditorPopover();
}); });
cy.waitForAppLoad();
cy.skipEditorPopover();
}); });
Cypress.Commands.add( Cypress.Commands.add(
@ -134,8 +93,10 @@ Cypress.Commands.add("appUILogin", () => {
cy.clearAndType(commonSelectors.workEmailInputField, "dev@tooljet.io"); cy.clearAndType(commonSelectors.workEmailInputField, "dev@tooljet.io");
cy.clearAndType(commonSelectors.passwordInputField, "password"); cy.clearAndType(commonSelectors.passwordInputField, "password");
cy.get(commonSelectors.signInButton).click(); cy.get(commonSelectors.signInButton).click();
cy.intercept("GET", "api/library_apps").as("apps");
cy.wait("@apps");
cy.wait(3000);
cy.get(commonSelectors.homePageLogo).should("be.visible"); cy.get(commonSelectors.homePageLogo).should("be.visible");
cy.wait(2000);
}); });
Cypress.Commands.add( Cypress.Commands.add(
@ -203,7 +164,6 @@ Cypress.Commands.add("openInCurrentTab", (selector) => {
Cypress.Commands.add("modifyCanvasSize", (x, y) => { Cypress.Commands.add("modifyCanvasSize", (x, y) => {
cy.get("[data-cy='left-sidebar-settings-button']").click(); cy.get("[data-cy='left-sidebar-settings-button']").click();
cy.clearAndType("[data-cy='maximum-canvas-width-input-field']", x); cy.clearAndType("[data-cy='maximum-canvas-width-input-field']", x);
cy.clearAndType("[data-cy='maximum-canvas-height-input-field']", y);
cy.forceClickOnCanvas(); cy.forceClickOnCanvas();
}); });
@ -212,6 +172,7 @@ Cypress.Commands.add("renameApp", (appName) => {
`{selectAll}{backspace}${appName}`, `{selectAll}{backspace}${appName}`,
{ force: true } { force: true }
); );
cy.forceClickOnCanvas();
cy.waitForAutoSave(); cy.waitForAutoSave();
}); });
@ -286,13 +247,38 @@ Cypress.Commands.add("reloadAppForTheElement", (elementText) => {
}); });
Cypress.Commands.add("skipEditorPopover", () => { Cypress.Commands.add("skipEditorPopover", () => {
cy.wait(3000); cy.wait(2000);
cy.get("body").then(($el) => { cy.get("body").then(($el) => {
if ($el.text().includes("Skip", { timeout: 2000 })) { if ($el.text().includes("Skip", { timeout: 2000 })) {
cy.wait(200);
cy.get(commonSelectors.skipButton).realClick(); cy.get(commonSelectors.skipButton).realClick();
} else {
cy.log("instructions modal is skipped ");
} }
}); });
const log = Cypress.log({
name: "Skip Popover",
displayName: "Skip Popover",
message: " Popover skipped",
});
});
Cypress.Commands.add("waitForAppLoad", () => {
const API_ENDPOINT =
Cypress.env("environment") === "Community"
? "/api/v2/data_sources"
: "/api/app-environments/**";
const TIMEOUT = 15000;
cy.intercept("GET", API_ENDPOINT).as("appDs");
cy.wait("@appDs", { timeout: TIMEOUT });
});
Cypress.Commands.add("visitTheWorkspace", (workspaceName) => {
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: `select id from organizations where name='${workspaceName}';`,
}).then((resp) => {
let workspaceId = resp.rows[0].id;
cy.visit(workspaceId);
});
cy.wait(2000);
}); });

View file

@ -1,10 +1,11 @@
import { commonText, path } from "Texts/common"; import { commonText, path } from "Texts/common";
import { usersSelector } from "Selectors/manageUsers"; import { usersSelector } from "Selectors/manageUsers";
import { profileSelector } from "Selectors/profile"; import { profileSelector } from "Selectors/profile";
import { commonSelectors } from "Selectors/common"; import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import moment from "moment"; import moment from "moment";
import { dashboardSelector } from "Selectors/dashboard"; import { dashboardSelector } from "Selectors/dashboard";
import { groupsSelector } from "Selectors/manageGroups"; import { groupsSelector } from "Selectors/manageGroups";
import { groupsText } from "Texts/manageGroups";
export const navigateToProfile = () => { export const navigateToProfile = () => {
cy.get(commonSelectors.profileSettings).click(); cy.get(commonSelectors.profileSettings).click();
@ -15,6 +16,9 @@ export const navigateToProfile = () => {
export const logout = () => { export const logout = () => {
cy.get(commonSelectors.profileSettings).click(); cy.get(commonSelectors.profileSettings).click();
cy.get(commonSelectors.logoutLink).click(); cy.get(commonSelectors.logoutLink).click();
cy.intercept("GET", "/api/metadata").as("publicConfig");
cy.wait("@publicConfig");
cy.wait(500);
}; };
export const navigateToManageUsers = () => { export const navigateToManageUsers = () => {
@ -25,13 +29,22 @@ export const navigateToManageUsers = () => {
export const navigateToManageGroups = () => { export const navigateToManageGroups = () => {
cy.get(commonSelectors.workspaceSettingsIcon).click(); cy.get(commonSelectors.workspaceSettingsIcon).click();
cy.get(commonSelectors.manageGroupsOption).click(); cy.get(commonSelectors.manageGroupsOption).click();
navigateToAllUserGroup();
};
export const navigateToAllUserGroup = () => {
cy.get(groupsSelector.groupLink("Admin")).click(); cy.get(groupsSelector.groupLink("Admin")).click();
cy.get(groupsSelector.groupLink("All users")).click(); cy.get(groupsSelector.groupLink("All users")).click();
cy.get(groupsSelector.groupLink("Admin")).click(); cy.get(groupsSelector.groupLink("Admin")).click();
cy.get(groupsSelector.groupLink("All users")).click(); cy.get(groupsSelector.groupLink("All users")).click();
cy.wait(500); cy.wait(1000);
cy.get("body").then(($title) => { cy.get("body").then(($title) => {
if ($title.text().includes("Admin has edit access to all apps. These are not editable")) { if (
$title
.text()
.includes("Admin has edit access to all apps. These are not editable")
) {
cy.get(groupsSelector.groupLink("Admin")).click(); cy.get(groupsSelector.groupLink("Admin")).click();
cy.get(groupsSelector.groupLink("All users")).click(); cy.get(groupsSelector.groupLink("All users")).click();
cy.get(groupsSelector.groupLink("Admin")).click(); cy.get(groupsSelector.groupLink("Admin")).click();
@ -96,7 +109,15 @@ export const navigateToAppEditor = (appName) => {
.trigger("mouseenter") .trigger("mouseenter")
.find(commonSelectors.editButton) .find(commonSelectors.editButton)
.click({ force: true }); .click({ force: true });
//cy.wait("@appEditor"); if (Cypress.env("environment") === "Community") {
cy.intercept("GET", "/api/v2/data_sources").as("appDs");
cy.wait("@appDs", { timeout: 15000 });
cy.skipEditorPopover();
} else {
cy.intercept("GET", "/api/app-environments/**").as("appDs");
cy.wait("@appDs", { timeout: 15000 });
cy.skipEditorPopover();
}
}; };
export const viewAppCardOptions = (appName) => { export const viewAppCardOptions = (appName) => {
@ -160,6 +181,12 @@ export const cancelModal = (buttonText) => {
cy.get(commonSelectors.modalComponent).should("not.exist"); cy.get(commonSelectors.modalComponent).should("not.exist");
}; };
export const navigateToAuditLogsPage = () => {
cy.get(profileSelector.profileDropdown).invoke("show");
cy.contains("Audit Logs").click();
cy.url().should("include", path.auditLogsPath, { timeout: 1000 });
};
export const manageUsersPagination = (email) => { export const manageUsersPagination = (email) => {
cy.wait(200); cy.wait(200);
cy.get("body").then(($email) => { cy.get("body").then(($email) => {
@ -187,7 +214,7 @@ export const createWorkspace = (workspaceName) => {
export const selectAppCardOption = (appName, appCardOption) => { export const selectAppCardOption = (appName, appCardOption) => {
viewAppCardOptions(appName); viewAppCardOptions(appName);
cy.get(appCardOption).should("be.visible").realClick(); cy.get(appCardOption).should("be.visible").click({ force: true });
}; };
export const navigateToDatabase = () => { export const navigateToDatabase = () => {
@ -206,3 +233,31 @@ export const verifyTooltip = (selector, message) => {
cy.get(".tooltip-inner").last().should("have.text", message); cy.get(".tooltip-inner").last().should("have.text", message);
}); });
}; };
export const pinInspector = () => {
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonSelectors.inspectorPinIcon).click();
cy.wait(500);
cy.get("body").then(($body) => {
if (!$body.find(commonSelectors.inspectorPinIcon).length > 0) {
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonSelectors.inspectorPinIcon).click();
}
});
};
export const createGroup = (groupName) => {
cy.get(groupsSelector.createNewGroupButton).click();
cy.clearAndType(groupsSelector.groupNameInput, groupName);
cy.get(groupsSelector.createGroupButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.groupCreatedToast
);
};
export const navigateToworkspaceConstants = () => {
cy.get(commonSelectors.workspaceSettingsIcon).click();
cy.get(commonSelectors.workspaceConstantsOption).click();
};

View file

@ -67,7 +67,7 @@ export const verifyAndModifyToggleFx = (
export const addDefaultEventHandler = (message) => { export const addDefaultEventHandler = (message) => {
cy.get(commonWidgetSelector.addEventHandlerLink) cy.get(commonWidgetSelector.addEventHandlerLink)
.should("have.text", commonWidgetText.addEventHandlerLink) .should("contain.text", commonWidgetText.addEventHandlerLink)
.click(); .click();
cy.get(commonWidgetSelector.eventHandlerCard).click(); cy.get(commonWidgetSelector.eventHandlerCard).click();
cy.get(commonWidgetSelector.alertMessageInputField) cy.get(commonWidgetSelector.alertMessageInputField)

View file

@ -24,7 +24,8 @@ export const verifypreview = (type, data) => {
cy.get(`[data-cy="preview-tab-${type}"]`).click(); cy.get(`[data-cy="preview-tab-${type}"]`).click();
cy.get(`[data-cy="preview-${type}-data-container"]`).verifyVisibleElement( cy.get(`[data-cy="preview-${type}-data-container"]`).verifyVisibleElement(
"contain.text", "contain.text",
data data,
[{ timeout: 15000 }]
); );
}; };
@ -45,6 +46,22 @@ export const deleteDatasource = (datasourceName) => {
); );
}); });
cy.get('[data-cy="yes-button"]').click(); cy.get('[data-cy="yes-button"]').click();
cy.wait(1000);
cy.get("body").then(($body) => {
if (
$body.find(`[data-cy="${cyParamName(datasourceName)}-button"]`).length > 0
) {
cy.get(`[data-cy="${cyParamName(datasourceName)}-button"]`)
.parent()
.within(() => {
cy.get(
`[data-cy="${cyParamName(datasourceName)}-delete-button"]`
).invoke("click");
});
cy.get('[data-cy="yes-button"]').click();
}
});
}; };
export const closeDSModal = () => { export const closeDSModal = () => {

View file

@ -17,6 +17,7 @@ export const openNode = (node, index = 0) => {
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .node-key`) cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .node-key`)
.eq(index) .eq(index)
.click(); .click();
cy.wait(1000);
}; };
export const verifyValue = (node, type, children, index = 0) => { export const verifyValue = (node, type, children, index = 0) => {

View file

@ -2,6 +2,7 @@ import { groupsSelector } from "Selectors/manageGroups";
import { groupsText } from "Texts/manageGroups"; import { groupsText } from "Texts/manageGroups";
import { commonSelectors } from "Selectors/common"; import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common"; import { commonText } from "Texts/common";
import { navigateToAllUserGroup } from "../utils/common";
export const manageGroupsElements = () => { export const manageGroupsElements = () => {
cy.get(groupsSelector.groupLink("All users")).verifyVisibleElement( cy.get(groupsSelector.groupLink("All users")).verifyVisibleElement(
@ -13,8 +14,7 @@ export const manageGroupsElements = () => {
groupsText.admin groupsText.admin
); );
cy.get(groupsSelector.groupLink("Admin")).click(); navigateToAllUserGroup();
cy.get(groupsSelector.groupLink("All users")).click();
cy.get(groupsSelector.groupPageTitle("All Users")).verifyVisibleElement( cy.get(groupsSelector.groupPageTitle("All Users")).verifyVisibleElement(
"have.text", "have.text",
@ -158,8 +158,7 @@ export const manageGroupsElements = () => {
); );
cy.get(groupsSelector.workspaceVarCheckbox).uncheck(); cy.get(groupsSelector.workspaceVarCheckbox).uncheck();
cy.get(groupsSelector.groupLink("Admin")).click(); navigateToAllUserGroup();
cy.get(groupsSelector.groupLink("All users")).click();
cy.get(groupsSelector.groupLink("Admin")).click(); cy.get(groupsSelector.groupLink("Admin")).click();
cy.get(groupsSelector.groupLink("Admin")).verifyVisibleElement( cy.get(groupsSelector.groupLink("Admin")).verifyVisibleElement(
"have.text", "have.text",
@ -235,3 +234,30 @@ export const manageGroupsElements = () => {
"be.disabled" "be.disabled"
); );
}; };
export const addAppToGroup = (appName) => {
cy.get(groupsSelector.appsLink).click();
cy.wait(500);
cy.get(groupsSelector.appSearchBox).realClick();
cy.wait(500);
cy.get(groupsSelector.searchBoxOptions).contains(appName).click();
cy.get(groupsSelector.selectAddButton).click();
cy.contains("tr", appName)
.parent()
.within(() => {
cy.get("td input").eq(1).check();
});
cy.verifyToastMessage(
commonSelectors.toastMessage,
"App permissions updated"
);
};
export const addUserToGroup = (groupName, email) => {
cy.get(groupsSelector.usersLink).click();
cy.get(".select-search__input").type(email);
cy.get(".item-renderer").within(() => {
cy.get("input").check();
});
cy.get(`[data-cy="${groupName}-group-add-button"]`).click();
};

View file

@ -206,6 +206,7 @@ export const visitWorkspaceLoginPage = () => {
cy.get(ssoSelector.workspaceLoginUrl).then(($temp) => { cy.get(ssoSelector.workspaceLoginUrl).then(($temp) => {
const url = $temp.text(); const url = $temp.text();
common.logout(); common.logout();
cy.wait(1000)
cy.visit(url); cy.visit(url);
}); });
}; };
@ -263,6 +264,7 @@ export const workspaceLogin = (workspaceName) => {
cy.clearAndType(commonSelectors.workEmailInputField, "dev@tooljet.io"); cy.clearAndType(commonSelectors.workEmailInputField, "dev@tooljet.io");
cy.clearAndType(commonSelectors.passwordInputField, "password"); cy.clearAndType(commonSelectors.passwordInputField, "password");
cy.get(commonSelectors.loginButton).click(); cy.get(commonSelectors.loginButton).click();
cy.wait(2000)
cy.get(commonSelectors.homePageLogo).should("be.visible"); cy.get(commonSelectors.homePageLogo).should("be.visible");
cy.get(commonSelectors.workspaceName).verifyVisibleElement( cy.get(commonSelectors.workspaceName).verifyVisibleElement(
"have.text", "have.text",
@ -297,14 +299,6 @@ export const signInPageElements = () => {
"have.text", "have.text",
ssoText.signInHeader ssoText.signInHeader
); );
cy.get(ssoSelector.googleSSOText).verifyVisibleElement(
"have.text",
ssoText.googleSSOText
);
cy.get(ssoSelector.gitSSOText).verifyVisibleElement(
"have.text",
ssoText.gitSignInText
);
cy.get(commonSelectors.workEmailLabel).verifyVisibleElement( cy.get(commonSelectors.workEmailLabel).verifyVisibleElement(
"have.text", "have.text",
commonText.workEmailLabel commonText.workEmailLabel
@ -325,6 +319,19 @@ export const signInPageElements = () => {
cy.get(commonSelectors.workEmailInputField).should("be.visible"); cy.get(commonSelectors.workEmailInputField).should("be.visible");
cy.get(commonSelectors.passwordInputField).should("be.visible"); cy.get(commonSelectors.passwordInputField).should("be.visible");
cy.get("body").then(($el) => {
if ($el.text().includes("Google")) {
cy.get(ssoSelector.googleSSOText).verifyVisibleElement(
"have.text",
ssoText.googleSSOText
);
cy.get(ssoSelector.gitSSOText).verifyVisibleElement(
"have.text",
ssoText.gitSignInText
);
}
});
}; };
export const SignUpPageElements = () => { export const SignUpPageElements = () => {

View file

@ -32,18 +32,15 @@ export const manageUsersElements = () => {
cy.contains("td", usersText.adminUserEmail) cy.contains("td", usersText.adminUserEmail)
.parent() .parent()
.within(() => { .within(() => {
cy.get(usersSelector.userName(usersText.adminUserName)).verifyVisibleElement( cy.get(
"have.text", usersSelector.userName(usersText.adminUserName)
usersText.adminUserName ).verifyVisibleElement("have.text", usersText.adminUserName);
); cy.get(
cy.get(usersSelector.userEmail(usersText.adminUserName)).verifyVisibleElement( usersSelector.userEmail(usersText.adminUserName)
"have.text", ).verifyVisibleElement("have.text", usersText.adminUserEmail);
usersText.adminUserEmail cy.get(
); usersSelector.userStatus(usersText.adminUserName)
cy.get(usersSelector.userStatus(usersText.adminUserName)).verifyVisibleElement( ).verifyVisibleElement("have.text", usersText.activeStatus);
"have.text",
usersText.activeStatus
);
cy.get("td button").verifyVisibleElement( cy.get("td button").verifyVisibleElement(
"have.text", "have.text",
usersText.adminUserState usersText.adminUserState
@ -77,8 +74,17 @@ export const manageUsersElements = () => {
"have.text", "have.text",
commonText.labelEmailInput commonText.labelEmailInput
); );
cy.get(commonSelectors.inputFieldEmailAddress).should("be.visible"); cy.get(commonSelectors.inputFieldEmailAddress).should("be.visible");
cy.get(commonSelectors.groupInputFieldLabel).verifyVisibleElement(
"have.text",
commonText.groupInputFieldLabel
);
cy.get(".dropdown-heading-value > .gray").verifyVisibleElement(
"have.text",
"Select groups to add for this user"
);
cy.get(commonSelectors.cancelButton).verifyVisibleElement( cy.get(commonSelectors.cancelButton).verifyVisibleElement(
"have.text", "have.text",
usersText.cancelButton usersText.cancelButton
@ -126,67 +132,14 @@ export const manageUsersElements = () => {
}; };
export const inviteUser = (firstName, email) => { export const inviteUser = (firstName, email) => {
let invitationToken, fillUserInviteForm(firstName, email);
organizationToken,
workspaceId,
userId,
url = "";
cy.get(usersSelector.buttonAddUsers).click();
cy.clearAndType(commonSelectors.inputFieldFullName, firstName);
cy.clearAndType(commonSelectors.inputFieldEmailAddress, email);
cy.get(usersSelector.buttonInviteUsers).click(); cy.get(usersSelector.buttonInviteUsers).click();
cy.verifyToastMessage( cy.verifyToastMessage(
commonSelectors.toastMessage, commonSelectors.toastMessage,
usersText.userCreatedToast usersText.userCreatedToast
); );
cy.task("updateId", { cy.wait(1000)
dbconfig: Cypress.env("app_db"), fetchAndVisitInviteLink(email);
sql: `select invitation_token from users where email='${email}';`,
}).then((resp) => {
invitationToken = resp.rows[0].invitation_token;
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: "select id from organizations where name='My workspace';",
}).then((resp) => {
workspaceId = resp.rows[0].id;
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: `select id from users where email='${email}';`,
}).then((resp) => {
userId = resp.rows[0].id;
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: `select invitation_token from organization_users where user_id='${userId}';`,
}).then((resp) => {
organizationToken = resp.rows[1].invitation_token;
url = `/invitations/${invitationToken}/workspaces/${organizationToken}?oid=${workspaceId}`;
common.logout();
cy.wait(500);
cy.visit(url);
});
});
});
});
};
export const addNewUser = (firstName, email) => {
cy.intercept("POST", "/api/organization_users").as("appLibrary");
cy.clearAndType(commonSelectors.inputFieldFullName, firstName);
cy.clearAndType(commonSelectors.inputFieldEmailAddress, email);
cy.get(usersSelector.createUserButton).click();
cy.wait("@appLibrary").then((res) => {
const invitation1 = res.response.body.users.user.invitation_token;
const invitation2 = res.response.body.users.invitation_token;
const url = `http://localhost:8082/invitations/${invitation1}/workspaces/${invitation2}`;
common.logout();
cy.visit(url);
});
}; };
export const confirmInviteElements = () => { export const confirmInviteElements = () => {
@ -269,3 +222,100 @@ export const bulkUserUpload = (file, fileName, toastMessage) => {
cy.wait(200); cy.wait(200);
}; };
export const inviteUserWithUserGroup = (firstName, email, group1, group2) => {
fillUserInviteForm(firstName, email);
selectUserGroup(group1);
selectUserGroup(group2);
cy.get(usersSelector.buttonInviteUsers).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
usersText.userCreatedToast
);
// copyInvitationLink(firstName, email);
cy.wait(1000)
fetchAndVisitInviteLink(email)
cy.clearAndType(commonSelectors.passwordInputField, "password");
cy.get(commonSelectors.acceptInviteButton).click();
};
export const copyInvitationLink = (firstName, email) => {
cy.window().then((win) => {
cy.stub(win, "prompt").returns(win.prompt).as("copyToClipboardPrompt");
});
common.searchUser(email);
cy.contains("td", email)
.parent()
.within(() => {
cy.get('[data-cy="copy-icon"]').click();
});
cy.verifyToastMessage(
commonSelectors.toastMessage,
usersText.inviteCopiedToast
);
cy.get("@copyToClipboardPrompt").then((prompt) => {
common.logout();
cy.visit(prompt.args[0][1]);
});
};
export const fillUserInviteForm = (firstName, email) => {
cy.get(usersSelector.buttonAddUsers).click();
cy.clearAndType(commonSelectors.inputFieldFullName, firstName);
cy.clearAndType(commonSelectors.inputFieldEmailAddress, email);
};
export const selectUserGroup = (groupName) => {
cy.wait(500)
cy.get("body").then(($body) => {
if (!$body.find(".search > input").length > 0) {
cy.get(".dropdown-heading-value > .gray").click();
cy.clearAndType(".search > input", groupName);
cy.get("li > .select-item > .item-renderer").last().click();
} else {
cy.clearAndType(".search > input", groupName);
cy.get("li > .select-item > .item-renderer").last().click();
}
});
};
export const fetchAndVisitInviteLink = (email) => {
let invitationToken,
organizationToken,
workspaceId,
userId,
url = "";
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: `select invitation_token from users where email='${email}';`,
}).then((resp) => {
invitationToken = resp.rows[0].invitation_token;
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: "select id from organizations where name='My workspace';",
}).then((resp) => {
workspaceId = resp.rows[0].id;
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: `select id from users where email='${email}';`,
}).then((resp) => {
userId = resp.rows[0].id;
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: `select invitation_token from organization_users where user_id='${userId}';`,
}).then((resp) => {
organizationToken = resp.rows[1].invitation_token;
url = `/invitations/${invitationToken}/workspaces/${organizationToken}?oid=${workspaceId}`;
common.logout();
cy.wait(1000);
cy.visit(url);
});
});
});
});
};

View file

@ -7,7 +7,7 @@ import { commonWidgetText } from "Texts/common";
import { openAccordion, openEditorSidebar } from "Support/utils/commonWidget"; import { openAccordion, openEditorSidebar } from "Support/utils/commonWidget";
import { postgreSqlSelector } from "Selectors/postgreSql"; import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql"; import { postgreSqlText } from "Texts/postgreSql";
import { closeDSModal} from "Support/utils/dataSource"; import { closeDSModal } from "Support/utils/dataSource";
export const addQuery = (queryName, query, dbName) => { export const addQuery = (queryName, query, dbName) => {
@ -15,18 +15,14 @@ export const addQuery = (queryName, query, dbName) => {
cy.get(`[data-cy="${dbName}-add-query-card"]`) cy.get(`[data-cy="${dbName}-add-query-card"]`)
.should("contain", dbName) .should("contain", dbName)
.click(); .click();
selectQueryMode(postgreSqlText.queryModeSql, "3"); // selectQueryMode(postgreSqlText.queryModeSql, "3");
cy.get('[data-cy="query-name-label"]').realHover().then(()=>{ cy.get('[data-cy="query-name-label"]').realHover().then(() => {
cy.get('[class*="breadcrum-rename-query-icon"]').click(); cy.get('[class*="breadcrum-rename-query-icon"]').click();
}); });
cy.get(postgreSqlSelector.queryLabelInputField).clear().type(queryName); cy.get(postgreSqlSelector.queryLabelInputField).clear().type(queryName);
cy.get(postgreSqlSelector.queryInputField).realMouseDown({ position: "center" }).realType(' '); cy.get(postgreSqlSelector.queryInputField).realMouseDown({ position: "center" }).realType(' ');
cy.get(postgreSqlSelector.queryInputField).clearAndTypeOnCodeMirror(query) cy.get(postgreSqlSelector.queryInputField).clearAndTypeOnCodeMirror(query)
cy.get(postgreSqlSelector.queryCreateAndRunButton).click(); cy.get(postgreSqlSelector.queryCreateAndRunButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${queryName}) completed.`
);
}; };
export const addQueryOnGui = (queryName, query) => { export const addQueryOnGui = (queryName, query) => {
@ -48,6 +44,7 @@ export const addQueryOnGui = (queryName, query) => {
export const selectDataSource = (dataSource) => { export const selectDataSource = (dataSource) => {
cy.get(commonSelectors.globalDataSourceIcon).click(); cy.get(commonSelectors.globalDataSourceIcon).click();
closeDSModal() closeDSModal()
cy.wait(500);
cy.get(commonSelectors.addNewDataSourceButton).click(); cy.get(commonSelectors.addNewDataSourceButton).click();
cy.get(postgreSqlSelector.dataSourceSearchInputField).type(dataSource); cy.get(postgreSqlSelector.dataSourceSearchInputField).type(dataSource);
cy.get(`[data-cy='data-source-${dataSource.toLowerCase()}']`).click(); cy.get(`[data-cy='data-source-${dataSource.toLowerCase()}']`).click();
@ -134,4 +131,4 @@ export const addWidgetsToAddUser = () => {
addEventHandlerToRunQuery("add_data_using_widgets"); addEventHandlerToRunQuery("add_data_using_widgets");
}; };
export const addListviewToVerifyData = () => {}; export const addListviewToVerifyData = () => { };

View file

@ -1,12 +1,12 @@
import { postgreSqlSelector } from "Selectors/postgreSql"; import { postgreSqlSelector } from "Selectors/postgreSql";
export const selectQuery = (dbName) => { export const selectQueryFromLandingPage = (dbName, label) => {
cy.get(postgreSqlSelector.buttonAddNewQueries).click();
cy.get( cy.get(
`[data-cy="${dbName.toLowerCase().replace(/\s+/g, "-")}-add-query-card"]` `[data-cy="${dbName.toLowerCase().replace(/\s+/g, "-")}-add-query-card"]`
) )
.should("contain", dbName) .should("contain", label)
.click(); .click();
cy.waitForAutoSave();
}; };
export const deleteQuery = (queryName) => { export const deleteQuery = (queryName) => {
@ -30,8 +30,16 @@ export const renameQueryFromEditor = (name) => {
}; };
export const addInputOnQueryField = (field, data) => { export const addInputOnQueryField = (field, data) => {
cy.get(`[data-cy="${field}-input-field"]`).clearAndTypeOnCodeMirror( cy.get(`[data-cy="${field}-input-field"]`)
`{backSpace}` .click()
); .clearAndTypeOnCodeMirror(`{backSpace}`);
cy.get(`[data-cy="${field}-input-field"]`).clearAndTypeOnCodeMirror(data); cy.get(`[data-cy="${field}-input-field"]`).clearAndTypeOnCodeMirror(data);
cy.forceClickOnCanvas();
};
export const waitForQueryAction = (action) => {
cy.get(`[data-cy="query-${action}-button"]`, { timeout: 20000 }).should(
"not.have.class",
"button-loading"
);
}; };

View file

@ -67,8 +67,10 @@ export const verifyAndEnterColumnOptionInput = (label, value) => {
cy.get(`[data-cy="input-and-label-${cyParamName(label)}"]`) cy.get(`[data-cy="input-and-label-${cyParamName(label)}"]`)
.realClick() .realClick()
.realPress(["Meta", "A"]) .realPress(["Meta", "A"])
.realType(`{backspace}{backspace}{backspace}{backspace}`)
.realPress(["Meta", "A"])
.realType( .realType(
`{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}${value}` `{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}${value}`
); );
}; };

View file

@ -11,6 +11,7 @@ import { dashboardSelector } from "Selectors/dashboard";
export const adminLogin = () => { export const adminLogin = () => {
common.logout(); common.logout();
cy.appUILogin(); cy.appUILogin();
cy.wait(2000);
common.navigateToManageGroups(); common.navigateToManageGroups();
}; };

View file

@ -105,7 +105,7 @@ export const deleteVersionAndVerify = (value, toastMessageText) => {
deleteVersionText.deleteModalText(value) deleteVersionText.deleteModalText(value)
); );
cy.get(confirmVersionModalSelectors.yesButton).click(); cy.get(confirmVersionModalSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, toastMessageText); cy.verifyToastMessage(commonSelectors.toastMessage, toastMessageText, false);
}; };
export const verifyDuplicateVersion = (newVersion = [], version) => { export const verifyDuplicateVersion = (newVersion = [], version) => {

View file

@ -0,0 +1,19 @@
import { commonSelectors } from "Selectors/common";
import { fake } from "Fixtures/fake";
import { workspaceConstantsSelectors } from "Selectors/workspaceConstants";
import { workspaceConstantsText } from "Texts/workspaceConstants";
import { commonText } from "Texts/common";
import * as common from "Support/utils/common";
export const contantsNameValidation = (value, error) => {
cy.clearAndType(commonSelectors.nameInputField, value);
cy.get(commonSelectors.nameErrorText).verifyVisibleElement("have.text", error)
cy.get(workspaceConstantsSelectors.addConstantButton).should("be.disabled");
}
export const AddNewconstants = (name, value) => {
cy.get(workspaceConstantsSelectors.addNewConstantButton).click();
cy.clearAndType(commonSelectors.nameInputField, name);
cy.clearAndType(commonSelectors.valueInputField, value);
cy.get(workspaceConstantsSelectors.addConstantButton).click();
}

View file

@ -0,0 +1,79 @@
# Create .env from this example file and replace values for the environment.
# The application expects a separate .env.test for test environment configuration
# Get detailed information about each variable here: https://docs.tooljet.com/docs/setup/env-vars
TOOLJET_HOST=http://localhost:80
LOCKBOX_MASTER_KEY= #replace_with_lockbox_master_key
SECRET_KEY_BASE= #replace_with_secret_key_base
# DATABASE CONFIG
ORM_LOGGING=all
PG_DB= # Databse name
PG_USER= # Postgres database username
PG_HOST= # Postgres database host
PG_PASS= # Postgres database password
PG_PORT=5432
# The above postgres values is set to its default state. If necessary, kindly modify it according to your personal preference.
# TOOLJET DATABASE
ENABLE_TOOLJET_DB=true
TOOLJET_DB= # Database name
TOOLJET_DB_USER= # Postgres database username
TOOLJET_DB_HOST= # Postgres database host
TOOLJET_DB_PASS= # Postgres database password
PGRST_HOST=postgrest
PGRST_DB_URI=
PGRST_JWT_SECRET= # If you have openssl installed, you can run the following command openssl rand -hex 32 to generate the value for PGRST_JWT_SECRET.
# Checks every 24 hours to see if a new version of ToolJet is available
# (Enabled by default. Set false to disable)
CHECK_FOR_UPDATES=true
# Checks every 24 hours to update app telemetry data to ToolJet hub.
# (Telemetry is enabled by default. Set value to true to disable.)
# DISABLE_TOOLJET_TELEMETRY=false
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# EMAIL CONFIGURATION
DEFAULT_FROM_EMAIL=hello@tooljet.io
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_DOMAIN=
SMTP_PORT=
# DISABLE USER SIGNUPS (true or false). only applicable if Multi-Workspace feature is enabled
DISABLE_SIGNUPS=
# OBSERVABILITY
APM_VENDOR=
SENTRY_DNS=
SENTRY_DEBUG=
# FEATURE TOGGLE
COMMENT_FEATURE_ENABLE=
ENABLE_MULTIPLAYER_EDITING=true
ENABLE_MARKETPLACE_FEATURE=
# SSO (Applicable only for Multi-Workspace)
SSO_GOOGLE_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_SECRET=
SSO_GIT_OAUTH2_HOST=
SSO_ACCEPTED_DOMAINS=
SSO_DISABLE_SIGNUPS=
#ONBOARDING
ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=
#session expiry in minutes
USER_SESSION_EXPIRY=2880
#TELEMETRY
DEPLOYMENT_PLATFORM=docker

View file

@ -3,25 +3,28 @@
# Get detailed information about each variable here: https://docs.tooljet.com/docs/setup/env-vars # Get detailed information about each variable here: https://docs.tooljet.com/docs/setup/env-vars
TOOLJET_HOST=http://localhost:80 TOOLJET_HOST=http://localhost:80
LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key LOCKBOX_MASTER_KEY= # replace_with_lockbox_master_key
SECRET_KEY_BASE=replace_with_secret_key_base SECRET_KEY_BASE= # replace_with_secret_key_base
# DATABASE CONFIG # DATABASE CONFIG
ORM_LOGGING=all ORM_LOGGING=all
PG_DB=tooljet_production PG_DB=tooljet_production
PG_USER=postgres PG_USER=postgres
PG_HOST=postgres PG_HOST=postgresql
PG_PASS=postgres PG_PASS= # postgres database password
# The above postgres values is set to its default state. If necessary, kindly modify it according to your personal preference.
# TOOLJET DATABASE # TOOLJET DATABASE
ENABLE_TOOLJET_DB= ENABLE_TOOLJET_DB=true
TOOLJET_DB= TOOLJET_DB=tooljet_db
TOOLJET_DB_USER= TOOLJET_DB_USER=postgres
TOOLJET_DB_HOST= TOOLJET_DB_HOST=postgresql
TOOLJET_DB_PASS= TOOLJET_DB_PASS=
PGRST_HOST=
PGRST_JWT_SECRET= PGRST_DB_URI= # postgres://<postgres_username>:<postgres_password><@postgres_hostname>/<database_name>
PGRST_HOST=postgrest
PGRST_JWT_SECRET= # If you have openssl installed, you can run the following command openssl rand -hex 32 to generate the value for PGRST_JWT_SECRET.
# Checks every 24 hours to see if a new version of ToolJet is available # Checks every 24 hours to see if a new version of ToolJet is available
# (Enabled by default. Set false to disable) # (Enabled by default. Set false to disable)

View file

@ -4,6 +4,7 @@ services:
tooljet: tooljet:
tty: true tty: true
stdin_open: true stdin_open: true
container_name: Tooljet-app
image: tooljet/tooljet-ce:latest image: tooljet/tooljet-ce:latest
restart: always restart: always
env_file: .env env_file: .env
@ -17,24 +18,25 @@ services:
command: npm run start:prod command: npm run start:prod
postgres: postgres:
container_name: ${PG_HOST}
image: postgres:13 image: postgres:13
restart: always restart: always
ports:
- 5432:5432
volumes: volumes:
- postgres:/var/lib/postgresql/data - postgres:/var/lib/postgresql/data
env_file: .env
environment: environment:
- POSTGRES_PASSWORD=postgres - POSTGRES_USER=${PG_USER}
- POSTGRES_PASSWORD=${PG_PASS}
# Uncomment if ENABLE_TOOLJET_DB=true postgrest:
# postgrest: container_name: postgrest
# image: postgrest/postgrest:v10.1.1.20221215 image: postgrest/postgrest:v10.1.1.20221215
# restart: always restart: always
# depends_on: depends_on:
# - postgres - postgres
# env_file: .env env_file: .env
# environment: environment:
# - PGRST_SERVER_PORT=80 - PGRST_SERVER_PORT=80
volumes: volumes:
postgres: postgres:

View file

@ -4,6 +4,7 @@ services:
tooljet: tooljet:
tty: true tty: true
stdin_open: true stdin_open: true
container_name: Tooljet-app
image: tooljet/tooljet-ce:latest image: tooljet/tooljet-ce:latest
restart: always restart: always
env_file: .env env_file: .env
@ -13,11 +14,10 @@ services:
SERVE_CLIENT: "true" SERVE_CLIENT: "true"
PORT: "80" PORT: "80"
command: npm run start:prod command: npm run start:prod
# Uncomment if ENABLE_TOOLJET_DB=true
# Uncomment if ENABLE_TOOLJET_DB=true postgrest:
# postgrest: image: postgrest/postgrest:v10.1.1.20221215
# image: postgrest/postgrest:v10.1.1.20221215 restart: always
# restart: always env_file: .env
# env_file: .env environment:
# environment: - PGRST_SERVER_PORT=80
# - PGRST_SERVER_PORT=80

124
deploy/docker/external.sh Normal file
View file

@ -0,0 +1,124 @@
#!/bin/bash
# Load the .env file
source .env
# Check if LOCKBOX_MASTER_KEY is present or empty
if [[ -z "$LOCKBOX_MASTER_KEY" ]]; then
# Generate LOCKBOX_MASTER_KEY
LOCKBOX_MASTER_KEY=$(openssl rand -hex 32)
# Update .env file
awk -v key="$LOCKBOX_MASTER_KEY" '
BEGIN { FS=OFS="=" }
/^LOCKBOX_MASTER_KEY=/ { $2=key; found=1 }
1
END { if (!found) print "LOCKBOX_MASTER_KEY="key }
' .env > temp.env && mv temp.env .env
echo "Generated a secure master key for the lockbox"
else
echo "The lockbox master key already exists."
fi
# Check if SECRET_KEY_BASE is present or empty
if [[ -z "$SECRET_KEY_BASE" ]]; then
# Generate SECRET_KEY_BASE
SECRET_KEY_BASE=$(openssl rand -hex 64)
# Update .env file
awk -v key="$SECRET_KEY_BASE" '
BEGIN { FS=OFS="=" }
/^SECRET_KEY_BASE=/ { $2=key; found=1 }
1
END { if (!found) print "SECRET_KEY_BASE="key }
' .env > temp.env && mv temp.env .env
echo "Created a secret key for secure operations."
else
echo "The secret key base is already in place."
fi
# Check if PGRST_JWT_SECRET is present or empty
if [[ -z "$PGRST_JWT_SECRET" ]]; then
# Generate PGRST_JWT_SECRET
PGRST_JWT_SECRET=$(openssl rand -hex 32)
# Update .env file
awk -v key="$PGRST_JWT_SECRET" '
BEGIN { FS=OFS="=" }
/^PGRST_JWT_SECRET=/ { $2=key; found=1 }
1
END { if (!found) print "PGRST_JWT_SECRET="key }
' .env > temp.env && mv temp.env .env
echo "Generated a unique secret for PGRST authentication."
else
echo "The PGRST JWT secret is already generated and in place."
fi
# Function to generate a random password
generate_password() {
openssl rand -base64 12 | tr -d '/+' | cut -c1-16
}
# Check if PG_USER, PG_HOST, PG_PASS, PG_DB are present or empty
if [[ -z "$PG_USER" ]] || [[ -z "$PG_HOST" ]] || [[ -z "$PG_PASS" ]] || [[ -z "$PG_DB" ]]; then
# Prompt user for values
read -p "Enter PostgreSQL database username: " PG_USER
read -p "Enter PostgreSQL database hostname: " PG_HOST
read -p "Enter PostgreSQL database password: " PG_PASS
read -p "Enter PostgreSQL database name: " PG_DB
# Update .env file
awk -v pg_user="$PG_USER" -v pg_host="$PG_HOST" -v pg_pass="$PG_PASS" -v pg_db="$PG_DB" '
BEGIN { FS=OFS="=" }
/^PG_USER=/ { $2=pg_user; found=1 }
/^PG_HOST=/ { $2=pg_host; found=1 }
/^PG_PASS=/ { $2=pg_pass; found=1 }
/^PG_DB=/ { $2=pg_db; found=1 }
1
END {
if (!found) {
print "PG_USER="pg_user
print "PG_HOST="pg_host
print "PG_PASS="pg_pass
print "PG_DB="pg_db
}
}
' .env > temp.env && mv temp.env .env
echo "Successfully updated postgresql database values .env file"
fi
# Copy values from PG to TOOLJET_DB
TOOLJET_DB_USER=$PG_USER
TOOLJET_DB_HOST=$PG_HOST
TOOLJET_DB_PASS=$PG_PASS
# Update .env file for TOOLJET_DB
awk -v tj_user="$TOOLJET_DB_USER" -v tj_host="$TOOLJET_DB_HOST" -v tj_pass="$TOOLJET_DB_PASS" '
BEGIN { FS=OFS="=" }
/^TOOLJET_DB_USER=/ { $2=tj_user; found=1 }
/^TOOLJET_DB_HOST=/ { $2=tj_host; found=1 }
/^TOOLJET_DB_PASS=/ { $2=tj_pass; found=1 }
1
END { if (!found) print "TOOLJET_DB_USER="tj_user ORS "TOOLJET_DB_HOST="tj_host ORS "TOOLJET_DB_PASS="tj_pass }
' .env > temp.env && mv temp.env .env
echo "Successfully updated tooljet database values in the .env file"
# Construct PGRST_DB_URI with user-provided values
PGRST_DB_URI="postgres://$PG_USER:$PG_PASS@$PG_HOST/tooljet_db"
# Update .env file for PGRST_DB_URI
awk -v uri="$PGRST_DB_URI" '
BEGIN { FS=OFS="=" }
/^PGRST_DB_URI=/ { $2=uri; found=1 }
1
END { if (!found) print "PGRST_DB_URI="uri }
' .env > temp.env && mv temp.env .env
echo "Successfully updated PGRST database URI"
exec "$@"

101
deploy/docker/internal.sh Normal file
View file

@ -0,0 +1,101 @@
#!/bin/bash
# Load the .env file
source .env
# Check if LOCKBOX_MASTER_KEY is present or empty
if [[ -z "$LOCKBOX_MASTER_KEY" ]]; then
# Generate LOCKBOX_MASTER_KEY
LOCKBOX_MASTER_KEY=$(openssl rand -hex 32)
# Update .env file
awk -v key="$LOCKBOX_MASTER_KEY" '
BEGIN { FS=OFS="=" }
/^LOCKBOX_MASTER_KEY=/ { $2=key; found=1 }
1
END { if (!found) print "LOCKBOX_MASTER_KEY="key }
' .env > temp.env && mv temp.env .env
echo "Generated a secure master key for the lockbox"
else
echo "The lockbox master key already exists."
fi
# Check if SECRET_KEY_BASE is present or empty
if [[ -z "$SECRET_KEY_BASE" ]]; then
# Generate SECRET_KEY_BASE
SECRET_KEY_BASE=$(openssl rand -hex 64)
# Update .env file
awk -v key="$SECRET_KEY_BASE" '
BEGIN { FS=OFS="=" }
/^SECRET_KEY_BASE=/ { $2=key; found=1 }
1
END { if (!found) print "SECRET_KEY_BASE="key }
' .env > temp.env && mv temp.env .env
echo "Created a secret key for secure operations."
else
echo "The secret key base is already in place."
fi
# Check if PGRST_JWT_SECRET is present or empty
if [[ -z "$PGRST_JWT_SECRET" ]]; then
# Generate PGRST_JWT_SECRET
PGRST_JWT_SECRET=$(openssl rand -hex 32)
# Update .env file
awk -v key="$PGRST_JWT_SECRET" '
BEGIN { FS=OFS="=" }
/^PGRST_JWT_SECRET=/ { $2=key; found=1 }
1
END { if (!found) print "PGRST_JWT_SECRET="key }
' .env > temp.env && mv temp.env .env
echo "Generated a unique secret for PGRST authentication."
else
echo "The PGRST JWT secret is already generated and in place."
fi
# Function to generate a random password
generate_password() {
openssl rand -base64 12 | tr -d '/+' | cut -c1-16
}
# Check if PG_PASS and TOOLJET_DB_PASS are present or empty
if [[ -z "$PG_PASS" ]] && [[ -z "$TOOLJET_DB_PASS" ]]; then
# Generate random passwords
PASSWORD=$(generate_password)
# Update .env file
awk -v pass="$PASSWORD" '
BEGIN { FS=OFS="=" }
/^(PG_PASS|TOOLJET_DB_PASS)=/ { $2=pass; found=1 }
1
END { if (!found) print "PG_PASS="pass ORS "TOOLJET_DB_PASS="pass }
' .env > temp.env && mv temp.env .env
echo "Successfully generated a secure password for the PostgreSQL database."
else
echo "Postgres password already exist"
fi
# Check if PGRST_DB_URI is present or empty
if [[ -z "$PGRST_DB_URI" ]]; then
# Construct PGRST_DB_URI with PG_PASS
PGRST_DB_URI="postgres://postgres:$PASSWORD@postgresql/tooljet_db"
# Update .env file for PGRST_DB_URI
awk -v uri="$PGRST_DB_URI" '
BEGIN { FS=OFS="=" }
/^PGRST_DB_URI=/ { $2=uri; found=1 }
1
END { if (!found) print "PGRST_DB_URI="uri }
' .env > temp.env && mv temp.env .env
echo "Successfully updated PGRST database URI"
else
echo "The PGRST DB URI is already configured and in use."
fi
exec "$@"

View file

@ -2,6 +2,7 @@
set -e set -e
# Setup prerequisite dependencies # Setup prerequisite dependencies
sudo apt-get update
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates apt-utils git curl postgresql-client sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates apt-utils git curl postgresql-client
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"

View file

@ -61,7 +61,7 @@ RUN apt-get update && \
# Install Instantclient Basic Light Oracle and Dependencies # Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle WORKDIR /opt/oracle
RUN wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip && \ RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linuxx64.zip && \
wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \ wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \
unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \ unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \
unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \ unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \

View file

@ -46,7 +46,7 @@ RUN apt-get update && apt-get install -y postgresql-client freetds-dev libaio1 w
# Install Instantclient Basic Light Oracle and Dependencies # Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle WORKDIR /opt/oracle
RUN wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip && \ RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linuxx64.zip && \
wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \ wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \
unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \ unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \
unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \ unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \

View file

@ -4,7 +4,7 @@ RUN apt-get update && apt-get install -y postgresql-client freetds-dev libaio1 w
# Install Instantclient Basic Light Oracle and Dependencies # Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle WORKDIR /opt/oracle
RUN wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip && \ RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linuxx64.zip && \
wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \ wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \
unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \ unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \
unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \ unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \

View file

@ -3,7 +3,7 @@ id: audit_logs
title: Audit logs title: Audit logs
--- ---
<div className='badge badge--primary heading-badge'>Available on: Enterprise Edition</div> <div className='badge badge--primary heading-badge'>Available on: Paid plans</div>
The audit log is the report of all the activities done in your ToolJet account. It will capture and display events automatically by recording who performed an activity, what when, and where the activity was performed, along with other information such as IP address. The audit log is the report of all the activities done in your ToolJet account. It will capture and display events automatically by recording who performed an activity, what when, and where the activity was performed, along with other information such as IP address.

View file

@ -3,7 +3,7 @@ id: superadmin
title: Super Admin title: Super Admin
--- ---
<div className='badge badge--primary heading-badge'>Available on: Enterprise Edition</div> <div className='badge badge--primary heading-badge'>Available on: Paid plans</div>
A Super Admin is the user who has full access to all the Workspaces, Users, and Groups of an instance. An instance can have more than one Super Admin. A Super Admin has full control over other users' workspaces and can create users, groups, and other super admins. A Super Admin is the user who has full access to all the Workspaces, Users, and Groups of an instance. An instance can have more than one Super Admin. A Super Admin has full control over other users' workspaces and can create users, groups, and other super admins.

View file

@ -3,7 +3,7 @@ id: white-label
title: White Label title: White Label
--- ---
<div className='badge badge--primary heading-badge'>Available on: Enterprise Edition</div> <div className='badge badge--primary heading-badge'>Available on: Paid plans</div>
White Label feature will allow you to remove the ToolJet branding from the ToolJet platform and add your own custom logo and text. White Label feature will allow you to remove the ToolJet branding from the ToolJet platform and add your own custom logo and text.

View file

@ -6,21 +6,23 @@ title: Generate file
# Generate file # Generate file
This action allows you to construct files on the fly and let users download it. This action allows you to construct files on the fly and let users download it.
Presently, the only file type supported is `CSV`.
## Options ## Options
| Option | Description | | Option | Description |
|--------|-------------| |--------|-------------|
| Type | Type of file to be generated | | Type | Type of file to be generated. Types: `CSV`, `Text` and `PDF` |
| File name | Name of the file to be generated | | File name | Name of the file to be generated |
| Data | Data that will be used to construct the file. Its format will depend on the file type, as specified in the following section | | Data | Data that will be used to construct the file. Its format will depend on the file type, as specified in the following section |
| Debounce | Debounce field is empty by default, you can enter a numerical value to specify the time in milliseconds after which the action will be performed. ex: `300` | | Debounce | Debounce field is empty by default, you can enter a numerical value to specify the time in milliseconds after which the action will be performed. ex: `300` |
### Data format for CSV :::tip
Check how to run **[generate file action using RunJS](/docs/how-to/run-actions-from-runjs/#generate-file)**.
:::
For `CSV` file type, the data field should be supplied with an array objects. ToolJet assumes that the keys of each of ### CSV Data Format
these objects are the same and that they represent the column headers of the csv file.
To use the `CSV` file format, the data field should contain an array of objects. ToolJet assumes that the keys in each object are the same and represent the column headers of the CSV file.
Example: Example:
@ -33,10 +35,22 @@ Example:
}} }}
``` ```
Supplying the above snippet will generate a csv file which looks like this: Using the above code snippet will generate a CSV file with the following content:
```csv ```csv
name,email name,email
John,john@tooljet.com John,john@tooljet.com
Sarah,sarah@tooljet.com Sarah,sarah@tooljet.com
``` ```
### Text Data Format
To use the `Text` file format, the data field should contain a string.
If you want to generate a text file based on an array of objects, you need to stringify the data before providing it.
For example, if you are using the table component to provide the data, you can enter **`{{JSON.stringify(components.table1.currentPageData)}}`** in the Data field.
### PDF data format
The PDF data format supports two types of input: either a `string` or an `array of objects`. When using an array of objects, the resulting PDF will display the data in a tabular format with columns and rows. On the other hand, if a string is provided, the generated PDF will consist of plain text.

View file

@ -37,7 +37,7 @@ They are commonly used to provide additional information to the server or to mod
## Using RunJS query to switch page ## Using RunJS query to switch page
Alternatively, the switch page command can be activated via a RunJS query using the following syntax: Alternatively, the switch page action can be activated via a RunJS query using the following syntax:
```js ```js
await actions.switchPage('<page-handle>') await actions.switchPage('<page-handle>')
``` ```
@ -46,3 +46,10 @@ await actions.switchPage('<page-handle>')
For instructions on how to run actions from a RunJS query, refer to the how-to guide [Running Actions from RunJS Query](/docs/how-to/run-actions-from-runjs). For instructions on how to run actions from a RunJS query, refer to the how-to guide [Running Actions from RunJS Query](/docs/how-to/run-actions-from-runjs).
::: :::
### Switch page with query params
The switch page action can also be triggered along with query parameters using the following syntax:
```js
actions.switchPage('<pageHandle>', [['param1', 'value1'], ['param2', 'value2']])
```

View file

@ -12,7 +12,7 @@ Canvas is the center area of the ToolJet app builder where the application is bu
</div> </div>
:::info :::info
- The Canvas height and width can be adjusted from the [Global Settings](/docs/app-builder/toolbar#global-settings). - The Canvas height and width can be adjusted from the [Global Settings](/docs/app-builder/topbar#global-settings).
- When the [Pages drawer](/docs/tutorial/pages) on the left is opened or pinned, the canvas becomes scrollable. - When the [Pages drawer](/docs/tutorial/pages) on the left is opened or pinned, the canvas becomes scrollable.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
@ -24,7 +24,7 @@ Canvas is the center area of the ToolJet app builder where the application is bu
### Arrange Components ### Arrange Components
All the components are fully interactive in editor mode - to prevent interaction you can **click and hold** the **[Component Handle](docs/app-builder/components-library)** to change component's position. All the components are fully interactive in editor mode - to prevent interaction you can **click and hold** the **Component Handle** to change component's position.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>

View file

@ -0,0 +1,56 @@
---
id: customstyles
title: Custom Styles
---
<div className='badge badge--primary heading-badge'>Available on: Paid plans</div>
Custom Styles feature enables the implementation of theming on ToolJet apps, allowing users to inject their own CSS styling to override the default app styling. This feature fulfills the requirement of allowing users to easily customize the appearance of their apps.
Custom Styles helps in maintaining consistent themes across the ToolJet apps, alleviating the repetitive burden of styling components whenever a new app is created. By enabling users to apply standardized styles, this feature ensures that each app adheres to a unified theme without the need to manually restyle the components from scratch. As a result, the ToolJet app development process becomes more efficient, and the visual coherence of the apps is preserved, providing users with a seamless experience across all applications.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/customcss/customcss.gif" alt="Custom CSS" />
</div>
## Applying Custom Styles
To add Custom Styles to ToolJet apps, users should follow these steps:
1. Go to the **Custom Styles** Page, accessible under **Workspace Settings** from the ToolJet dashboard.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/customcss/settings.png" alt="Custom CSS" />
</div>
2. When creating a new app on ToolJet, the default button color is **blue**. If you wish to change the default button color to **red**, you must identify the class of the button component, which follows the format `_tooljet-<component>`.
- The browser's inspector can also help you find the class of the component. Classes are added for both **pages** and **components**, and there are two types of selectors for classes: **Common** (`_tooljet-<component>`) and **Individual** (`_tooljet-<defaultComponentName>`).
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/customcss/selectors.png" alt="Custom CSS" />
</div>
3. Once the class (**`_tooljet-Button`**) is identified, navigate to the Custom Styles page and apply the desired CSS changes for that class, as shown in the following CSS code:
```css
._tooljet-Button button {
background: red !important;
}
._tooljet-Button button:hover {
background: green !important;
}
```
4. By applying this custom styles, all future instances of the app will have buttons with a red default color, and they will turn green on hover. This eliminates the need for users to individually edit button properties, streamlining the customization process.
:::info
Custom Styles are injected at the workspace level, ensuring consistent theming across all apps within the workspace.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/customcss/styledapp.gif" alt="Custom CSS" />
</div>

View file

@ -12,7 +12,7 @@ Left-sidebar has the following options:
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/leftsidebar/newside.png" alt="App Builder: Left-sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/leftsidebar/newui.png" alt="App Builder: Left-sidebar"/>
</div> </div>
@ -42,14 +42,24 @@ Check the detailed guide on **[using Inspector](/docs/how-to/use-inspector)**.
## Debugger ## Debugger
The debugger captures errors that happens while running the queries. For example, when a database query fails due to the unavailability of a database or when a REST API query fails due to an incorrect URL, the errors will be displayed on the debugger. The debugger also displays relevant data related to the error along with the error message. Debugger records any errors that occur during the execution of queries. For instance, if a database query fails because the database is unavailable or if a REST API query fails due to an incorrect URL, the errors will be captured and shown in the debugger. Additionally, the debugger provides pertinent information associated with the error alongside the error message.
If you wish to prevent the debugger from closing, you can simply click on the pin icon located in the top-right corner. By doing so, the debugger will stay open until you decide to unpin it.
Debugger consists of two main sections:
1. **All Log:** In this section, you can view a comprehensive list of all the logs generated during the execution of the application. These logs may include various types of messages, such as success messages, warning, and error messages.
2. **Errors:** This section specifically focuses on displaying the error messages that occurred during the program's execution. These error messages indicate issues or problems that need attention, as they may lead to unexpected behaviors of the application.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/leftsidebar/newdebug.png" alt="App Builder: Left-sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/leftsidebar/newdebugger.gif" alt="App Builder: Left-sidebar"/>
</div> </div>
## Theme switch ## Theme Switch
Switch ToolJet into light or dark mode from this button. Use the theme switch button to toggle ToolJet between light and dark modes.
While developers can access the current theme's value through global variables using `{{globals.theme.name}}`, it is not currently feasible to change the theme programmatically.

View file

@ -7,7 +7,7 @@ title: Overview
ToolJet's App Builder allows you to build applications. ToolJet's app builder has the following major components: ToolJet's App Builder allows you to build applications. ToolJet's app builder has the following major components:
- **[Topbar](/docs/app-builder/toolbar)**: configure app settings - **[Topbar](/docs/app-builder/topbar)**: configure app settings
- **[Canvas](/docs/app-builder/canvas)**: Arrange the components to build the interface of app - **[Canvas](/docs/app-builder/canvas)**: Arrange the components to build the interface of app
- **[Left-sidebar](/docs/app-builder/left-sidebar)**: Add **[pages](/docs/tutorial/pages)**, **[inspect](/docs/how-to/use-inspector)** the components, queries or variables, and **[debug](#debugger)** the errors. - **[Left-sidebar](/docs/app-builder/left-sidebar)**: Add **[pages](/docs/tutorial/pages)**, **[inspect](/docs/how-to/use-inspector)** the components, queries or variables, and **[debug](#debugger)** the errors.
- **[Components library](/docs/app-builder/components-library)**(right sidebar): Drag any component or modify the property or styling - **[Components library](/docs/app-builder/components-library)**(right sidebar): Drag any component or modify the property or styling

View file

@ -3,21 +3,58 @@ id: query-panel
title: Query Panel title: Query Panel
--- ---
The Query Panel is present at the bottom of the app-builder, this is where you create queries to interact with connected **local** and **global** datasources. You can perform API requests, query **[databases](/docs/data-sources/overview)**, or **[transform](/docs/tutorial/transformations)** or manipulate data with **[JavaScript](/docs/data-sources/run-js)** & **[Python](/docs/data-sources/run-py)**. The Query Panel, located at the bottom of the app-builder, allows you to create and manage queries for interacting with connected **Default** and **Global** datasources. It provides the capability to perform API requests, query **[databases](/docs/data-sources/overview)**, and apply **[transformations](/docs/tutorial/transformations)** or data manipulation using **[JavaScript](/docs/data-sources/run-js)** and **[Python](/docs/data-sources/run-py)**.
The Query Panel has two sections: The Query Panel consists of two sections:
- **[Query Manager](#query-manager)** on the right that includes a list of all the created queries - The **[Query Manager](#query-manager)** on the right side, which displays a list of all the created queries.
- **[Query Editor](#query-editor)** is used to configure the selected query - The **[Query Editor](#query-editor)**, used to configure the selected query.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui/querypanel.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/querypanel.png" alt="App Builder: Query Panel"/>
</div> </div>
## Query Manager ## Query Manager
Query Manager will list all the queries that has been created in the application. Query Manager is used to: Query Manager will list all the queries that has been created in the application. Query Manager helps in managing the queries that have been created, you can **add**, **edit**, **delete**, **duplicate**, **search**, **sort** and **filter** through them.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/querymanager.png" alt="App Builder: Query Panel"/>
</div>
### Add
Add button is used to add new query in the application. When Add button is clicked, a menu will open with a list of options for creating a query from **Default** datasources such as **Rest API**, **ToolJet Database**, **JavaScript Code**, **Python Code** or from connected **Global Datasources**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/addquery.gif" alt="App Builder: Query Panel"/>
</div>
### Sort/Filter
On the top of Query Manager, there is button to Sort or Filter queries. The following options are there:
**Filter:**
- By Datasource
**Sort:**
- Name: A-Z
- Name: Z-A
- Type: A-Z
- Type: Z-A
- Last modified: oldest first
- Last modified: newest First
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/sortfilter.gif" alt="App Builder: Query Panel"/>
</div>
### Search ### Search
@ -25,37 +62,37 @@ On the top of the query manager is search box that can be used to search for a s
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/search.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/search.gif" alt="App Builder: Query Panel"/>
</div>
### Add
Add button is used to add more queries in the application. When Add button is clicked, the Query Editor will show you a list of options for creating a query from: **Rest API**, connected **[datasources](/docs/data-sources/overview)**, **[ToolJet Database](/docs/tooljet-database)**, **[JavaScript Code](/docs/data-sources/run-js)**, **[Python Code](/docs/data-sources/run-py)** or Add a new datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui/add.png" alt="App Builder: Component library- right sidebar"/>
</div> </div>
### Delete ### Delete
Delete button will delete the selected query, the button will only show up when you hover over the query name. Delete button will delete the selected query, the button will only show up when you hover over the query name. When you click on the delete button, a confirmation dialog will open to confirm the deletion of the query.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/delete.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/delete.png" alt="App Builder: Query Panel"/>
</div> </div>
### Edit ### Duplicate
Edit button is used edit the name of the selected query, the button will only show up when you hover over the query name. Duplicate button will duplicate the selected query, the button will only show up when you hover over the query name. The duplicate query will be named as `<query name>_copy`.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/edit.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/duplicate.png" alt="App Builder: Query Panel"/>
</div>
### Rename
Rename button is used to rename the selected query, the button will only show up when you hover over the query name. When you click on the rename button, the query name becomes editable and you can change the name of the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/rename.png" alt="App Builder: Query Panel"/>
</div> </div>
@ -63,9 +100,13 @@ Edit button is used edit the name of the selected query, the button will only sh
Query editor used to configure the query parameters, preview or transform the data return by the query. Query editor used to configure the query parameters, preview or transform the data return by the query.
:::info
The changes made in the query panel will be saved automatically.
:::
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/editor.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/editor.png" alt="App Builder: Query Panel"/>
</div> </div>
@ -73,45 +114,25 @@ Query editor used to configure the query parameters, preview or transform the da
On the top of the query panel there are a few options: On the top of the query panel there are a few options:
#### Query Name editor #### Query Name
Edit the name of the query by clicking on the edit button next to the default query name. The name of query is displayed on the top of the query panel. You can click on it to make it editable and change the name of the query.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/nameedit.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/renameeditor.gif" alt="App Builder: Query Panel"/>
</div> </div>
#### Preview #### Preview
Preview gives you a quick look at the data returned by the query without triggering the query in the app. Preview button is used to preview the data returned by the query. The data will be displayed on the preview section present at the bottom of the query panel. This helps in debugging the query and see the data returned by the query without triggering the query in the app.
The Preview of data is returned in two different formats: The Preview of data is returned in two different formats: **Raw** & **JSON**. You can click on the clear button to clear the preview data.
**Raw**
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/raw.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/preview.gif" alt="App Builder: Query Panel"/>
</div>
**JSON**
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/json.png" alt="App Builder: Component library- right sidebar"/>
</div>
#### Save
Save is used to save the changes whenever a change is made in query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/save.png" alt="App Builder: Component library- right sidebar"/>
</div> </div>
@ -121,17 +142,29 @@ Run is used to trigger the query, running the query will interact with the appli
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/run.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/run.gif" alt="App Builder: Query Panel"/>
</div> </div>
### Query Parameters ### Query Parameters
Query Parameters are the values required for the query to return a response from the server. Parameters include **endpoints**, **methods**, or **operations**. Query Parameters are different for each datasource. Query Parameters are essential values that must be provided in a query for the server to generate a response. These parameters encompass **endpoints**, **methods**, or **operations**. It's important to note that the specific set of Query Parameters varies for each datasource.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/params.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/params.png" alt="App Builder: Query Panel"/>
</div>
#### Datasource
The primary and default parameter found in all queries is **Datasource**. This option allows you to choose the appropriate datasource for your query.
In cases where multiple datasources of the same type are connected, you can easily switch the query's datasource using the dropdown menu.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/switch.png" alt="App Builder: Query Panel"/>
</div> </div>
@ -141,27 +174,35 @@ Transformations can be enabled on queries to transform the query results. ToolJe
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/transform.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/transform.gif" alt="App Builder: Query Panel"/>
</div> </div>
### Advanced options ### Settings
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/settings.png" alt="App Builder: Query Panel"/>
</div>
#### Run this query on application load? #### Run this query on application load?
Enabling this option will fire the query every time the app is loaded. Enabling this option will execute the query every time the app is loaded.
#### Request confirmation before running the query? #### Request confirmation before running the query?
Enabling this option show a confirmation modal to confirm `Yes` or `No` if you want to fire that query. Enabling this option show a confirmation modal to confirm `Yes` or `No` if you want to fire that query.
#### Run this query on application load? #### Show notification on success?
Enabling this option show a success toast notification when the query is successfully triggered. Enabling this option show a success toast notification when the query is successfully triggered.
#### Event Handlers You can provide a custom **success message** and **notification duration** in milliseconds.
Event Handler are used to add some action when a particular event happens. You can add event handlers to the query for the following events: ### Events
Event handlers can be added to queries for the following events:
- **Query Success** - **Query Success**
- **Query Failure** - **Query Failure**
@ -172,17 +213,6 @@ Learn more about [Event Handlers and Actions](/docs/widgets/overview#component-e
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/advanced.png" alt="App Builder: Component library- right sidebar"/> <img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui2/events.png" alt="App Builder: Query Panel"/>
</div>
### Change Datasource
If more than one datasources are connected of same type then you can change the datasource of the query from this dropdown.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/querypanel/newui/switch.png" alt="App Builder: Component library- right sidebar"/>
</div> </div>

View file

@ -17,7 +17,7 @@ Check the **[Components Catalog](/docs/widgets/overview)** here to know more abo
## Component Config Inspector ## Component Config Inspector
The Component Config Inspector is also called as component inspector. It contains all the available settings for the selected component and is where you **set values**, **update component names**, and **create event handlers**. The Compoenent Inspector organizes settings into different sections, such as **Property** and **Styles**. The Component Config Inspector is also called as component inspector. It contains all the available settings for the selected component and is where you **set values**, **update component names**, and **create event handlers**. The Component Inspector organizes settings into different sections, such as **Property** and **Styles**.
To open the Component Config Inspector, click on the component handle that is present on the top of the component including **⚙️ + Component Name** and the component inspector will open up on the right side: To open the Component Config Inspector, click on the component handle that is present on the top of the component including **⚙️ + Component Name** and the component inspector will open up on the right side:

View file

@ -0,0 +1,52 @@
---
id: share
title: Share
---
ToolJet apps offer two sharing options: they can either be shared privately with workspace users or publicly by generating a shareable link. To obtain the shareable URL, you can easily do so by clicking on the Share button located on the top bar of the App builder.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/share/modal.png" alt="Share modal" width='700'/>
</div>
### Making the app public
To share the app with external end users and make it accessible to anyone on the internet without requiring a ToolJet login, you can toggle on the Switch for "Make the application public?" in the Share modal.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/share/shareon.png" alt="Share modal" width='700'/>
</div>
### Customizing the app URL
By default, ToolJet will generate a unique URL for your application. However, you also have the option to edit the slug of the URL to make it more customized and user-friendly.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/share/edit.png" alt="Share modal" width='700'/>
</div>
### Embedding ToolJet Apps
ToolJet apps can be directly shared with end users and embedded into web apps using `iframes`. If you want to make your application public, you can use the Share modal to obtain the embeddable link.
:::info
For embedding private ToolJet apps, you'll need to set an environment variable in the `.env` file.
| Variable | Description |
| --------------- | ------------------------------------- |
| ENABLE_PRIVATE_APP_EMBED | `true` or `false` |
You can learn more [here](/docs/setup/env-vars#enabling-embedding-of-private-apps).
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/app-builder/share/embeddtj.gif" alt="Share modal" />
</div>

View file

@ -27,7 +27,7 @@ When a new app is created, by default its name is set to **Untitled app**
To configure the app's global settings, click on the kebab menu(three vertical dots) on the left of the app name. Global settings include: To configure the app's global settings, click on the kebab menu(three vertical dots) on the left of the app name. Global settings include:
- **Hide heaeder for launched apps**: Toggle this on to the hide the tooljet's header when the applications are launched - **Hide header for launched apps**: Toggle this on to the hide the tooljet's header when the applications are launched
- **Maintenance mode**: Toggle this on to put the application in maintenance mode. When in **maintenance mode**, on launching the app, the user will get an error message that **the app is under maintenance**. - **Maintenance mode**: Toggle this on to put the application in maintenance mode. When in **maintenance mode**, on launching the app, the user will get an error message that **the app is under maintenance**.
- **Max width of canvas**: Modify the width of the canvas in **px** or **%**. The default width is 1292 px. - **Max width of canvas**: Modify the width of the canvas in **px** or **%**. The default width is 1292 px.
- **Max height of canvas**: Modify the width of the canvas in **px** or **%**. The default height is 2400 px and currently it is the maximum height limit. - **Max height of canvas**: Modify the width of the canvas in **px** or **%**. The default height is 2400 px and currently it is the maximum height limit.

View file

@ -15,4 +15,4 @@ After successfully installing the Ubuntu environment, you will have access to a
If you are setting up ToolJet on a Windows machine, ensure that the line endings in the **.env** file are changed to LF. By default, they may be set to CRLF, which is not compatible unless configured specifically for Windows machines. If you are setting up ToolJet on a Windows machine, ensure that the line endings in the **.env** file are changed to LF. By default, they may be set to CRLF, which is not compatible unless configured specifically for Windows machines.
::: :::
Once the environment is set up, you can proceed with the steps outlined in the Ubuntu documentation at **[Contributin Guide- Ubuntu Setup](/docs/contributing-guide/setup/ubuntu)**. Once the environment is set up, you can proceed with the steps outlined in the Ubuntu documentation at **[Contributing Guide - Ubuntu Setup](/docs/contributing-guide/setup/ubuntu)**.

View file

@ -3,8 +3,10 @@ id: tooljet-copilot
title: Copilot title: Copilot
--- ---
<div className='badge badge--primary heading-badge'>Available on: Paid plans</div>
:::info BETA :::info BETA
ToolJet Copilot is currently in private beta for **Enterprise** and **Cloud** users only. It is not yet available for ToolJet Community Edition, but we plan to make it accessible in the future. ToolJet Copilot is currently in private beta for **Business** and **Enterprise** users only. It is not yet available for ToolJet Community Edition, but we plan to make it accessible in the future.
::: :::
**ToolJet Copilot** helps you write your queries faster. It uses OpenAI to suggest queries based on your data. **ToolJet Copilot** helps you write your queries faster. It uses OpenAI to suggest queries based on your data.

View file

@ -0,0 +1,140 @@
---
id: azureblob
title: Azure Blob
---
ToolJet offers the capability to establish a connection with Azure Blob storage in order to read and store large objects.
## Connection
To connect ToolJet with the Azure Blob global datasource, you have two options:
1. Click on the `+Add new global datasource` button in the query panel.
2. Go to the **[Global Datasources](/docs/data-sources/overview)** page on the ToolJet dashboard.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/azureblob/gdsazure.gif" alt="Azure Blob - ToolJet" />
</div>
To successfully establish the connection, ToolJet requires the following details:
- **Connection String**: The connection string can be found on the dashboard of Azure Blob Storage.
Once you have entered the connection string, click on the **Test connection** button to verify the connection's success. To save the datasource, click on the **Save** button.
## Querying Azure Blob
Once you have connected to the Azure Blob global datasource, follow these steps to create queries and interact with a Azure Blob storage from the ToolJet application:
1. Open the ToolJet application and navigate to the query panel at the bottom of the app builder.
2. Click the `+Add` button to open the list of available `local` and `global datasources`.
3. Select **Azure Blob** from the global datasource section.
4. Select the desired **operation** from the dropdown and enter the required **parameters**.
5. **Rename**(optional) and **Create** the query.
6. Click **Preview** to view the data returned from the query or click **Run** to execute the query.
:::tip
Query results can be transformed using Transformation. For more information on transformations, please refer to our documentation at **[link](/docs/tutorial/transformations)**.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/azureblob/queries.png" alt="Azure Blob - ToolJet" />
</div>
## Supported operations
1. **[Create container](#create-container)**
2. **[List containers](#list-containers)**
3. **[List blobs](#list-blobs)**
4. **[Upload blob](#upload-blob)**
5. **[Read blob](#read-blob)**
6. **[Delete blob](#delete-blob)**
### Create container
The create container operation enables the creation of new containers within Azure Blob storage. Containers serve as logical units for organizing and managing blob data. Users can provide a unique name for the container. Once created, the container is available for use in storing and organizing blob data. If the container with the same name already exists, the operation fails.
#### Required parameters:
- **Container Name:** Name of the container that you want to create.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/azureblob/createcontainer.png" alt="Azure blob: create container operation" />
</div>
### List containers
The list container operation allows you to retrieve a list of containers within Azure Blob storage.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/azureblob/listcon.png" alt="Azure blob: list container operation" />
</div>
### List blobs
The list blobs operation enables you to retrieve a list of blobs within a specific container in Azure Blob storage.
#### Required parameter:
- **Container:** Specify the name of the container from which you wish to retrieve a list of blobs.
- **Page Size:** Specify the maximum number of blobs to be returned per page or request.
#### Optional parameters:
- **Prefix:** Filter the list of blobs based on a specific prefix or prefix pattern, allowing you to narrow down the results to blobs with names that start with the specified prefix.
- **Continuation Token:** A marker or token used to retrieve the next page of results when there are more blobs available beyond the initial page.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/azureblob/listblobs.png" alt="Azure blob: list blobs operation" />
</div>
### Upload blob
The upload blob operation allows you to upload a new blob or update an existing blob in Azure Blob storage. It provides a convenient way to store data such as files, images, or documents in the specified container.
#### Required parameters:
- **Container**: Specify the name of the container where the blob will be uploaded or updated.
- **Blob Name**: Provide a unique name for the blob. This name is used to identify and access the blob within the specified container.
- **Content Type**: Set the content type of the blob, which indicates the type of data being stored. It helps clients interpret the blob content correctly when accessing it. example: **image/jpeg** for JPEG images or **image/png** for PNG image. You can also get the content type from the exposed variable of the file picker component.
- **Upload Data**: Select or provide the data to be uploaded as the content of the blob. This can be a file from your local system, binary data, or text content. You can also get the data from the exposed variable of the file picker component.
- **Encoding**: Choose the encoding format for the uploaded data if applicable. This parameter determines how the data is encoded before being stored as the blob content. If the value is left blank then it takes **UTF-8** by default.
### Read blob
The read blob operation allows you to retrieve the content of a specific blob stored in Azure Blob storage. It enables you to access and retrieve the data stored within the blob for further processing or display.
#### Required parameters:
- **Container**: Specify the name of the container where the blob is located.
- **Blob Name**: Provide the unique name of the blob you want to read. This identifies the specific blob within the specified container
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/azureblob/read.png" alt="Azure blob: read blob operation" />
</div>
### Delete blob
The delete blob operation allows you to remove a specific blob from Azure Blob storage. This operation permanently deletes the blob and its associated data, freeing up storage space and removing the blob from the container.
#### Required parameters:
- **Container**: Specify the name of the container from which you want to delete the blob.
- **Blob Name**: Provide the unique name of the blob you want to delete. This identifies the specific blob within the specified container.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/azureblob/delete.png" alt="Azure blob: delete blob operation" />
</div>

View file

@ -3,17 +3,76 @@ id: run-js
title: Run JavaScript code title: Run JavaScript code
--- ---
# Run JavaScript code You can write custom JavaScript code to interact with components and queries. To do that, you just need to create a new query and select **Run JavaScript Code** from the default datasources section.
You can write custom JavaScript code to interact with components and queries. To do that, you just need to create a new query and select **Run JavaScript Code** from the data sources dropdown.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/custom-javascript/run-js.png" alt="Run JavaScript code" /> <img className="screenshot-full" src="/img/datasource-reference/custom-javascript/defaultds.png" alt="Run JavaScript code" />
</div> </div>
#### Example: Displaying random number ## JS parameters
JS parameters in RunJS queries offer a convenient way to customize JavaScript code execution without altering the code directly. You can add parameters by clicking the **Add** button in the RunJS query editor.
Each parameter requires:
- **Name**: Name for the parameter
- **Default value**: The value can be constant strings, numbers and object.
**Syntax for calling the parameter:** `parameters.<name>`
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/custom-javascript/addparam.png" alt="Run JavaScript code" />
</div>
### Example: Alert a parameter
Let's create a new parameter named `object1` and set the value as object `{key1: 'value1'}` and use the alert js method to show the value on the pop-up.
Syntax:
```
alert(parameters.object1)
```
When the query is triggered the alert will show the parameters value.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/custom-javascript/popup.png" alt="Run JavaScript code" />
</div>
### Example: Providing custom parameters by calling another query
Let's demonstrate how to utilize parameters in RunJS queries and call one query from another by providing custom parameter values:
1. Begin by creating a new RunJS query named `multiply`. In this query, add the following parameters: **num1** with a default value of `10` and **num2** with a default value of `2`. To display the result, place a text component on the canvas and set its text to **{{queries.multiply.data}}**. Save and Run the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/custom-javascript/multiply.png" alt="Run JavaScript code" />
</div>
2. Now, let's create another RunJS query called `callMultiply`, where we will invoke the `multiply` query created earlier using custom parameter values. Here's the code snippet for `callMultiply`:
```js
queries.multiply.run({num1: 20, num2: 20})
```
By executing this code within `callMultiply`, we trigger the `multiply` query with specific values for its parameters.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/custom-javascript/callmultiply.png" alt="Run JavaScript code" />
</div>
With this setup, the `multiply` query can be called from other queries, such as `callMultiply`, by providing custom parameter values. This allows you to reuse the `multiply` query with different inputs and display the results accordingly.
## RunJS query examples
### Displaying random number
- Let's drag a **button** and a **text** widget inside a container widget. - Let's drag a **button** and a **text** widget inside a container widget.
- Click on the `+` on the query panel to create a query and select **Run JavaScript code** from the available datasources - Click on the `+` on the query panel to create a query and select **Run JavaScript code** from the available datasources
@ -32,16 +91,40 @@ ex: `{{queries.runjs1.data}}`
- Add an event handler to the button - Select **On Click** event, **Run Query** action, and select the `runjs1` query that we created. This will run the JavaScript code every time the button is clicked. - Add an event handler to the button - Select **On Click** event, **Run Query** action, and select the `runjs1` query that we created. This will run the JavaScript code every time the button is clicked.
- Edit the property of text widget - In the text field enter **Random number: `{{queries.runjs1.data}}`**. It will display the output as Random number: *result from JS code* - Edit the property of text widget - In the text field enter **Random number: `{{queries.runjs1.data}}`**. It will display the output as Random number: *result from JS code*
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/custom-javascript/jsrandom.gif" alt="Run JavaScript code" /> <img className="screenshot-full" src="/img/datasource-reference/custom-javascript/jsrandom.gif" alt="Run JavaScript code" />
</div> </div>
You can also write custom JavaScript code to get the data from **External APIs** and manipulate the response for graphical representation. Here's the [tutorial](https://blog.tooljet.com/build-github-stars-history-app-in-5-minutes-using-low-code/) on how we used custom JavaScript code to build an app using GitHub API. ### Generating Unique ID
#### Code 1:
### Libraries ```js
var id = "id" + Math.random().toString(16).slice(2);
return id;
```
In this code, the resulting ID will have the format "id" followed by a sequence of random hexadecimal characters. For example, it could be something like "id2f4a1b".
#### Code 2:
```js
return String(Date.now().toString(32) + Math.random().toString(16)).replace(/\./g, '');
```
In this code, the resulting ID will have the format "timestamp + randomHex", where "timestamp" is the current time in base-32 and "randomHex" is a random hexadecimal value. This ID will be longer than the one generated by Code 1, and it could look like "2g3h1d6a4h3".
Both code snippets will produce IDs that are highly likely to be unique. However, Code 1 generates shorter IDs and follows a more straightforward approach with a fixed prefix ("id"). On the other hand, Code 2 generates longer IDs by incorporating the current timestamp and using a combination of base-32 and hexadecimal representations. The choice between the two methods depends on the specific requirements of the application and the desired length of the generated IDs.
:::tip Resources
- You can also write custom JavaScript code to get the data from **External APIs** and manipulate the response for graphical representation. Here's the [tutorial](https://blog.tooljet.com/build-github-stars-history-app-in-5-minutes-using-low-code/) on how we used custom JavaScript code to build an app using GitHub API.
- [Import external libraries](/docs/how-to/import-external-libraries-using-runjs) using RunJS.
- [Intentionally Fail](docs/how-to/intentionally-fail-js-query) a RunJS query.
- [Trigger query at specified intervals](/docs/how-to/run-query-at-specified-intervals) using RunJS.
:::
## Libraries
ToolJet allows you to internally utilize these libraries: ToolJet allows you to internally utilize these libraries:

View file

@ -71,6 +71,16 @@ To execute the query, click the 'Run' button. Note that the query must be saved
You can apply transformations to the query results. Refer to our transformations documentation for more information: [link](/docs/tutorial/transformations) You can apply transformations to the query results. Refer to our transformations documentation for more information: [link](/docs/tutorial/transformations)
::: :::
- **[List Tables](#list-tables)**
- **[Get Item](#get-item)**
- **[Query Table](#query-table)**
- **[Scan Table](#scan-table)**
- **[Delete Item](#delete-item)**
- **[Update Item](#update-item)**
- **[Describe Table](#describe-table)**
- **[Create Table](#create-table)**
- **[Put Item](#put-item)**
### List Tables ### List Tables
Returns an array of table names associated with the current account and endpoint. The output from List Tables is paginated, with each page returning a maximum of 100 table names. Returns an array of table names associated with the current account and endpoint. The output from List Tables is paginated, with each page returning a maximum of 100 table names.
@ -187,3 +197,133 @@ Syntax for Key name:
<img className="screenshot-full" src="/img/datasource-reference/dynamodb/deleteitem.png" alt="ToolJet - DynamoDB operations" /> <img className="screenshot-full" src="/img/datasource-reference/dynamodb/deleteitem.png" alt="ToolJet - DynamoDB operations" />
</div> </div>
### Update Item
Update an item in DynamoDB by specifying the primary key and providing new attribute values. If the primary key does not exist in the table then instead of updating it will insert a new row.
**Required parameters:**
- **Update Condition**
Syntax for Update Condition:
```json
{
"TableName": "USER_DETAILS_with_local",
"Key": {
"USER_ID": 1,
"USER_NAME": "Nick"
},
"UpdateExpression": "set USER_AGE = :age, USER_FEE = :fee",
"ExpressionAttributeValues": {
":age": 40,
":fee": 230545
}
}
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/dynamodb/updateitem.png" alt="ToolJet - DynamoDB operations" />
</div>
### Describe Table
This operation in DynamoDB retrieves metadata and configuration details about a specific table. It provides information such as the table's name, primary key schema, provisioned throughput settings, and any secondary indexes defined on the table.
**Required parameters:**
- **Table**
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/dynamodb/describetable.png" alt="ToolJet - DynamoDB operations" />
</div>
### Create Table
This operation in DynamoDB enables you to create a new table by specifying its name, primary key schema, and optional configurations.
**Required parameters:**
- **Table Parameters**
Syntax for Table Parameters:
```json
{
"AttributeDefinitions": [
{
"AttributeName": "USER_ID",
"AttributeType": "N"
},
{
"AttributeName": "USER_FEE",
"AttributeType": "N"
}
],
"KeySchema": [
{
"AttributeName": "USER_ID",
"KeyType": "HASH"
}
],
"LocalSecondaryIndexes": [
{
"IndexName": "USER_FEE",
"KeySchema": [
{
"AttributeName": "USER_ID",
"KeyType": "HASH"
},
{
"AttributeName": "USER_FEE",
"KeyType": "RANGE"
}
],
"Projection": {
"ProjectionType": "KEYS_ONLY"
}
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
},
"TableName": "USER_FEE_LOCAL",
"StreamSpecification": {
"StreamEnabled": false
}
}
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/dynamodb/createtable.png" alt="ToolJet - DynamoDB operations" />
</div>
### Put Item
This operation allows you to create or replace an item in a table. It enables you to specify the table name, provide the attribute values for the new item, and define the primary key attributes to uniquely identify the item.
**Required parameters:**
- **New Item Details**
Syntax for New Item Details:
```json
{
"TableName": "USER_DETAILS_with_localS",
"Item": {
"USER_ID": 1,
"USER_NAME": "NICK",
"USER_AGE": 34,
"USER_FEE": 1234.56,
}
}
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/dynamodb/updateitem.png" alt="ToolJet - DynamoDB operations" />
</div>

View file

@ -3,36 +3,31 @@ id: graphql
title: GraphQL title: GraphQL
--- ---
# GraphQL ToolJet can establish connections with GraphQL endpoints, enabling the execution of queries and mutations.
ToolJet can connect to GraphQL endpoints to execute queries and mutations.
## Connection ## Connection
To add a new GraphQL datasource, click the `+` button on data sources panel at the bottom-left corner of the app builder and then select GraphQL from the modal that pops up. To establish a connection with the GraphQL global datasource, you can either click on the **Add new global datasource** button located on the query panel or navigate to the **[Global Datasources](/docs/data-sources/overview)** page through the ToolJet dashboard.
ToolJet requires the following to connect to a GraphQL datasource:
- **URL of the GraphQL endpoint**
The following optional parameters are also supported:
| Type | Description |
| ----------- | ----------- |
| URL params | Additional query string parameters|
| headers | Any headers the GraphQL source requires|
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/graphql/graphql-ds.png" alt="ToolJet - Data source - GraphQl"/> <img className="screenshot-full" src="/img/datasource-reference/graphql/graphgds.gif" alt="ToolJet - Data source - REST API" />
</div> </div>
Click on the **Save** button to save the data source. ToolJet requires the following to connect to a GraphQL datasource:
- **URL**: URL of the GraphQL endpoint
- **Headers**: Any headers the GraphQL source requires
- **URL parameters**: Additional query string parameters
- __Authentication Type__: The method of authentication to use with GraphQL requests. Supported Types: None, Basic, Bearer, and OAuth 2.0
- **Basic**: Requires Username and Password
- **Bearer**: Requires a token, typically a JSON Web Token (JWT), to grant access
- **OAuth 2.0**: The OAuth 2.0 protocol mandates the provision of the following parameters: access token URL, access token URL custom headers, client ID, client secret, scopes, custom query parameters, authorization URL, custom authentication parameters, and client authentication.
## Querying GraphQL ## Querying GraphQL
Click on `+` button of the query manager at the bottom panel of the editor and select the GraphQL endpoint added in the previous step as the data source. Click on **`+Add`** button of the query manager at the bottom panel of the editor and select the GraphQL global datasource added in previous step.
### Required Parameters: ### Required Parameters:
- **Query** - **Query**
@ -48,7 +43,7 @@ Click on `+` button of the query manager at the bottom panel of the editor and s
</div> </div>
Click on the 'Create' button to create the query or Click on the `Run` button to create and trigger the query. Click on the **Create** button to create the query or Click on the **Run** button to create and trigger the query.
:::tip :::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations) Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)

View file

@ -66,7 +66,7 @@ Global datasources are available only on **ToolJet version 2.3.0 and above**.
On ToolJet versions below 2.3.0, the datasource connection was made from within the individual apps. To make it backward compatible, we added an option to change the scope of the datasources and make it global datasource. On ToolJet versions below 2.3.0, the datasource connection was made from within the individual apps. To make it backward compatible, we added an option to change the scope of the datasources and make it global datasource.
1. If you open an app created on previos versions of ToolJet, you'll find the datasource manager on the left sidebar of the App Builder. 1. If you open an app created on previous versions of ToolJet, you'll find the datasource manager on the left sidebar of the App Builder.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/leftsidebar.png" alt="Datasources: Overview" /> <img className="screenshot-full" src="/img/datasource-reference/overview/leftsidebar.png" alt="Datasources: Overview" />
@ -80,7 +80,7 @@ On ToolJet versions below 2.3.0, the datasource connection was made from within
</div> </div>
3. Once you change the scope of the datasource and make it global, you'll see that the **datasource manager** is removed from the left sidebar and now you'll find the datasource on the **query panel** under Global Datasources. You can now configure the datasource fromt the Global Datasource page on the **dashboard**. 3. Once you change the scope of the datasource and make it global, you'll see that the **datasource manager** is removed from the left sidebar and now you'll find the datasource on the **query panel** under Global Datasources. You can now configure the datasource from the Global Datasource page on the **dashboard**.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/queryadd.png" alt="Datasources: Overview" /> <img className="screenshot-full" src="/img/datasource-reference/overview/queryadd.png" alt="Datasources: Overview" />

View file

@ -3,42 +3,85 @@ id: redis
title: Redis title: Redis
--- ---
# Redis ToolJet enables you to execute Redis commands on your Redis instances.
ToolJet can run Redis commands on your Redis instances.
## Connection ## Connecting to Redis
ToolJet requires the following to connect to your Redis instances. To establish a connection with the Redis global datasource, you have two options. You can either click on the **`+Add new global datasource`** button on the query panel or access the **[Global Datasources](/docs/data-sources/overview)** page from the ToolJet dashboard.
<img class="screenshot-full" src="/img/redis/connect.png" alt="ToolJet - Redis connection" height="250"/> <div style={{textAlign: 'center'}}>
- **Host** <img className="screenshot-full" src="/img/datasource-reference/redis/gdsredis.gif" alt="Redis" />
- **Port** - The default port for Redis server is 6379
- **Username**
- **Password**
Click on "Test" button to test the connection and click "Save" to save the data source. </div>
**To connect ToolJet with Redis, you need to provide the following connection details:**
- **Host**: The address or hostname of the Redis server
- **Port**: The port number used by the Redis server (default is 6379)
- **Username**: The username used for authentication
- **Password**: The password used for authentication
:::info
Click on **Test connection** button to verify if the credentials are correct and that the Redis is accessible to ToolJet server. Click on **Save** button to save the data source.
:::
## Redis Queries ## Redis Queries
List of supported commands: [Redis Official Documentation](https://redis.io/commands) Here are some examples of Redis commands and their usage. You can refer to the [Redis Official Documentation](https://redis.io/commands) for a complete list of supported commands.
### Examples ### PING Command
`PING` command to test the Redis connection. If the connection is ready, the Redis server will respond with `PONG`. The `PING` command is used to test the connection to Redis. If the connection is successful, the Redis server will respond with `PONG`.
```shell ```shell
PING PING
``` ```
`SET` command can be used to set the value for a key ### SET Command
The `SET` command is used in Redis to assign a value to a specific key.
```shell ```shell
SET key value SET key value
``` ```
`GET` command can be used to retrieve the value of a key **Example 1/2:**
When the input value contains spaces, you should encode the value before providing it as an input:
```shell
SET products {{encodeURI('John Doe')}}
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/redis/encode.png" alt="Redis" />
</div>
### GET Command
The `GET` command is used in Redis to retrieve the value associated with a specific key.
```shell ```shell
GET key GET key
``` ```
**Example 2/2:**
To retrieve a value that was previously encoded while setting, you can use transformations.
- Enter the GET command in the editor:
```shell
GET products
```
- Enable Transformations (JS) and use `decodeURI`:
```js
return JSON.parse(decodeURI(data));
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/redis/decode.png" alt="Redis" />
</div>

View file

@ -17,7 +17,7 @@ ToolJet ships with its built-in database called **[ToolJet DB](/docs/tooljet-dat
Once the data sources are connected, ToolJet can run **queries** on these data sources to fetch and update data. The data fetched from data sources can be **visualised and modified** using the UI widgets such as tables, charts, forms, etc. You can also use **[Javascript](/docs/data-sources/run-js)** or **[Python](/docs/data-sources/run-py)** queries for writing business logic or interacting with the user interface of the application. Once the data sources are connected, ToolJet can run **queries** on these data sources to fetch and update data. The data fetched from data sources can be **visualised and modified** using the UI widgets such as tables, charts, forms, etc. You can also use **[Javascript](/docs/data-sources/run-js)** or **[Python](/docs/data-sources/run-py)** queries for writing business logic or interacting with the user interface of the application.
<img src="/img/v2-beta/getting_started/intro.png" alt="Getting started Demo app" /> <img src="/img/v2-beta/getting_started/intro.webp" alt="Getting started Demo app" width="100%" height="100%" loading="eager" />
<!-- Why ToolJet section is commented out. <!-- Why ToolJet section is commented out.
@ -36,7 +36,7 @@ When you're building an internal tool, there are a lot of tools and frameworks a
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/howtjworks.png" alt="How ToolJet works flow" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/howtjworks.webp" width="100%" height="100%" alt="How ToolJet works flow" />
</div> </div>
@ -83,21 +83,21 @@ Before getting into the quickstart, Sign up and create your account on **[ToolJe
1. Navigate to **ToolJet DB Editor** from the left sidebar on the dashboard 1. Navigate to **ToolJet DB Editor** from the left sidebar on the dashboard
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/111c.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/tooljetdbeditor.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
2. Click on **Create New Table** button, enter **Table name** and **Add columns** from the drawer that slides from the right. Click on **Create** to add the table. 2. Click on **Create New Table** button, enter **Table name** and **Add columns** from the drawer that slides from the right. Click on **Create** to add the table.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/prodtable.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/createnewtable.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
3. Once the table is created, click on the **Add new row** button to add the data to the table and click **Create**. 3. Once the table is created, click on the **Add new row** button to add the data to the table and click **Create**.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/newrow1.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/addnewrow.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
@ -111,7 +111,7 @@ Learn more about the **[ToolJet Database here](/docs/tooljet-database)**
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/newapp1.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/createnewapplication.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
@ -122,7 +122,7 @@ Learn more about the **[ToolJet Database here](/docs/tooljet-database)**
2. When you click on create new app the **App-builder** will open up. You can rename your application from `untitled` to **Support Tool** from the top left of app-builder. 2. When you click on create new app the **App-builder** will open up. You can rename your application from `untitled` to **Support Tool** from the top left of app-builder.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/appbuilder2.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/appbuilder.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
@ -138,7 +138,7 @@ Learn more about the **[ToolJet Database here](/docs/tooljet-database)**
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/ui.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/buildingui.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
@ -151,7 +151,7 @@ ToolJet application's User interface is constructed using Components like Tables
1. We can add a new datasource from the **[Global datasources](/docs/data-sources/overview)** page from the dashboard but since we are using **ToolJet Database** we don't need to add any external datasource. Go to the **Query Panel and select ToolJet Database** 1. We can add a new datasource from the **[Global datasources](/docs/data-sources/overview)** page from the dashboard but since we are using **ToolJet Database** we don't need to add any external datasource. Go to the **Query Panel and select ToolJet Database**
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/tjdb2.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/tooljetdb.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
@ -164,14 +164,14 @@ ToolJet application's User interface is constructed using Components like Tables
3. Click on **Create** to create the query and then click **Run** to trigger the query and get response. You can also check the query response by clicking **Preview** button without firing the query. 3. Click on **Create** to create the query and then click **Run** to trigger the query and get response. You can also check the query response by clicking **Preview** button without firing the query.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/listrowsq.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/createquery.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
4. Go to the **Table properties** by clicking on the component handle and bind the data returned by the query in the **Table data** property. When building apps in ToolJet anything inside `{{}}` is JavaScript and we javascript dot notation to get the data from query and populate the table using **{{queries.tooljetdb1.data}}**. The table will be auto-populated once the table data is entered. 4. Go to the **Table properties** by clicking on the component handle and bind the data returned by the query in the **Table data** property. When building apps in ToolJet anything inside `{{}}` is JavaScript and we javascript dot notation to get the data from query and populate the table using **{{queries.tooljetdb1.data}}**. The table will be auto-populated once the table data is entered.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/tabledata.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/tabledata.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
@ -183,7 +183,7 @@ ToolJet application's User interface is constructed using Components like Tables
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/query2.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/selecttablecustomers.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
@ -194,7 +194,7 @@ ToolJet application's User interface is constructed using Components like Tables
6. Now, let's bind this query to the **Add Product** button. Click on the button handle to open its properties, **Add an handler** -> **Select Event (On Click)** -> **Select Action (Run Query)** -> **Select Query (tooljetdb2)**. 6. Now, let's bind this query to the **Add Product** button. Click on the button handle to open its properties, **Add an handler** -> **Select Event (On Click)** -> **Select Action (Run Query)** -> **Select Query (tooljetdb2)**.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/event.png" alt="Getting started: Quickstart" /> <img className="screenshot-full" src="/img/v2-beta/getting_started/quickstart/compressed/addproductbutton.webp" width="100%" height="100%" alt="Getting started: Quickstart" />
</div> </div>
@ -206,7 +206,7 @@ ToolJet application's User interface is constructed using Components like Tables
### Preview, Release and Share app ### Preview, Release and Share app
1. Click on the **Preview** on the top-right of app builder to immediately check the currently opened version of the app in production. 1. Click on the **Preview** on the top-right of app builder to immediately check the currently opened version of the app in production.
2. Click on the **Release** button to publish the currently opneded version of the app and push the changes to production. 2. Click on the **Release** button to publish the currently opened version of the app and push the changes to production.
3. **Share** option allows you to share the **released version** of the application with other users or you can also make the app **public** and anyone with the URL will be able to use the app. 3. **Share** option allows you to share the **released version** of the application with other users or you can also make the app **public** and anyone with the URL will be able to use the app.
:::tip :::tip
@ -239,7 +239,7 @@ To contribute to ToolJet code, plugins, and documentation, refer to our **[Contr
[![GitHub license](https://img.shields.io/github/license/ToolJet/ToolJet)](https://github.com/ToolJet/ToolJet) [![GitHub license](https://img.shields.io/github/license/ToolJet/ToolJet)](https://github.com/ToolJet/ToolJet)
<a href="https://github.com/tooljet/tooljet/graphs/contributors"> <a href="https://github.com/tooljet/tooljet/graphs/contributors">
<img src="https://contrib.rocks/image?repo=tooljet/tooljet&max=360&columns=20" /> <img src="https://contrib.rocks/image?repo=tooljet/tooljet&max=360&columns=20" width="100%" height="100%" alt="contributors" />
</a> </a>
## Help and Support ## Help and Support

View file

@ -19,7 +19,7 @@ To protect the user's privacy, Geolocation API requests permission to locate the
</div> </div>
- In the app editor, go to the query panel at the bottom and create a **[RunJS query](/docs/data-sources/custom-js)** by selecting **Run JavaScript Code** as the datasource - In the app editor, go to the query panel at the bottom and create a **[RunJS query](/docs/data-sources/run-js/#runjs-query-examples)** by selecting **Run JavaScript Code** as the datasource
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>

View file

@ -0,0 +1,151 @@
---
id: delete-multiple-rows
title: Delete multiple rows in table
---
The table component in the ToolJet has the option for bulk selection of rows that can have various use cases such as **updating** or **deleting** records. However, the datasources does not support bulk delete or bulk update operations.
In this guide, we will learn how we can delete multiple rows in a table. We have assumed that you have successfully connected the data source. For this guide, we will be using the PostgreSQL data source as an example database, currently, this workaround can be used only for PostgreSQL and MySQL.
## 1. Create a query to fetch the data from the database
Create a new query, name it `getRecords` and use SQL mode:
```sql
SELECT * FROM tooljet // replace tooljet with your table name
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/getRecords.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
Enable the **Run the query on application load?** option. This will ensure that the query is executed when the application is loaded.
## 2. Load the data on the table
Now, we will load the data on the table. For this, we will use the `getRecords` query that we created in the previous step. Drag the table component from the right sidebar and drop it on the canvas.
On table properties, go to the table data property and set the value to `{{queries.getRecords.data}}`. This will load the data from the `getRecords` query on the table.
Run the query and you should see the data loaded on the table.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/querydata.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
## 3. Enable bulk row selection on table
Now, we will enable the bulk row selection on the table. For this, go to the table properties and enable the **Bulk selection** option. Enabling this option will allow you to select multiple rows on the table. This option is disabled by default.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/bulkselection.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
## 4. Create a custom javascript query
Now, we will create a custom javascript query that will **generate a SQL statement** to delete the selected rows from the table component based on a list of selected IDs, assuming the IDs are stored in the **id** column and that the name of the table component is **table1**. The actual database name should be replaced with **tooljet** as indicated in the SQL statemnent in the code below:
```js
const uniqueIdentifier = "id";
const idsToDelete = Object.values(components.table1.selectedRows).map(dataUpdate => dataUpdate[uniqueIdentifier]);
const idsString = idsToDelete.map(id => `'${id}'`).join(', ');
const SQL = `DELETE FROM tooljet WHERE ${uniqueIdentifier} IN (${idsString});`;
return SQL;
```
If you click on the **Preview** button, you should see the SQL statement generated by the query:
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/runjs.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
Now, let's select a few rows on the table and then preview the SQL query generated by the javascript query:
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/runjs1.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
## 5. Create a new query to delete the rows
Now, we will create a new query to delete the rows from the table. Create a new query, name it `delete` and use SQL mode:
```sql
{{queries.runjs1.data}} // replace runjs1 with the name of the javascript query
```
In this query, we are dynamically loading the SQL statement generated by the javascript query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/delete.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
## 6. Add a button to delete the selected rows
Now, we will add a button to delete the selected rows from the table. Drag the button component from the right sidebar and drop it on the canvas. Edit its properties and set the **Button text** to **Delete**.
Add a new **Event** to the button on **On click** event to trigger the **Run Query** action and select the `runjs1` query that we created in the previously.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/button.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
Optionally, we can add a loading state to the button whenever the `delete` or `getRecords` query is running:
```js
{{queries.delete.isLoading || queries.getRecords.isLoading}}
```
Now, whenever you click on the button, the javascript query will generate a SQL statement to delete the selected rows from the table but to delete the rows from the database, we need to add event handler to the **runjs1** query to trigger the **delete** query whenever the `runjs1` query is **executed and successfull**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/eventrunjs.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
Now, whenever you click on the button, the javascript query will generate a delete SQL statement with selected rows on the table and the `delete` query will delete the rows from the database.
Similarly, you can add an Event to the **delete** query to trigger the **getRecords** query whenever the `delete` query is executed and successful. This will ensure that the table is updated with the latest data from the database.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/delete-rows/eventdelete.png" alt="How-to: Delete multiple rows in table" />
</div>
<br/>
## 7. Preview the application
The application is now ready. Click on the **Preview** button on the topbar of the app builder to preview the application.

View file

@ -11,7 +11,7 @@ In this how-to guide, we will import a few JavaScript libraries and use it in th
You can import any of the available libraries using their **CDN**. Find free CDN of the open source projects at **[jsDelivr](https://www.jsdelivr.com/)** You can import any of the available libraries using their **CDN**. Find free CDN of the open source projects at **[jsDelivr](https://www.jsdelivr.com/)**
::: :::
- Create a new application and then create a new RunPy query from the query panel. - Create a new application and then create a new RunJS query from the query panel.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/import-js/newquery.png" alt="Import external libraries using RunJS" /> <img className="screenshot-full" src="/img/how-to/import-js/newquery.png" alt="Import external libraries using RunJS" />
@ -52,7 +52,7 @@ You can import any of the available libraries using their **CDN**. Find free CDN
</div> </div>
:::tip :::tip
Enable the **Run this query on application load?** option to make the libraries available throughout the application as soon as the app is laoded. Enable the **Run this query on application load?** option to make the libraries available throughout the application as soon as the app is loaded.
::: :::
## Examples ## Examples

View file

@ -139,11 +139,23 @@ actions.copyToClipboard('contentToCopy')
**Syntax:** **Syntax:**
```javascript ```js
actions.generateFile('fileName', 'fileType', 'data') actions.generateFile('fileName', 'fileType', 'data')
``` ```
`fileName` is the name that you want to give the file(string), `fileType` can be `csv`, `plaintext`, or `pdf` and the `data` is the data that you want to store in the file.
**Example:** `fileName` is the name that you want to give the file(string), `fileType` can be `csv` or `text`, and `data` is the data that you want to store in the file. Example for generating CSV file:
```js
actions.generateFile('csvfile1', 'csv', '{{components.table1.currentPageData}}') // generate a csv file named csvfile1 with the data from the current page of table
```
Example for generating Text file:
```js
actions.generateFile('textfile1', 'plaintext', '{{JSON.stringify(components.table1.currentPageData)}}') // generate a text file named textfile1 with the data from the current page of table (stringified)
```
Example for generating PDF file:
```js
actions.generateFile('Pdffile1', 'pdf', '{{components.table1.currentPageData}}') // generate a text file named Pdffile1 with the data from the current page of table
```
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
@ -189,7 +201,7 @@ actions.showAlert('error' , 'This is an error' )
To run multiple actions from a runjs query, you'll have to use **async-await** in the function. To run multiple actions from a runjs query, you'll have to use **async-await** in the function.
Here is a example code snippet for running the queries and showing alert after specific intervals. Check the complete guide on running queries at specified intervals **[here](/docs/next/how-to/run-query-at-specified-intervals)**. Here is a example code snippet for running the queries and showing alert after specific intervals. Check the complete guide on running queries at specified intervals **[here](/docs/how-to/run-query-at-specified-intervals)**.
```js ```js
actions.setVariable('interval',setInterval(countdown, 5000)); actions.setVariable('interval',setInterval(countdown, 5000));

View file

@ -0,0 +1,68 @@
---
id: use-server-side-pagination
title: Using server side pagination for efficient data handling in tables
---
In this guide we will learn how to use server side pagination in table component. This will be helpful if you have a large data set and you want to load data in chunks. This will also help you to improve the performance of your application. This guide will be helpful if you are using datasources like MySQL, PostgreSQL, MSSQL, MongoDB, etc. in which you can use `limit` and `offset` to fetch data in chunks. We have also included an example to load data from Google Sheets in chunks.
## Loading data from PostgreSQL in chunks
- Let's say you have a table `users` in your PostgreSQL database and you want to load data from this table in chunks. You can use `limit` and `offset` to fetch data in chunks. Here is the SQL query to fetch data in chunks:
```sql
SELECT *
FROM users
ORDER BY id
LIMIT 100 OFFSET {{(components.table1.pageIndex-1)*100}};
```
The query will fetch 100 rows at a time from the postgresql users table, and the number of rows returned is determined by the current value of `pageIndex`(exposed variable) in the Table component.
1. `ORDER BY id`: This part of the query specifies the ordering of the result set. It orders the rows based on the `id` column. You can replace `id` with the appropriate column name based on how you want the rows to be ordered.
2. `LIMIT 100`: The `LIMIT` clause limits the number of rows returned to 100. This means that each time the query is executed, it will fetch 100 rows from the table.
3. `OFFSET {{(components.table1.pageIndex-1)*100}}`: The `OFFSET` clause determines where to start fetching rows from the result set. In this case, the offset value is calculated based on the `pageIndex`(exposed variable) in the Table component. The formula `(components.table1.pageIndex-1)*100` calculates the starting row number for the current page. Since the index is 1-based, we subtract 1 from `pageIndex` to convert it to a 0-based index. Then we multiply it by 100 to get the offset for the current page. For example, if `pageIndex` is 1, the offset will be 0, which means it will fetch rows from the first 100 rows. If `pageIndex` is 2, the offset will be 100, which means it will fetch rows from rows 101 to 200, and so on.
- Create a new query that will return the count of the records on the `users` table in postgresql db. This query will be used to calculate the total number of pages in the Table component. Here is the SQL query to fetch the count of records:
```sql
SELECT COUNT(*)
FROM users;
```
- Enable the option to run the query on page load so that the query is executed when the app loads.
- Add an event handler to run the query that fetches data from the PostgreSQL table and then save the changes.
- Once the count query is created, execute it to get the total number of records. You can dynamically access the count of records using `{{queries.<countquery>.data[0].count}}`.
**Now, let's edit the properties of the Table component:**
- Set the value of the **Table data** property to `{{queries.<postgresquery>.data}}`
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/server-side/data.png" alt="Table data"/>
</div>
- Enable the **server-side pagination** option
- Click on the `Fx` next to **Enable previous page button** and set it's value to `{{components.table1.pageIndex >=2 ? true : false}}`. This condition disables the previous page button when the current page is page `1`.
- Click on the `Fx` next to **Enable next page button** and set it's value to `{{components.table1.pageIndex < queries.<countquery>.data[0].count/100 ? true : false}}`. This condition disables the next page button when the current page is the last page.
- Set the value of the **Total records server side** property to `{{queries.<countquery>.data[0].count}}`. This will set the total number of records in the Table component.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/server-side/pagination.png" alt="Table data"/>
</div>
- Now, the last step is to set the **loading state** and add the **event handler**:
- Loading State: Set the loading state property to `{{queries.<postgresquery>.isLoading}}`. This will show the loading indicator on the table component when the query is executing.
- Event Handler: Select the **Page changed** event and choose the **Run Query** action. Then, select the **Query** from the dropdown that fetches data from the PostgreSQL table
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/server-side/event.png" alt="Table data" />
</div>
Now, whenever the page is changed, the query will be executed, and the data will be fetched from the PostgreSQL table in chunks.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/server-side/change.gif" alt="Table data" />
</div>

View file

@ -23,7 +23,7 @@ var data = (await axios.get(url)).data;
return data return data
``` ```
In the code snippet, a variable url is declared which is assigned the URL of the JSON API. Then another variable is decalared which sends a GET request to the JSON API. Save the query and hit Preview to view the data returned by the API. In the code snippet, a variable url is declared which is assigned the URL of the JSON API. Then another variable is declared which sends a GET request to the JSON API. Save the query and hit Preview to view the data returned by the API.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
@ -56,7 +56,7 @@ The Axios POST request uses an object after the request URL to define the proper
</div> </div>
:::tip :::tip
Check out the tutorial on **[Build GitHub star history tracker](https://blog.tooljet.com/build-github-stars-history-app-in-5-minutes-using-low-code/)** that utlizes the axios library. Check out the tutorial on **[Build GitHub star history tracker](https://blog.tooljet.com/build-github-stars-history-app-in-5-minutes-using-low-code/)** that utilizes the axios library.
::: :::

View file

@ -66,7 +66,7 @@ In this how-to guide, we will be building a simple application that will leverag
</div> </div>
- Let's create a query that will get the data from the form and add a record in the sheet. Create a new google sheeet query and from the operation choose **Append data to a spreadsheet** - Let's create a query that will get the data from the form and add a record in the sheet. Create a new google sheet query and from the operation choose **Append data to a spreadsheet**
```js ```js
[ [
{ {

View file

@ -31,7 +31,7 @@ Let's take a look at the layout of the Inspector panel:
</div> </div>
- On hovering an item on the inspector, the **copy path** and **copy value** buttons will appear on the right of the item. Copying the path and pasting it onto the component property or query parameter will always get the dynamic value but using `Copy value` uption will copy the current value of the item and will be static when pasted in a component property or query parameter. - On hovering an item on the inspector, the **copy path** and **copy value** buttons will appear on the right of the item. Copying the path and pasting it onto the component property or query parameter will always get the dynamic value but using `Copy value` option will copy the current value of the item and will be static when pasted in a component property or query parameter.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/use-inspector/path.png" alt="How to - Use Inspector" width="500" /> <img className="screenshot-full" src="/img/how-to/use-inspector/path.png" alt="How to - Use Inspector" width="500" />
@ -84,23 +84,35 @@ components section can be used to inspect the properties and values of the compo
### globals ### globals
globals section includes the following sub-sections:
- **currentUser:** The currentUser object contains information about the currently logged-in user such as **email**, **firstName**, and **lastName**.
- **groups:** The groups array contains the name of the groups the currently logged-in user is added to. Note: The `all_users` is default groups for everyone.
- **theme:** The theme object contains the name of the currently active theme.
- **urlparam:** The urlparams contains the information about the url parameters of the application.
:::info
All the global variables can be accessed anywhere inside the ToolJet applications. Here's an **[example use-case](/docs/how-to/access-currentuser)** of using these variables.
:::
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/use-inspector/globals.png" alt="How to - Use Inspector" width="500" /> <img className="screenshot-full" src="/img/how-to/use-inspector/globals.png" alt="How to - Use Inspector" width="500" />
</div> </div>
The globals section consists of the following sub-sections:
- **currentUser:** The currentUser object contains information about the currently logged-in user, such as their **email**, **firstName**, and **lastName**.
- **groups:** The groups array contains the names of the groups to which the currently logged-in user belongs. Note: The `all_users` group is a default group for everyone.
- **theme:** The theme object contains the name of the currently active theme.
- **urlparams:** The urlparams contain information about the URL parameters of the application.
- **environment:** This variable holds two keys: **id** and **name**. The **id** is a unique identifier generated automatically, and the **name** holds the name of the currently opened environment of the app version.
- **modes:** This variable holds one of three values: **edit**, **preview**, or **view**, depending on the current state of the app. If the app is opened in editing mode, the mode will be set to **edit**. If the app is opened by clicking the preview button on the app builder, the variable will be set to **preview**. If the app is opened using the URL from the **Share** modal, the mode will be set to **view**.
:::tip
The **environment** and **mode** variables are only available in **ToolJet Enterprise Edition v2.2.3** and above.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/use-inspector/env.png" alt="How to - Use Inspector" width="500" />
</div>
:::
:::info
All the global variables can be accessed anywhere within ToolJet applications. Here's an **[example use-case](/docs/how-to/access-currentuser)** that demonstrates the usage of these variables.
:::
### variables ### variables
variables section include all the variables set by the user in the application. These variables can be set from the event handlers from the components or from the queries. The variables will be in the **key-value** pair and can be accessed throughout the application. variables section include all the variables set by the user in the application. These variables can be set from the event handlers from the components or from the queries. The variables will be in the **key-value** pair and can be accessed throughout the application.

107
docs/docs/licensing.md Normal file
View file

@ -0,0 +1,107 @@
---
id: licensing
title: Licensing
---
<div className='badge badge--primary heading-badge'>Available on: Paid plans</div>
## Setting up for Licensing
This comprehensive guide aims to assist you in the process of configuring paid plans on your self-hosted platform. We offer three different licenses tailored to your specific requirements. Let's explore how to get the most out of ToolJet's powerful features! 🚀
Let's look into three types of licenses:
1. **Trial License**: This is a free license that grants access to premium features for a 14-day trial period. In order to get this, please reach out to our team.
2. **Business License**: This is a paid license that you can purchase **[directly](https://www.tooljet.com/pricing)**.
3. **Enterprise License**: This is a paid license with customizable options. To obtain this license, you have to contact our sales team.
After selecting the appropriate license that aligns with your needs, the next step is to proceed with the purchase. Once you have completed the onboarding process, we will generate a unique license key specifically tailored to your chosen specifications. Keep reading to gain a more comprehensive understanding of this process.
**To update your trial license key, please follow these steps:**
1. Set up the instance and log in as a **[Super Admin](/docs/Enterprise/superadmin)**.
2. Navigate to the instance settings page.
3. In the license key tab, make the necessary updates to the provided license key.
4. Within the license tab of the instance settings page, you can access the limit tab, which displays the current status of available super admins, builders, and end users.
:::caution Note
The trial license key will be valid for 14 days. To fully enjoy ToolJet, we recommend upgrading to premium plans within this period. If you wish to upgrade from the trial to the business or enterprise edition, you can click the **Upgrade or Renew** button or contact our team via **[Slack](https://tooljet.com/slack)**. Upon expiration, access to premium features like OpenID SSO login and Audit logs will be restricted, ensuring no data loss occurs. However, don't worry! You can still upgrade to any of our premium plans and enjoy the benefits of ToolJet.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/licensing/licensingpage1.png" alt="Licensing" />
</div>
**Ref: Screenshot to update license key**
## A) Chosen Plan: Business Plan
If you decide to proceed with the Business Plan and have made the purchase, please wait for our team to get back to you within 24-48 hours to get you onboarded.
**To update the business license key, follow these steps:**
1. Log in as a **[Super Admin](/docs/Enterprise/superadmin)**, ensuring that you are on the correct instance URL.
2. Go to the instance settings page.
3. In the license key tab, update the provided license key.
4. Within the license tab of the instance settings page, you can access the limit tab, which provides details about available super admins, builders, and end users.
:::info Note
As a super admin, you can conveniently view the remaining days of your enterprise edition period on the dashboard. (Refer to screenshots below)
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/licensing/licensingpage2.png" alt="Licensing" />
</div>
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/licensing/licensingpage3.png" alt="Licensing" />
</div>
:::warning Important
The business license key will be valid for 3 months only. You can renew it to continue using ToolJet to its fullest potential.
:::
## B) Chosen Plan: Enterprise Plan
If you choose to move forward with the Enterprise Plan and have completed the purchase process with our sales team, kindly anticipate a response from our team within 24-48 hours to facilitate your onboarding process.
**To update the enterprise license key, follow these steps:**
1. Log in as a **[Super Admin](/docs/Enterprise/superadmin)**, ensuring that you are on the correct instance URL.
2. Go to the instance settings page.
3. In the license key tab, update the provided license key.
4. Within the license tab of the instance settings page, you can access the limit tab, which provides details about available super admins, builders, and end users.
:::info Note
As a super admin, you can conveniently view the remaining days of your enterprise edition period on the dashboard.
:::
---
## Frequently Asked Questions (FAQs)
### 1) How can I upgrade or renew my license?
If your business or enterprise edition license key is nearing expiration, please click the **Upgrade or Renew** button or contact us via email at hello@tooljet.com to obtain an extended license key. If you intend to increase the number of users, please reach out to us via **[Slack](https://tooljet.com/slack)** or review our pricing page at https://www.tooljet.com/pricing before making a request.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/licensing/licensingpage4.png" alt="Licensing" />
</div>
**Ref: Screenshot addressing upgrade/renew CTAs. Note that there are a couple of other pages which will display banners or CTAs, from where you can upgrade/renew.**
### 2) What happens if my license expires?
If your business or enterprise license key expires, your instance will revert to operating as a free plan. While you can still create unlimited apps, workspaces, and add users, premium features such as OpenID and Audit logs will no longer be accessible. For further information, please refer to the relevant **[plans](https://www.tooljet.com/pricing)**.
### 3) How can I add more users?
There are different methods to do this:
**a)** You can renew directly using the **[business plan](https://www.tooljet.com/pricing)**. (Note: Please do check the list of premium features available with this plan)
**b)** You can directly reach out to us via **[Slack](https://tooljet.com/slack)** or **[email](mailto:hello@tooljet.com)** and we will be happy to provide you the support.
***Lastly, please keep in mind that your license key is private and strictly prohibited from being shared with any third parties.***

View file

@ -3,7 +3,7 @@ id: marketplace-plugin-harperdb
title: HarperDB title: HarperDB
--- ---
HarperDB is a managed non-relational database service known for its flexibility and scalability, offering efficient data storage and retrieval. ToolJet seamlessly connects to HarperDB, enabling easy reading and writing of data. HarperDB is a database and application development platform that is focused on performance and ease of use. With flexible user-defined APIs, simple HTTP/S interface, and a high-performance single-model data store that accommodates both NoSQL and SQL workloads, HarperDB scales with your application from proof of concept to production. ToolJet integrates with HarperDB, providing a streamlined interface for reading and writing data.
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
@ -43,6 +43,11 @@ To perform queries on HarperDB, click the `+Add` button in the query manager loc
SQL mode enables you to perform various operations on the database using SQL statements. SQL mode enables you to perform various operations on the database using SQL statements.
- **[Select](#select)**
- **[Insert](#insert)**
- **[Update](#update)**
- **[Delete](#delete)**
#### Select #### Select
The SELECT statement is used to query data from the database. The SELECT statement is used to query data from the database.
@ -98,3 +103,170 @@ DELETE FROM sampleorg.people WHERE id = 5
<img className="screenshot-full" src="/img/marketplace/plugins/harperdb/delete.png" alt="Marketplace: HarperDB" /> <img className="screenshot-full" src="/img/marketplace/plugins/harperdb/delete.png" alt="Marketplace: HarperDB" />
</div> </div>
### NoSQL mode
NoSQL mode enables you to perform schema-less storage and retrieval of JSON documents.
- **[Insert](#insert-nosql)**
- **[Update](#update-nosql)**
- **[Delete](#delete-nosql)**
- **[Search by hash](#search-by-hash)**
- **[Search by value](#search-by-value)**
- **[SeleSearch by conditions](#search-by-conditions)**
#### Insert (NoSQL)
Insert operation allows to add one or more rows of data to a database table.
| Parameters | Description |
| ---------- | ----------- |
| Schema (required) | schema where the table you are inserting records into lives |
| Table (required) | table name where you want to insert records |
| Records (required) | array of one or more records for insert |
**Example Records:**
```js
[{id: 22, name: "James Scott", age: 26, country:"Italy", hobby: "football"},...]
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/marketplace/plugins/harperdb/nosql_insert.png" alt="Marketplace: HarperDB" />
</div>
#### Update (NoSQL)
The Update operation modifies the values of specified attributes in one or more rows of a database table based on the hash attribute(primary key) that identifies the rows.
| Parameters | Description |
| ---------- | ----------- |
| Schema (required) | schema where the table you are updating records into lives |
| Table (required) | table name where you want to update records |
| Records (required) | array of one or more records for update |
**Example Records:**
```js
[{id:12, name:"Jeff Hannistor"},...] // Record having 12 as Primary key value will be updated
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/marketplace/plugins/harperdb/nosql_update.png" alt="Marketplace: HarperDB" />
</div>
#### Delete (NoSQL)
Removes one or more rows of data from a specified table.
| Parameters | Description |
| ---------- | ----------- |
| Schema (required) | schema where the table you are deleting records into lives |
| Table (required) | table name where you want to delete records |
| Hash Values (required) | array of one or more hash attribute (primary key) values, which identifies records to delete |
**Example Hash Values:**
```js
[6, 15] // Records having 6 and 15 as Primary key value will be deleted
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/marketplace/plugins/harperdb/nosql_delete.png" alt="Marketplace: HarperDB" />
</div>
#### Search by hash
Returns data from a table for one or more hash values.
| Parameters | Description |
| ---------- | ----------- |
| Schema (required) | schema where the table you are searching lives |
| Table (required) | table you wish to search |
| Hash Values (required) | array of hashes to retrieve |
| Table Attributes (required) | define which attributes you want returned. |
**Example Hash Values:**
```js
[124, 66] // Records having 6 and 15 as Primary key value will be retrieved
```
**Example Table Attributes:**
```js
['id', 'name', 'age', 'hobby', 'country'] // Only the provided columns will be retrieved from the table
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/marketplace/plugins/harperdb/searchbyhash.png" alt="Marketplace: HarperDB" />
</div>
#### Search by value
Returns data from a table for a matching value.
| Parameters | Description |
| ---------- | ----------- |
| Schema (required) | schema where the table you are searching lives |
| Table (required) | table you wish to search |
| Hash Values (required) | array of hashes to retrieve |
| Search Attribute (required) | attribute you wish to search can be any attribute |
| Search Value (required) | value you wish to search - wild cards are allowed. |
| Table Attributes (required) | define which attributes you want returned. |
**Example Search Attribute:**
```bash
name
```
**Example Search Value:**
```bash
John Doe
or
Joh* // using wild card
```
**Example Table Attributes:**
```js
['id', 'name', 'age', 'hobby', 'country'] // Only the provided columns will be retrieved from the table
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/marketplace/plugins/harperdb/searchbyvalue.png" alt="Marketplace: HarperDB" />
</div>
#### Search by conditions
Returns data from a table for one or more matching conditions.
| Parameters | Description |
| ---------- | ----------- |
| Schema (required) | schema where the table you are searching lives |
| Table (required) | table you wish to search |
| Operator inbetween each condition (optional) | the operator used between each condition - 'And', 'Or'. The default is 'And'. |
| Offset (optional) | the number of records that the query results will skip. The default is 0. |
| Limit (optional) | the number of records that the query results will include. The default is null, resulting in no limit. |
| Table Attributes (required) | define which attributes you want returned. |
| Conditions to filter (required) | the array of conditions objects, to filter by. Must include one or more object in the array. **search_attribute** (required) - the attribute you wish to search, can be any attribute. **search_type** (required) - the type of search to perform - 'equals', 'contains', 'starts_with', 'ends_with', 'greater_than', 'greater_than_equal', 'less_than', 'less_than_equal', 'between'. **search_value** (required) - case-sensitive value you wish to search. If the search_type is 'between' then use an array of two values to search between. Check the example below. |
**Example Table Attributes:**
```js
['id', 'name', 'age', 'hobby', 'country'] // Only the provided columns will be retrieved from the table
```
**Example Conditions to filter:**
```js
[{'search_attribute': 'age', 'search_type': 'between', 'search_value': [20, 28]}, {'search_attribute': 'name', 'search_type': 'contains', 'search_value': 'Ray'}]
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/marketplace/plugins/harperdb/searchbyconditions.png" alt="Marketplace: HarperDB" />
</div>

Some files were not shown because too many files have changed in this diff Show more