Merge branch 'appbuilder/sprint-14' into fix/event-action-ui

This commit is contained in:
devanshu052000 2025-06-25 12:55:11 +05:30
commit 80bcc03b08
565 changed files with 40828 additions and 54952 deletions

View file

@ -33,7 +33,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.18.2
node-version: 22.15.1
- name: Set up Git authentication for private submodules
run: |
@ -160,9 +160,9 @@ jobs:
name: screenshots-appbuilder-${{ matrix.edition }}
path: cypress-tests/cypress/screenshots
# Cypress-App-builder-Subpath:
# Cypress-App-builder-Subpath:
# runs-on: ubuntu-22.04
# if: contains(github.event.pull_request.labels.*.name, 'run-cypress') ||
# if: contains(github.event.pull_request.labels.*.name, 'run-cypress') ||
# contains(github.event.pull_request.labels.*.name, 'run-cypress-app-builder-subpath')
# steps:

View file

@ -12,91 +12,106 @@ env:
jobs:
Cypress-Platform:
runs-on: ubuntu-22.04
if: contains(github.event.pull_request.labels.*.name, 'run-cypress') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce')
if: contains(github.event.pull_request.labels.*.name, 'run-cypress') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce')
strategy:
fail-fast: false
matrix:
edition: >-
${{
contains(github.event.pull_request.labels.*.name, 'run-cypress') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') && fromJson('["ee"]') ||
fromJson('[]')
}}
edition:
- ${{ contains(github.event.pull_request.labels.*.name, 'run-cypress') && 'ce' || contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') && 'ce' || contains(github.event.pull_request.labels.*.name, 'run-cypress-ce') && 'ce' || '' }}
- ${{ contains(github.event.pull_request.labels.*.name, 'run-cypress') && 'ee' || contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') && 'ee' || '' }}
exclude:
- edition: ""
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.18.2
- name: Set up Git authentication for private submodules
- name: Debug labels and matrix edition
run: |
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
echo "Labels: ${{ toJSON(github.event.pull_request.labels.*.name) }}"
echo "Matrix edition: ${{ matrix.edition }}"
- name: Checkout with Submodules
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Checking out the correct branch for submodules EE
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64
docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
- name: Docker Login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set SAFE_BRANCH_NAME
run: echo "SAFE_BRANCH_NAME=$(echo ${{ env.BRANCH_NAME }} | tr '/' '-')" >> $GITHUB_ENV
- name: Build CE Docker image
if: matrix.edition == 'ce'
uses: docker/build-push-action@v4
with:
context: .
file: docker/ce-production.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Build EE Docker image
if: matrix.edition == 'ee'
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main'
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
- 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: Local development setup
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: Run PostgREST Docker Container
run: |
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
-e PGRST_DB_URI="postgres://postgres:postgres@localhost:5432/tooljet" \
-e PGRST_DB_ANON_ROLE="postgres" \
-e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
-e PGRST_DB_PRE_CONFIG=postgrest.pre_config \
postgrest/postgrest:v12.2.0
uses: docker/build-push-action@v4
with:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=${{ github.event.pull_request.head.ref }}
file: cypress-tests/cypress.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up environment variables
run: |
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'ee' || 'ce' }}" >> .env
echo "TOOLJET_HOST=http://localhost:8082" >> .env
echo "TOOLJET_EDITION=${{ matrix.edition }}" >> .env
echo "TOOLJET_HOST=http://localhost:3000" >> .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_HOST=postgres" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet_db" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=localhost" >> .env
echo "TOOLJET_DB_HOST=postgres" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "TOOLJET_DB_STATEMENT_TIMEOUT=60000" >> .env
echo "TOOLJET_DB_RECONFIG=true" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=localhost:3001" >> .env
echo "PGRST_DB_PRE_CONFIG=postgrest.pre_config" >> .env
echo "PGRST_DB_URI=postgres://postgres:postgres@localhost:5432/tooljet" >> .env
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
echo "ENABLE_MARKETPLACE_FEATURE=true" >> .env
echo "ENABLE_MARKETPLACE_DEV_MODE=true" >> .env
echo "ENABLE_PRIVATE_APP_EMBED=true" >> .env
@ -105,29 +120,50 @@ jobs:
echo "SSO_GIT_OAUTH2_CLIENT_ID=1234567890" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=3346shfvkdjjsfkvxce32854e026a4531ed" >> .env
- name: Set up database
run: |
npm run --prefix server db:create
npm run --prefix server db:reset
sleep 5
# Only add EE-specific env vars if edition is ee
if [ "${{ matrix.edition }}" = "ee" ]; then
echo "SSO_OPENID_NAME=tj-oidc-simulator" >> .env
echo "SSO_OPENID_CLIENT_ID=${{ secrets.SSO_OPENID_CLIENT_ID }}" >> .env
echo "SSO_OPENID_CLIENT_SECRET=${{ secrets.SSO_OPENID_CLIENT_SECRET }}" >> .env
echo "SSO_OPENID_WELL_KNOWN_URL=http://34.66.166.236:8080/.well-known/openid-configuration" >> .env
echo "LICENSE_KEY=${{ secrets.RENDER_LICENSE_KEY }}" >> .env
fi
- name: Start services
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
- name: Update docker-compose file
run: |
cd plugins && npm start &
cd server && npm run start:dev &
cd frontend && npm start &
# Update docker-compose.yaml with the appropriate image based on edition
if [ "${{ matrix.edition }}" = "ce" ]; then
sed -i '/^[[:space:]]*tooljet:/,/^$/ s|^\([[:space:]]*image:[[:space:]]*\).*|\1tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce|' docker-compose.yaml
elif [ "${{ matrix.edition }}" = "ee" ]; then
sed -i '/^[[:space:]]*tooljet:/,/^$/ s|^\([[:space:]]*image:[[:space:]]*\).*|\1tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee|' docker-compose.yaml
fi
- name: Install Docker Compose
run: |
curl -L "https://github.com/docker/compose/releases/download/v2.10.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
- name: Run docker-compose file
run: docker-compose up -d
- name: Checking containers
run: docker ps -a
- name: docker logs
run: sudo docker logs Tooljet-app
- name: Wait for the server to be ready
run: |
timeout 300 bash -c '
until curl --silent --fail http://localhost:8082; do
timeout 500 bash -c '
until curl --silent --fail http://localhost:3000; do
sleep 5
done'
- name: Postgres logs
run: docker logs postgrest
- name: Create Cypress environment file
- name: Create Cypress environment file for CE
if: matrix.edition == 'ce'
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
@ -135,13 +171,30 @@ jobs:
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Run Cypress tests
- name: Run Cypress tests for CE
if: matrix.edition == 'ce'
uses: cypress-io/github-action@v6
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:8082"
config: "baseUrl=http://localhost:3000"
config-file: cypress-platform.config.js
- name: Create Cypress environment file for EE
if: matrix.edition == 'ee'
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_EE_SECRETS }}
dir: "./cypress-tests"
- name: Run Cypress tests for EE
if: matrix.edition == 'ee'
uses: cypress-io/github-action@v6
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:3000"
config-file: cypress-ee-platform.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v4
if: always()
@ -175,7 +228,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.18.2
node-version: 22.15.1
- name: Set up Git authentication for private submodules
run: |
@ -311,7 +364,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.18.2
node-version: 22.15.1
- name: Set up Git authentication for private submodules
run: |
@ -459,7 +512,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.18.2
node-version: 22.15.1
- name: Set up Git authentication for private submodules
run: |

View file

@ -10,14 +10,12 @@ env:
PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:
deploy-marketplace-plugin:
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'deploy-marketplace-plugin' }}
runs-on: ubuntu-latest
steps:
- name: Sync repo
uses: actions/checkout@v3
@ -49,28 +47,58 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_SECRET_MAR_ACCESS_KEY }}
aws-region: us-east-2
- name: Install and build dependencies
- name: Install and build dependencies in order
run: |
cd marketplace && npm install && npm run build --workspaces
continue-on-error: true
- name: Build marketplace plugins
run: |
cd marketplace && AWS_BUCKET=tooljet-plugins-stage node scripts/upload-to-s3.js
cd marketplace
echo "🔧 Installing all workspace dependencies"
npm install
- name: Comment deployment URL
echo "🏗️ Building 'common' plugin first"
npm run build --workspace=plugins/common || exit 1
echo "🔁 Building all remaining plugins"
PLUGINS=$(ls plugins | grep -v '^common$')
for plugin in $PLUGINS; do
echo "🔨 Building plugin: $plugin"
npm run build --workspace=plugins/$plugin || exit 1
done
- name: Build marketplace plugins and capture summary
run: |
cd marketplace
echo "🚀 Uploading to S3"
AWS_BUCKET=tooljet-plugins-stage node scripts/upload-to-s3.js | tee upload_summary.log
- name: Extract upload summary
id: upload_summary
run: |
SUMMARY=$(awk '/UPLOAD SUMMARY/,0' marketplace/upload_summary.log)
echo "UPLOAD_SUMMARY<<EOF" >> $GITHUB_ENV
echo "$SUMMARY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Comment on success
if: success()
uses: actions/github-script@v5
with:
github-token: ${{secrets.GITHUB_TOKEN}}
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const runId = process.env.GITHUB_RUN_ID;
const runUrl = `https://github.com/${{ github.repository }}/actions/runs/${runId}`;
const summary = process.env.UPLOAD_SUMMARY;
const body = `Marketplace Plugin added to stage bucket\n\n🔍 [View Deployment Logs & Summary](${runUrl})\n\n\`\`\`\n${summary}\n\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Marketplace Plugin added to stage bucket'
})
body
});
- uses: actions/github-script@v6
- name: Label update on success
if: success()
uses: actions/github-script@v6
with:
script: |
try {
@ -90,3 +118,19 @@ jobs:
repo: context.repo.repo,
labels: ['marketplace-plugin-deployed']
})
- name: Comment on failure
if: failure()
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const runId = process.env.GITHUB_RUN_ID;
const runUrl = `https://github.com/${{ github.repository }}/actions/runs/${runId}`;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `❌ Marketplace Plugin deployment failed.\n\n🔍 [View Deployment Logs & Summary](${runUrl})`
});

75
.github/workflows/merging-pr.yml vendored Normal file
View file

@ -0,0 +1,75 @@
name: Merge Submodule PRs
on:
pull_request:
types: [closed, labeled]
jobs:
merge-submodules:
if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
steps:
- name: Extract Branch Name
run: echo "BRANCH_NAME=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV
- name: Merge PR in ee-server (if exists)
run: |
PR=$(gh pr list -R ToolJet/ee-server --head "$BRANCH_NAME" --state open --json number -q '.[0].number')
if [ -n "$PR" ]; then
echo "Found ee-server PR: #$PR"
gh pr merge -R ToolJet/ee-server "$PR" --merge --admin
else
echo "No open ee-server PR for branch $BRANCH_NAME"
fi
env:
GH_TOKEN: ${{ secrets.TOKEN_PR }}
- name: Merge PR in ee-frontend (if exists)
run: |
PR=$(gh pr list -R ToolJet/ee-frontend --head "$BRANCH_NAME" --state open --json number -q '.[0].number')
if [ -n "$PR" ]; then
echo "Found ee-frontend PR: #$PR"
gh pr merge -R ToolJet/ee-frontend "$PR" --merge --admin
else
echo "No open ee-frontend PR for branch $BRANCH_NAME"
fi
env:
GH_TOKEN: ${{ secrets.TOKEN_PR }}
check-submodule-prs:
if: github.event.action == 'labeled' && github.event.label.name == 'ready-to-merge'
runs-on: ubuntu-latest
steps:
- name: Extract Branch Name
run: echo "BRANCH_NAME=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV
- name: Check and Comment Linked Submodule PRs
run: |
echo "🔍 Checking linked submodule PRs for \`$BRANCH_NAME\`:" > comment.md
echo "" >> comment.md
SERVER_URL=$(gh pr list -R ToolJet/ee-server --head "$BRANCH_NAME" --state open --json url -q '.[0].url')
FRONTEND_URL=$(gh pr list -R ToolJet/ee-frontend --head "$BRANCH_NAME" --state open --json url -q '.[0].url')
if [ -n "$SERVER_URL" ]; then
echo "✅ ee-server PR - $SERVER_URL" >> comment.md
else
echo "❌ No open PR in ee-server" >> comment.md
fi
if [ -n "$FRONTEND_URL" ]; then
echo "✅ ee-frontend PR - $FRONTEND_URL" >> comment.md
else
echo "❌ No open PR in ee-frontend" >> comment.md
fi
echo "" >> comment.md
echo "📝 **Note**: The submodule PRs will be auto-merged once you merge this base PR-$PR_NUMBER into \`main\`." >> comment.md
gh pr comment "$PR_NUMBER" --repo ToolJet/ToolJet --body-file comment.md
env:
GH_TOKEN: ${{ secrets.TOKEN_PR }}
PR_NUMBER: ${{ github.event.pull_request.number }}

View file

@ -1,7 +1,9 @@
name: Render review deploy
on:
pull_request_target:
types: [labeled, unlabeled, closed]
types: [labeled, unlabeled, closed, synchronize, opened]
issue_comment:
types: [created, edited, deleted]
env:
PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
@ -1153,3 +1155,234 @@ jobs:
# } catch (e) {
# console.log(e)
# }
redeploy-review-app:
if: ${{ github.event.action == 'synchronize' || github.event.action == 'opened' }}
runs-on: ubuntu-latest
steps:
- name: Get PR labels
id: get_labels
uses: actions/github-script@v6
with:
script: |
const labels = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
return labels.data.map(l => l.name);
- name: Redeploy CE review app if active
if: contains(steps.get_labels.outputs.result, 'active-ce-review-app')
id: redeploy_ce
env:
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
PR_NUMBER: ${{ github.event.number }}
run: |
SERVICE_ID=$(curl --request GET \
--url "https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${PR_NUMBER}&limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY" | jq -r '.[0].service.id')
DEPLOY_RESPONSE=$(curl --request POST \
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys" \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header "authorization: Bearer $RENDER_API_KEY" \
--data '{"clearCache":"clear"}')
DEPLOY_ID=$(echo $DEPLOY_RESPONSE | jq -r '.id')
echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
echo "DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
- name: Redeploy EE review app if active
if: contains(steps.get_labels.outputs.result, 'active-ee-review-app')
id: redeploy_ee
env:
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
PR_NUMBER: ${{ github.event.number }}
run: |
SERVICE_ID=$(curl --request GET \
--url "https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${PR_NUMBER}&limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY" | jq -r '.[0].service.id')
DEPLOY_RESPONSE=$(curl --request POST \
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys" \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header "authorization: Bearer $RENDER_API_KEY" \
--data '{"clearCache":"clear"}')
DEPLOY_ID=$(echo $DEPLOY_RESPONSE | jq -r '.id')
echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
echo "DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
render-bot-check-deployment:
runs-on: ubuntu-latest
if: github.event.action == 'labeled' && github.event.label.name == 'render-check-deployment'
steps:
- name: Get PR labels
id: get_labels
uses: actions/github-script@v6
with:
script: |
const labels = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
return labels.data.map(l => l.name);
- name: Fetch CE service and deploy ID
run: |
response=$(curl --silent --request GET \
--url "https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${PR_NUMBER}&limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY")
SERVICE_ID=$(echo "$response" | jq -r 'if type=="array" and length > 0 then .[0].service.id else empty end')
if [[ -z "$SERVICE_ID" ]]; then
echo "No CE service found for PR #$PR_NUMBER. Skipping deployment ID fetch."
exit 0
fi
response_deploy=$(curl --silent --request GET \
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys?limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY")
DEPLOY_ID=$(echo "$response_deploy" | jq -r 'if type=="array" and length > 0 then .[0].deploy.id else empty end')
echo "CE_SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
echo "CE_DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
- name: Comment CE deployment details
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = process.env.PR_NUMBER;
const apiKey = process.env.RENDER_API_KEY;
const ceServiceRes = await fetch(`https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${prNumber}&limit=1`, {
headers: {
'accept': 'application/json',
'authorization': `Bearer ${apiKey}`
}
});
const ceServices = await ceServiceRes.json();
const ceServiceId = ceServices[0]?.service?.id || null;
let ceInfo = 'No active CE review app deployment found.';
if (ceServiceId) {
const deployRes = await fetch(`https://api.render.com/v1/services/${ceServiceId}/deploys?limit=1`, {
headers: {
'accept': 'application/json',
'authorization': `Bearer ${apiKey}`
}
});
const deployData = await deployRes.json();
const deploy = deployData[0]?.deploy || {};
const ceCommit = deploy.commit || {};
const status = deploy.status || 'unknown';
ceInfo = `### Community Edition\n- App: https://tooljet-ce-pr-${prNumber}.onrender.com\n- Dashboard: https://dashboard.render.com/web/${ceServiceId}\n- Commit: ${ceCommit.id || ''}\n- Message: ${ceCommit.message || ''}\n- Status: ${status}`;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: ceInfo
});
- name: Fetch EE service and deploy ID
run: |
response=$(curl --silent --request GET \
--url "https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${PR_NUMBER}&limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY")
SERVICE_ID=$(echo "$response" | jq -r 'if type=="array" and length > 0 then .[0].service.id else empty end')
if [[ -z "$SERVICE_ID" ]]; then
echo "No EE service found for PR #$PR_NUMBER. Skipping deployment ID fetch."
exit 0
fi
response_deploy=$(curl --silent --request GET \
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys?limit=1" \
--header 'accept: application/json' \
--header "authorization: Bearer $RENDER_API_KEY")
DEPLOY_ID=$(echo "$response_deploy" | jq -r 'if type=="array" and length > 0 then .[0].deploy.id else empty end')
echo "EE_SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
echo "EE_DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
- name: Comment EE deployment details
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = process.env.PR_NUMBER;
const apiKey = process.env.RENDER_API_KEY;
const eeServiceRes = await fetch(`https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${prNumber}&limit=1`, {
headers: {
'accept': 'application/json',
'authorization': `Bearer ${apiKey}`
}
});
const eeServices = await eeServiceRes.json();
const eeServiceId = eeServices[0]?.service?.id || null;
let eeInfo = 'No active EE review app deployment found.';
if (eeServiceId) {
const deployRes = await fetch(`https://api.render.com/v1/services/${eeServiceId}/deploys?limit=1`, {
headers: {
'accept': 'application/json',
'authorization': `Bearer ${apiKey}`
}
});
const deployData = await deployRes.json();
const deploy = deployData[0]?.deploy || {};
const eeCommit = deploy.commit || {};
const status = deploy.status || 'unknown';
eeInfo = `### Enterprise Edition\n- App: https://tooljet-ee-pr-${prNumber}.onrender.com\n- Dashboard: https://dashboard.render.com/web/${eeServiceId}\n- Commit: ${eeCommit.id || ''}\n- Message: ${eeCommit.message || ''}\n- Status: ${status}`;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: eeInfo
});
- name: Remove label
if: contains(steps.get_labels.outputs.result, 'render-check-deployment')
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'render-check-deployment'
})
} catch (e) {
console.log(e)
}

View file

@ -1 +1 @@
3.13.0
3.15.1

View file

@ -8,8 +8,8 @@
"typescript",
"typescriptreact"
],
"eslint.format.enable": true,
"editor.formatOnSave": true,
"eslint.format.enable": false,
"editor.formatOnSave": false,
"json.schemas": [
{
"fileMatch": [

View file

@ -76,8 +76,9 @@ module.exports = defineConfig({
experimentalRunAllSpecs: true,
baseUrl: "http://localhost:8082",
specPattern: [
"cypress/e2e/happyPath/appbuilder/commonTestcases/newSuits/**/*.cy.js",
// "cypress/e2e/happyPath/appbuilder/ceTestcases/**/*.cy.js"
// "cypress/e2e/happyPath/appbuilder/commonTestcases/newSuits/**/*.cy.js",
// "cypress/e2e/happyPath/appbuilder/ceTestcases/**/*.cy.js",
"cypress/e2e/happyPath/appbuilder/commonTestcases/newSuits/globalSetingsHappyPath.cy.js"
],
numTestsKeptInMemory: 1,
redirectionLimit: 7,

View file

@ -0,0 +1,114 @@
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");
const environments = {
'run-cypress-platform': {
baseUrl: "http://localhost:3000",
configFile: "cypress-platform.config.js"
},
'run-cypress-platform-subpath': {
baseUrl: "http://localhost:3000/apps",
configFile: "cypress-platform.config.js"
},
'run-cypress-platform-proxy': {
baseUrl: "http://localhost:4001",
configFile: "cypress-platform.config.js"
},
'run-cypress-platform-proxy-subpath': {
baseUrl: "http://localhost:4001/apps",
configFile: "cypress-platform.config.js"
}
};
const githubLabel = process.env.GITHUB_LABEL || 'run-cypress-platform';
const environment = environments[githubLabel];
module.exports = defineConfig({
execTimeout: 1800000,
defaultCommandTimeout: 30000,
requestTimeout: 30000,
pageLoadTimeout: 30000,
responseTimeout: 30000,
viewportWidth: 1440,
viewportHeight: 960,
chromeWebSecurity: false,
trashAssetsBeforeRuns: true,
e2e: {
setupNodeEvents (on, config) {
config.baseUrl = environment.baseUrl;
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);
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", {
dbConnection ({ 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,
baseUrl: environment.baseUrl,
configFile: environment.configFile,
specPattern: [
"cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js",
"cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
"cypress/e2e/happyPath/platform/eeTestcases/**/*.cy.js",
],
numTestsKeptInMemory: 1,
redirectionLimit: 15,
experimentalMemoryManagement: true,
video: false,
videoUploadOnPasses: false,
retries: {
runMode: 2,
openMode: 0,
},
},
});

View file

@ -8,7 +8,7 @@ const pdf = require("pdf-parse");
const environments = {
'run-cypress-platform': {
baseUrl: "http://localhost:8082",
baseUrl: "http://localhost:3000",
configFile: "cypress-platform.config.js"
},
'run-cypress-platform-subpath': {

View file

@ -0,0 +1,190 @@
FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN mkdir -p /app
WORKDIR /app
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME
# Clone and checkout the frontend repositorys
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
RUN git config --global http.version HTTP/1.1
RUN git config --global http.postBuffer 524288000
RUN git clone https://github.com/ToolJet/ToolJet.git .
# The branch name needs to be changed the branch with modularisation in CE repo
RUN git checkout ${BRANCH_NAME}
RUN git submodule update --init --recursive
# Checkout the same branch in submodules if it exists, otherwise stay on default branch
RUN git submodule foreach 'git checkout ${BRANCH_NAME} || true'
# Scripts for building
COPY ./package.json ./package.json
# Build plugins
COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/
RUN npm --prefix plugins install
COPY ./plugins/ ./plugins/
RUN NODE_ENV=production npm --prefix plugins run build
RUN npm --prefix plugins prune --production
ENV TOOLJET_EDITION=ee
# Build frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
RUN npm --prefix frontend install
COPY ./frontend/ ./frontend/
RUN npm --prefix frontend run build --production
RUN npm --prefix frontend prune --production
ENV NODE_ENV=production
ENV TOOLJET_EDITION=ee
# Build server
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM node:22.15.1
RUN apt-get update -yq \
&& apt-get install curl wget gnupg zip -yq \
&& apt-get install -yq build-essential \
&& apt -y install redis \
&& apt-get clean -y
# copy postgrest executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
ENV NODE_ENV=production
ENV TOOLJET_EDITION=ee
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt-get update && apt-get install -y freetds-dev libaio1 wget supervisor
# Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle
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 && \
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 && \
cd /opt/oracle/instantclient_21_10 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
cd /opt/oracle/instantclient_11_2 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
echo /opt/oracle/instantclient* > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
# Set the Instant Client library paths
ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}"
WORKDIR /
# copy npm scripts
COPY --from=builder /app/package.json ./app/package.json
# copy plugins dependencies
COPY --from=builder /app/plugins/dist ./app/plugins/dist
COPY --from=builder /app/plugins/client.js ./app/plugins/client.js
COPY --from=builder /app/plugins/node_modules ./app/plugins/node_modules
COPY --from=builder /app/plugins/packages/common ./app/plugins/packages/common
COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
# copy frontend build
COPY --from=builder /app/frontend/build ./app/frontend/build
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
WORKDIR /app
# Install PostgreSQL
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor --fix-missing
# Explicitly create PG main directory with correct ownership
RUN mkdir -p /var/lib/postgresql/13/main && \
chown -R postgres:postgres /var/lib/postgresql
RUN mkdir -p /var/log/supervisor /var/run/postgresql && \
chown -R postgres:postgres /var/run/postgresql /var/log/supervisor
# Remove existing data and create directory with proper ownership
RUN rm -rf /var/lib/postgresql/13/main && \
mkdir -p /var/lib/postgresql/13/main && \
chown -R postgres:postgres /var/lib/postgresql
# Initialize PostgreSQL
RUN su - postgres -c "/usr/lib/postgresql/13/bin/initdb -D /var/lib/postgresql/13/main"
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \
"user=root \n" \
"\n" \
"[program:redis] \n" \
"command=redis-server /etc/redis/redis.conf \n" \
"user=redis \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/var/log/redis/redis-server.log \n" \
"stdout_logfile=/var/log/redis/redis-server.log \n" \
"\n" \
"[program:postgrest] \n" \
"command=/bin/postgrest \n" \
"autostart=true \n" \
"autorestart=true \n" \
"\n" \
"[program:tooljet] \n" \
"user=root \n" \
"command=/bin/bash -c '/app/server/scripts/boot.sh' \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# ENV defaults
ENV TOOLJET_HOST=http://localhost \
PORT=3000 \
NODE_ENV=production \
LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \
SECRET_KEY_BASE=replace_with_secret_key_base \
PG_DB=tooljet_production \
PG_USER=postgres \
PG_PASS=postgres \
PG_HOST=localhost \
ENABLE_TOOLJET_DB=true \
TOOLJET_DB_HOST=localhost \
TOOLJET_DB_USER=postgres \
TOOLJET_DB_PASS=postgres \
TOOLJET_DB=tooljet_db \
PGRST_HOST=http://localhost:3001 \
PGRST_SERVER_PORT=3001 \
PGRST_DB_URI=postgres://postgres:postgres@localhost/tooljet_db \
PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \
PGRST_DB_PRE_CONFIG=postgrest.pre_config \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_USER= \
REDIS_PASSWORD= \
ORM_LOGGING=true \
DEPLOYMENT_PLATFORM=docker:local \
HOME=/home/appuser \
TERM=xterm
RUN chmod +x ./server/scripts/preview.sh
# Set the entrypoint
ENTRYPOINT ["./server/scripts/preview.sh"]

View file

@ -52,7 +52,7 @@ Cypress.Commands.add("apiCreateGDS", (url, name, kind, options) => {
log: false;
}
expect(response.status).to.equal(201);
Cypress.env(`${kind}`, response.body.id);
Cypress.env(`${name}`, response.body.id);
Cypress.log({
name: "Create Data Source",
@ -80,13 +80,14 @@ Cypress.Commands.add("apiFetchDataSourcesId", () => {
Cypress.log({
name: "DS Fetch",
displayName: "Data Sources Fetched",
message: dataSources.map(ds => `\nKind: '${ds.kind}', Name: '${ds.id}'`).join(','),
message: dataSources
.map((ds) => `\nKind: '${ds.kind}', Name: '${ds.id}'`)
.join(","),
});
});
});
});
Cypress.Commands.add("apiCreateApp", (appName = "testApp") => {
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("walkthroughCompleted", "true");
@ -168,7 +169,6 @@ Cypress.Commands.add(
Cypress.env("editingVersionId", responseData.editing_version.id);
Cypress.env("environmentId", responseData.editorEnvironment.id);
});
cy.get(componentSelector, { timeout: 10000 });
}
@ -221,21 +221,21 @@ Cypress.Commands.add(
const requestBody =
envVar === "Enterprise"
? {
email: userEmail,
firstName: userName,
groups: [],
lastName: "",
role: userRole,
userMetadata: metaData,
}
email: userEmail,
firstName: userName,
groups: [],
lastName: "",
role: userRole,
userMetadata: metaData,
}
: {
email: userEmail,
firstName: userName,
groups: [],
lastName: "",
role: userRole,
userMetadata: metaData,
};
email: userEmail,
firstName: userName,
groups: [],
lastName: "",
role: userRole,
userMetadata: metaData,
};
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
@ -289,7 +289,9 @@ Cypress.Commands.add("apiAddQuery", (queryName, query, dataQueryId) => {
Cypress.Commands.add(
"apiAddQueryToApp",
(queryName, options, dsName, dsKind) => {
cy.log(`${Cypress.env("server_host")}/api/data-queries/data-sources/${Cypress.env(dsKind)}/versions/${Cypress.env("editingVersionId")}`)
cy.log(
`${Cypress.env("server_host")}/api/data-queries/data-sources/${Cypress.env(dsKind)}/versions/${Cypress.env("editingVersionId")}`
);
cy.getCookie("tj_auth_token", { log: false }).then((cookie) => {
const authToken = `tj_auth_token=${cookie.value}`;
const workspaceId = Cypress.env("workspaceId");
@ -737,3 +739,55 @@ Cypress.Commands.add("apiGetAppData", (appId = Cypress.env("appId")) => {
});
});
});
Cypress.Commands.add("apiDeleteGDS", (name) => {
const dataSourceId = Cypress.env(`${name}`);
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/data-sources/${dataSourceId}`,
headers: {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
},
failOnStatusCode: false,
}).then((response) => {
console.log("Delete response:", response);
expect(response.status, "Delete status code").to.eq(200);
Cypress.log({
name: "Delete Data Source",
displayName: "Data source deleted",
message: `Name: '${name}' | ID: '${dataSourceId}'`,
});
});
});
});
Cypress.Commands.add(
"apiUpdateGDS",
({ name, options, envName = "development" }) => {
cy.getAuthHeaders().then((headers) => {
cy.apiGetEnvironments().then((environments) => {
const environment = environments.find((env) => env.name === envName);
const environmentId = environment.id;
const dataSourceId = Cypress.env(`${name}`);
cy.request({
method: "PUT",
url: `${Cypress.env("server_host")}/api/data-sources/${dataSourceId}?environment_id=${environmentId}`,
headers: headers,
body: {
name: name,
options: options,
},
}).then((response) => {
expect(response.status).to.equal(200);
cy.log(`Datasource "${name}" updated successfully.`);
});
});
});
}
);

View file

@ -226,9 +226,9 @@ Cypress.Commands.add(
.invoke("text")
.then((text) => {
cy.wrap(subject).realType(createBackspaceText(text)),
{
delay: 0,
};
{
delay: 0,
};
});
}
);
@ -429,7 +429,6 @@ Cypress.Commands.add("visitSlug", ({ actualUrl }) => {
});
});
Cypress.Commands.add("releaseApp", () => {
if (Cypress.env("environment") !== "Community") {
cy.get(commonEeSelectors.promoteButton).click();
@ -549,7 +548,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => {
}
});
function installPlugin (pluginName) {
function installPlugin(pluginName) {
cy.get('[data-cy="-list-item"]').eq(1).click();
cy.wait(1000);
@ -605,3 +604,20 @@ Cypress.Commands.add("uninstallMarketplacePlugin", (pluginName) => {
});
});
});
Cypress.Commands.add(
"verifyRequiredFieldValidation",
(fieldName, expectedColor) => {
cy.get(commonSelectors.textField(fieldName)).should(
"have.css",
"border-color",
expectedColor
);
cy.get(commonSelectors.labelFieldValidation(fieldName))
.should("be.visible")
.and("have.text", `${fieldName} is required`);
cy.get(commonSelectors.labelFieldAlert(fieldName))
.should("be.visible")
.and("have.text", `${fieldName} is required`);
}
);

View file

@ -1,5 +1,5 @@
export const cyParamName = (paramName = "") => {
return paramName.toLowerCase().replace(/\s+/g, "-");
return String(paramName).toLowerCase().replace(/\s+/g, "-");
};
export const commonSelectors = {
@ -278,6 +278,16 @@ export const commonSelectors = {
defaultModalTitle: '[data-cy="modal-title"]',
workspaceConstantsIcon: '[data-cy="icon-workspace-constants"]',
confirmationButton: '[data-cy="confirmation-button"]',
textField: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-text-field"]`;
},
labelFieldValidation: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-is-required-validation-label"]`;
},
labelFieldAlert: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-is-required-field-alert-text"]`;
},
};
export const commonWidgetSelector = {
@ -343,7 +353,7 @@ export const commonWidgetSelector = {
buttonCloseEditorSideBar: "[data-cy='inspector-close-icon']",
buttonStylesEditorSideBar: "#inspector-tab-styles",
WidgetNameInputField: "[data-cy=edit-widget-name]",
constantInspectorIcon: '[data-cy="inspector-node-constants"] > .node-key',
constantInspectorIcon: '[data-cy="inspector-constants-expand-button"]',
inspectorIcon: '[data-cy="left-sidebar-inspect-button"]',
tooltipInputField: "[data-cy='tooltip-input-field']",
tooltipLabel: "[id=button-tooltip]",

View file

@ -28,7 +28,7 @@ export const dataSourceSelector = {
buttonTestConnection: '[data-cy="test-connection-button"]',
connectionFailedText: '[data-cy="test-connection-failed-text"]',
buttonSave: '[data-cy="db-connection-save-button"] > .tj-base-btn',
dangerAlertNotSupportSSL: '.go3958317564',
dangerAlertNotSupportSSL: ".go3958317564",
passwordTextField: '[data-cy="password-text-field"]',
textConnectionVerified: '[data-cy="test-connection-verified-text"]',
@ -101,7 +101,48 @@ export const dataSourceSelector = {
unSavedModalTitle: '[data-cy="unsaved-changes-title"]',
eventQuerySelectionField: '[data-cy="query-selection-field"]',
connectionAlertText: '[data-cy="connection-alert-text"]',
requiredIndicator: '[data-cy="required-indicator"]',
informationIcon: '[data-cy="information-icon"]',
deleteDSButton: (datasourceName) => {
return `[data-cy="${cyParamName(datasourceName)}-delete-button"]`
return `[data-cy="${cyParamName(datasourceName)}-delete-button"]`;
},
labelFieldName: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-field-label"]`;
},
dataSourceNameButton: (dataSourceName) => {
return `[data-cy="${cyParamName(dataSourceName)}-button"]`;
},
dropdownLabel: (label) => {
return `[data-cy="${cyParamName(label)}-dropdown-label"]`;
},
textField: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-text-field"]`;
},
subSection: (header) => {
return `[data-cy="${cyParamName(header)}-section"]`;
},
toggleInput: (toggleName) => {
return `[data-cy="${cyParamName(toggleName)}-toggle-input"]`;
},
button: (buttonName) => {
return `[data-cy="button-${cyParamName(buttonName)}"]`;
},
keyInputField: (header, index) => {
return `[data-cy="${cyParamName(header)}-key-input-field-${cyParamName(index)}"]`;
},
valueInputField: (header, index) => {
return `[data-cy="${cyParamName(header)}-value-input-field-${cyParamName(index)}"]`;
},
deleteButton: (header, index) => {
return `[data-cy="${cyParamName(header)}-delete-button-${cyParamName(index)}"]`;
},
addMoreButton: (header) => {
return `[data-cy="${cyParamName(header)}-add-button"]`;
},
dropdownField: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-select-dropdown"]`;
},
labelFieldValidation: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-is-required-validation-label"]`;
},
};

View file

@ -40,7 +40,7 @@ export const groupsSelector = {
resourceLabel: '[data-cy="resource-label"]',
allAppsRadio: '[data-cy="all-apps-radio"]',
allAppsLabel: '[data-cy="all-apps-label"]',
allAppsHelperText: '[data-cy="all-apps-info-text"]',
allAppsHelperText: '[data-cy="this-will-select-all-apps-in-the-workspace-including-any-new-apps-created-info-text"]',
customradio: '[data-cy="custom-radio"]',
customLabel: '[data-cy="custom-label"]',
customHelperText: '[data-cy="custom-info-text"]',

View file

@ -87,6 +87,8 @@ export const postgreSqlSelector = {
recordsInputField: '[data-cy="records-input-field"]',
eventQuerySelectionField: '[data-cy="query-selection-field"]',
sslToggleInput: '[data-cy="ssl-enabled-toggle-input"]',
labelEncryptedText: '[data-cy="encrypted-text"]',
};
export const airTableSelector = {

View file

@ -1,6 +1,7 @@
export const airtableText = {
airtable: "Airtable",
cypressairtable: "cypress-Airtable",
ApiKey: "Personal access token",
apikeyPlaceholder: "**************",
};
airtable: "Airtable",
cypressairtable: "cypress-Airtable",
ApiKey: "Personal access token",
apikeyPlaceholder: "**************",
invalidAccessToken: "Authentication failed: Invalid personal access token",
};

View file

@ -78,7 +78,7 @@ export const groupsText = {
allAppsLabel: 'All apps',
allAppsHelperText: 'This will select all apps in the workspace including any new apps created',
customLabel: 'Custom',
customHelperText: 'Select specific applications you want to add to the group',
customHelperText: 'Select specific apps you want to add to the group',
updateButtonText: 'Update',
addButtonText: 'Add',
userRole: 'User role',

View file

@ -17,13 +17,17 @@ export const postgreSqlText = {
allCloudStorage: "Cloud Storages (4)",
postgreSQL: "PostgreSQL",
labelConnectionType: "Connection type",
manualConnectionOption: "Manual connection",
connectionStringOption: "Connection string",
labelHost: "Host",
labelPort: "Port",
labelSSL: "SSL",
labelDbName: "Database name",
labelUserName: "Username",
labelPassword: "Password",
label: "Encrypted",
labelEncrypted: "Encrypted",
labelConnectionOptions: "Connection options",
sslCertificate: "SSL certificate",
whiteListIpText:
"Please white-list our IP address if the data source is not publicly accessible",
@ -74,6 +78,8 @@ export const postgreSqlText = {
guiOptionBulkUpdate: "Bulk update using primary key",
buttonTextTestConnection: "Test connection",
editButtonText: "Edit",
unableAcquireConnectionAlertText: "Unable to acquire a connection",
tabAdvanced: "Advanced",
labelNoEventhandler: "No event handlers",

View file

@ -1,34 +1,5 @@
import { fake } from "Fixtures/fake";
import { textInputText } from "Texts/textInput";
import { commonWidgetText, widgetValue, customValidation } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { buttonText } from "Texts/button";
import {
verifyControlComponentAction,
randomString,
} from "Support/utils/editor/textInput";
import {
openAccordion,
verifyAndModifyParameter,
openEditorSidebar,
verifyAndModifyToggleFx,
addDefaultEventHandler,
verifyComponentValueFromInspector,
selectColourFromColourPicker,
verifyBoxShadowCss,
verifyLayout,
verifyTooltip,
editAndVerifyWidgetName,
verifyPropertiesGeneralAccordion,
verifyStylesGeneralAccordion,
randomNumber,
closeAccordions,
} from "Support/utils/commonWidget";
import {
selectCSA,
selectEvent,
addSupportCSAData,
} from "Support/utils/events";
import { commonWidgetSelector } from "Selectors/common";
describe("Editor title", () => {
const data = {};
@ -42,10 +13,10 @@ describe("Editor title", () => {
afterEach(() => {
cy.apiDeleteApp();
});
it("should verify titles", () => {
it.skip("should verify titles", () => {
cy.url().should("include", "/tooljets-workspace");
// cy.title().should("eq", "Dashboard | ToolJet");
cy.title().should("eq", "ToolJet");
cy.title().should("eq", "Dashboard | ToolJet");
// cy.title().should("eq", "ToolJet");
cy.log(data.appName);
@ -57,7 +28,7 @@ describe("Editor title", () => {
cy.url().should("include", `/applications/${Cypress.env("appId")}`);
// cy.title().should("eq", `${data.appName} | ToolJet`);
// cy.title().should("eq", `Preview - ${data.appName} | ToolJet`);
cy.title().should("eq", `Preview - ${data.appName} | ToolJet`);
cy.go("back");
cy.releaseApp();

View file

@ -5,7 +5,7 @@ import {
verifyCSA
} from "Support/utils/editor/textInput";
import { addMultiEventsWithAlert } from "Support/utils/events";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyValue } from "Support/utils/inspector";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyNodeData } from "Support/utils/inspector";
describe('Button Component Tests', () => {
@ -75,22 +75,21 @@ describe('Button Component Tests', () => {
cy.apiLogin();
cy.apiCreateApp(`${fake.companyName}-Button-App`);
cy.openApp();
cy.dragAndDropWidget("Button", 50, 50);
cy.dragAndDropWidget("Button", 500, 500);
cy.get('[data-cy="query-manager-toggle-button"]').click();
});
it('should verify all the exposed values on inspector', () => {
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide");
openNode("components");
openAndVerifyNode("button1", exposedValues, verifyValue);
verifyNodes(functions, verifyfunctions);
openAndVerifyNode("button1", exposedValues, verifyNodeData);
verifyNodes(functions, verifyNodeData);
//id is pending
});
it.skip('should verify all the events from the button', () => {
it('should verify all the events from the button', () => {
const events = [
{ event: "On hover", message: "On hover Event" },
{ event: "On Click", message: "On Click Event" },

View file

@ -5,7 +5,7 @@ import {
verifyCSA
} from "Support/utils/editor/textInput";
import { addMultiEventsWithAlert } from "Support/utils/events";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyValue } from "Support/utils/inspector";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyNodeData } from "Support/utils/inspector";
describe('Checkbox Component Tests', () => {
@ -83,7 +83,7 @@ describe('Checkbox Component Tests', () => {
cy.apiLogin();
cy.apiCreateApp(`${fake.companyName}-Checkbox-App`);
cy.openApp();
cy.dragAndDropWidget("Checkbox", 50, 50);
cy.dragAndDropWidget("Checkbox", 500, 500);
cy.get('[data-cy="query-manager-toggle-button"]').click();
});
@ -92,8 +92,8 @@ describe('Checkbox Component Tests', () => {
cy.get(".tooltip-inner").invoke("hide");
openNode("components");
openAndVerifyNode("checkbox1", exposedValues, verifyValue);
verifyNodes(functions, verifyfunctions);
openAndVerifyNode("checkbox1", exposedValues, verifyNodeData);
verifyNodes(functions, verifyNodeData);
//id is pending
});

View file

@ -5,7 +5,7 @@ import {
verifyCSA
} from "Support/utils/editor/textInput";
import { addMultiEventsWithAlert } from "Support/utils/events";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyValue } from "Support/utils/inspector";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyNodeData } from "Support/utils/inspector";
describe('Dropdown Component Tests', () => {
@ -101,7 +101,7 @@ describe('Dropdown Component Tests', () => {
cy.get(".tooltip-inner").invoke("hide");
openNode("components");
openAndVerifyNode("dropdown1", exposedValues, verifyValue);
openAndVerifyNode("dropdown1", exposedValues, verifyNodeData);
verifyNodes(functions, verifyfunctions);
//id is pending

View file

@ -15,7 +15,7 @@ import { deleteDownloadsFolder } from "Support/utils/common";
import {
resizeQueryPanel
} from "Support/utils/dataSource";
import { openNode, verifyNodeData, verifyValue } from "Support/utils/inspector";
import { openNode, verifyNodeData } from "Support/utils/inspector";
import {
addNewPage
} from "Support/utils/multipage";
@ -49,11 +49,11 @@ describe("Global Actions", () => {
verifyNodeData("variables", "Object", "1 entry ");
openNode("variables", 0);
verifyValue("var", "String", `"test"`);
verifyNodeData("var", "String", `"test"`);
openNode("page");
openNode("variables", 1);
verifyValue("pageVar", "String", `"pageTest"`);
verifyNodeData("pageVar", "String", `"pageTest"`);
addInputOnQueryField(
"runjs",

View file

@ -5,7 +5,7 @@ import {
verifyCSA
} from "Support/utils/editor/textInput";
import { addMultiEventsWithAlert } from "Support/utils/events";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyValue } from "Support/utils/inspector";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyNodeData } from "Support/utils/inspector";
describe('Multiselect Component Tests', () => {
@ -104,7 +104,7 @@ describe('Multiselect Component Tests', () => {
cy.get(".tooltip-inner").invoke("hide");
openNode("components");
openAndVerifyNode("multiselect1", exposedValues, verifyValue);
openAndVerifyNode("multiselect1", exposedValues, verifyNodeData);
verifyNodes(functions, verifyfunctions);
//id is pending

View file

@ -5,7 +5,7 @@ import {
verifyCSA
} from "Support/utils/editor/textInput";
import { addMultiEventsWithAlert } from "Support/utils/events";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyValue } from "Support/utils/inspector";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyNodeData } from "Support/utils/inspector";
describe('Number Input Component Tests', () => {
@ -86,7 +86,7 @@ describe('Number Input Component Tests', () => {
cy.apiLogin();
cy.apiCreateApp(`${fake.companyName}-Numberinput-App`);
cy.openApp();
cy.dragAndDropWidget("Number Input", 50, 50);
cy.dragAndDropWidget("Number Input", 500, 500);
cy.get('[data-cy="query-manager-toggle-button"]').click();
});
@ -95,8 +95,8 @@ describe('Number Input Component Tests', () => {
cy.get(".tooltip-inner").invoke("hide");
openNode("components");
openAndVerifyNode("numberinput1", exposedValues, verifyValue);
verifyNodes(functions, verifyfunctions);
openAndVerifyNode("numberinput1", exposedValues, verifyNodeData);
verifyNodes(functions, verifyNodeData);
//id is pending
});

View file

@ -5,7 +5,7 @@ import {
verifyCSA
} from "Support/utils/editor/textInput";
import { addMultiEventsWithAlert } from "Support/utils/events";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyValue } from "Support/utils/inspector";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyNodeData } from "Support/utils/inspector";
describe('Password Input Component Tests', () => {
@ -95,7 +95,7 @@ describe('Password Input Component Tests', () => {
cy.get(".tooltip-inner").invoke("hide");
openNode("components");
openAndVerifyNode("passwordinput1", exposedValues, verifyValue);
openAndVerifyNode("passwordinput1", exposedValues, verifyNodeData);
verifyNodes(functions, verifyfunctions);
//id is pending

View file

@ -5,7 +5,7 @@ import {
verifyCSA
} from "Support/utils/editor/textInput";
import { addMultiEventsWithAlert } from "Support/utils/events";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyValue } from "Support/utils/inspector";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyNodeData } from "Support/utils/inspector";
describe('Text Input Component Tests', () => {
@ -27,14 +27,14 @@ describe('Text Input Component Tests', () => {
"key": "setBlur",
"type": "Function"
},
{
"key": "disable",
"type": "Function"
},
{
"key": "visibility",
"type": "Function"
},
// {
// "key": "disable",
// "type": "Function"
// },
// {
// "key": "visibility",
// "type": "Function"
// },
{
"key": "setVisibility",
"type": "Function"
@ -94,17 +94,17 @@ describe('Text Input Component Tests', () => {
cy.apiLogin();
cy.apiCreateApp(`${fake.companyName}-Textinput-App`);
cy.openApp();
cy.dragAndDropWidget("Text Input", 50, 50);
cy.dragAndDropWidget("Text Input", 500, 500);
cy.get('[data-cy="query-manager-toggle-button"]').click();
});
it.skip('should verify all the exposed values on inspector', () => {
it('should verify all the exposed values on inspector', () => {
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide");
openNode("components");
openAndVerifyNode("textinput1", exposedValues, verifyValue);
verifyNodes(functions, verifyfunctions);
openAndVerifyNode("textinput1", exposedValues, verifyNodeData);
verifyNodes(functions, verifyNodeData);
//id is pending
});

View file

@ -5,7 +5,7 @@ import {
verifyCSA
} from "Support/utils/editor/textInput";
import { addMultiEventsWithAlert } from "Support/utils/events";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyValue } from "Support/utils/inspector";
import { openAndVerifyNode, openNode, verifyfunctions, verifyNodes, verifyNodeData } from "Support/utils/inspector";
describe('ToggleSwitch Component Tests', () => {
@ -88,7 +88,7 @@ describe('ToggleSwitch Component Tests', () => {
cy.get(".tooltip-inner").invoke("hide");
openNode("components");
openAndVerifyNode("toggleswitch1", exposedValues, verifyValue);
openAndVerifyNode("toggleswitch1", exposedValues, verifyNodeData);
verifyNodes(functions, verifyfunctions);
//id is pending

View file

@ -10,7 +10,7 @@ import {
selectColourFromColourPicker,
verifyWidgetColorCss,
} from "Support/utils/commonWidget";
import { verifyNodeData, openNode, verifyValue } from "Support/utils/inspector";
// import { verifyNodeData, openNode, verifyNodeData } from "Support/utils/inspector";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import {
commonText,

View file

@ -3,7 +3,7 @@ import { commonWidgetSelector } from "Selectors/common";
import { multipageSelector } from "Selectors/multipage";
import { addSupportCSAData, selectEvent } from "Support/utils/events";
import { createNewVersion } from "Support/utils/exportImport";
import { deleteComponentFromInspector, openNode, verifyNodeData, verifyValue, verifyNodes, openAndVerifyNode } from "Support/utils/inspector";
import { deleteComponentFromInspector, openNode, verifyNodeData, verifyNodes, openAndVerifyNode } from "Support/utils/inspector";
import { addNewPage } from "Support/utils/multipage";
import { navigateToCreateNewVersionModal } from "Support/utils/version";
import testData from "Fixtures/inspectorItems.json";
@ -20,15 +20,15 @@ describe("Editor- Inspector", () => {
cy.viewport(1800, 1800);
});
it("should verify the values of inspector", () => {
it.skip("should verify the values of inspector", () => {
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide");
openAndVerifyNode("globals", testData.globalsNodes, verifyNodeData);
openAndVerifyNode("currentUser", testData.currentUserNodes, verifyValue);
openAndVerifyNode("theme", testData.themeNodes, verifyValue);
openAndVerifyNode("mode", testData.modeNodes, verifyValue);
openAndVerifyNode("urlparams", testData.urlparamsNode, verifyValue);
openAndVerifyNode("currentUser", testData.currentUserNodes, verifyNodeData);
openAndVerifyNode("theme", testData.themeNodes, verifyNodeData);
openAndVerifyNode("mode", testData.modeNodes, verifyNodeData);
openAndVerifyNode("urlparams", testData.urlparamsNode, verifyNodeData);
if (Cypress.env("environment") !== "Community") {
const ssoUserInfoNode = '[data-cy="inspector-node-ssouserinfo"]';
@ -39,7 +39,7 @@ describe("Editor- Inspector", () => {
openNode("theme");
openNode("environment");
verifyValue("name", "String", `"development"`);
verifyNodeData("name", "String", `"development"`);
cy.get(`${inspectorNodeId} > .node-key`).should("have.text", "id");
}
@ -92,7 +92,7 @@ describe("Editor- Inspector", () => {
cy.get(commonWidgetSelector.draggableWidget("button3")).click();
cy.get(commonWidgetSelector.sidebarinspector).click();
openAndVerifyNode("variables", testData.variablesNodes, verifyValue);
openAndVerifyNode("variables", testData.variablesNodes, verifyNodeData);
cy.forceClickOnCanvas()
cy.wait(500)
@ -101,9 +101,9 @@ describe("Editor- Inspector", () => {
// openNode("page");
openAndVerifyNode("page", testData.testPageNodes, verifyValue);
openAndVerifyNode("page", testData.testPageNodes, verifyNodeData);
openNode("variables", 1);
verifyValue("pageVar", "String", `"pageVar"`);
verifyNodeData("pageVar", "String", `"pageVar"`);
openAndVerifyNode("components", testData.componentsNodes, verifyNodeData);
@ -111,10 +111,10 @@ describe("Editor- Inspector", () => {
cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.get(commonWidgetSelector.sidebarinspector).click();
openAndVerifyNode("page", testData.pageNodes, verifyValue);
openAndVerifyNode("page", testData.pageNodes, verifyNodeData);
openNode("globals");
openNode("urlparams");
verifyValue("key", "String", `"value"`);
verifyNodeData("key", "String", `"value"`);
cy.get(`[data-cy="inspector-node-key"] > .mx-1`)
.realHover()

View file

@ -104,6 +104,7 @@ describe("Chaining of queries", () => {
cy.wait(1000)
cy.get('[data-cy="query-tab-setup"]').click();
cy.wait(1500);
openEditorSidebar(buttonText.defaultWidgetName);
selectEvent("On Click", "Run Query", 0, `[data-cy="add-event-handler"]`, 0);
cy.wait(500);
@ -122,7 +123,7 @@ describe("Chaining of queries", () => {
// cy.verifyToastMessage(commonSelectors.toastMessage, "Hello World");
});
it.skip("should verify query duplication", () => {
it("should verify query duplication", () => {
const data = {};
let dsName = fake.companyName;
@ -146,6 +147,7 @@ describe("Chaining of queries", () => {
chainQuery("runjs", "runpy");
addSuccessNotification("runjs");
cy.wait(1500);
openEditorSidebar(buttonText.defaultWidgetName);
selectEvent("On Click", "Run Query", 0, `[data-cy="add-event-handler"]`, 0);
cy.wait(500);
@ -170,6 +172,8 @@ describe("Chaining of queries", () => {
"have.text",
"runjs_copy "
);
cy.get('[data-cy="query-tab-settings"]').click();
cy.get('[data-cy="notification-on-success-toggle-switch"]').should(
"have.value",
"on"
@ -184,7 +188,7 @@ describe("Chaining of queries", () => {
});
cy.get(
`[data-cy="action-selection"] > .select-search > .react-select__control > .react-select__value-container > `
).should("have.text", "Run Query");
).should("have.text", "Run query");
cy.get('[data-cy="query-selection-field"]').should("have.text", "runpy");
});
});

View file

@ -23,7 +23,7 @@ import {
resizeQueryPanel,
verifypreview
} from "Support/utils/dataSource";
import { openNode, verifyValue } from "Support/utils/inspector";
import { openNode, verifyNodeData } from "Support/utils/inspector";
describe("RunJS", () => {
beforeEach(() => {
@ -42,15 +42,15 @@ describe("RunJS", () => {
selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField("runjs", "return true");
query("preview");
query("run");
verifypreview("raw", "true");
query("run");
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide");
openNode("queries");
openNode("runjs1");
verifyValue("data", "Boolean", "true");
verifyValue("rawData", "Boolean", "true");
verifyNodeData("data", "Boolean", "true");
verifyNodeData("rawData", "Boolean", "true");
cy.apiDeleteApp();
});
@ -60,11 +60,11 @@ describe("RunJS", () => {
selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField("runjs", "return [page.handle,page.name]");
query("preview");
query("run");
verifypreview("raw", `["home","Home"]`);
addInputOnQueryField("runjs", "return globals.theme");
query("preview");
query("run");
verifypreview("raw", `{"name":"light"}`);
// addInputOnQueryField("runjs", "return globals.currentUser");

View file

@ -24,7 +24,7 @@ import {
resizeQueryPanel,
verifypreview
} from "Support/utils/dataSource";
import { openNode, verifyNodeData, verifyValue } from "Support/utils/inspector";
import { openNode, verifyNodeData } from "Support/utils/inspector";
import {
addNewPage
} from "Support/utils/multipage";
@ -54,8 +54,8 @@ describe("runpy", () => {
cy.get(".tooltip-inner").invoke("hide");
openNode("queries");
openNode("runpy1");
verifyValue("data", "Boolean", "true");
verifyValue("rawData", "Boolean", "true");
verifyNodeData("data", "Boolean", "true");
verifyNodeData("rawData", "Boolean", "true");
cy.apiDeleteApp();
});
@ -76,11 +76,11 @@ actions.setPageVariable('pageVar', 'pageTest')`
verifyNodeData("variables", "Object", "1 entry ");
openNode("variables", 0);
verifyValue("var", "String", `"test"`);
verifyNodeData("var", "String", `"test"`);
openNode("page");
openNode("variables", 1);
verifyValue("pageVar", "String", `"pageTest"`);
verifyNodeData("pageVar", "String", `"pageTest"`);
addInputOnQueryField(
"runpy",

View file

@ -3,24 +3,15 @@ import { postgreSqlSelector, airTableSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { airtableText } from "Texts/airTable";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import {
fillDataSourceTextField,
selectAndAddDataSource,
} from "Support/utils/postgreSql";
import {
deleteDatasource,
closeDSModal,
deleteAppandDatasourceAfterExecution,
} from "Support/utils/dataSource";
import { closeDSModal } from "Support/utils/dataSource";
import { dataSourceSelector } from "../../../../../constants/selectors/dataSource";
const data = {};
data.queryName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
const airTable_apiKey = Cypress.env("airTable_apikey");
const airTable_baseId = Cypress.env("airtabelbaseId");
const airTable_tableName = Cypress.env("airtable_tableName");
const airTable_recordID = Cypress.env("airtable_recordId");
describe("Data source Airtable", () => {
beforeEach(() => {
@ -54,72 +45,142 @@ describe("Data source Airtable", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
cy.get(postgreSqlSelector.buttonSave).verifyVisibleElement(
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dsName}-airtable`,
"airtable",
[
{
key: "personal_access_token",
value: `${Cypress.env("airTable_apikey")}`,
encrypted: true,
},
]
);
cy.reload();
cy.get(
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
)
.should("be.visible")
.click();
cy.get(
dataSourceSelector.labelFieldName(airtableText.ApiKey)
).verifyVisibleElement("have.text", `${airtableText.ApiKey}*`);
cy.get(postgreSqlSelector.labelEncryptedText).verifyVisibleElement(
"have.text",
postgreSqlText.buttonTextSave
postgreSqlText.labelEncrypted
);
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).should(
"be.visible"
);
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).click();
cy.verifyRequiredFieldValidation(airtableText.ApiKey, "rgb(226, 99, 103)");
cy.get(dataSourceSelector.textField(airtableText.ApiKey)).should(
"be.visible"
);
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
"have.text",
postgreSqlText.whiteListIpText
);
cy.get(postgreSqlSelector.buttonCopyIp).verifyVisibleElement(
"have.text",
postgreSqlText.textCopy
);
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
cy.get(postgreSqlSelector.linkReadDocumentation).verifyVisibleElement(
"have.text",
postgreSqlText.readDocumentation
);
deleteDatasource(`cypress-${data.dsName}-airtable`);
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)
.and("be.disabled");
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
airtableText.invalidAccessToken
);
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
});
it("Should verify the functionality of AirTable connection form.", () => {
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
fillDataSourceTextField(
airtableText.ApiKey,
airtableText.apikeyPlaceholder,
Cypress.env("airTable_apikey")
);
cy.get(postgreSqlSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dsName}-airtable`,
"airtable",
[
{
key: "personal_access_token",
value: "Invalid access token",
encrypted: true,
},
]
);
cy.get(
`[data-cy="cypress-${data.dsName}-airtable-button"]`
).verifyVisibleElement("have.text", `cypress-${data.dsName}-airtable`);
deleteDatasource(`cypress-${data.dsName}-airtable`);
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(dataSourceSelector.connectionFailedText, {
timeout: 10000,
}).should("have.text", postgreSqlText.couldNotConnect);
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
airtableText.invalidAccessToken
);
cy.reload();
cy.apiUpdateGDS({
name: `cypress-${data.dsName}-airtable`,
options: [
{
key: "personal_access_token",
value: `${Cypress.env("airTable_apikey")}`,
encrypted: true,
},
],
});
cy.get(
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(postgreSqlSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
});
it("Should able to run the query with valid conection", () => {
const airTable_apiKey = Cypress.env("airTable_apikey");
const airTable_baseId = Cypress.env("airtabelbaseId");
const airTable_tableName = Cypress.env("airtable_tableName");
const airTable_recordID = Cypress.env("airtable_recordId");
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
fillDataSourceTextField(
airtableText.ApiKey,
airtableText.apikeyPlaceholder,
airTable_apiKey
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dsName}-airtable`,
"airtable",
[
{
key: "personal_access_token",
value: `${airTable_apiKey}`,
encrypted: true,
},
]
);
cy.wait(1000);
cy.get(postgreSqlSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
`[data-cy="cypress-${data.dsName}-airtable-button"]`
).verifyVisibleElement("have.text", `cypress-${data.dsName}-airtable`);
cy.get(commonSelectors.dashboardIcon).click();
cy.get(commonSelectors.appCreateButton).click();
cy.get(commonSelectors.appNameInput).click().type(data.dsName);
cy.get(commonSelectors.createAppButton).click();
cy.skipWalkthrough();
cy.apiCreateApp(`${data.dsName}-airtable-app`);
cy.openApp();
cy.get('[data-cy="show-ds-popover-button"]').click();
cy.get(".css-4e90k9").type(`${data.dsName}`);
@ -280,10 +341,9 @@ describe("Data source Airtable", () => {
commonSelectors.toastMessage,
`Query (${data.queryName}) completed.`
);
deleteAppandDatasourceAfterExecution(
data.dsName,
`cypress-${data.dsName}-airtable`
);
cy.apiDeleteApp(`${data.dsName}-airtable-app`);
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
});
});
});

View file

@ -66,10 +66,9 @@ describe("Data source BigQuery", () => {
`cypress-${data.dataSourceName}-bigquery`
);
cy.get('[data-cy="label-private-key"]').verifyVisibleElement(
"have.text",
firestoreText.labelPrivateKey
);
cy.get(
dataSourceSelector.labelFieldName(firestoreText.labelPrivateKey)
).verifyVisibleElement("have.text", "Private key*");
cy.get(".datasource-edit-btn").should("be.visible");
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
"have.text",
@ -98,7 +97,7 @@ describe("Data source BigQuery", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
bigqueryText.errorInvalidEmailId
);
@ -110,38 +109,30 @@ describe("Data source BigQuery", () => {
});
it("Should verify the functionality of BigQuery connection form.", () => {
selectAndAddDataSource(
"databases",
bigqueryText.bigQuery,
data.dataSourceName
);
fillDataSourceTextField(
firestoreText.privateKey,
bigqueryText.placehlderPrivateKey,
`${JSON.stringify(Cypress.env("bigquery_pvt_key"))}`,
"contain",
{ parseSpecialCharSequences: false, delay: 0 }
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-bigquery`,
"bigquery",
[
{
key: "private_key",
value: `${JSON.stringify(Cypress.env("bigquery_pvt_key"))}`,
encrypted: true,
},
]
);
cy.get(
dataSourceSelector.dataSourceNameButton(
`cypress-${data.dataSourceName}-bigquery`
)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(postgreSqlSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.get(postgreSqlSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
`[data-cy="cypress-${data.dataSourceName}-bigquery-button"]`
).verifyVisibleElement(
"have.text",
`cypress-${data.dataSourceName}-bigquery`
);
deleteDatasource(`cypress-${data.dataSourceName}-bigquery`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-bigquery`);
});
});

View file

@ -101,7 +101,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Invalid URL"
);

View file

@ -127,7 +127,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Invalid URL"
);

View file

@ -115,7 +115,7 @@ describe("Data source DynamoDB", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
dynamoDbText.errorMissingRegion
);

View file

@ -122,7 +122,7 @@ describe("Data source Elasticsearch", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
elasticsearchText.errorConnectionRefused
);

View file

@ -94,7 +94,7 @@ describe("Data source Firestore", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
firestoreText.errorGcpKeyCouldNotBeParsed
);

View file

@ -118,7 +118,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Invalid URL"
);

View file

@ -125,7 +125,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
// cy.get('[data-cy="connection-alert-text"]').should("be.visible")
// cy.get(dataSourceSelector.connectionAlertText).should("be.visible")
deleteDatasource(`cypress-${data.dataSourceName}-mariadb`);
});

View file

@ -129,7 +129,7 @@ describe("Data source MongoDB", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
mongoDbText.errorConnectionRefused
);
@ -164,7 +164,7 @@ describe("Data source MongoDB", () => {
}).verifyVisibleElement("have.text", postgreSqlText.couldNotConnect, {
timeout: 95000,
});
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Cannot read properties of null (reading '2')"
);

View file

@ -27,7 +27,7 @@ describe("Data sources", () => {
.replaceAll("[^A-Za-z]", "");
});
it.skip("Should verify elements on connection form", () => {
it("Should verify elements on connection form with validation", () => {
cy.log(process.env.NODE_ENV);
cy.log(postgreSqlText.allDatabase());
cy.get(commonSelectors.globalDataSourceIcon).click();
@ -81,30 +81,147 @@ describe("Data sources", () => {
`cypress-${data.dataSourceName}-postgresql`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
cy.get(
dataSourceSelector.dropdownLabel(postgreSqlText.labelConnectionType)
).verifyVisibleElement("have.text", postgreSqlText.labelConnectionType);
cy.get(dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType))
.should("be.visible")
.click();
cy.contains(
`[id*="react-select-"]`,
postgreSqlText.connectionStringOption
).click();
cy.get(
dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType)
).should("be.visible");
cy.get(
dataSourceSelector.labelFieldName(postgreSqlText.connectionStringOption)
).verifyVisibleElement(
"have.text",
postgreSqlText.labelHost
`${postgreSqlText.connectionStringOption}*`
);
cy.get(postgreSqlSelector.labelPort).verifyVisibleElement(
cy.get(postgreSqlSelector.labelEncryptedText).verifyVisibleElement(
"have.text",
postgreSqlText.labelPort
postgreSqlText.labelEncrypted
);
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).should(
"be.visible"
);
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).click();
cy.verifyRequiredFieldValidation(
postgreSqlText.connectionStringOption,
"rgb(226, 99, 103)"
);
cy.get(
dataSourceSelector.textField(postgreSqlText.connectionStringOption)
).should("be.visible");
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)
.and("be.disabled");
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
postgreSqlText.unableAcquireConnectionAlertText
);
cy.get(dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType))
.should("be.visible")
.click();
cy.contains(
`[id*="react-select-"]`,
postgreSqlText.manualConnectionOption
).click();
cy.get(
dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType)
).should("be.visible");
const requiredFields = [
postgreSqlText.labelHost,
postgreSqlText.labelPort,
postgreSqlText.labelUserName,
postgreSqlText.labelPassword,
];
const sections = [
postgreSqlText.labelHost,
postgreSqlText.labelPort,
postgreSqlText.labelDbName,
postgreSqlText.labelUserName,
postgreSqlText.labelPassword,
postgreSqlText.labelConnectionOptions,
];
sections.forEach((section) => {
if (section === postgreSqlText.labelConnectionOptions) {
cy.get(dataSourceSelector.keyInputField(section, 0)).should(
"be.visible"
);
cy.get(dataSourceSelector.valueInputField(section, 0)).should(
"be.visible"
);
cy.get(dataSourceSelector.deleteButton(section, 0)).should(
"be.visible"
);
cy.get(dataSourceSelector.addMoreButton(section)).should("be.visible");
} else if (requiredFields.includes(section)) {
cy.get(dataSourceSelector.labelFieldName(section)).verifyVisibleElement(
"have.text",
`${section}*`
);
cy.get(dataSourceSelector.textField(section)).should("be.visible");
if (section === postgreSqlText.labelPassword) {
cy.get(
dataSourceSelector.button(postgreSqlText.editButtonText)
).click();
cy.verifyRequiredFieldValidation(section, "rgb(215, 45, 57)");
} else {
cy.get(dataSourceSelector.textField(section)).click();
cy.get(commonSelectors.textField(section)).should(
"have.css",
"border-color",
"rgba(0, 0, 0, 0)"
);
cy.get(dataSourceSelector.textField(section))
.type("123")
.clear()
.blur();
cy.verifyRequiredFieldValidation(section, "rgb(215, 45, 57)");
}
} else {
cy.get(dataSourceSelector.labelFieldName(section)).verifyVisibleElement(
"have.text",
section
);
cy.get(dataSourceSelector.textField(section)).should("be.visible");
}
});
cy.get(postgreSqlSelector.labelSsl).verifyVisibleElement(
"have.text",
postgreSqlText.labelSSL
);
cy.get(postgreSqlSelector.labelDbName).verifyVisibleElement(
"have.text",
postgreSqlText.labelDbName
);
cy.get(postgreSqlSelector.labelUserName).verifyVisibleElement(
"have.text",
postgreSqlText.labelUserName
);
cy.get(postgreSqlSelector.labelPassword).verifyVisibleElement(
"have.text",
postgreSqlText.labelPassword
);
cy.get(postgreSqlSelector.sslToggleInput).should("be.visible");
cy.get(postgreSqlSelector.labelSSLCertificate).verifyVisibleElement(
"have.text",
postgreSqlText.sslCertificate
@ -132,72 +249,85 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.couldNotConnect
);
cy.get(postgreSqlSelector.buttonSave).verifyVisibleElement(
cy.get(postgreSqlSelector.buttonSave)
.verifyVisibleElement("have.text", postgreSqlText.buttonTextSave)
.and("be.disabled");
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
postgreSqlText.buttonTextSave
"connect ECONNREFUSED 127.0.0.1:5432"
);
cy.get('[data-cy="connection-alert-text"]').should("be.visible");
deleteDatasource(`cypress-${data.dataSourceName}-postgresql`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-postgresql`);
});
it.skip("Should verify the functionality of PostgreSQL connection form.", () => {
selectAndAddDataSource(
"databases",
postgreSqlText.postgreSQL,
data.dataSourceName
it("Should verify the functionality of PostgreSQL connection form.", () => {
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-manual-pgsql`,
"postgresql",
[
{ key: "connection_type", value: "manual", encrypted: false },
{ key: "host", value: `${Cypress.env("pg_host")}`, encrypted: false },
{ key: "port", value: 5432, encrypted: false },
{ key: "ssl_enabled", value: false, encrypted: false },
{ key: "database", value: "postgres", encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{
key: "username",
value: `${Cypress.env("pg_user")}`,
encrypted: false,
},
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
{ key: "ca_cert", value: null, encrypted: true },
{ key: "client_key", value: null, encrypted: true },
{ key: "client_cert", value: null, encrypted: true },
{ key: "root_cert", value: null, encrypted: true },
{ key: "connection_string", value: null, encrypted: true },
]
);
fillDataSourceTextField(
postgreSqlText.labelHost,
postgreSqlText.placeholderEnterHost,
Cypress.env("pg_host")
);
fillDataSourceTextField(
postgreSqlText.labelPort,
postgreSqlText.placeholderEnterPort,
"5432"
);
cy.get('[data-cy="-toggle-input"]').then(($el) => {
if ($el.is(":checked")) {
cy.get('[data-cy="-toggle-input"]').uncheck();
}
});
fillDataSourceTextField(
postgreSqlText.labelDbName,
postgreSqlText.placeholderNameOfDB,
"postgres"
);
fillDataSourceTextField(
postgreSqlText.labelUserName,
postgreSqlText.placeholderEnterUserName,
"postgres"
);
fillDataSourceTextField(
postgreSqlText.labelPassword,
"**************",
Cypress.env("pg_password")
);
cy.get(
dataSourceSelector.dataSourceNameButton(
`cypress-${data.dataSourceName}-manual-pgsql`
)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(postgreSqlSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.get(postgreSqlSelector.buttonSave).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
postgreSqlText.toastDSSaved
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-manual-pgsql`);
cy.reload();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-string-pgsql`,
"postgresql",
[
{ key: "connection_type", value: "string", encrypted: false },
{
key: "connection_string",
value: `${Cypress.env("pg_string")}`,
encrypted: true,
},
]
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
`[data-cy="cypress-${data.dataSourceName}-postgresql-button"]`
).verifyVisibleElement(
"have.text",
`cypress-${data.dataSourceName}-postgresql`
);
deleteDatasource(`cypress-${data.dataSourceName}-postgresql`);
dataSourceSelector.dataSourceNameButton(
`cypress-${data.dataSourceName}-string-pgsql`
)
)
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get(postgreSqlSelector.textConnectionVerified, {
timeout: 10000,
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-string-pgsql`);
});
it.skip("Should verify elements of the Query section.", () => {

View file

@ -3,7 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { redisText } from "Texts/redis";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
fillDataSourceTextField,
@ -96,7 +96,7 @@ describe("Data source Redis", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorMaxRetries
);
@ -137,7 +137,7 @@ describe("Data source Redis", () => {
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorInvalidUserOrPassword
);
@ -152,7 +152,7 @@ describe("Data source Redis", () => {
"108299"
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorPort
);
@ -170,7 +170,7 @@ describe("Data source Redis", () => {
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorInvalidUserOrPassword
);
@ -187,7 +187,7 @@ describe("Data source Redis", () => {
"redis"
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
redisText.errorInvalidUserOrPassword
);

View file

@ -329,9 +329,9 @@ describe("Data source Rest API", () => {
);
cy.contains("Save").click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Data Source Saved");
deleteDatasource(`cypress-${data.dataSourceName}-restapi`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
});
it("Should verify basic connection for Rest API", () => {
it("Should verify connection response for all methods", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi`,
@ -367,88 +367,388 @@ describe("Data source Rest API", () => {
);
cy.reload();
cy.apiCreateApp(`${fake.companyName}-restAPI-App`);
cy.apiCreateApp(`${fake.companyName}-restAPI-CURD-App`);
cy.openApp();
createAndRunRestAPIQuery(
"get_restapi",
`cypress-${data.dataSourceName}-restapi`,
"GET",
"/api/users"
);
createAndRunRestAPIQuery(
"post_restapi",
`cypress-${data.dataSourceName}-restapi`,
"POST",
"",
[["Content-Type", "application/json"]],
[],
{
price: 200,
name: "Violin",
},
true,
"/api/users"
);
createAndRunRestAPIQuery({
queryName: "get_beeceptor_data",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: "/api/users",
run: true,
});
createAndRunRestAPIQuery({
queryName: "post_restapi",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "POST",
headersList: [["Content-Type", "application/json"]],
rawBody: '{"price": 200,"name": "Violin"}',
urlSuffix: "/api/users",
expectedResponseShape: { price: 200, name: "Violin", id: true },
});
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
(postResponseID) => {
const id1 = postResponseID.id;
createAndRunRestAPIQuery(
"put_restapi_id",
`cypress-${data.dataSourceName}-restapi`,
"PUT",
"",
[["Content-Type", "application/json"]],
[],
{
price: 500,
name: "Guitar",
},
true,
`/api/users/${id1}`
);
}
);
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
(putResponseID) => {
const id2 = putResponseID.id;
createAndRunRestAPIQuery(
"patch_restapi_id",
`cypress-${data.dataSourceName}-restapi`,
"PATCH",
"",
[["Content-Type", "application/json"]],
[],
{ price: 999 },
true,
`/api/users/${id2}`
);
}
);
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
(patchResponseID) => {
const id3 = patchResponseID.id;
createAndRunRestAPIQuery(
"get_restapi_id",
`cypress-${data.dataSourceName}-restapi`,
"GET",
"",
[],
[],
true,
`/api/users/${id3}`
);
createAndRunRestAPIQuery(
"delete_restapi_id",
`cypress-${data.dataSourceName}-restapi`,
"DELETE",
"",
[],
[],
true,
`/api/users/${id3}`
);
createAndRunRestAPIQuery({
queryName: "put_restapi_id",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "PUT",
headersList: [["Content-Type", "application/json"]],
rawBody: '{"price": 500,"name": "Guitar"}',
urlSuffix: `/api/users/${id1}`,
expectedResponseShape: { price: 500, name: "Guitar", id: id1 },
});
createAndRunRestAPIQuery({
queryName: "patch_restapi_id",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "PATCH",
headersList: [["Content-Type", "application/json"]],
rawBody: '{"price": 999 }',
urlSuffix: `/api/users/${id1}`,
run: true,
expectedResponseShape: { price: 999, id: id1 },
});
createAndRunRestAPIQuery({
queryName: "get_restapi_id",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: `/api/users/${id1}`,
run: true,
expectedResponseShape: { price: 999, name: "Guitar", id: id1 },
});
createAndRunRestAPIQuery({
queryName: "delete_restapi_id",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "DELETE",
urlSuffix: `/api/users/${id1}`,
run: true,
expectedResponseShape: { success: true },
});
}
);
cy.apiDeleteApp(`${fake.companyName}-restAPI-CURD-App`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
});
it("Should verify response for basic authentication type connection", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi`,
"restapi",
[
{ key: "url", value: "https://httpbin.org" },
{ key: "auth_type", value: "basic" },
{ key: "grant_type", value: "authorization_code" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{ key: "access_token_url", value: "" },
{ key: "client_id", value: "" },
{
key: "client_secret",
encrypted: true,
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
},
{ key: "audience", value: "" },
{ key: "scopes", value: "read, write" },
{ key: "username", value: "user", encrypted: false },
{ key: "password", value: "pass", encrypted: true },
{
key: "bearer_token",
encrypted: true,
credential_id: "21caf3cb-dbde-43c7-9f42-77feffb63062",
},
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "retry_network_errors", value: true, encrypted: false },
{ key: "url_parameters", value: [["", ""]], encrypted: false },
{ key: "tokenData", encrypted: false },
]
);
cy.reload();
cy.intercept("GET", "/api/library_apps").as("appLibrary");
cy.apiCreateApp(`${fake.companyName}-restAPI-Basic-App`);
createAndRunRestAPIQuery({
queryName: "get_basic_auth_valid",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: "/basic-auth/user/pass",
expectedResponseShape: { authenticated: true, user: "user" },
});
createAndRunRestAPIQuery({
queryName: "get_basic_auth_invalid",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: "/basic-auth/invaliduser/invalidpass",
});
cy.apiDeleteApp(`${fake.companyName}-restAPI-Basic-App`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
});
it("Should verify response for bearer authentication type connection", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi`,
"restapi",
[
{ key: "url", value: "https://httpbin.org" },
{ key: "auth_type", value: "bearer" },
{ key: "grant_type", value: "authorization_code" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{ key: "access_token_url", value: "" },
{ key: "client_id", value: "" },
{
key: "client_secret",
encrypted: true,
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
},
{ key: "audience", value: "" },
{ key: "scopes", value: "read, write" },
{ key: "username", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
{
key: "bearer_token",
value: "my-token-123",
encrypted: true,
},
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "retry_network_errors", value: true, encrypted: false },
{ key: "url_parameters", value: [["", ""]], encrypted: false },
{ key: "tokenData", encrypted: false },
]
);
cy.reload();
cy.intercept("GET", "/api/library_apps").as("appLibrary");
cy.apiCreateApp(`${fake.companyName}-restAPI-Bearer-App`);
cy.openApp();
createAndRunRestAPIQuery({
queryName: "get_bearer_auth_valid",
dsName: `cypress-${data.dataSourceName}-restapi`,
method: "GET",
urlSuffix: "/bearer",
expectedResponseShape: { authenticated: true, token: "my-token-123" },
});
cy.apiDeleteApp(`${fake.companyName}-restAPI-Bearer-App`);
cy.intercept("GET", "api/data_sources?**").as("datasource");
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi-invalid`,
"restapi",
[
{ key: "url", value: "https://httpbin.org" },
{ key: "auth_type", value: "bearer" },
{ key: "grant_type", value: "authorization_code" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{ key: "access_token_url", value: "" },
{ key: "client_id", value: "" },
{
key: "client_secret",
encrypted: true,
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
},
{ key: "audience", value: "" },
{ key: "scopes", value: "read, write" },
{ key: "username", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
{
key: "bearer_token",
value: "",
encrypted: true,
},
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "retry_network_errors", value: true, encrypted: false },
{ key: "url_parameters", value: [["", ""]], encrypted: false },
{ key: "tokenData", encrypted: false },
]
);
cy.apiCreateApp(`${fake.companyName}-restAPI-Bearer-invalid`);
cy.openApp();
createAndRunRestAPIQuery({
queryName: "get_bearer_auth_invalid",
dsName: `cypress-${data.dataSourceName}-restapi-invalid`,
method: "GET",
urlSuffix: "/bearer",
});
cy.apiDeleteApp(`${fake.companyName}-restAPI-Bearer-invalid`);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
});
it.skip("Should verify response for authentication code grant type connection", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-restapi`,
"restapi",
[
{
key: "url",
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/api/v2/users",
},
{ key: "auth_type", value: "oauth2" },
{ key: "grant_type", value: "client_credentials" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{
key: "access_token_url",
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/oauth/token",
},
{ key: "client_id", value: "JBDuuLU9vaSTP6Do7zYSkw0GvVgWhfyZ" },
{
key: "client_secret",
encrypted: true,
credential_id: "a6d26607-4d09-42a2-8bc0-e5c185c7c2f7",
},
{
key: "audience",
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/api/v2/",
},
{ key: "scopes", value: "" },
{ key: "username", value: "", encrypted: false },
{
key: "password",
encrypted: true,
credential_id: "4502a906-b512-447a-a128-39f67e9778d2",
},
{
key: "bearer_token",
encrypted: true,
credential_id: "c94262c7-d2c5-4d7f-96f8-657689f2b1f0",
},
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "retry_network_errors", value: true, encrypted: false },
]
);
cy.reload();
cy.intercept("GET", "/api/library_apps").as("appLibrary");
cy.apiCreateApp(`${fake.companyName}-client-Grant-RestAPI`);
});
it("Should verify response for content-type", () => {
cy.apiCreateApp(`${fake.companyName}-restAPI-Content-App`);
createAndRunRestAPIQuery({
queryName: "post_json",
dsName: "restapidefault",
method: "POST",
url: "https://jsonplaceholder.typicode.com/posts",
headersList: [["Content-Type", "application/json"]],
rawBody: '{"title": "foo","body": "bar","userId": 1}',
run: true,
urlSuffix: "",
expectedResponseShape: { id: true, title: "foo", body: "bar", userId: 1 },
});
createAndRunRestAPIQuery({
queryName: "post_raw_text",
dsName: "restapidefault",
method: "POST",
url: "https://httpbin.org/post",
headersList: [["Content-Type", "text/plain"]],
rawBody: "This is plain text content",
jsonBody: null,
run: true,
expectedResponseShape: { data: "This is plain text content" },
});
createAndRunRestAPIQuery({
queryName: "post_form_urlencoded",
dsName: "restapidefault",
method: "POST",
url: "https://httpbin.org/post",
headersList: [["Content-Type", "application/x-www-form-urlencoded"]],
bodyList: [
["name", "Jane"],
["age", "30"],
],
expectedResponseShape: {
"form.name": "Jane",
"form.age": "30",
},
});
createAndRunRestAPIQuery({
queryName: "post_xml_soap",
dsName: "restapidefault",
method: "POST",
url: "http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL",
headersList: [["Content-Type", "text/xml; charset=utf-8"]],
rawBody: `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ListOfContinentsByName xmlns="http://www.oorsprong.org/websamples.countryinfo">
</ListOfContinentsByName>
</soap:Body>
</soap:Envelope>`,
jsonBody: null,
bodyList: [],
cookiesList: [["session", "abc123"]],
paramsList: [["lang", "en"]],
run: true,
shouldSucceed: true,
expectedResponseShape: {},
});
// createAndRunRestAPIQuery({
// queryName: "post_text_csv",
// dsName: "restapidefault",
// method: "POST",
// url: `https://tejasvi.free.beeceptor.com/csv-upload`,
// headersList: [["Content-Type", "text/csv"]],
// rawBody:
// "id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com",
// expectedResponseShape: {
// data: '{\n "status": "ok",\n "message": "File uploaded successfully",\n "body": id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com\n}',
// },
// });
// const filename = "tooljet.png";
// createAndRunRestAPIQuery({
// queryName: "upload_image",
// dsName: "restapidefault",
// method: "POST",
// url: `https://tejasvi.free.beeceptor.commultipart-upload`,
// headersList: [["Content-Type", "multipart/form-data"]],
// bodyList: [
// ["Image_File", "fixture:Image/tooljet.png"],
// ["filename", filename],
// ],
// expectedResponseShape: {
// filename: filename,
// },
// });
});
});

View file

@ -120,7 +120,7 @@ describe("Data sources AWS S3", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.alertRegionIsMissing
);
@ -144,7 +144,7 @@ describe("Data sources AWS S3", () => {
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.alertRegionIsMissing
);
@ -170,7 +170,7 @@ describe("Data sources AWS S3", () => {
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.alertInvalidUrl
);
@ -188,7 +188,7 @@ describe("Data sources AWS S3", () => {
);
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.accessKeyError
);
@ -207,7 +207,7 @@ describe("Data sources AWS S3", () => {
cy.get(postgreSqlSelector.buttonTestConnection).click();
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
s3Text.sinatureError
);

View file

@ -105,7 +105,7 @@ describe("Data source SMTP", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
"Invalid credentials"
);

View file

@ -128,7 +128,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
"Invalid account. The specified value must be a valid subdomain string."
);

View file

@ -132,7 +132,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"Failed to connect to localhost:1433 - Could not connect (sequence)"
);

View file

@ -96,7 +96,7 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.buttonTextSave
);
cy.get('[data-cy="connection-alert-text"]').should(
cy.get(dataSourceSelector.connectionAlertText).should(
"have.text",
"Ensure that apiKey is set"
);

View file

@ -5,213 +5,214 @@ import { commonText } from "Texts/common";
import { exportAppModalText } from "Texts/exportImport";
import {
clickOnExportButtonAndVerify,
exportAllVersionsAndVerify,
verifyElementsOfExportModal,
clickOnExportButtonAndVerify,
exportAllVersionsAndVerify,
verifyElementsOfExportModal,
} from "Support/utils/exportImport";
import { selectAppCardOption, closeModal } from "Support/utils/common";
describe("App Export", () => {
const TEST_DATA = {
appFiles: {
multiVersion: "cypress/fixtures/templates/three-versions.json",
singleVersion: "cypress/fixtures/templates/one_version.json",
},
};
const TEST_DATA = {
appFiles: {
multiVersion: "cypress/fixtures/templates/three-versions.json",
singleVersion: "cypress/fixtures/templates/one_version.json",
},
};
let data;
let data;
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
beforeEach(() => {
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
cy.exec("mkdir -p ./cypress/downloads/");
cy.exec("cd ./cypress/downloads/ && rm -rf *");
cy.exec("mkdir -p ./cypress/downloads/");
cy.wait(3000);
beforeEach(() => {
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
cy.exec("mkdir -p ./cypress/downloads/");
cy.wait(3000);
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
});
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
});
it("Verify the elements of export dialog box", () => {
cy.skipWalkthrough();
it("Verify the elements of export dialog box", () => {
cy.skipWalkthrough()
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
cy.get(importSelectors.importOptionInput)
.eq(0)
.selectFile(TEST_DATA.appFiles.multiVersion, { force: true });
cy.wait(2000);
cy.clearAndType(commonSelectors.appNameInput, data.appName);
cy.get(importSelectors.importAppButton).click();
cy.wait(3000);
cy.backToApps();
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
cy.get(importSelectors.importOptionInput)
.eq(0)
.selectFile(TEST_DATA.appFiles.multiVersion, {
force: true,
});
cy.wait(1500);
cy.clearAndType(commonSelectors.appNameInput, data.appName);
cy.get(importSelectors.importAppButton).click();
cy.wait(3000);
cy.backToApps();
// Select the app card option to export the app
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
);
// Select the app card option to export the app
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
// Verify the elements of the export modal
verifyElementsOfExportModal("v3", ["v2", "v1"], [true, false, false]);
// Close the modal
closeModal(exportAppModalText.modalCloseButton);
// Ensure the modal title is no longer visible
cy.get(
commonSelectors.modalTitle(exportAppModalText.selectVersionTitle)
).should("not.exist");
// Re-open the export modal and click the export button
cy.wait(2000);
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
);
clickOnExportButtonAndVerify(exportAppModalText.exportAll, data.appName);
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
// Validate the schema for the student table in tooljetdb
const tooljetDatabase = appData.tooljet_database.find(
(db) => db.table_name === "student"
);
expect(tooljetDatabase).to.exist;
expect(tooljetDatabase.schema).to.exist;
// Validate components and queries
const components = appData.app[0].definition.appV2.components;
const text2Component = components.find(
(component) => component.name === "text2"
);
expect(text2Component).to.exist;
expect(text2Component.properties.text.value).to.equal(
"{{constants.pageHeader}}"
);
// Verify the elements of the export modal
verifyElementsOfExportModal("v3", ["v2", "v1"], [true, false, false]);
// Close the modal
closeModal(exportAppModalText.modalCloseButton);
// Ensure the modal title is no longer visible
cy.get(
commonSelectors.modalTitle(exportAppModalText.selectVersionTitle)
).should("not.exist");
// Re-open the export modal and click the export button
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
const textinput1 = components.find(
(component) => component.name === "textinput1"
);
clickOnExportButtonAndVerify(exportAppModalText.exportAll, data.appName);
expect(textinput1).to.exist;
expect(textinput1.properties.value.value).to.include("queries");
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
// Validate the schema for the student table in tooljetdb
const tooljetDatabase = appData.tooljet_database.find(
(db) => db.table_name === "student"
);
expect(tooljetDatabase).to.exist;
expect(tooljetDatabase.schema).to.exist;
// Validate components and queries
const components = appData.app[0].definition.appV2.components;
const text2Component = components.find(
(component) => component.name === "text2"
);
expect(text2Component).to.exist;
expect(text2Component.properties.text.value).to.equal(
"{{constants.pageHeader}}"
);
const textinput1 = components.find(
(component) => component.name === "textinput1"
);
expect(textinput1).to.exist;
expect(textinput1.properties.value.value).to.include("queries");
const textinput2 = components.find(
(component) => component.name === "textinput2"
);
expect(textinput2).to.exist;
expect(textinput2.properties.value.value).to.include("queries");
const textinput3 = components.find(
(component) => component.name === "textinput3"
);
expect(textinput3).to.exist;
expect(textinput3.properties.value.value).to.include("queries");
// Validate the data queries
const dataQueries = appData.app[0].definition.appV2.dataQueries;
const postgresqlQuery = dataQueries.find(
(query) => query.name === "postgresql1"
);
expect(postgresqlQuery).to.exist;
expect(postgresqlQuery.options.query).to.include(
"Select * from {{secrets.db_name}}"
);
const restapiQuery = dataQueries.find(
(query) => query.name === "restapi1"
);
expect(restapiQuery).to.exist;
expect(restapiQuery.options.url).to.equal(
"https://jsonplaceholder.typicode.com/users/1"
);
const tooljetdbQuery = dataQueries.find(
(query) => query.name === "tooljetdb1"
);
expect(tooljetdbQuery).to.exist;
expect(tooljetdbQuery.options.operation).to.equal("list_rows");
// Ensure appVersions exists
const appVersions = appData.app[0].definition.appV2.appVersions;
expect(appVersions).to.exist;
// Map and verify app version names
const versionNames = appVersions.map((version) => version.name);
expect(versionNames).to.include.members(["v1", "v2", "v3"]);
});
});
cy.exec("cd ./cypress/downloads/ && rm -rf *");
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
const textinput2 = components.find(
(component) => component.name === "textinput2"
);
cy.get(`[data-cy="v1-radio-button"]`).check();
cy.get(
commonSelectors.buttonSelector(exportAppModalText.exportSelectedVersion)
).click();
expect(textinput2).to.exist;
expect(textinput2.properties.value.value).to.include("queries");
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
const textinput3 = components.find(
(component) => component.name === "textinput3"
);
expect(textinput3).to.exist;
expect(textinput3.properties.value.value).to.include("queries");
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
// Validate the data queries
const dataQueries = appData.app[0].definition.appV2.dataQueries;
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
});
});
const postgresqlQuery = dataQueries.find(
(query) => query.name === "postgresql1"
);
expect(postgresqlQuery).to.exist;
expect(postgresqlQuery.options.query).to.include(
"Select * from {{secrets.db_name}}"
);
const restapiQuery = dataQueries.find(
(query) => query.name === "restapi1"
);
expect(restapiQuery).to.exist;
expect(restapiQuery.options.url).to.equal(
"https://jsonplaceholder.typicode.com/users/1"
);
const tooljetdbQuery = dataQueries.find(
(query) => query.name === "tooljetdb1"
);
expect(tooljetdbQuery).to.exist;
expect(tooljetdbQuery.options.operation).to.equal("list_rows");
// Ensure appVersions exists
const appVersions = appData.app[0].definition.appV2.appVersions;
expect(appVersions).to.exist;
// Map and verify app version names
const versionNames = appVersions.map((version) => version.name);
expect(versionNames).to.include.members(["v1", "v2", "v3"]);
});
});
it.skip("Verify 'Export app' functionality of an application inside app editor", () => {
data.appName2 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName2);
cy.openApp(data.appName2);
cy.exec("cd ./cypress/downloads/ && rm -rf *");
cy.dragAndDropWidget("Text Input", 50, 50);
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
);
cy.get(`[data-cy="v1-radio-button"]`).check();
cy.get(
commonSelectors.buttonSelector(exportAppModalText.exportSelectedVersion)
).click();
cy.get('[data-cy="left-sidebar-settings-button"]').click();
cy.get('[data-cy="button-user-status-change"]').click();
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
verifyElementsOfExportModal("v1");
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
exportAllVersionsAndVerify(data.appName1, "v1");
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
});
});
});
it.skip("Verify 'Export app' functionality of an application inside app editor", () => {
data.appName2 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName2);
cy.openApp(data.appName2);
cy.dragAndDropWidget("Text Input", 50, 50);
cy.get('[data-cy="left-sidebar-settings-button"]').click();
cy.get('[data-cy="button-user-status-change"]').click();
verifyElementsOfExportModal("v1");
exportAllVersionsAndVerify(data.appName1, "v1");
});
});

View file

@ -84,7 +84,7 @@ describe("App Slug", () => {
// Release and verify URLs
releaseApp();
verifyURLs(workspaceId, data.slug, false);
verifyURLs(workspaceId, data.slug, true);
// Verify duplicate slug validation
cy.visit("/my-workspace");

View file

@ -114,17 +114,30 @@ describe("App Version", () => {
cy.wait(3000);
// cy.reload();
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", { timeout: 10000 });
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", {
timeout: 10000,
});
// Preview and release verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", "/home?version=v2");
cy.openApp("", Cypress.env("workspaceId"), Cypress.env("appId"), commonWidgetSelector.draggableWidget("text1"));
cy.openApp(
"",
Cypress.env("workspaceId"),
Cypress.env("appId"),
commonWidgetSelector.draggableWidget("text1")
);
releasedVersionAndVerify("v2");
});
it("should verify version management with components and queries", () => {
// Initial setup with component and datasource
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
data.datasourceName,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
cy.apiAddComponentToApp(
data.appName,
"text1",
@ -134,19 +147,15 @@ describe("App Version", () => {
);
cy.waitForAutoSave();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
data.datasourceName,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
createRestAPIQuery(data.query1, data.datasourceName, "", "", "/1", true);
// Version v2 creation and verification
navigateToCreateNewVersionModal("v1");
createNewVersion(["v2"], "v1");
cy.get(commonWidgetSelector.draggableWidget("text1"))
.verifyVisibleElement("have.text", "Leanne Graham");
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
cy.get(`[data-cy="list-query-${data.query1}"]`).should("be.visible");
// Modify v2 with new components and queries
@ -170,67 +179,79 @@ describe("App Version", () => {
create: { version: "v3", from: "v2" },
verify: {
component: { selector: "textInput", value: "Ervin Howell" },
query: data.query2
}
query: data.query2,
},
},
{
create: { version: "v4", from: "v1" },
verify: {
component: { selector: "text1", text: "Leanne Graham" },
query: data.query1
}
query: data.query1,
},
},
{
create: { version: "v5", from: "v3" },
verify: {
component: { selector: "textInput", value: "Ervin Howell" },
query: data.query2
}
}
query: data.query2,
},
},
];
versionChecks.forEach(check => {
versionChecks.forEach((check) => {
navigateToCreateNewVersionModal(check.create.from);
createNewVersion([check.create.version], check.create.from);
if (check.verify.component.value) {
cy.get(commonWidgetSelector.draggableWidget(check.verify.component.selector))
.verifyVisibleElement("have.value", check.verify.component.value);
cy.get(
commonWidgetSelector.draggableWidget(check.verify.component.selector)
).verifyVisibleElement("have.value", check.verify.component.value);
} else {
cy.get(commonWidgetSelector.draggableWidget(check.verify.component.selector))
.verifyVisibleElement("have.text", check.verify.component.text);
cy.get(
commonWidgetSelector.draggableWidget(check.verify.component.selector)
).verifyVisibleElement("have.text", check.verify.component.text);
}
cy.get(`[data-cy="list-query-${check.verify.query}"]`).should("be.visible");
cy.get(`[data-cy="list-query-${check.verify.query}"]`).should(
"be.visible"
);
});
// Release and version state verification
releasedVersionAndVerify("v5");
cy.get(appVersionSelectors.currentVersionField("v5"))
.should("have.class", "color-light-green");
cy.get(appVersionSelectors.currentVersionField("v5")).should(
"have.class",
"color-light-green"
);
// Version switching and component verification
cy.get(appVersionSelectors.currentVersionField("v5")).click();
cy.contains(`[id*="react-select-"]`, "v4").click();
cy.get(appVersionSelectors.currentVersionField("v4"))
.should("not.have.class", "color-light-green");
cy.get(commonWidgetSelector.draggableWidget("text1"))
.verifyVisibleElement("have.text", "Leanne Graham");
cy.get(appVersionSelectors.currentVersionField("v4")).should(
"not.have.class",
"color-light-green"
);
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
cy.get(`[data-cy="list-query-${data.query1}"]`).should("be.visible");
// Preview and version switching verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", "/home?version=v4");
cy.get(commonWidgetSelector.draggableWidget("text1"))
.verifyVisibleElement("have.text", "Leanne Graham");
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
cy.get('[data-cy="preview-settings"]').click();
switchVersionAndVerify("v4", "v5");
cy.get(commonWidgetSelector.draggableWidget("textInput"))
.verifyVisibleElement("have.value", "Ervin Howell");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
//url validation should be added after bug fix
// cy.url().should("include", "/home?version=v5");
});
});

View file

@ -19,6 +19,7 @@ import {
} from "Support/utils/common";
import { inviteUserBasedOnRole } from "Support/utils/manageGroups";
import { resolveHost } from "Support/utils/apps";
import { addSuccessNotification } from "Support/utils/queries";
const data = {};
data.firstName = fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", "");
@ -32,7 +33,7 @@ describe("Datasource Manager", () => {
beforeEach(() => {
cy.apiLogin();
cy.visit(`${workspaceSlug}`);
cy.viewport(1200, 1300);
cy.viewport(1800, 1800);
cy.skipWalkthrough();
});
@ -199,7 +200,7 @@ describe("Datasource Manager", () => {
cy.apiCreateApp(data.appName);
cy.openApp();
pinInspector();
// pinInspector();
addQuery(
"table_preview",
@ -212,9 +213,11 @@ describe("Datasource Manager", () => {
"table_preview "
);
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get('[data-cy="query-tab-settings"]').click();
addSuccessNotification("table_preview");
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
verifyValueOnInspector("table_preview", "10 items ");
cy.verifyToastMessage(commonSelectors.toastMessage, "table_preview");
cy.get('[data-cy="show-ds-popover-button"]').click();
cy.get(".p-2 > .tj-base-btn")
@ -247,9 +250,13 @@ describe("Datasource Manager", () => {
cy.get("#react-select-4-listbox")
.contains(`cypress-${data.dsName2}-postgresql`)
.click();
cy.get('[data-cy="query-tab-settings"]').click();
addSuccessNotification("postgresql");
cy.waitForAutoSave();
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
verifyValueOnInspector("table_preview", "4 items ");
cy.verifyToastMessage(commonSelectors.toastMessage, "postgresql");
});
it.skip("Should verify the query creation and scope changing functionality.", () => {

View file

@ -143,11 +143,11 @@ describe("Workspace constants", () => {
cy.get(dataSourceSelector.previewTabRawContainer).contains("secrets is not defined");
//verify global const should be visible, secrets and deleted const are not in Inspector
cy.get(commonWidgetSelector.inspectorIcon).click();
cy.get(commonWidgetSelector.constantInspectorIcon).click();
cy.get('[data-cy="inspector-node-restapiheaderkey"]').should('exist');
cy.get('[data-cy="inspector-node-deleteconst"]').should('not.exist');
cy.get('[data-cy="inspector-node-sconst"]').should('not.exist');
// cy.get(commonWidgetSelector.sidebarinspector).click();
// cy.get(commonWidgetSelector.constantInspectorIcon).click();
// cy.get('[data-cy="inspector-node-restapiheaderkey"]').should('exist');
// cy.get('[data-cy="inspector-node-deleteconst"]').should('not.exist');
// cy.get('[data-cy="inspector-node-sconst"]').should('not.exist');
//Preview app and verify components
cy.openInCurrentTab(commonWidgetSelector.previewButton);

View file

@ -136,6 +136,7 @@ export const resolveHost = () => {
const urlMapping = {
"http://localhost:8082": "http://localhost:8082",
"http://localhost:3000": "http://localhost:3000",
"http://localhost:3000/apps": "http://localhost:3000/apps",
"http://localhost:4001": "http://localhost:3000",
"http://localhost:4001/apps": "http://localhost:3000/apps",

View file

@ -31,8 +31,11 @@ export const verifyAndModifyParameter = (paramName, value) => {
export const openEditorSidebar = (widgetName = "") => {
cy.hideTooltip();
cy.get(`${commonWidgetSelector.draggableWidget(widgetName)}:eq(0)`).realHover()
cy.get(commonWidgetSelector.widgetConfigHandle(widgetName)).click();
cy.get(`${commonWidgetSelector.draggableWidget(widgetName)}:eq(0)`).realHover().then(() => {
cy.wait(1000);
cy.get(commonWidgetSelector.widgetConfigHandle(widgetName)).click();
})
};
export const verifyAndModifyToggleFx = (

View file

@ -25,7 +25,7 @@ export const query = (operation) => {
};
export const verifypreview = (type, data) => {
cy.get(`[data-cy="preview-tab-${type}"]`).click();
cy.get(`[data-cy="preview-tab-${type}"]`, { timeout: 15000 }).click();
cy.get(`[data-cy="preview-${type}-data-container"]`).verifyVisibleElement(
"contain.text",
data,
@ -255,7 +255,7 @@ export const createRestAPIQuery = (
}).then((response) => {
const editingVersionId = response.body.editing_version.id;
const data_source_id = Cypress.env(kind);
const data_source_id = Cypress.env(`${dsName}`);
const requestBody = {
app_id: Cypress.env("appId"),

View file

@ -1,60 +1,49 @@
export const verifyNodeData = (node, type, children, index = 0) => {
cy.get(
`[data-cy="inspector-node-${node.toLowerCase()}"] > .node-length-color`
)
.eq(index)
.realHover()
.verifyVisibleElement("have.text", `${children}`);
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .node-key`)
.eq(index)
.verifyVisibleElement("have.text", node);
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .node-type`)
.eq(index)
.verifyVisibleElement("have.text", type);
};
import { commonWidgetSelector } from "Selectors/common";
export const openNode = (node, index = 0, time = 1000) => {
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .node-key`)
.eq(index)
.click();
cy.wait(time);
};
export const verifyValue = (node, type, children, index = 0) => {
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .mx-2`)
.eq(index)
.realHover()
.verifyVisibleElement("contain.text", `${children}`);
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .node-key`)
.eq(index)
.verifyVisibleElement("contain.text", node);
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .mx-1`)
.eq(index)
.verifyVisibleElement("contain.text", type);
};
export const deleteComponentFromInspector = (node) => {
cy.get('[data-cy="inspector-node-components"] > .node-key').click();
cy.get(`[data-cy="inspector-node-${node}"] > .node-key`).realHover().parent().find('[style="height: 13px; width: 13px;"] > img').last().click();
};
export const verifyfunctions = (node, type, index = 0) => {
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .fs-10`)
.eq(index)
.realHover()
.verifyVisibleElement("contain.text", `${type}`);
cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .node-key`)
.eq(index)
.verifyVisibleElement("contain.text", node);
// cy.get(`[data-cy="inspector-node-${node.toLowerCase()}"] > .mx-1`)
// .eq(index)
// .verifyVisibleElement("contain.text", type);
export const openAndVerifyNode = (nodeName, nodes, verificationFunction) => {
openStateFromComponent(nodeName);
verifyNodes(nodes, verificationFunction);
};
export const verifyNodes = (nodes, verificationFunction) => {
nodes.forEach(node => verificationFunction(node.key, node.type, node.value));
};
export const openAndVerifyNode = (nodeName, nodes, verificationFunction) => {
openNode(nodeName);
verifyNodes(nodes, verificationFunction);
export const openNode = (node, index = 0, time = 1000) => {
cy.get(`[data-cy="inspector-${node.toLowerCase()}-expand-button"]`, { timeout: time })
.eq(index)
.click();
};
export const openStateFromComponent = (widgetName) => {
cy.get(commonWidgetSelector.draggableWidget(widgetName))
.realHover()
.realHover();
cy.get(commonWidgetSelector.draggableWidget(widgetName))
.realHover()
.then(() => {
cy.get(`[data-cy="${widgetName}-inspect-button"]`)
.realHover({ position: "topRight" })
.last()
.realClick();
});
}
export const verifyNodeData = (node, type, value, index = 0) => {
cy.get(
`[data-cy="inspector-${node.toLowerCase()}-label"]`
)
.eq(index)
.realHover()
.verifyVisibleElement("have.text", `${node}`);
cy.get(`[data-cy="inspector-${node.toLowerCase()}-value"]`)
.eq(index)
.verifyVisibleElement("have.text", type == 'Function' ? 'function' : value);
};
export const deleteComponentFromInspector = (node) => {
cy.get('[data-cy="inspector-menu-icon"]').click();
cy.get(`[data-cy="inspector-delete-component-action"`).realHover().parent().find('[style="height: 13px; width: 13px;"] > img').last().click();
};

View file

@ -1,19 +1,31 @@
export const createAndRunRestAPIQuery = (
export const createAndRunRestAPIQuery = ({
queryName,
dsName,
method = "GET",
url = "",
urlSuffix = "",
headersList = [],
bodyList = [],
jsonBody = null,
rawBody = null,
cookiesList = [],
paramsList = [],
run = true,
urlSuffix = ""
) => {
expectedResponseShape = {},
authType = "",
authToken = "",
}) => {
cy.getCookie("tj_auth_token").then((cookie) => {
const headers = {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
};
// if (authType === "bearer" || authType === "oauth2") {
// headers["Authorization"] = `Bearer ${authToken}`;
// } else if (authType === "basic") {
// headers["Authorization"] = `Basic ${btoa(authToken)}`;
// }
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/apps/${Cypress.env("appId")}`,
@ -27,84 +39,108 @@ export const createAndRunRestAPIQuery = (
url: `${Cypress.env("server_host")}/api/data-sources/${Cypress.env("workspaceId")}/environments/${currentEnvironmentId}/versions/${editingVersionId}`,
headers,
}).then((dsResponse) => {
expect(dsResponse.status).to.eq(200);
const dataSource = dsResponse.body.data_sources.find(
(ds) => ds.name === dsName
);
if (!dataSource) {
throw new Error(`Data source '${dsName}' not found.`);
}
const data_source_id = dataSource.id;
const useJsonBody =
["POST", "PATCH", "PUT"].includes(method.toUpperCase()) &&
jsonBody !== null;
const useJson = jsonBody !== null;
const useRaw = rawBody !== null;
const useForm = bodyList?.length && !useJson && !useRaw;
const queryOptions = {
method: method.toLowerCase(),
url: url + urlSuffix,
url_params: [["", ""]],
url_params: paramsList.length ? paramsList : [["", ""]],
headers: headersList.length ? headersList : [["", ""]],
body: !useJsonBody && bodyList.length ? bodyList : [["", ""]],
json_body: useJsonBody ? jsonBody : null,
body_toggle: useJsonBody,
cookies: cookiesList.length ? cookiesList : [["", ""]],
body: useForm ? bodyList : [["", ""]],
json_body: useJson ? jsonBody : null,
raw_body: useRaw ? rawBody : "",
body_toggle: useJson || useRaw,
runOnPageLoad: run,
transformationLanguage: "javascript",
enableTransformation: false,
};
const requestBody = {
app_id: Cypress.env("appId"),
app_version_id: editingVersionId,
name: queryName,
kind: "restapi",
options: queryOptions,
data_source_id,
data_source_id: dataSource.id,
plugin_id: null,
};
cy.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/data-queries/data-sources/${data_source_id}/versions/${editingVersionId}`,
url: `${Cypress.env("server_host")}/api/data-queries/data-sources/${dataSource.id}/versions/${editingVersionId}`,
headers,
body: requestBody,
}).then((createResponse) => {
expect(createResponse.status).to.equal(201);
expect(createResponse.status).to.eq(201);
const queryId = createResponse.body.id;
cy.log("Query created successfully:", queryId);
const createdOptions = createResponse.body.options;
expect(createdOptions.method).to.equal(queryOptions.method);
expect(createdOptions.url).to.equal(queryOptions.url);
expect(createdOptions.headers).to.deep.equal(queryOptions.headers);
if (useJsonBody) {
expect(createdOptions.json_body).to.deep.equal(
queryOptions.json_body
);
expect(createdOptions.body_toggle).to.equal(true);
} else {
expect(createdOptions.body).to.deep.equal(queryOptions.body);
expect(createdOptions.body_toggle).to.equal(false);
}
expect(createdOptions.runOnPageLoad).to.equal(run);
cy.log("Metadata verified successfully");
if (run) {
cy.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/data-queries/${queryId}/run`,
headers,
failOnStatusCode: false,
}).then((runResponse) => {
expect([200, 201]).to.include(runResponse.status);
cy.log("Query executed successfully:", runResponse.body);
if (runResponse.body?.data.id) {
cy.writeFile("cypress/fixtures/restAPI/storedId.json", {
id: runResponse.body.data.id,
});
cy.log("Stored ID:", runResponse.body.data.id);
const responseData = runResponse.body?.data;
const requestHeaders =
runResponse.body?.metadata?.request?.headers || {};
if (runResponse.body.status === "ok") {
expect([200, 201]).to.include(runResponse.status);
cy.log("Response:", responseData);
if (
expectedResponseShape &&
typeof expectedResponseShape === "object"
) {
Object.entries(expectedResponseShape).forEach(
([path, expected]) => {
const value = path
.split(".")
.reduce((obj, key) => obj?.[key], responseData);
if (expected === true) {
expect(value).to.not.be.undefined;
} else {
expect(value).to.eq(expected);
}
}
);
}
const expectedContentType = headersList.find(
([key]) => key.toLowerCase() === "content-type"
)?.[1];
if (expectedContentType && requestHeaders["content-type"]) {
expect(requestHeaders["content-type"]).to.include(
expectedContentType
);
}
if (Array.isArray(responseData)) {
responseData.forEach((item) => {
expect(item).to.have.any.keys("id", "name", "price");
});
}
if (responseData?.id) {
cy.writeFile("cypress/fixtures/restAPI/storedId.json", {
id: responseData.id,
});
}
} else if (runResponse.body.status === "failed") {
expect(runResponse.body.message).to.eq(
"Query could not be completed"
);
const statusCode =
runResponse.body?.metadata?.response?.statusCode;
expect([400, 401, 403, 404, 500]).to.include(statusCode);
cy.log(
"Failure validated as expected with status:",
statusCode
);
}
});
}

View file

@ -7,10 +7,12 @@ sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates apt-u
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 18.18.2
nvm install 22.15.1
sudo ln -s "$(which node)" /usr/bin/node
sudo ln -s "$(which npm)" /usr/bin/npm
sudo npm i -g npm@10.9.2
# Setup openresty
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu bionic main" > openresty.list
@ -56,10 +58,10 @@ envsubst "${VARS_TO_SUBSTITUTE}" < /tmp/nginx.conf > /tmp/nginx-substituted.conf
sudo cp /tmp/nginx-substituted.conf /usr/local/openresty/nginx/conf/nginx.conf
# Download and setup postgrest binary
curl -OL https://github.com/PostgREST/postgrest/releases/download/v10.1.1/postgrest-v10.1.1-linux-static-x64.tar.xz
tar xJf postgrest-v10.1.1-linux-static-x64.tar.xz
curl -OL https://github.com/PostgREST/postgrest/releases/download/v12.2.0/postgrest-v12.2.0-linux-static-x64.tar.xz
tar xJf postgrest-v12.2.0-linux-static-x64.tar.xz
sudo mv ./postgrest /bin/postgrest
sudo rm postgrest-v10.1.1-linux-static-x64.tar.xz
sudo rm postgrest-v12.2.0-linux-static-x64.tar.xz
# Setup app and postgrest as systemd service
sudo cp /tmp/nest.service /lib/systemd/system/nest.service
@ -74,7 +76,7 @@ mv /tmp/.env ~/app/.env
mv /tmp/setup_app ~/app/setup_app
sudo chmod +x ~/app/setup_app
npm install -g npm@9.8.1
npm install -g npm@10.9.2
# Building ToolJet app
npm install -g @nestjs/cli

View file

@ -7,11 +7,11 @@ sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates apt-u
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 18.18.2
nvm install 22.15.1
sudo ln -s "$(which node)" /usr/bin/node
sudo ln -s "$(which npm)" /usr/bin/npm
sudo npm i -g npm@9.8.1
sudo npm i -g npm@10.9.2
# Setup openresty
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
@ -58,10 +58,10 @@ envsubst "${VARS_TO_SUBSTITUTE}" < /tmp/nginx.conf > /tmp/nginx-substituted.conf
sudo cp /tmp/nginx-substituted.conf /usr/local/openresty/nginx/conf/nginx.conf
# Download and setup postgrest binary
curl -OL https://github.com/PostgREST/postgrest/releases/download/v12.0.2/postgrest-v12.0.2-linux-static-x64.tar.xz
tar xJf postgrest-v12.0.2-linux-static-x64.tar.xz
curl -OL https://github.com/PostgREST/postgrest/releases/download/v12.2.0/postgrest-v12.2.0-linux-static-x64.tar.xz
tar xJf postgrest-v12.2.0-linux-static-x64.tar.xz
sudo mv ./postgrest /bin/postgrest
sudo rm postgrest-v12.0.2-linux-static-x64.tar.xz
sudo rm postgrest-v12.2.0-linux-static-x64.tar.xz
# Add the Redis APT repository
sudo add-apt-repository ppa:redislabs/redis -y
@ -92,7 +92,7 @@ mv /tmp/.env ~/app/.env
mv /tmp/setup_app ~/app/setup_app
sudo chmod +x ~/app/setup_app
npm install -g npm@9.8.1
npm install -g npm@10.9.2
# Building ToolJet app
npm install -g @nestjs/cli

View file

@ -1,4 +1,4 @@
FROM node:18.18.2-buster AS builder
FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
@ -30,9 +30,10 @@ COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM node:18.18.2-bullseye
FROM node:22.15.1-bullseye
# copy postgrest executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin

View file

@ -1,9 +1,9 @@
FROM node:18.18.2-buster AS builder
FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV NODE_OPTIONS="--max-old-space-size=8096"
RUN npm i -g npm@9.8.1
RUN npm i -g npm@10.9.2
RUN mkdir -p /app
WORKDIR /app
@ -31,10 +31,11 @@ ENV NODE_ENV=production
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM debian:11
FROM debian:12
RUN apt-get update -yq \
&& apt-get install curl gnupg zip -yq \
@ -42,12 +43,12 @@ RUN apt-get update -yq \
&& apt-get clean -y
RUN curl -O https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz \
&& tar -xf node-v18.18.2-linux-x64.tar.xz \
&& mv node-v18.18.2-linux-x64 /usr/local/lib/nodejs \
RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \
&& tar -xf node-v22.15.1-linux-x64.tar.xz \
&& mv node-v22.15.1-linux-x64 /usr/local/lib/nodejs \
&& echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \
&& /bin/bash -c "source /etc/profile.d/nodejs.sh" \
&& rm node-v18.18.2-linux-x64.tar.xz
&& rm node-v22.15.1-linux-x64.tar.xz
ENV PATH=/usr/local/lib/nodejs/bin:$PATH
ENV NODE_ENV=production

View file

@ -0,0 +1,31 @@
#!/bin/bash
set -e
npm cache clean --force
if [ -d "./server/dist" ]; then
SETUP_CMD='npm run cloud:setup:prod'
else
SETUP_CMD='npm run cloud:setup'
fi
npm cache clean --force
if [ -f "./.env" ]; then
declare $(grep -v '^#' ./.env | xargs)
fi
if [ -z "$DATABASE_URL" ]; then
./server/scripts/wait-for-it.sh $PG_HOST:${PG_PORT:-5432} --strict --timeout=300 -- $SETUP_CMD
else
PG_HOST=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $6}')
PG_PORT=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $7}')
if [ -z "$DATABASE_PORT" ]; then
DATABASE_PORT="5432"
fi
./server/scripts/wait-for-it.sh "$PG_HOST:$PG_PORT" --strict --timeout=300 -- $SETUP_CMD
fi
exec "$@"

View file

@ -0,0 +1,121 @@
FROM node:18.18.2-buster AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm i -g npm@9.8.1
RUN mkdir -p /app
# RUN npm cache clean --force
WORKDIR /app
# Scripts for building
COPY ./package.json ./package.json
# Build plugins
COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/
RUN npm --prefix plugins install
COPY ./plugins/ ./plugins/
RUN NODE_ENV=production npm --prefix plugins run build
RUN npm --prefix plugins prune --production
# Build frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
RUN npm --prefix frontend install
COPY ./frontend/ ./frontend/
RUN npm --prefix frontend run build --production
RUN npm --prefix frontend prune --production
ENV NODE_ENV=production
# Build server
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm --prefix server run build
FROM debian:11
RUN apt-get update -yq \
&& apt-get install curl gnupg zip -yq \
&& apt-get install -yq build-essential \
&& apt-get clean -y
RUN curl -O https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz \
&& tar -xf node-v18.18.2-linux-x64.tar.xz \
&& mv node-v18.18.2-linux-x64 /usr/local/lib/nodejs \
&& echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \
&& /bin/bash -c "source /etc/profile.d/nodejs.sh" \
&& rm node-v18.18.2-linux-x64.tar.xz
ENV PATH=/usr/local/lib/nodejs/bin:$PATH
ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt-get update && \
apt-get install -y postgresql-client freetds-dev libaio1 wget && \
apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes && \
apt-get -y autoremove && \
apt-get -y autoclean
# Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle
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 && \
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 && \
cd /opt/oracle/instantclient_21_10 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
cd /opt/oracle/instantclient_11_2 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
echo /opt/oracle/instantclient* > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
# Set the Instant Client library paths
ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}"
WORKDIR /
RUN mkdir -p /app
# copy npm scripts
COPY --from=builder /app/package.json ./app/package.json
# copy plugins dependencies
COPY --from=builder /app/plugins/dist ./app/plugins/dist
COPY --from=builder /app/plugins/client.js ./app/plugins/client.js
COPY --from=builder /app/plugins/node_modules ./app/plugins/node_modules
COPY --from=builder /app/plugins/packages/common ./app/plugins/packages/common
COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY ./docker/cloud/cloud-entrypoint.sh ./app/server/cloud-entrypoint.sh
# Define non-sudo user
RUN useradd --create-home --home-dir /home/appuser appuser \
&& chown -R appuser:0 /app \
&& chown -R appuser:0 /home/appuser \
&& chmod u+x /app \
&& chmod -R g=u /app
# Set npm cache directory
ENV npm_config_cache /home/appuser/.npm
ENV HOME=/home/appuser
# Installing git for simple git commands
RUN apt-get update && apt-get install -y git && apt-get clean
USER appuser
WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
ENTRYPOINT ["./server/cloud-entrypoint.sh"]

View file

@ -91,12 +91,13 @@ COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/entrypoint.sh ./app/server/entrypoint.sh
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY ./docker/cloud/cloud-entrypoint.sh ./app/server/cloud-entrypoint.sh
# Define non-sudo user
RUN useradd --create-home --home-dir /home/appuser appuser \
&& chown -R appuser:0 /app \
@ -108,10 +109,14 @@ RUN useradd --create-home --home-dir /home/appuser appuser \
ENV npm_config_cache /home/appuser/.npm
ENV HOME=/home/appuser
# Installing git for simple git commands
RUN apt-get update && apt-get install -y git && apt-get clean
USER appuser
WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
ENTRYPOINT ["./server/entrypoint.sh"]
ENTRYPOINT ["./server/cloud-entrypoint.sh"]

View file

@ -42,6 +42,115 @@ else
echo "Using external PostgREST at $PGRST_HOST."
fi
# Neo4j configuration
# ----------------------------------
# Default Neo4j environment values
# ----------------------------------
export NEO4J_USER=${NEO4J_USER:-"neo4j"}
export NEO4J_PASSWORD=${NEO4J_PASSWORD:-"appaqvyvRLbeukhFE"}
export NEO4J_AUTH=${NEO4J_AUTH:-"neo4j/appaqvyvRLbeukhFE"}
export NEO4J_URI=${NEO4J_URI:-"bolt://localhost:7687"}
export NEO4J_PLUGINS=${NEO4J_PLUGINS:-'["apoc"]'}
export NEO4J_AUTH
# Extract username and password from NEO4J_AUTH if set
if [ -n "$NEO4J_AUTH" ]; then
# Extract username and password from NEO4J_AUTH (format: username/password)
NEO4J_USERNAME=$(echo "$NEO4J_AUTH" | cut -d'/' -f1)
NEO4J_PASSWORD=$(echo "$NEO4J_AUTH" | cut -d'/' -f2)
# Export these for application use
export NEO4J_USERNAME
export NEO4J_PASSWORD
echo "Neo4j authentication configured with username: $NEO4J_USERNAME"
else
echo "NEO4J_AUTH not set, using default authentication"
fi
# Check if Neo4j is already initialized and set password if necessary
if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then
echo "Setting Neo4j initial password..."
# Ensure Neo4j is not running before setting the initial password
neo4j stop || true
# Set the initial password using the correct command format for Neo4j 5.x
NEO4J_ADMIN_CMD=$(which neo4j-admin)
NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1)
echo "Detected Neo4j version: $NEO4J_VERSION"
# Use version-specific command format
MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1)
if [ "$MAJOR_VERSION" -ge "5" ]; then
# For Neo4j 5.x and higher
echo "Using Neo4j 5.x+ password command format"
$NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set"
}
else
# For Neo4j 4.x and lower
echo "Using Neo4j 4.x password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set"
}
fi
fi
# Update Neo4j configuration
echo "Configuring Neo4j..."
cat > /etc/neo4j/neo4j.conf << EOF
# Neo4j configuration
dbms.security.auth_enabled=true
server.bolt.enabled=true
server.bolt.listen_address=0.0.0.0:7687
server.directories.data=/var/lib/neo4j/data
server.directories.logs=/var/log/neo4j
initial.dbms.default_database=neo4j
server.directories.plugins=/var/lib/neo4j/plugins
server.directories.import=/var/lib/neo4j/import
# APOC Settings
dbms.security.procedures.unrestricted=apoc.*
dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*
EOF
if [ -w "$NEO4J_LOG_DIR" ]; then
chmod -R 770 "$NEO4J_LOG_DIR" || echo "Warning: Could not set log directory permissions" >/dev/null 2>&1
fi
# Start Neo4j
echo "Starting Neo4j service..."
neo4j console >/dev/null 2>&1 &
# Add a wait for Neo4j to be ready with more robust checking
echo "Waiting for Neo4j to be ready..."
NEO4J_READY=false
for i in {1..60}; do
# First try standard status check
if neo4j status >/dev/null 2>&1; then
echo "Neo4j is ready (via status check)"
NEO4J_READY=true
break
fi
# Also try connecting to the bolt port as a fallback
if command -v nc >/dev/null 2>&1; then
if nc -z localhost 7687 >/dev/null 2>&1; then
echo "Neo4j is ready (port 7687 is open)"
NEO4J_READY=true
break
fi
fi
echo "Waiting for Neo4j to start... ($i/60)"
sleep 2
done
if [ "$NEO4J_READY" = false ]; then
echo "WARNING: Neo4j may not be fully started yet, but continuing..."
fi
# Check WORKLOW_WORKER and skip SETUP_CMD if true
if [ "${WORKFLOW_WORKER}" == "true" ]; then
echo "WORKFLOW_WORKER is set to true. Running worker process."

View file

@ -1,4 +1,4 @@
FROM node:18.18.2-buster AS builder
FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
@ -50,10 +50,11 @@ ENV TOOLJET_EDITION=ee
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM node:18.18.2-bullseye
FROM node:22.15.1-bullseye
RUN apt-get update -yq \
&& apt-get install curl gnupg zip -yq \

View file

@ -1,11 +1,11 @@
FROM node:18.18.2-buster AS builder
FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm i -g npm@9.8.1
RUN npm i -g npm@10.9.2
RUN mkdir -p /app
# RUN npm cache clean --force
RUN npm cache clean --force
WORKDIR /app
@ -54,13 +54,14 @@ ENV TOOLJET_EDITION=ee
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM debian:11
FROM debian:12
RUN apt-get update -yq \
&& apt-get install curl gnupg zip -yq \
&& apt-get install curl wget gnupg zip -yq \
&& apt-get install -yq build-essential \
&& apt -y install redis \
&& apt-get clean -y
@ -80,13 +81,29 @@ RUN echo "[supervisord]\n" \
"nodaemon=true\n" \
"\n" \
"[program:postgrest]\n" \
"command=/bin/postgrest \n" \
"command=/bin/postgrest\n" \
"autostart=true\n" \
"autorestart=true\n" \
"stdout_logfile=/dev/stdout\n" \
"stderr_logfile=/dev/stderr\n" \
"stdout_logfile_maxbytes=0\n" \
"stderr_logfile_maxbytes=0\n" \
"\n" \
"[program:neo4j]\n" \
"command=neo4j console\n" \
"autostart=true\n" \
"autorestart=unexpected\n" \
"startsecs=30\n" \
"startretries=999\n" \
"priority=90\n" \
"exitcodes=0,1,2\n" \
"stopsignal=SIGTERM\n" \
"stopasgroup=true\n" \
"killasgroup=true\n" \
"redirect_stderr=true\n" \
"stdout_logfile=/var/log/neo4j/neo4j.log\n" \
"stdout_logfile_backups=10\n" \
"stderr_capture_maxbytes=20MB\n" \
"\n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# Create a wrapper for PostgREST to prefix its logs
@ -97,12 +114,12 @@ exec /bin/postgrest-original "$@" 2>&1 | sed "s/^/[PostgREST] /"\n\
chmod +x /bin/postgrest
RUN curl -O https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz \
&& tar -xf node-v18.18.2-linux-x64.tar.xz \
&& mv node-v18.18.2-linux-x64 /usr/local/lib/nodejs \
RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \
&& tar -xf node-v22.15.1-linux-x64.tar.xz \
&& mv node-v22.15.1-linux-x64 /usr/local/lib/nodejs \
&& echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \
&& /bin/bash -c "source /etc/profile.d/nodejs.sh" \
&& rm node-v18.18.2-linux-x64.tar.xz
&& rm node-v22.15.1-linux-x64.tar.xz
ENV PATH=/usr/local/lib/nodejs/bin:$PATH
ENV NODE_ENV=production
@ -114,6 +131,48 @@ RUN apt-get update && \
apt-get -y autoremove && \
apt-get -y autoclean
# Install Neo4j
RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \
echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \
apt-get update && \
apt-get install -y neo4j=1:5.26.6 && \
apt-mark hold neo4j && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Set the necessary Neo4j environment variables
ENV NEO4J_HOME=/opt/neo4j
ENV NEO4J_CONF=/etc/neo4j
ENV NEO4J_DATA=/var/lib/neo4j/data
ENV NEO4J_LOG=/var/log/neo4j
ENV NEO4J_PLUGIN=/var/lib/neo4j/plugins
ENV NEO4J_IMPORT=/var/lib/neo4j/import
# Create the necessary directories for Neo4j
RUN mkdir -p /data/db /data/logs /data/plugins
RUN mkdir -p /opt/neo4j/plugins
# Configure APOC plugin for Neo4j
ENV NEO4J_dbms_active_plugins=apoc
# Download and install APOC plugin for Neo4j 5.x (BEFORE creating user)
RUN mkdir -p /var/lib/neo4j/plugins && \
wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \
# Try to download extended version
(wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-extended.jar || \
wget -P /var/lib/neo4j/plugins https://neo4j-contrib.github.io/neo4j-apoc-procedures/5.26.6/apoc-5.26.6-extended.jar || \
echo "Extended JAR not available, continuing with core only")
# Configure Neo4j with APOC
RUN echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf
# Configure Neo4j to use authentication
RUN if [ -f "/etc/neo4j/neo4j.conf" ]; then \
sed -i '/dbms.security.auth_enabled/d' /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf; \
fi
# Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle
@ -149,6 +208,7 @@ COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY --from=builder /app/server/src/assets ./app/server/src/assets
COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh
@ -161,14 +221,21 @@ RUN useradd --create-home --home-dir /home/appuser appuser \
&& chmod -R g=u /app \
&& chmod -R g=u /home
# Create directory /home/appuser and set ownership to appuser (Refer doc for understanding the changes https://app.clickup.com/37484951/v/dc/13qycq-4081)
RUN mkdir -p /var/lib/neo4j/data/databases /var/lib/neo4j/data/transactions /var/log/neo4j /opt/neo4j/run && \
chown -R appuser:0 /var/lib/neo4j /var/log/neo4j /etc/neo4j /opt/neo4j/run && \
chmod -R 770 /var/lib/neo4j /var/log/neo4j /etc/neo4j /opt/neo4j/run && \
chmod -R 644 /var/lib/neo4j/plugins/*.jar && \
chown -R appuser:0 /var/lib/neo4j/plugins && \
chmod 755 /var/lib/neo4j/plugins
# Create directory /home/appuser and set ownership to appuser
RUN mkdir -p /home/appuser \
&& chown -R appuser:0 /home/appuser \
&& chmod g+s /home/appuser \
&& chmod -R g=u /home/appuser \
&& npm cache clean --force
# Create directory /tmp/.npm/npm-cache/ and set ownership to appuser (Refer doc for understanding the changes https://app.clickup.com/37484951/v/dc/13qycq-4081)
# Create directory /tmp/.npm/npm-cache/ and set ownership to appuser
RUN mkdir -p /tmp/.npm/npm-cache/ \
&& chown -R appuser:0 /tmp/.npm/npm-cache/ \
&& chmod g+s /tmp/.npm/npm-cache/ \
@ -206,6 +273,9 @@ RUN mkdir -p /var/lib/postgrest /var/log/postgrest /etc/postgrest \
ENV HOME=/home/appuser
# Installing git for simple git commands
RUN apt-get update && apt-get install -y git && apt-get clean
# Switch back to appuser
USER appuser

View file

@ -1 +1 @@
3.13.0
3.15.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

View file

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.02811 1.31381C2.22904 1.11288 2.50157 1 2.78572 1H7.07144C7.16616 1 7.257 1.03763 7.32397 1.10461L10.1812 3.96175C10.2481 4.02872 10.2857 4.11956 10.2857 4.21429V9.92857C10.2857 10.2127 10.1729 10.4853 9.97194 10.6862C9.77101 10.8871 9.49844 11 9.21429 11H2.78572C2.50156 11 2.22904 10.8871 2.02811 10.6862C1.82718 10.4853 1.71429 10.2127 1.71429 9.92857V2.07143C1.71429 1.78727 1.82718 1.51475 2.02811 1.31381ZM5.30739 5.26405C5.51659 5.47326 5.51659 5.81246 5.30739 6.02166L4.25762 7.07143L5.30739 8.12119C5.51659 8.33043 5.51659 8.66957 5.30739 8.87879C5.09818 9.088 4.75898 9.088 4.54977 8.87879L3.1212 7.45024C2.91199 7.24103 2.91199 6.90183 3.1212 6.69262L4.54977 5.26405C4.75898 5.05484 5.09818 5.05484 5.30739 5.26405ZM6.69263 6.02166C6.48342 5.81246 6.48342 5.47326 6.69263 5.26405C6.90184 5.05484 7.24104 5.05484 7.45024 5.26405L8.87879 6.69262C9.08801 6.90183 9.08801 7.24103 8.87879 7.45024L7.45024 8.87879C7.24104 9.088 6.90184 9.088 6.69263 8.87879C6.48342 8.66957 6.48342 8.33043 6.69263 8.12119L7.74239 7.07143L6.69263 6.02166Z" fill="#6A727C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75815 0.483576C6.95842 0.283307 7.28312 0.283307 7.48338 0.483576L9.79108 2.79127C9.99135 2.99154 9.99135 3.31624 9.79108 3.5165L7.48338 5.8242C7.28312 6.02447 6.95842 6.02447 6.75815 5.8242L4.45046 3.5165C4.25018 3.31624 4.25018 2.99154 4.45046 2.79127L6.75815 0.483576ZM2.912 4.32973C3.11227 4.12946 3.43696 4.12946 3.63723 4.32973L5.94492 6.63743C6.1452 6.83769 6.1452 7.16239 5.94492 7.36266L3.63723 9.67035C3.43696 9.87063 3.11227 9.87063 2.912 9.67035L0.604304 7.36266C0.404034 7.16239 0.404034 6.83769 0.604304 6.63743L2.912 4.32973ZM11.3296 4.32973C11.1293 4.12946 10.8046 4.12946 10.6043 4.32973L8.29662 6.63743C8.09634 6.83769 8.09634 7.16239 8.29662 7.36266L10.6043 9.67035C10.8046 9.87063 11.1293 9.87063 11.3296 9.67035L13.6373 7.36266C13.8375 7.16239 13.8375 6.83769 13.6373 6.63743L11.3296 4.32973ZM7.48338 8.17589C7.28312 7.97561 6.95842 7.97561 6.75815 8.17589L4.45046 10.4835C4.25018 10.6838 4.25018 11.0086 4.45046 11.2089L6.75815 13.5166C6.95842 13.7168 7.28312 13.7168 7.48338 13.5166L9.79108 11.2089C9.99135 11.0086 9.99135 10.6838 9.79108 10.4835L7.48338 8.17589Z" fill="#1E823B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="25" viewBox="0 0 20 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.60152 6.16675C2.53311 6.16675 1.66699 7.03286 1.66699 8.10128C1.66699 8.5122 2.00011 8.84532 2.41104 8.84532C2.82197 8.84532 3.15509 8.5122 3.15509 8.10128C3.15509 7.85472 3.35496 7.65485 3.60152 7.65485C4.01245 7.65485 4.34557 7.32172 4.34557 6.9108C4.34557 6.49987 4.01245 6.16675 3.60152 6.16675ZM5.98248 6.16675C5.57155 6.16675 5.23843 6.49987 5.23843 6.9108C5.23843 7.32172 5.57155 7.65485 5.98248 7.65485H7.7682C8.17912 7.65485 8.51224 7.32172 8.51224 6.9108C8.51224 6.49987 8.17912 6.16675 7.7682 6.16675H5.98248ZM9.4051 6.9108C9.4051 6.49987 9.73822 6.16675 10.1492 6.16675C11.2176 6.16675 12.0837 7.03286 12.0837 8.10128C12.0837 8.5122 11.7506 8.84532 11.3396 8.84532C10.9287 8.84532 10.5956 8.5122 10.5956 8.10128C10.5956 7.85472 10.3957 7.65485 10.1492 7.65485C9.73822 7.65485 9.4051 7.32172 9.4051 6.9108ZM3.15509 10.4822C3.15509 10.0713 2.82197 9.73818 2.41104 9.73818C2.00011 9.73818 1.66699 10.0713 1.66699 10.4822V12.2679C1.66699 12.6789 2.00011 13.012 2.41104 13.012C2.82197 13.012 3.15509 12.6789 3.15509 12.2679V10.4822ZM2.41104 13.9049C2.82197 13.9049 3.15509 14.238 3.15509 14.6489C3.15509 14.8955 3.35496 15.0953 3.60152 15.0953C4.01245 15.0953 4.34557 15.4285 4.34557 15.8394C4.34557 16.2503 4.01245 16.5834 3.60152 16.5834C2.53311 16.5834 1.66699 15.7173 1.66699 14.6489C1.66699 14.238 2.00011 13.9049 2.41104 13.9049ZM5.23843 11.5239C5.23843 10.5377 6.03792 9.73818 7.02415 9.73818H13.5718C14.558 9.73818 15.3575 10.5377 15.3575 11.5239V14.9372L12.1665 14.1394C10.6407 13.758 9.25865 15.14 9.6401 16.6658L10.438 19.8572H7.02415C6.03792 19.8572 5.23843 19.0577 5.23843 18.0715V11.5239ZM18.1596 21.7151L17.2157 22.659C16.9832 22.8915 16.6063 22.8915 16.3739 22.659L14.4138 20.699L13.3715 21.7413C13.0443 22.0684 12.4853 21.9137 12.3731 21.4648L11.083 16.3043C10.974 15.8683 11.3689 15.4735 11.8048 15.5825L16.9654 16.8726C17.4142 16.9848 17.569 17.5438 17.2419 17.8709L16.1995 18.9133L18.1596 20.8733C18.392 21.1058 18.392 21.4827 18.1596 21.7151Z" fill="#CCD1D5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="25" viewBox="0 0 20 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.91058 8.08983C3.73357 8.08983 3.59007 8.23333 3.59007 8.41034V9.05136C3.59007 9.5824 3.15957 10.0129 2.62853 10.0129C2.09749 10.0129 1.66699 9.5824 1.66699 9.05136V8.41034C1.66699 7.17124 2.67148 6.16675 3.91058 6.16675H4.55161C5.08265 6.16675 5.51315 6.59724 5.51315 7.12829C5.51315 7.65933 5.08265 8.08983 4.55161 8.08983H3.91058ZM14.4875 7.12829C14.4875 6.59724 14.918 6.16675 15.449 6.16675H16.0901C17.3292 6.16675 18.3337 7.17124 18.3337 8.41034V9.05136C18.3337 9.5824 17.9031 10.0129 17.3721 10.0129C16.8411 10.0129 16.4106 9.5824 16.4106 9.05136V8.41034C16.4106 8.23333 16.2671 8.08983 16.0901 8.08983H15.449C14.918 8.08983 14.4875 7.65933 14.4875 7.12829ZM18.3337 19.9488C18.3337 19.4178 17.9031 18.9873 17.3721 18.9873C16.8411 18.9873 16.4106 19.4178 16.4106 19.9488V20.5898C16.4106 20.7669 16.2671 20.9103 16.0901 20.9103H15.449C14.918 20.9103 14.4875 21.3409 14.4875 21.8719C14.4875 22.4029 14.918 22.8334 15.449 22.8334H16.0901C17.3292 22.8334 18.3337 21.8289 18.3337 20.5898V19.9488ZM2.62853 18.9873C3.15957 18.9873 3.59007 19.4178 3.59007 19.9488V20.5898C3.59007 20.7669 3.73357 20.9103 3.91058 20.9103H4.55161C5.08265 20.9103 5.51315 21.3409 5.51315 21.8719C5.51315 22.4029 5.08265 22.8334 4.55161 22.8334H3.91058C2.67148 22.8334 1.66699 21.8289 1.66699 20.5898V19.9488C1.66699 19.4178 2.09749 18.9873 2.62853 18.9873ZM8.39776 20.9103C7.86672 20.9103 7.43622 21.3409 7.43622 21.8719C7.43622 22.4029 7.86672 22.8334 8.39776 22.8334H11.6029C12.1339 22.8334 12.5644 22.4029 12.5644 21.8719C12.5644 21.3409 12.1339 20.9103 11.6029 20.9103H8.39776ZM7.43622 7.12829C7.43622 6.59724 7.86672 6.16675 8.39776 6.16675H11.6029C12.1339 6.16675 12.5644 6.59724 12.5644 7.12829C12.5644 7.65933 12.1339 8.08983 11.6029 8.08983H8.39776C7.86672 8.08983 7.43622 7.65933 7.43622 7.12829ZM3.59007 12.8975C3.59007 12.3665 3.15957 11.936 2.62853 11.936C2.09749 11.936 1.66699 12.3665 1.66699 12.8975V16.1026C1.66699 16.6337 2.09749 17.0642 2.62853 17.0642C3.15957 17.0642 3.59007 16.6337 3.59007 16.1026V12.8975ZM17.3721 11.936C17.9031 11.936 18.3337 12.3665 18.3337 12.8975V16.1026C18.3337 16.6337 17.9031 17.0642 17.3721 17.0642C16.8411 17.0642 16.4106 16.6337 16.4106 16.1026V12.8975C16.4106 12.3665 16.8411 11.936 17.3721 11.936ZM7.43622 10.0123C6.37413 10.0123 5.51315 10.8733 5.51315 11.9354V17.0636C5.51315 18.1256 6.37413 18.9866 7.43622 18.9866H12.5644C13.6265 18.9866 14.4875 18.1256 14.4875 17.0636V11.9354C14.4875 10.8733 13.6265 10.0123 12.5644 10.0123H7.43622Z" fill="#CCD1D5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,3 @@
<svg width="103" height="42" viewBox="0 0 103 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.8298 0.809768C39.1863 -0.0104135 41.9933 -0.35598 44.1701 0.486052C45.5729 1.02867 46.4514 2.04627 46.896 3.25806C47.3183 4.40936 47.3337 5.68124 47.1897 6.83915C46.9018 9.15345 45.9098 11.5232 45.1884 12.8463C42.2889 18.1639 38.4247 22.5794 33.9787 26.2876C34.5399 26.5461 35.1642 26.7825 35.8578 26.9918C41.9402 28.8269 49.132 29.0238 56.4351 28.3262C63.7231 27.6301 71.0241 26.0534 77.2795 24.4159C82.4789 23.0549 88.4083 21.1217 93.2877 18.5872H89.6629V15.5872C92.9426 15.5872 96.5411 15.0656 99.4739 14.2229C99.907 14.0984 100.423 13.9818 100.902 14.0288C101.15 14.0531 101.586 14.1389 101.965 14.489C102.401 14.8919 102.519 15.4129 102.498 15.8141C102.48 16.1636 102.362 16.4455 102.281 16.6106C102.192 16.7928 102.085 16.9574 101.986 17.0947C101.788 17.3689 101.541 17.6478 101.3 17.9028C100.838 18.3892 100.271 18.9171 99.8104 19.3451L99.7374 19.4131C99.4924 19.6411 99.2855 19.8344 99.1255 19.9913C98.9808 20.1332 98.9314 20.1911 98.9302 20.1901C98.9301 20.19 98.9304 20.1894 98.931 20.1883C98.2154 21.2383 97.4441 22.6219 96.8553 24.0608C96.2546 25.5287 95.8916 26.9391 95.8916 28.062C95.8916 28.8904 95.22 29.562 94.3916 29.562C93.5632 29.562 92.8916 28.8904 92.8916 28.062C92.8916 26.3835 93.4101 24.5588 94.0788 22.9247C94.3303 22.3099 94.6099 21.7058 94.9046 21.1268C89.6711 23.8822 83.3845 25.9189 78.0392 27.3182C71.7006 28.9774 64.229 30.5954 56.7204 31.3126C49.227 32.0284 41.5983 31.8574 34.9912 29.8639C33.6317 29.4537 32.4359 28.9358 31.3973 28.3162C22.759 34.7179 12.321 38.8406 2.44859 41.9316C1.65801 42.1791 0.816456 41.7389 0.568925 40.9483C0.321395 40.1577 0.761626 39.3162 1.55221 39.0687C11.0603 36.0917 20.8465 32.2185 28.9544 26.3854C26.3865 23.6838 25.8234 20.144 26.3038 16.6476C26.8596 12.6029 28.8172 8.40718 30.9138 5.00092C32.0894 3.09103 34.4739 1.6298 36.8298 0.809768ZM31.3635 24.5523C35.8622 20.9286 39.7152 16.6174 42.5545 11.4101C43.1627 10.2948 43.9859 8.29206 44.2126 6.46889C44.3259 5.55807 44.2734 4.81998 44.0795 4.29123C43.9077 3.82297 43.6215 3.49045 43.0878 3.28402C41.8659 2.81135 39.8705 2.92794 37.816 3.64305C35.761 4.35831 34.126 5.50551 33.4687 6.57347C31.4736 9.81466 29.7509 13.5989 29.2759 17.056C28.8625 20.0646 29.3991 22.6539 31.3635 24.5523Z" fill="#ACB2B9"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -4,6 +4,8 @@
"cancel": "Cancel",
"save": "Save",
"savechanges": "Save changes",
"execute": "Execute",
"Build": "Build",
"back": "Back",
"edit": "Edit",
"search": "Search",

33256
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -85,6 +85,7 @@
"query-string": "^8.1.0",
"rc-slider": "^10.1.1",
"react": "^18.2.0",
"react-accessible-treeview": "^2.11.1",
"react-beautiful-dnd": "^13.1.1",
"react-big-calendar": "^1.6.5",
"react-bootstrap": "^2.7.2",
@ -101,6 +102,7 @@
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-highlight-words": "^0.21.0",
"react-hot-toast": "^2.4.0",
"react-hotkeys-hook": "^4.3.5",
"react-i18next": "^12.1.5",
@ -185,11 +187,11 @@
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"jest": "^29.4.2",
"node-sass": "^8.0.0",
"path": "^0.12.7",
"postcss": "^8.4.35",
"postcss-loader": "^8.1.0",
"prettier": "^2.8.4",
"sass": "^1.78.0",
"sass-loader": "^13.2.0",
"storybook": "^7.2.1",
"style-loader": "^3.3.1",
@ -264,4 +266,4 @@
"jsx"
]
}
}
}

View file

@ -2,7 +2,7 @@ import React, { Suspense } from 'react';
// eslint-disable-next-line no-unused-vars
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { authorizeWorkspace, updateCurrentSession } from '@/_helpers/authorizeWorkspace';
import { authenticationService, tooljetService } from '@/_services';
import { authenticationService, tooljetService, licenseService } from '@/_services';
import { withRouter } from '@/_hoc/withRouter';
import { PrivateRoute, AdminRoute, AppsRoute, SwitchWorkspaceRoute } from '@/Routes';
import { HomePage } from '@/HomePage';
@ -42,7 +42,6 @@ import { shallow } from 'zustand/shallow';
import useStore from '@/AppBuilder/_stores/store';
import { checkIfToolJetCloud } from '@/_helpers/utils';
import { BasicPlanMigrationBanner } from '@/HomePage/BasicPlanMigrationBanner/BasicPlanMigrationBanner';
import { licenseService } from '@/_services';
const AppWrapper = (props) => {
const { isAppDarkMode } = useAppDarkMode();
@ -283,9 +282,9 @@ class AppComponent extends React.Component {
exact
path="/:workspaceId/workflows/*"
element={
<AdminRoute {...this.props}>
<PrivateRoute>
<Workflows switchDarkMode={this.switchDarkMode} darkMode={this.darkMode} />
</AdminRoute>
</PrivateRoute>
}
/>
)}
@ -295,6 +294,15 @@ class AppComponent extends React.Component {
></Route>
<Route path="settings/*" element={<InstanceSettings {...this.props} />}></Route>
<Route path="/:workspaceId/settings/*" element={<Settings {...this.props} />}></Route>
<Route
exact
path="/:workspaceId/modules"
element={
<PrivateRoute>
<HomePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} appType={'module'} />
</PrivateRoute>
}
/>
{getAuditLogsRoutes(this.props)}
<Route

View file

@ -14,6 +14,7 @@ import EditorHeader from '@/AppBuilder/Header';
import LeftSidebar from '@/AppBuilder/LeftSidebar';
import Popups from './Popups';
import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
import { shallow } from 'zustand/shallow';
// const EditorHeader = lazy(() => import('@/AppBuilder/Header'));
// const LeftSidebar = lazy(() => import('@/AppBuilder/LeftSidebar'));
@ -22,12 +23,13 @@ import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
// const QueryPanel = lazy(() => import('@/AppBuilder/QueryPanel'));
// TODO: split Loader into separate component and remove editor loading state from Editor
export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMode }) => {
export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMode, appType = 'front-end' }) => {
useAppData(appId, moduleId, darkMode);
const isEditorLoading = useStore((state) => state.isEditorLoading);
const currentMode = useStore((state) => state.currentMode);
const isEditorLoading = useStore((state) => state.loaderStore.modules[moduleId].isEditorLoading, shallow);
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
const isModuleEditor = appType === 'module';
const updateIsTJDarkMode = useStore((state) => state.updateIsTJDarkMode);
const updateIsTJDarkMode = useStore((state) => state.updateIsTJDarkMode, shallow);
const changeToDarkMode = (newMode) => {
updateIsTJDarkMode(newMode);
@ -45,19 +47,19 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
return (
<div className={cx('wrapper', { editor: currentMode === 'edit' })}>
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<EditorHeader darkMode={darkMode} />
<LeftSidebar switchDarkMode={changeToDarkMode} darkMode={darkMode} />
</Suspense>
{window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && <RealtimeCursors />}
<DndProvider backend={HTML5Backend}>
<ModuleProvider moduleId={moduleId}>
<AppCanvas moduleId={moduleId} appId={appId} />
<ModuleProvider moduleId={moduleId} appType={appType} isModuleMode={false} isModuleEditor={isModuleEditor}>
<Suspense fallback={<div>Loading...</div>}>
<EditorHeader darkMode={darkMode} />
<LeftSidebar switchDarkMode={changeToDarkMode} darkMode={darkMode} />
</Suspense>
{window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && <RealtimeCursors />}
<DndProvider backend={HTML5Backend}>
<AppCanvas appId={appId} />
<QueryPanel darkMode={darkMode} />
<RightSideBar darkMode={darkMode} />
</ModuleProvider>
</DndProvider>
<Popups darkMode={darkMode} />
</DndProvider>
<Popups darkMode={darkMode} />
</ModuleProvider>
</ErrorBoundary>
</div>
);

View file

@ -1,8 +1,8 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Container } from './Container';
import Grid from './Grid';
import { EditorSelecto } from './Selecto';
import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { HotkeyProvider } from './HotkeyProvider';
import './appCanvas.scss';
import useStore from '@/AppBuilder/_stores/store';
@ -18,15 +18,18 @@ import useAppCanvasMaxWidth from './useAppCanvasMaxWidth';
import { DeleteWidgetConfirmation } from './DeleteWidgetConfirmation';
import useSidebarMargin from './useSidebarMargin';
export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => {
export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) => {
const { moduleId, isModuleMode, appType } = useModuleContext();
const canvasContainerRef = useRef();
const handleCanvasContainerMouseUp = useStore((state) => state.handleCanvasContainerMouseUp, shallow);
const canvasHeight = useStore((state) => state.canvasHeight);
const creationMode = useStore((state) => state.app.creationMode);
const environmentLoadingState = useStore((state) => state.environmentLoadingState || state.isEditorLoading);
const [canvasWidth, setCanvasWidth] = useState(getCanvasWidth());
const canvasHeight = useStore((state) => state.appStore.modules[moduleId].canvasHeight);
const creationMode = useStore((state) => state.appStore.modules[moduleId].app.creationMode);
const environmentLoadingState = useStore(
(state) => state.environmentLoadingState || state.loaderStore.modules[moduleId].isEditorLoading
);
const [canvasWidth, setCanvasWidth] = useState(getCanvasWidth(moduleId));
const gridWidth = canvasWidth / NO_OF_GRIDS;
const currentMode = useStore((state) => state.currentMode, shallow);
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
const pageSidebarStyle = useStore((state) => state?.pageSettings?.definition?.properties?.style, shallow);
const currentLayout = useStore((state) => state.currentLayout, shallow);
const queryPanelHeight = useStore((state) => state?.queryPanel?.queryPanelHeight || 0);
@ -42,23 +45,79 @@ export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => {
const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
const getPageId = useStore((state) => state.getCurrentPageId, shallow);
const hideSidebar = isModuleMode || isPagesSidebarHidden || appType === 'module';
useEffect(() => {
// Need to remove this if we shift setExposedVariable Logic outside of components
// Currently present to run onLoadQueries after the component is mounted
setIsComponentLayoutReady(true);
return () => setIsComponentLayoutReady(false);
setIsComponentLayoutReady(true, moduleId);
return () => setIsComponentLayoutReady(false, moduleId);
}, []);
useEffect(() => {
function handleResize() {
const _canvasWidth = document.getElementById('real-canvas')?.getBoundingClientRect()?.width;
const _canvasWidth =
moduleId === 'canvas'
? document.getElementById('real-canvas')?.getBoundingClientRect()?.width
: document.getElementById(moduleId)?.getBoundingClientRect()?.width;
if (_canvasWidth !== 0) setCanvasWidth(_canvasWidth);
}
window.addEventListener('resize', handleResize);
if (moduleId === 'canvas') {
window.addEventListener('resize', handleResize);
} else {
const elem = document.getElementById(moduleId);
const resizeObserver = new ResizeObserver(handleResize);
if (elem) resizeObserver.observe(elem);
return () => {
if (elem) resizeObserver.unobserve(elem);
resizeObserver.disconnect();
};
}
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, [currentLayout, canvasMaxWidth, isViewerSidebarPinned]);
}, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId]);
const styles = useMemo(() => {
const canvasBgColor =
currentMode === 'view'
? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor)
: !isAppDarkMode
? '#EBEBEF'
: '#2F3C4C';
if (isModuleMode) {
return {
borderLeft: 'none',
height: '100%',
background: canvasBgColor,
};
}
return {
borderLeft: currentMode === 'edit' && editorMarginLeft + 'px solid',
height: currentMode === 'edit' ? canvasContainerHeight : '100%',
background: canvasBgColor,
marginLeft:
isViewerSidebarPinned && !hideSidebar && currentLayout !== 'mobile' && currentMode !== 'edit'
? pageSidebarStyle === 'icon'
? '65px'
: '210px'
: 'auto',
};
}, [
currentMode,
isAppDarkMode,
isModuleMode,
editorMarginLeft,
canvasContainerHeight,
isViewerSidebarPinned,
hideSidebar,
currentLayout,
pageSidebarStyle,
]);
return (
<div
@ -73,29 +132,14 @@ export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => {
className={cx(
'canvas-container align-items-center page-container',
{ 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned },
{ 'overflow-x-auto': (currentMode === 'edit' && isSidebarOpen) || currentMode === 'view' }
{ 'overflow-x-auto': (currentMode === 'edit' && isSidebarOpen) || currentMode === 'view' },
{ 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode
)}
style={{
// transform: `scale(1)`,
borderLeft: currentMode === 'edit' && editorMarginLeft + 'px solid',
height: currentMode === 'edit' ? canvasContainerHeight : '100%',
background:
currentMode === 'view'
? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor)
: !isAppDarkMode
? '#EBEBEF'
: '#2F3C4C',
marginLeft:
isViewerSidebarPinned && !isPagesSidebarHidden && currentLayout !== 'mobile' && currentMode !== 'edit'
? pageSidebarStyle === 'icon'
? '65px'
: '210px'
: 'auto',
}}
style={styles}
>
<div
style={{
minWidth: `calc((100vw - 300px) - 48px)`,
minWidth: isModuleMode ? '100%' : `calc((100vw - 300px) - 48px)`,
}}
className={`app-${appId} _tooljet-page-${getPageId()}`}
>
@ -107,7 +151,7 @@ export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => {
{environmentLoadingState !== 'loading' && (
<div>
<Container
id="canvas"
id={moduleId}
gridWidth={gridWidth}
canvasWidth={canvasWidth}
canvasHeight={canvasHeight}
@ -115,8 +159,9 @@ export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => {
canvasMaxWidth={canvasMaxWidth}
isViewerSidebarPinned={isViewerSidebarPinned}
pageSidebarStyle={pageSidebarStyle}
appType={appType}
/>
<div id="component-portal" />
{appType !== 'module' && <div id="component-portal" />}
</div>
)}

View file

@ -4,6 +4,8 @@ import './configHandle.scss';
import useStore from '@/AppBuilder/_stores/store';
import { findHighestLevelofSelection } from '../Grid/gridUtils';
import SolidIcon from '@/_ui/Icon/solidIcons/index';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { DROPPABLE_PARENTS } from '../appCanvasConstants';
const CONFIG_HANDLE_HEIGHT = 20;
const BUFFER_HEIGHT = 1;
@ -18,10 +20,12 @@ export const ConfigHandle = ({
showHandle,
componentType,
visibility,
isModuleContainer,
subContainerIndex,
}) => {
const { moduleId } = useModuleContext();
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow);
const componentName = useStore((state) => state.getComponentDefinition(id, moduleId)?.component?.name || '', shallow);
const isMultipleComponentsSelected = useStore(
(state) => (findHighestLevelofSelection(state?.selectedComponents)?.length > 1 ? true : false),
shallow
@ -43,6 +47,7 @@ export const ConfigHandle = ({
return (
(subContainerIndex === 0 || subContainerIndex === null) &&
(isWidgetHovered ||
isModuleContainer ||
(showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered))
);
}, shallow);
@ -67,7 +72,9 @@ export const ConfigHandle = ({
if (componentType === 'Tabs') {
setFocusedParentId(`${id}-${currentTab}`);
} else {
setFocusedParentId(id);
if (DROPPABLE_PARENTS.has(componentType)) {
setFocusedParentId(id);
}
}
}}
>
@ -125,20 +132,22 @@ export const ConfigHandle = ({
data-cy={`${componentName.toLowerCase()}-inspect-button`}
className="config-handle-inspect"
/>
<span
style={{ cursor: 'pointer', marginLeft: '5px' }}
onClick={() => {
deleteComponents([id]);
}}
data-cy={`${componentName.toLowerCase()}-delete-button`}
>
<SolidIcon
name="trash"
width="12"
height="12"
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
/>
</span>
{!isModuleContainer && (
<span
style={{ cursor: 'pointer', marginLeft: '5px' }}
onClick={() => {
deleteComponents([id]);
}}
data-cy={`${componentName.toLowerCase()}-delete-button`}
>
<SolidIcon
name="trash"
width="12"
height="12"
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
/>
</span>
)}
</div>
)}
</span>

View file

@ -25,7 +25,10 @@ import NoComponentCanvasContainer from './NoComponentCanvasContainer';
import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants';
import { isPDFSupported } from '@/_helpers/appUtils';
import toast from 'react-hot-toast';
import { ModuleContainerBlank } from '@/modules/Modules/components';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import useSortedComponents from '../_hooks/useSortedComponents';
import { noop } from 'lodash';
//TODO: Revisit the logic of height (dropRef)
@ -50,9 +53,12 @@ export const Container = React.memo(
isViewerSidebarPinned,
pageSidebarStyle,
componentType,
appType,
}) => {
const { moduleId } = useModuleContext();
const realCanvasRef = useRef(null);
const components = useStore((state) => state.getContainerChildrenMapping(id), shallow);
const components = useStore((state) => state.getContainerChildrenMapping(id, moduleId), shallow);
const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow);
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow);
const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow);
@ -61,12 +67,14 @@ export const Container = React.memo(
shallow
);
const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow);
const currentMode = useStore((state) => state.currentMode, shallow);
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
const currentLayout = useStore((state) => state.currentLayout, shallow);
const setFocusedParentId = useStore((state) => state.setFocusedParentId, shallow);
const setShowModuleBorder = useStore((state) => state.setShowModuleBorder, shallow) || noop;
const isContainerReadOnly = useMemo(() => {
return (index !== 0 && (componentType === 'Listview' || componentType === 'Kanban')) || currentMode === 'view';
}, [componentType, index, currentMode]);
}, [index, componentType, currentMode]);
const [{ isOverCurrent }, drop] = useDrop({
accept: 'box',
@ -75,7 +83,9 @@ export const Container = React.memo(
item.canvasId = id;
item.canvasWidth = getContainerCanvasWidth();
},
drop: async ({ componentType }, monitor) => {
drop: async ({ componentType, component }, monitor) => {
setShowModuleBorder(false); // Hide the module border when dropping
if (currentMode === 'view' || (appType === 'module' && componentType !== 'ModuleContainer')) return;
const didDrop = monitor.didDrop();
if (didDrop) return;
if (componentType === 'PDF' && !isPDFSupported()) {
@ -84,15 +94,41 @@ export const Container = React.memo(
);
return;
}
// IMPORTANT: This logic needs to be changed when we implement the module versioning
const moduleInfo = component?.moduleId
? {
moduleId: component.moduleId,
versionId: component.versionId,
environmentId: component.environmentId,
moduleName: component.displayName,
moduleContainer: component.moduleContainer,
}
: undefined;
if (WIDGETS_WITH_DEFAULT_CHILDREN.includes(componentType)) {
const parentComponent = addNewWidgetToTheEditor(componentType, monitor, currentLayout, realCanvasRef, id);
const parentComponent = addNewWidgetToTheEditor(
componentType,
monitor,
currentLayout,
realCanvasRef,
id,
moduleInfo
);
const childComponents = addChildrenWidgetsToParent(componentType, parentComponent?.id, currentLayout);
const newComponents = [parentComponent, ...childComponents];
await addComponentToCurrentPage(newComponents);
// setSelectedComponents([parentComponent?.id]);
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
} else {
const newComponent = addNewWidgetToTheEditor(componentType, monitor, currentLayout, realCanvasRef, id);
const newComponent = addNewWidgetToTheEditor(
componentType,
monitor,
currentLayout,
realCanvasRef,
id,
moduleInfo
);
await addComponentToCurrentPage([newComponent]);
// setSelectedComponents([newComponent?.id]);
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
@ -103,19 +139,24 @@ export const Container = React.memo(
}),
});
const showEmptyContainer = currentMode === 'edit' && id === 'canvas' && components.length === 0 && !isOverCurrent;
const showEmptyContainer =
currentMode === 'edit' &&
(id === 'canvas' || componentType === 'ModuleContainer') &&
components.length === 0 &&
!isOverCurrent;
function getContainerCanvasWidth() {
if (canvasWidth !== undefined) {
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
if (id === 'canvas') return canvasWidth;
return getSubContainerWidthAfterPadding(canvasWidth, componentType, id);
return getSubContainerWidthAfterPadding(canvasWidth, componentType, id, realCanvasRef);
}
return realCanvasRef?.current?.offsetWidth;
}
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
useEffect(() => {
useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS);
useGridStore.getState().actions.setSubContainerWidths(id, gridWidth);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canvasWidth, listViewMode, columns]);
@ -125,7 +166,8 @@ export const Container = React.memo(
!isPagesSidebarHidden &&
isViewerSidebarPinned &&
currentLayout !== 'mobile' &&
currentMode !== 'edit'
currentMode !== 'edit' &&
appType !== 'module'
) {
return `calc(100% - ${pageSidebarStyle === 'icon' ? '65px' : '210px'})`;
}
@ -147,6 +189,23 @@ export const Container = React.memo(
[setLastCanvasClickPosition]
);
/* Due to some reason react-dnd does not identify the dragover element if this element is dynamically removed on drag.
Hence display is set to none on dragover and removed only when the component is added */
const renderEmptyContainer = () => {
if (components && components?.length !== 0) return;
const styles = {
display: showEmptyContainer ? 'block' : 'none',
...(componentType === 'ModuleContainer' ? { height: '100%', width: '100%' } : {}),
};
return (
<div style={styles}>
{componentType === 'ModuleContainer' ? <ModuleContainerBlank /> : <NoComponentCanvasContainer />}
</div>
);
};
const sortedComponents = useSortedComponents(components, currentLayout, id);
return (
@ -182,7 +241,7 @@ export const Container = React.memo(
}}
className={cx('real-canvas', {
'sub-canvas': id !== 'canvas',
'show-grid': isOverCurrent && (index === 0 || index === null),
'show-grid': isOverCurrent && (index === 0 || index === null) && currentMode === 'edit',
})}
id={id === 'canvas' ? 'real-canvas' : `canvas-${id}`}
data-cy="real-canvas"
@ -216,14 +275,7 @@ export const Container = React.memo(
/>
))}
</div>
{/* Due to some reason react-dnd does not identify the dragover element if this element is dynamically removed on drag.
Hence display is set to none on dragover and removed only when the component is added */}
{(!components || components?.length === 0) && (
<div style={{ display: showEmptyContainer ? 'block' : 'none' }}>
<NoComponentCanvasContainer />
</div>
)}
{renderEmptyContainer()}
</div>
);
}

View file

@ -28,8 +28,8 @@ import {
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
import useStore from '@/AppBuilder/_stores/store';
import './Grid.css';
import { NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
import { DROPPABLE_PARENTS, NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, position: 'css' };
const RESIZABLE_CONFIG = {
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
@ -38,17 +38,19 @@ const RESIZABLE_CONFIG = {
export const GRID_HEIGHT = 10;
export default function Grid({ gridWidth, currentLayout }) {
const { moduleId, isModuleEditor } = useModuleContext();
const lastDraggedEventsRef = useRef(null);
const updateCanvasBottomHeight = useStore((state) => state.updateCanvasBottomHeight, shallow);
const setComponentLayout = useStore((state) => state.setComponentLayout, shallow);
const mode = useStore((state) => state.currentMode, shallow);
const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
const [boxList, setBoxList] = useState([]);
const currentPageComponents = useStore((state) => state.getCurrentPageComponents(), shallow);
const currentPageComponents = useStore((state) => state.getCurrentPageComponents(moduleId), shallow);
const selectedComponents = useStore((state) => state.selectedComponents, shallow);
const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow);
const getComponentTypeFromId = useStore((state) => state.getComponentTypeFromId, shallow);
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
const isGroupHandleHoverd = useIsGroupHandleHoverd();
const openModalWidgetId = useOpenModalWidgetId();
const moveableRef = useRef(null);
const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow);
@ -132,7 +134,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const noOfBoxs = Object.values(boxList || []).length;
useEffect(() => {
updateCanvasBottomHeight(boxList);
updateCanvasBottomHeight(boxList, moduleId);
noOfBoxs != 0;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [noOfBoxs, triggerCanvasUpdater]);
@ -333,8 +335,8 @@ export default function Grid({ gridWidth, currentLayout }) {
};
const isComponentVisible = (id) => {
const component = getResolvedComponent(id);
const componentExposedVisibility = getExposedValueOfComponent(id)?.isVisible;
const component = getResolvedComponent(id, null, moduleId);
const componentExposedVisibility = getExposedValueOfComponent(id, moduleId)?.isVisible;
if (componentExposedVisibility === false) return false;
let visibility;
if (isArray(component)) {
@ -422,6 +424,10 @@ export default function Grid({ gridWidth, currentLayout }) {
const moveableBox = document.querySelector(`.moveable-control-box`);
const showConfigHandle = (e) => {
const targetId = e.target.offsetParent.getAttribute('target-id');
const componentType = getComponentTypeFromId(targetId);
if (componentType === 'ModuleContainer') {
return;
}
useStore.getState().setHoveredComponentBoundaryId(targetId);
};
const hideConfigHandle = () => {
@ -461,9 +467,7 @@ export default function Grid({ gridWidth, currentLayout }) {
widgetId = widgetId.split('-').slice(0, -1).join('-');
widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
}
if (
!['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(widgetType)
) {
if (!DROPPABLE_PARENTS.has(widgetType)) {
isDroppable = false;
}
}
@ -477,10 +481,15 @@ export default function Grid({ gridWidth, currentLayout }) {
.map(({ component }) => component.component);
const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId;
const parentWidgetType = getComponentTypeFromId(parentId);
const restrictedWidgetsTobeDropped =
let restrictedWidgetsTobeDropped =
RESTRICTED_WIDGETS_CONFIG?.[parentWidgetType]?.filter((widgetType) =>
widgetsTypeToBeDropped.includes(widgetType)
) || [];
if (isModuleEditor && parentId === undefined) {
restrictedWidgetsTobeDropped = widgetsTypeToBeDropped;
// useGridStore.getState().actions.setIsGroupHandleHoverd(false);
}
const isParentChangeAllowed = isEmpty(restrictedWidgetsTobeDropped);
if (!isParentChangeAllowed) {
@ -505,7 +514,12 @@ export default function Grid({ gridWidth, currentLayout }) {
});
// Show error message
toast.error(`${restrictedWidgetsTobeDropped} is not compatible as a child component of ${parentWidgetType}`);
if (isModuleEditor) {
// Added this to hide configHandle when multiple components were dragged using the configHandle and placed outside the module
setSelectedComponents([]);
} else {
toast.error(`${restrictedWidgetsTobeDropped} is not compatible as a child component of ${parentWidgetType}`);
}
}
const parentElm = draggedOverElem || document.getElementById('real-canvas');
@ -588,11 +602,11 @@ export default function Grid({ gridWidth, currentLayout }) {
keepRatio={false}
individualGroupableProps={individualGroupableProps}
onResize={(e) => {
if(resizingComponentId !== e.target.id) {
if (resizingComponentId !== e.target.id) {
useGridStore.getState().actions.setResizingComponentId(e.target.id);
showGridLines();
}
const currentWidget = boxList.find(({ id }) => id === e.target.id);
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
if (currentWidget.component?.parent) {
@ -874,7 +888,7 @@ export default function Grid({ gridWidth, currentLayout }) {
if (!e.lastEvent) return;
// Build the drag context from the event
const dragContext = dragContextBuilder({ event: e, widgets: boxList });
const dragContext = dragContextBuilder({ event: e, widgets: boxList, isModuleEditor });
const { target, source, dragged } = dragContext;
const targetSlotId = target?.slotId;
@ -967,6 +981,19 @@ export default function Grid({ gridWidth, currentLayout }) {
left: modalRect.left - mainRect.left,
};
setCanvasBounds({ ...relativePosition });
} else if (isModuleEditor) {
const moduleContainer = e.target.closest('.module-container-canvas');
const mainCanvas = document.getElementById('real-canvas');
const mainRect = mainCanvas.getBoundingClientRect();
const modalRect = moduleContainer.getBoundingClientRect();
const relativePosition = {
top: modalRect.top - mainRect.top,
right: mainRect.right - modalRect.right + moduleContainer.offsetWidth,
bottom: modalRect.height + (modalRect.top - mainRect.top),
left: modalRect.left - mainRect.left,
};
setCanvasBounds({ ...relativePosition });
}
// This block is to show grid lines on the canvas when the dragged element is over a new canvas

View file

@ -54,6 +54,7 @@ import {
RESTRICTED_WIDGETS_CONFIG,
RESTRICTED_WIDGET_SLOTS_CONFIG,
} from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig';
import { DROPPABLE_PARENTS } from '../../appCanvasConstants';
const CANVAS_ID = 'canvas';
const REAL_CANVAS_ID = 'real-canvas';
@ -84,8 +85,6 @@ export class DragEntity {
* This class helps determine if a slot is valid and handles various properties like modals.
*/
export class DropAreaEntity {
static dropAreaWidgets = ['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'ModalV2', 'Listview', 'Container', 'Table'];
constructor(widget, slotId) {
this.widget = widget; // The widget that owns this slot
this.id = widget?.id || CANVAS_ID; // ID of the widget
@ -119,7 +118,7 @@ export class DropAreaEntity {
// Determines if the slot is a valid drop target
get isDroppable() {
return DropAreaEntity.dropAreaWidgets.includes(this.widgetType);
return DROPPABLE_PARENTS.has(this.widgetType);
}
// Returns the type of slot (header, footer, body, etc.)
@ -143,7 +142,7 @@ export class DropAreaEntity {
* - Any restrictions based on parent-child relationships
*/
export class DragContext {
constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets }) {
constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets, isModuleEditor = false }) {
const sourceWidgetId = sourceSlotId?.slice(0, 36);
const sourceWidget = getWidgetById(widgets, sourceWidgetId);
@ -156,6 +155,7 @@ export class DragContext {
this.target = new DropAreaEntity(targetWidget, targetSlotId);
this.dragged = new DragEntity(draggedWidget);
this.widgets = widgets;
this.isModuleEditor = isModuleEditor;
}
/**
@ -168,7 +168,13 @@ export class DragContext {
}
get isDroppable() {
const { dragged, target } = this;
const { dragged, target, isModuleEditor } = this;
// If the target is the canvas and we are in module editor,
// then we don't want to drop the widget outside the module
if (isModuleEditor && target.id === 'canvas') {
return false;
}
const restrictedWidgetsOnTarget = RESTRICTED_WIDGETS_CONFIG?.[target.widgetType] || [];
const restrictedWidgetsOnTargetSlot = RESTRICTED_WIDGET_SLOTS_CONFIG?.[target.slotType] || [];
@ -181,13 +187,19 @@ export class DragContext {
/**
* Constructs the **dragging context** by gathering all relevant details from the event.
*/
export function dragContextBuilder({ event, widgets }) {
export function dragContextBuilder({ event, widgets, isModuleEditor = false }) {
const draggedWidgetId = event.target.id;
const draggedWidget = getWidgetById(widgets, draggedWidgetId);
const sourceSlotId = draggedWidget.parent;
// Initialize drag context
const context = new DragContext({ widgets, draggedWidgetId, sourceSlotId, targetSlotId: sourceSlotId });
const context = new DragContext({
widgets,
draggedWidgetId,
sourceSlotId,
targetSlotId: sourceSlotId,
isModuleEditor,
});
// Determine the potential drop target
const targetSlotId = getDroppableSlotIdOnScreen(event, widgets);
@ -209,7 +221,7 @@ export const getDroppableSlotIdOnScreen = (event, widgets) => {
.map((ele) => extractSlotId(ele))
.filter((slotId) => {
const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID;
return DropAreaEntity.dropAreaWidgets.includes(widgetType);
return DROPPABLE_PARENTS.has(widgetType);
});
return slotId;

View file

@ -4,8 +4,10 @@ import useStore from '@/AppBuilder/_stores/store';
import { pasteComponents, copyComponents } from './appCanvasUtils';
import useKeyHooks from '@/_hooks/useKeyHooks';
import { shallow } from 'zustand/shallow';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }) => {
const { isModuleEditor } = useModuleContext();
const canvasRef = useRef(null);
const focusedParentId = useStore((state) => state.focusedParentId, shallow);
const handleUndo = useStore((state) => state.handleUndo);
@ -18,10 +20,13 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }
const getSelectedComponents = useStore((state) => state.getSelectedComponents, shallow);
const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow);
const containerChildrenMapping = useStore((state) => state.containerChildrenMapping, shallow);
const getComponentTypeFromId = useStore((state) => state.getComponentTypeFromId, shallow);
useHotkeys('meta+z, control+z', handleUndo, { enabled: mode === 'edit' });
useHotkeys('meta+shift+z, control+shift+z', handleRedo, { enabled: mode === 'edit' });
const paste = async () => {
if (isModuleEditor && !focusedParentId) return;
if (navigator.clipboard && typeof navigator.clipboard.readText === 'function') {
try {
const cliptext = await navigator.clipboard.readText();
@ -61,6 +66,24 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }
enableReleasedVersionPopupState();
return;
}
// Disable cut, copy, paste, delete shortcuts in module editor
// or when a ModuleContainer is selected
if (isModuleEditor) {
const selectedComponents = getSelectedComponents();
if (
selectedComponents.length > 0 &&
selectedComponents.some((id) => {
const componentType = getComponentTypeFromId(id, 'canvas');
return componentType === 'ModuleContainer';
})
) {
if (['KeyC', 'KeyX', 'KeyV', 'KeyD', 'Backspace'].includes(key)) {
return;
}
}
}
switch (key) {
case 'Escape':
handleEscapeKeyPress(); // clears the selected components

View file

@ -7,6 +7,7 @@ import { renderTooltip } from '@/_helpers/appUtils';
import { useTranslation } from 'react-i18next';
import ErrorBoundary from '@/_ui/ErrorBoundary';
import { BOX_PADDING } from './appCanvasConstants';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [
'Table',
@ -47,23 +48,27 @@ const RenderWidget = ({
inCanvas = false,
darkMode,
}) => {
const componentDefinition = useStore((state) => state.getComponentDefinition(id), shallow);
const { moduleId } = useModuleContext();
const componentDefinition = useStore((state) => state.getComponentDefinition(id, moduleId), shallow);
const getDefaultStyles = useStore((state) => state.debugger.getDefaultStyles, shallow);
const component = componentDefinition?.component;
const componentName = component?.name;
const [key, setKey] = useState(Math.random());
const resolvedProperties = useStore(
(state) => state.getResolvedComponent(id, subContainerIndex)?.properties,
(state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.properties,
shallow
);
const resolvedStyles = useStore(
(state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.styles,
shallow
);
const resolvedStyles = useStore((state) => state.getResolvedComponent(id, subContainerIndex)?.styles, shallow);
const fireEvent = useStore((state) => state.eventsSlice.fireEvent, shallow);
const resolvedGeneralProperties = useStore(
(state) => state.getResolvedComponent(id, subContainerIndex)?.general,
(state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.general,
shallow
);
const resolvedGeneralStyles = useStore(
(state) => state.getResolvedComponent(id, subContainerIndex)?.generalStyles,
(state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.generalStyles,
shallow
);
const unResolvedValidation = componentDefinition?.component?.definition?.validation || {};
@ -73,10 +78,13 @@ const RenderWidget = ({
const setExposedValue = useStore((state) => state.setExposedValue, shallow);
const setExposedValues = useStore((state) => state.setExposedValues, shallow);
const setDefaultExposedValues = useStore((state) => state.setDefaultExposedValues, shallow);
const resolvedValidation = useStore((state) => state.getResolvedComponent(id)?.validation, shallow);
const resolvedValidation = useStore(
(state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.validation,
shallow
);
const parentId = component?.parent;
const customResolvables = useStore(
(state) => state.resolvedStore.modules.canvas?.customResolvables?.[parentId],
(state) => state.resolvedStore.modules[moduleId]?.customResolvables?.[parentId],
shallow
);
const { t } = useTranslation();
@ -110,31 +118,31 @@ const RenderWidget = ({
(key, value) => {
// Check if the component is inside the subcontainer and it has its own onOptionChange(setExposedValue) function
if (onOptionChange === null) {
setExposedValue(id, key, value);
setExposedValue(id, key, value, moduleId);
// Trigger an update when the child components is directly linked to any component
updateDependencyValues(`components.${id}.${key}`);
updateDependencyValues(`components.${id}.${key}`, moduleId);
} else {
onOptionChange(key, value, id, subContainerIndex);
}
},
[id, setExposedValue, updateDependencyValues, subContainerIndex, onOptionChange]
[id, setExposedValue, updateDependencyValues, subContainerIndex, onOptionChange, moduleId]
);
const setExposedVariables = useCallback(
(exposedValues) => {
if (onOptionsChange === null) {
setExposedValues(id, 'components', exposedValues);
setExposedValues(id, 'components', exposedValues, moduleId);
} else {
onOptionsChange(exposedValues, id, subContainerIndex);
}
},
[id, setExposedValues, onOptionsChange]
[id, setExposedValues, onOptionsChange, moduleId]
);
const fireEventWrapper = useCallback(
(eventName, options) => {
fireEvent(eventName, id, 'canvas', customResolvables?.[subContainerIndex] ?? {}, options);
fireEvent(eventName, id, moduleId, customResolvables?.[subContainerIndex] ?? {}, options);
return Promise.resolve();
},
[fireEvent, id, customResolvables, subContainerIndex]
[fireEvent, id, customResolvables, subContainerIndex, moduleId]
);
const onComponentClick = useStore((state) => state.eventsSlice.onComponentClickEvent);
@ -155,17 +163,18 @@ const RenderWidget = ({
? null
: ['hover', 'focus']
: !resolvedGeneralProperties?.tooltip?.toString().trim()
? null
: ['hover', 'focus']
? null
: ['hover', 'focus']
}
overlay={(props) =>
renderTooltip({
props,
text: inCanvas
? `${SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY.includes(component?.component)
? resolvedProperties?.tooltip
: resolvedGeneralProperties?.tooltip
}`
? `${
SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY.includes(component?.component)
? resolvedProperties?.tooltip
: resolvedGeneralProperties?.tooltip
}`
: `${t(`widget.${component?.name}.description`, component?.description)}`,
})
}

View file

@ -5,8 +5,10 @@ import './selecto.scss';
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
import { shallow } from 'zustand/shallow';
import { findHighestLevelofSelection } from './Grid/gridUtils';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
export const EditorSelecto = () => {
const { moduleId } = useModuleContext();
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab);
const setSelectedComponents = useStore((state) => state.setSelectedComponents);
const getSelectedComponents = useStore((state) => state.getSelectedComponents, shallow);
@ -16,7 +18,7 @@ export const EditorSelecto = () => {
const filterSelectedComponentsByHighestLevel = (selectedIds) => {
const highestLevelComponents = findHighestLevelofSelection(
selectedIds.map((id) => {
const component = getComponentDefinition(id);
const component = getComponentDefinition(id, moduleId);
return {
...component,
id,

View file

@ -6,6 +6,8 @@ import { ConfigHandle } from './ConfigHandle/ConfigHandle';
import { useGridStore } from '@/_stores/gridStore';
import cx from 'classnames';
import RenderWidget from './RenderWidget';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { NO_OF_GRIDS } from './appCanvasConstants';
const WidgetWrapper = memo(
({
@ -20,24 +22,31 @@ const WidgetWrapper = memo(
mode,
darkMode,
}) => {
const { moduleId } = useModuleContext();
const calculateMoveableBoxHeightWithId = useStore((state) => state.calculateMoveableBoxHeightWithId, shallow);
const stylesDefinition = useStore(
(state) => state.getComponentDefinition(id)?.component?.definition?.styles,
(state) => state.getComponentDefinition(id, moduleId)?.component?.definition?.styles,
shallow
);
const layoutData = useStore(
(state) => state.getComponentDefinition(id, moduleId)?.layouts?.[currentLayout],
shallow
);
const layoutData = useStore((state) => state.getComponentDefinition(id)?.layouts?.[currentLayout], shallow);
const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow);
const isDragging = useStore((state) => state.draggingComponentId === id);
const isResizing = useGridStore((state) => state.resizingComponentId === id);
const componentType = useStore((state) => state.getComponentDefinition(id)?.component?.component, shallow);
const componentType = useStore(
(state) => state.getComponentDefinition(id, moduleId)?.component?.component,
shallow
);
const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow);
const canShowInCurrentLayout = useStore((state) => {
const others = state.getResolvedComponent(id, subContainerIndex)?.others;
const others = state.getResolvedComponent(id, subContainerIndex, moduleId)?.others;
return others?.[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'];
});
const visibility = useStore((state) => {
const component = state.getResolvedComponent(id, subContainerIndex);
const componentExposedVisibility = state.getExposedValueOfComponent(id)?.isVisible;
const component = state.getResolvedComponent(id, subContainerIndex, moduleId);
const componentExposedVisibility = state.getExposedValueOfComponent(id, moduleId)?.isVisible;
if (componentExposedVisibility === false) return false;
if (component?.properties?.visibility === false || component?.styles?.visibility === false) return false;
return true;
@ -47,16 +56,24 @@ const WidgetWrapper = memo(
return null;
}
const width = gridWidth * layoutData?.width;
let newLayoutData = layoutData;
if (componentType === 'ModuleContainer' && mode === 'view') {
newLayoutData = { ...layoutData, top: 0, left: 0, width: NO_OF_GRIDS };
}
const width = gridWidth * newLayoutData?.width;
const height = calculateMoveableBoxHeightWithId(id, currentLayout, stylesDefinition);
const styles = {
width: width + 'px',
height: visibility === false ? '10px' : `${height}px`,
transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`,
transform: `translate(${newLayoutData.left * gridWidth}px, ${newLayoutData.top}px)`,
WebkitFontSmoothing: 'antialiased',
border: visibility === false && mode === 'edit' ? `1px solid var(--border-default)` : 'none',
};
const isModuleContainer = componentType === 'ModuleContainer';
if (!componentType) return null;
return (
<>
@ -67,6 +84,7 @@ const WidgetWrapper = memo(
'position-absolute': readOnly,
'active-target': isWidgetActive,
'opacity-0': isDragging || isResizing,
'module-container': isModuleContainer,
})}
data-id={`${id}`}
id={id}
@ -76,30 +94,32 @@ const WidgetWrapper = memo(
// zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null,
...styles,
}}
onMouseEnter={(e) => {
if (isDragging) return;
onMouseEnter={() => {
if (isDragging || isModuleContainer) return;
setHoveredComponentForGrid(id);
}}
onMouseLeave={() => {
if (isDragging) return;
if (isDragging || isModuleContainer) return;
setHoveredComponentForGrid('');
}}
>
{mode == 'edit' && (
<ConfigHandle
id={id}
widgetTop={layoutData.top}
widgetHeight={layoutData.height}
widgetTop={newLayoutData.top}
widgetHeight={newLayoutData.height}
showHandle={isWidgetActive}
componentType={componentType}
visibility={visibility}
customClassName={isModuleContainer ? 'module-container' : ''}
isModuleContainer={isModuleContainer}
subContainerIndex={subContainerIndex}
/>
)}
<RenderWidget
id={id}
componentType={componentType}
widgetHeight={layoutData.height}
widgetHeight={newLayoutData.height}
widgetWidth={width}
inCanvas={inCanvas}
subContainerIndex={subContainerIndex}

View file

@ -24,6 +24,20 @@ export const SUBCONTAINER_CANVAS_BORDER_WIDTH = 1;
export const BOX_PADDING = 2;
export const DROPPABLE_PARENTS = new Set([
'Calendar',
'Kanban',
'Form',
'Tabs',
'Modal',
'ModalV2',
'Listview',
'Container',
'Table',
'ModuleContainer',
]);
export const TAB_CANVAS_PADDING = 7.5;
export const MODAL_CANVAS_PADDING = 5;
export const LISTVIEW_CANVAS_PADDING = 7;

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