Merge branch 'modularisation/v3' into fix/codehinter-search-replace

This commit is contained in:
devanshu052000 2025-03-07 17:00:21 +05:30
commit e4aaaadf89
241 changed files with 10001 additions and 1252 deletions

View file

@ -139,3 +139,10 @@
echo \"Pushing changes in submodules...\"; \
git submodule foreach --quiet --recursive \"git push\"; \
}; f"
status-all = "!f() { \
echo \"Status of base repo...\"; \
git status; \
echo \"Status of submodules...\"; \
git submodule foreach --quiet --recursive \"git status\"; \
}; f"

View file

@ -14,7 +14,23 @@ jobs:
Cypress-App-Builder:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && (github.event.label.name == 'run-cypress-app-builder' || github.event.label.name == 'run-cypress') }}
if: |
github.event.action == 'labeled' &&
(
github.event.label.name == 'run-cypress' ||
github.event.label.name == 'run-ce-app-builder' ||
github.event.label.name == 'run-ee-app-builder'
)
strategy:
matrix:
edition: >-
${{
contains(github.event.pull_request.labels.*.name, 'run-cypress') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ce-app-builder') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ee-app-builder') && fromJson('["ee"]') ||
fromJson('[]')
}}
steps:
- name: Setup Node.js
@ -22,6 +38,23 @@ jobs:
with:
node-version: 18.18.2
- name: Set up Git authentication for private submodules
run: |
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
- name: Checkout with Submodules
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Checking out the correct branch for submodules EE
if: matrix.edition == 'ee'
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout modularisation/v3'
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
@ -45,6 +78,7 @@ jobs:
- name: Set up environment variables
run: |
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'EE' || 'CE' }}" >> .env
echo "TOOLJET_HOST=http://localhost:8082" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env

View file

@ -14,7 +14,23 @@ jobs:
Cypress-Marketplace:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && (github.event.label.name == 'run-cypress-marketplace' || github.event.label.name == 'run-cypress') }}
if: |
github.event.action == 'labeled' &&
(
github.event.label.name == 'run-cypress' ||
github.event.label.name == 'run-ce-cypress-marketplace' ||
github.event.label.name == 'run-ee-cypress-marketplace'
)
strategy:
matrix:
edition: >-
${{
contains(github.event.pull_request.labels.*.name, 'run-cypress') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ce-cypress-marketplace') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ee-cypress-marketplace') && fromJson('["ee"]') ||
fromJson('[]')
}}
steps:
- name: Checkout
@ -46,12 +62,25 @@ jobs:
- name: Set SAFE_BRANCH_NAME
run: echo "SAFE_BRANCH_NAME=$(echo ${{ env.BRANCH_NAME }} | tr '/' '-')" >> $GITHUB_ENV
- name: Build and Push Docker image
- name: Build CE Docker image
uses: docker/build-push-action@v4
with:
context: .
file: docker/production.Dockerfile
push: true
file: docker/ce-production.Dockerfile
push: false
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Build EE Docker image
if: matrix.edition == 'ee'
uses: docker/build-push-action@v4
with:
context: .
file: docker/ee/ee-production.Dockerfile
push: false
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}
platforms: linux/amd64
env:
@ -60,6 +89,7 @@ jobs:
- name: Set up environment variables
run: |
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'EE' || 'CE' }}" >> .env
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env

View file

@ -13,9 +13,22 @@ jobs:
Cypress-Platform:
runs-on: ubuntu-22.04
if: |
github.event.action == 'labeled' &&
(github.event.label.name == 'run-cypress-platform' ||
github.event.label.name == 'run-cypress')
github.event.action == 'labeled' &&
(
github.event.label.name == 'run-cypress' ||
github.event.label.name == 'run-ce-cypress-platform' ||
github.event.label.name == 'run-ee-cypress-platform'
)
strategy:
matrix:
edition: >-
${{
contains(github.event.pull_request.labels.*.name, 'run-cypress') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ce-cypress-platform') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ee-cypress-platform') && fromJson('["ee"]') ||
fromJson('[]')
}}
steps:
- name: Setup Node.js
@ -23,11 +36,22 @@ jobs:
with:
node-version: 18.18.2
- name: Checkout
- name: Set up Git authentication for private submodules
run: |
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
- name: Checkout with Submodules
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Checking out the correct branch for submodules EE
if: matrix.edition == 'ee'
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout modularisation/v3'
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
@ -55,9 +79,10 @@ jobs:
- name: Set up environment variables
run: |
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'EE' || 'CE' }}" >> .env
echo "TOOLJET_HOST=http://localhost:8082" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=localhost" >> .env
@ -69,7 +94,7 @@ jobs:
echo "TOOLJET_DB_HOST=localhost" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "TOOLJET_DB_STATEMENT_TIMEOUT=60000" >> .env
echo "TOOLJET_DB_RECONFIG=true" >> .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
@ -77,10 +102,6 @@ jobs:
echo "ENABLE_MARKETPLACE_FEATURE=true" >> .env
echo "ENABLE_MARKETPLACE_DEV_MODE=true" >> .env
echo "ENABLE_PRIVATE_APP_EMBED=true" >> .env
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=123456789.apps.googleusercontent.com" >> .env
echo "SSO_GOOGLE_OAUTH2_CLIENT_SECRET=ABCGFDNF-FHSDVFY-bskfh6234" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_ID=1234567890" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=3346shfvkdjjsfkvxce32854e026a4531ed" >> .env
- name: Set up database
run: |
@ -123,9 +144,10 @@ jobs:
uses: actions/upload-artifact@v4
if: always()
with:
name: screenshots
name: screenshots-${{ matrix.edition }}
path: cypress-tests/cypress/screenshots
Cypress-Platform-Subpath:
runs-on: ubuntu-22.04
if: |

View file

@ -1,39 +0,0 @@
name: Documentation version release
on:
workflow_dispatch:
inputs:
create-branch:
description: "Branch name"
version:
description: "RELEASE_VERSION"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup node 16.14
uses: actions/setup-node@v2
with:
node-version: 16.14
- run: cd docs && yarn install && npm run docusaurus docs:version ${{ github.event.inputs.version }}
- name: Create Pull Request
id: doc
uses: peter-evans/create-pull-request@v5
with:
title: "Creating a new version folder ${{ github.event.version }}"
body: "Created a new version folder for version: ${{ github.event.inputs.version }}"
branch: ${{ github.event.inputs.create-branch }}
base: "develop"
token: ${{ secrets.GITHUB }}
delete-branch : true
labels: versioned-docs, automated pr
commit-message: added new version folder

232
.github/workflows/docker-release.yml vendored Normal file
View file

@ -0,0 +1,232 @@
name: ToolJet Edition docker images builds
on:
release:
types: [published]
jobs:
build-tooljet-image-for-ce-edtion:
runs-on: ubuntu-latest
if: "${{ github.event.release }}"
steps:
- name: Checkout code to main for Beta CE edition
if: "!contains(github.event.release.tag_name, 'ce-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/main
- name: Checkout code to LTS for CE LTS edition
if: "contains(github.event.release.tag_name, '-ce-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/lts-4.0
# 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,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
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: Build and Push Docker image for Beta tag
if: "!contains(github.event.release.tag_name, '-ce-lts')"
uses: docker/build-push-action@v4
with:
context: .
file: docker/ce-production.Dockerfile
push: true
tags: tooljet/tooljet-ce:${{ github.event.release.tag_name }},tooljet/tooljet-ce:ce-latest
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker image for LTS tag
if: "contains(github.event.release.tag_name, '-ce-lts')"
uses: docker/build-push-action@v4
with:
context: .
file: docker/ce-production.Dockerfile
push: true
tags: tooljet/tooljet-ce:${{ github.event.release.tag_name }},tooljet/tooljet-ce:ce-lts-latest
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="ToolJet community image published:\n\`tooljet/tooljet-ce:${{ github.event.release.tag_name }}\`"
else
message="Job '${{ env.JOB_NAME }}' failed! tooljet/tooljet-ce:${{ github.event.release.tag_name }}"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="ToolJet community image published:\n\`tooljet/tooljet-ce:${{ github.event.release.tag_name }}\`"
else
message="Job '${{ env.JOB_NAME }}' failed! tooljet/tooljet-ce:${{ github.event.release.tag_name }}"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
build-tooljet-image-for-ee-edtion:
runs-on: ubuntu-latest
if: "${{ github.event.release }}"
steps:
- name: Checkout code to main for Beta EE edition
if: "!contains(github.event.release.tag_name, 'ee-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/main
- name: Checkout code to LTS for EE LTS edition
if: "contains(github.event.release.tag_name, '-ee-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/lts-4.0
# 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,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
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: Build and Push Docker image for Beta tag
if: "!contains(github.event.release.tag_name, '-ee-lts')"
uses: docker/build-push-action@v4
with:
context: .
args: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
file: docker/ee-production.Dockerfile
push: true
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-lts-latest,tooljet/tooljet:ee-lts-latest,tooljet/tooljet:${{ github.event.release.tag_name }}
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker image for LTS tag
if: "contains(github.event.release.tag_name, '-ee-lts')"
uses: docker/build-push-action@v4
with:
context: .
args: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
file: docker/ee-production.Dockerfile
push: true
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-lts-latest,tooljet/tooljet:ee-lts-latest,tooljet/tooljet:${{ github.event.release.tag_name }}
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="ToolJet enterprise image published:\n\`tooljet/tooljet-ee:${{ github.event.release.tag_name }}\`\n\`tooljet/tooljet:${{ github.event.release.tag_name }}\`"
else
message="Job '${{ env.JOB_NAME }}' failed! Image built:\n\`tooljet/tooljet-ee:${{ github.event.release.tag_name }}\`\n\`tooljet/tooljet:${{ github.event.release.tag_name }}\`"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
build-tooljet-image-for-cloud-edtion:
runs-on: ubuntu-latest
if: "${{ github.event.release }}"
steps:
- name: Checkout code to LTS for Cloud LTS edition
if: "contains(github.event.release.tag_name, '-cloud-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/lts-4.0
# 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,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
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: Build and Push Docker image for LTS tag
if: "contains(github.event.release.tag_name, '-cloud-lts')"
uses: docker/build-push-action@v4
with:
context: .
args: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
file: docker/cloud/cloud-server.Dockerfile
push: true
tags: tooljet/saas:${{ github.event.release.tag_name }}
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="ToolJet cloud image published:\n\`tooljet/saas:${{ github.event.release.tag_name }}\`"
else
message="Job '${{ env.JOB_NAME }}' failed! Image built:\n\`tooljet/saas:${{ github.event.release.tag_name }}\`"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}

View file

@ -11,13 +11,16 @@ on:
description: "RELEASE_VERSION"
jobs:
packer:
packer-ee:
runs-on: ubuntu-latest
name: packer
name: packer-ee
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Checkout code to lts-4.0
if: contains(github.event.release.tag_name, '-ee-lts')
uses: actions/checkout@v2
with:
ref: refs/heads/lts-4.0
- name: Setting tag
if: "${{ github.event.inputs.version != '' }}"
@ -28,7 +31,7 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@v1-node16
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@ -40,7 +43,7 @@ jobs:
with:
command: init
target: .
working_directory: deploy/ec2
working_directory: deploy/ec2/ee
# validate templates
- name: Validate Template
@ -49,25 +52,35 @@ jobs:
command: validate
arguments: -syntax-only
target: .
working_directory: deploy/ec2
working_directory: deploy/ec2/ee
# Echo RENDER_GITHUB_PAT
- name: Set PACKER_GITHUB_PAT
run: echo "PACKER_GITHUB_PAT=${{ secrets.PACKER_GITHUB_PAT}}" >> $GITHUB_ENV
# Dynamically update setup_machine.sh with PAT
- name: Validate PAT
run: |
sed -i "s|git config --global url."https://x-access-token:CUSTOM_GITHUB_TOKEN@github.com/".insteadOf "https://github.com/"|git config --global url."https://x-access-token:${ secrets.CUSTOM_GITHUB_TOKEN }@github.com/".insteadOf "https://github.com/"|g" ./deploy/ec2/ee/setup_machine.sh
# build artifact
- name: Build Artifact
uses: hashicorp/packer-github-actions@master
with:
command: build
#The the below argument is specific for building EE AMI image
arguments: -color=false -on-error=abort -var ami_name=tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal
target: .
working_directory: deploy/ec2
working_directory: deploy/ec2/ee
env:
PACKER_LOG: 1
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="Job '${{ env.JOB_NAME }}' succeeded! AMI = tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal"
message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`"
else
message="Job '${{ env.JOB_NAME }}' failed! AMI = tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal"
message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}

View file

@ -492,10 +492,6 @@ jobs:
"key": "REDIS_PORT",
"value": "${{ secrets.RENDER_REDIS_PORT }}"
},
{
"key": "LICENSE_KEY",
"value": "${{ secrets.RENDER_LICENSE_KEY }}"
},
{
"key": "TEMPORAL_SERVER_ADDRESS",
"value": "https://auto-setup-1-25-1.onrender.com"
@ -866,10 +862,6 @@ jobs:
# "value": "${{ secrets.RENDER_REDIS_PORT }}"
# },
# {
# "key": "LICENSE_KEY",
# "value": "${{ secrets.RENDER_LICENSE_KEY }}"
# },
# {
# "key": "TEMPORAL_SERVER_ADDRESS",
# "value": "https://auto-setup-1-25-1.onrender.com"
# },

View file

@ -1,65 +0,0 @@
name: Tooljet develop docker image build
on:
push:
branches:
- develop
workflow_dispatch:
inputs:
job-to-run:
description: Enter the job name (tooljet-develop-image)
options: ["tooljet-develop-image"]
required: true
jobs:
tooljet-develop-image:
runs-on: ubuntu-latest
if: |
${{ github.ref == 'refs/heads/develop' }} &&
${{ github.event_name == 'workflow_dispatch' && github.event.inputs.job-to-run == 'tooljet-develop-image' }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: refs/heads/develop
# 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,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
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: Build and Push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: docker/production.Dockerfile
push: true
tags: tooljet/tooljet-ce:develop
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Send Slack Notification on Failure
if: failure()
run: |
message="Job '${{ env.JOB_NAME }}' failed! tooljet/tooljet-ce:develop"
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL_OPS_CHANNEL }}

View file

@ -1,40 +0,0 @@
name: LTS Test system deploy
on:
workflow_run:
workflows: ["Tooljet release docker images build"]
types:
- completed
jobs:
Build-and-update-image:
runs-on: ubuntu-22.04
steps:
- name: SSH into GCP VM instance
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_INSTANCE_IP }}
username: ${{ secrets.GCP_USERNAME }}
key: ${{ secrets.EC2_INSTANCE_SSH_KEY }}
script: |
ls -lah
# Stop the Docker containers
sudo docker-compose down
# Remove the existing tooljet/* images
sudo docker images -a | grep 'tooljet/' | awk '{print $3}' | xargs sudo docker rmi -f
# Check remaining images
sudo docker images
# Update docker-compose.yml with the new image for tooljet service
sed -i '/^[[:space:]]*tooljet:/,/^[[:space:]]*[^:]*$/ { /^[[:space:]]*image:[[:space:]]*tooljet\/tj-osv/s|\(image:[[:space:]]*\).*|\1tooljet/tj-osv:'"${{ env.SAFE_BRANCH_NAME }}"'| }' docker-compose.yaml
# Start the Docker containers
cat docker-compose.yaml
sudo docker-compose up -d
#View containers
sudo docker ps

View file

@ -1,132 +0,0 @@
name: Test system deploy
on:
pull_request_target:
types: [labeled, unlabeled, closed]
workflow_dispatch:
env:
PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:
Build-and-update-image:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'test-system-deploy' }}
steps:
- name: Check authorization
run: |
allowed_user1=${{ secrets.ALLOWED_USER1_USERNAME }}
allowed_user2=${{ secrets.ALLOWED_USER2_USERNAME }}
allowed_user3=${{ secrets.ALLOWED_USER3_USERNAME }}
allowed_user4=${{ secrets.ALLOWED_USER4_USERNAME }}
allowed_user5=${{ secrets.ALLOWED_USER5_USERNAME }}
allowed_user6=${{ secrets.ALLOWED_USER6_USERNAME }}
allowed_user6=${{ secrets.ALLOWED_USER7_USERNAME }}
if [[ "${{ github.actor }}" != "$allowed_user1" && \
"${{ github.actor }}" != "$allowed_user2" && \
"${{ github.actor }}" != "$allowed_user3" && \
"${{ github.actor }}" != "$allowed_user4" && \
"${{ github.actor }}" != "$allowed_user5" && \
"${{ github.actor }}" != "$allowed_user6" && \
"${{ github.actor }}" != "$allowed_user7" ]]; then
echo "User not authorized to trigger this workflow"
exit 1
fi
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
# 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,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
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 and Push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: docker/production.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: SSH into GCP VM instance
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_INSTANCE_IP }}
username: ${{ secrets.GCP_USERNAME }}
key: ${{ secrets.EC2_INSTANCE_SSH_KEY }}
script: |
ls -lah
# Stop the Docker containers
sudo docker-compose down
# Remove the existing tooljet/* images
sudo docker images -a | grep 'tooljet/' | awk '{print $3}' | xargs sudo docker rmi -f
# Check remaining images
sudo docker images
# Update docker-compose.yml with the new image for tooljet service
sed -i '/^[[:space:]]*tooljet:/,/^[[:space:]]*[^:]*$/ { /^[[:space:]]*image:[[:space:]]*tooljet\/tj-osv/s|\(image:[[:space:]]*\).*|\1tooljet/tj-osv:'"${{ env.SAFE_BRANCH_NAME }}"'| }' docker-compose.yaml
# Start the Docker containers
cat docker-compose.yaml
sudo docker-compose up -d
#View containers
sudo docker ps
- uses: actions/github-script@v6
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'test-system-deploy'
})
} catch (e) {
console.log(e)
}
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['test-system-deployed']
})

18
.vscode/launch.json vendored
View file

@ -11,7 +11,21 @@
{
"type": "node",
"request": "launch",
"name": "Server",
"name": "Server (Dev Mode)",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"start:dev"
],
"sourceMaps": true,
"cwd": "${workspaceRoot}/server",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
},
{
"type": "node",
"request": "launch",
"name": "Server (Original)",
"args": [
"${workspaceFolder}/server/src/main.ts"
],
@ -49,7 +63,7 @@
"remoteRoot": "/app/server",
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"]
},
}
}
]
}

View file

@ -35,7 +35,7 @@ then
fi
fi
npm --prefix server run db:setup:prod
TOOLJET_EDTION=ce npm --prefix server run db:setup:prod
if sudo systemctl start nest
then

View file

@ -78,4 +78,4 @@ npm install -g npm@9.8.1
# Building ToolJet app
npm install -g @nestjs/cli
npm run build
TOOLJET_EDTION=ce npm run build

68
deploy/ec2/ee/.env Normal file
View file

@ -0,0 +1,68 @@
# https://docs.tooljet.io/docs/setup/env-vars
TOOLJET_HOST=http://localhost
LOCKBOX_MASTER_KEY=
SECRET_KEY_BASE=
PG_USER=
PG_HOST=
PG_PASS=
PG_DB=tooljet_prod
ORM_LOGGING=true
NODE_ENV=production
DEPLOYMENT_PLATFORM=ec2
# ToolJet Database
TOOLJET_DB=tooljet_db
TOOLJET_DB_USER=
TOOLJET_DB_HOST=
TOOLJET_DB_PASS=
PGRST_HOST=localhost:3001
PGRST_SERVER_PORT=3001
PGRST_JWT_SECRET=
PGRST_DB_URI=
PGRST_DB_PRE_CONFIG=postgrest.pre_config
#Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USER=default
REDIS_PASSWORD=
# Checks every 24 hours to see if a new version of ToolJet is available
# (Enabled by default. Set 0 to disable)
CHECK_FOR_UPDATES=
# Checks every 24 hours to update app telemetry data to ToolJet hub.
# (Telemetry is enabled by default. Set value to true to disable.)
# DISABLE_APP_TELEMETRY=false
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# EMAIL CONFIGURATION
DEFAULT_FROM_EMAIL=hello@tooljet.io
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_DOMAIN=
SMTP_PORT=
# DISABLE USER SIGNUPS (true or false). Default: true
DISABLE_SIGNUPS=
# OBSERVABILITY
APM_VENDOR=
SENTRY_DNS=
SENTRY_DEBUG=
# FEATURE TOGGLE
COMMENT_FEATURE_ENABLE=true
ENABLE_MULTIPLAYER_EDITING=true
ENABLE_MARKETPLACE_FEATURE=true
#SSO
SSO_DISABLE_SIGNUP=
SSO_RESTRICTED_DOMAIN=
SSO_GOOGLE_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_SECRET=
SSO_GIT_OAUTH2_HOST=

View file

@ -0,0 +1,17 @@
[Unit]
Description=Nest Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/app
Environment="NODE_ENV=production"
EnvironmentFile=/home/ubuntu/app/.env
RestartSec=1
ExecStart=/usr/bin/npm --prefix /home/ubuntu/app run start:prod
Restart=always
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,16 @@
[Unit]
Description=PostgREST Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/bin
EnvironmentFile=/home/ubuntu/app/.env
RestartSec=1
ExecStart=/bin/postgrest
Restart=always
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,45 @@
[Unit]
Description=Advanced key-value store
After=network.target
Documentation=http://redis.io/documentation, man:redis-server(1)
[Service]
Type=forking
ExecStart=/usr/bin/redis-server /etc/redis/redis.conf
PIDFile=/run/redis/redis-server.pid
TimeoutStopSec=0
Restart=always
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=2755
UMask=007
PrivateTmp=yes
LimitNOFILE=65535
PrivateDevices=yes
ProtectHome=yes
ReadOnlyDirectories=/
ReadWritePaths=-/var/lib/redis
ReadWritePaths=-/var/log/redis
ReadWritePaths=-/var/run/redis
NoNewPrivileges=true
CapabilityBoundingSet=CAP_SETGID CAP_SETUID CAP_SYS_RESOURCE
MemoryDenyWriteExecute=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# redis-server can write to its own config file when in cluster mode so we
# permit writing there by default. If you are not using this feature, it is
# recommended that you replace the following lines with "ProtectSystem=full".
ProtectSystem=true
ReadWriteDirectories=-/etc/redis
[Install]
WantedBy=multi-user.target
Alias=redis-server.service

175
deploy/ec2/ee/setup_app Executable file
View file

@ -0,0 +1,175 @@
#!/bin/bash
# Load the .env file
source .env
# Check if LOCKBOX_MASTER_KEY is present or empty
if [[ -z "$LOCKBOX_MASTER_KEY" ]]; then
# Generate LOCKBOX_MASTER_KEY
LOCKBOX_MASTER_KEY=$(openssl rand -hex 32)
# Update .env file
awk -v key="$LOCKBOX_MASTER_KEY" '
BEGIN { FS=OFS="=" }
/^LOCKBOX_MASTER_KEY=/ { $2=key; found=1 }
1
END { if (!found) print "LOCKBOX_MASTER_KEY="key }
' .env > temp.env && mv temp.env .env
echo "Generated a secure master key for the lockbox"
else
echo "The lockbox master key already exists."
fi
# Check if SECRET_KEY_BASE is present or empty
if [[ -z "$SECRET_KEY_BASE" ]]; then
# Generate SECRET_KEY_BASE
SECRET_KEY_BASE=$(openssl rand -hex 64)
# Update .env file
awk -v key="$SECRET_KEY_BASE" '
BEGIN { FS=OFS="=" }
/^SECRET_KEY_BASE=/ { $2=key; found=1 }
1
END { if (!found) print "SECRET_KEY_BASE="key }
' .env > temp.env && mv temp.env .env
echo "Created a secret key for secure operations."
else
echo "The secret key base is already in place."
fi
# Check if PGRST_JWT_SECRET is present or empty
if [[ -z "$PGRST_JWT_SECRET" ]]; then
# Generate PGRST_JWT_SECRET
PGRST_JWT_SECRET=$(openssl rand -hex 32)
# Update .env file
awk -v key="$PGRST_JWT_SECRET" '
BEGIN { FS=OFS="=" }
/^PGRST_JWT_SECRET=/ { $2=key; found=1 }
1
END { if (!found) print "PGRST_JWT_SECRET="key }
' .env > temp.env && mv temp.env .env
echo "Generated a unique secret for PGRST authentication."
else
echo "The PGRST JWT secret is already generated and in place."
fi
# Function to generate a random password
generate_password() {
openssl rand -base64 12 | tr -d '/+' | cut -c1-16
}
# Check if PG_USER, PG_HOST, PG_PASS, PG_DB are present or empty
if [[ -z "$PG_USER" ]] || [[ -z "$PG_HOST" ]] || [[ -z "$PG_PASS" ]] || [[ -z "$PG_DB" ]]; then
# Prompt user for values
read -p "Enter PostgreSQL database username: " PG_USER
read -p "Enter PostgreSQL database hostname: " PG_HOST
read -p "Enter PostgreSQL database password: " PG_PASS
read -p "Enter PostgreSQL database name: " PG_DB
# Update .env file
awk -v pg_user="$PG_USER" -v pg_host="$PG_HOST" -v pg_pass="$PG_PASS" -v pg_db="$PG_DB" '
BEGIN { FS=OFS="=" }
/^PG_USER=/ { $2=pg_user; found=1 }
/^PG_HOST=/ { $2=pg_host; found=1 }
/^PG_PASS=/ { $2=pg_pass; found=1 }
/^PG_DB=/ { $2=pg_db; found=1 }
1
END {
if (!found) {
print "PG_USER="pg_user
print "PG_HOST="pg_host
print "PG_PASS="pg_pass
print "PG_DB="pg_db
}
}
' .env > temp.env && mv temp.env .env
echo "Successfully updated postgresql database values .env file"
fi
# Copy values from PG to TOOLJET_DB
TOOLJET_DB_USER=$PG_USER
TOOLJET_DB_HOST=$PG_HOST
TOOLJET_DB_PASS=$PG_PASS
# Update .env file for TOOLJET_DB
awk -v tj_user="$TOOLJET_DB_USER" -v tj_host="$TOOLJET_DB_HOST" -v tj_pass="$TOOLJET_DB_PASS" '
BEGIN { FS=OFS="=" }
/^TOOLJET_DB_USER=/ { $2=tj_user; found=1 }
/^TOOLJET_DB_HOST=/ { $2=tj_host; found=1 }
/^TOOLJET_DB_PASS=/ { $2=tj_pass; found=1 }
1
END { if (!found) print "TOOLJET_DB_USER="tj_user ORS "TOOLJET_DB_HOST="tj_host ORS "TOOLJET_DB_PASS="tj_pass }
' .env > temp.env && mv temp.env .env
echo "Successfully updated tooljet database values in the .env file"
# Construct PGRST_DB_URI with user-provided values
PGRST_DB_URI="postgres://$PG_USER:$PG_PASS@$PG_HOST/tooljet_db"
# Update .env file for PGRST_DB_URI
awk -v uri="$PGRST_DB_URI" '
BEGIN { FS=OFS="=" }
/^PGRST_DB_URI=/ { $2=uri; found=1 }
1
END { if (!found) print "PGRST_DB_URI="uri }
' .env > temp.env && mv temp.env .env
echo "Successfully updated PGRST database URI"
if [[ -z $PG_USER || -z $PG_PASS || -z $PG_HOST ]]
then
echo "Please set the required PG_USER, PG_PASS, and PG_HOST values within the .env file"
exit 1
fi
export $(grep -v '^#' .env | xargs)
if psql -d postgresql://$PG_USER:$PG_PASS@$PG_HOST/postgres -c 'select now()' > /dev/null 2>&1
then
echo "Successfully pinged the database!";
else
echo "Can't connect to the database. Kindly check the credenials provided in the .env file!"
exit 1
fi
if sudo systemctl start redis-server && sudo systemctl enable redis-server
then
echo "Successfully started Redis!"
else
echo "Failed to start and enable Redis"
fi
if sudo -E systemctl start openresty
then
echo "Successfully started reverse proxy!"
else
echo "Failed to start reverse proxy"
exit 1
fi
if sudo -E systemctl start postgrest
then
echo "Successfully started PostgREST server!"
else
echo "Failed to start PostgREST server"
exit 1
fi
TOOLJET_EDTION=ee npm --prefix server run db:setup:prod
if sudo -E systemctl start nest
then
echo "The app will be served at ${TOOLJET_HOST}"
else
echo "Failed to start the server!"
exit 1
fi
sudo systemctl restart nest
sudo -E systemctl restart postgrest

View file

@ -0,0 +1,99 @@
#!/bin/bash
set -e
# Setup prerequisite dependencies
sudo apt-get update
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates apt-utils git curl postgresql-client
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
sudo ln -s "$(which node)" /usr/bin/node
sudo ln -s "$(which npm)" /usr/bin/npm
sudo npm i -g npm@9.8.1
# 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
sudo mv openresty.list /etc/apt/sources.list.d/
sudo apt-get update
sudo apt-get -y install --no-install-recommends openresty
sudo apt-get install -y curl g++ gcc autoconf automake bison libc6-dev \
libffi-dev libgdbm-dev libncurses5-dev libsqlite3-dev libtool \
libyaml-dev make pkg-config sqlite3 zlib1g-dev libgmp-dev \
libreadline-dev libssl-dev libmysqlclient-dev build-essential \
freetds-dev libpq-dev
sudo apt-get install -y luarocks
sudo luarocks install lua-resty-auto-ssl
sudo mkdir /etc/resty-auto-ssl /var/log/openresty /etc/fallback-certs
sudo chown -R www-data:www-data /etc/resty-auto-ssl
# Oracle db client library setup
sudo apt install -y libaio1
curl -o instantclient-basiclite.zip https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip -SL && \
curl -o instantclient-basiclite-11.zip https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip -SL && \
unzip instantclient-basiclite.zip && \
unzip instantclient-basiclite-11.zip && \
sudo mkdir -p /usr/lib/instantclient && sudo mv instantclient*/ /usr/lib/instantclient && \
rm instantclient-basiclite.zip && \
rm instantclient-basiclite-11.zip && \
echo /usr/lib/instantclient/* | sudo tee /etc/ld.so.conf.d/oracle-instantclient.conf > /dev/null && sudo ldconfig
# Set the Instant Client library paths
export LD_LIBRARY_PATH="/usr/lib/instantclient/instantclient_11_2:/usr/lib/instantclient/instantclient_21_10${LD_LIBRARY_PATH}"
# Gen fallback certs
sudo openssl rand -out /home/ubuntu/.rnd -hex 256
sudo chown www-data:www-data /home/ubuntu/.rnd
sudo openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
-subj '/CN=sni-support-required-for-valid-ssl' \
-keyout /etc/fallback-certs/resty-auto-ssl-fallback.key \
-out /etc/fallback-certs/resty-auto-ssl-fallback.crt
# Setup nginx config
export SERVER_HOST="${SERVER_HOST:=localhost}"
export SERVER_USER="${SERVER_USER:=www-data}"
VARS_TO_SUBSTITUTE='$SERVER_HOST:$SERVER_USER'
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
sudo mv ./postgrest /bin/postgrest
sudo rm postgrest-v12.0.2-linux-static-x64.tar.xz
# Add the Redis APT repository
sudo add-apt-repository ppa:redislabs/redis -y
# Install redis
sudo apt-get update
sudo apt-get install redis-server -y
# Setup app, postgrest and redis as systemd service
sudo cp /tmp/nest.service /lib/systemd/system/nest.service
sudo cp /tmp/postgrest.service /lib/systemd/system/postgrest.service
sudo cp /tmp/redis-server.service /lib/systemd/system/redis-server.service
# Start and enable Redis service
sudo systemctl daemon-reload
# Setup app directory
mkdir -p ~/app
git config --global url."https://x-access-token:CUSTOM_GITHUB_TOKEN@github.com/".insteadOf "https://github.com/"
#The below url will be edited dynamically when actions is triggered
git clone -b main https://github.com/ToolJet/ToolJet.git ~/app && cd ~/app
git submodule update --init --recursive
git submodule foreach 'git checkout main || true'
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
# Building ToolJet app
npm install -g @nestjs/cli
TOOLJET_EDTION=ee npm run build

View file

@ -0,0 +1,77 @@
packer {
required_plugins {
amazon = {
version = ">= 0.0.1"
source = "github.com/hashicorp/amazon"
}
}
}
source "amazon-ebs" "ubuntu" {
ami_name = "${var.ami_name}"
instance_type = "${var.instance_type}"
region = "${var.ami_region}"
ami_regions = "${var.ami_regions}"
ami_groups = "${var.ami_groups}"
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
ssh_username = "ubuntu"
ssh_clear_authorized_keys = "true"
shutdown_behavior = "terminate"
force_delete_snapshot = "true"
launch_block_device_mappings {
device_name = "/dev/sda1"
volume_size = 10
delete_on_termination = true
}
}
build {
sources = [
"source.amazon-ebs.ubuntu"
]
provisioner "file" {
source = "nest.service"
destination = "/tmp/nest.service"
}
provisioner "file" {
source = "../../frontend/config/nginx.conf.template"
destination = "/tmp/nginx.conf"
}
provisioner "file" {
source = ".env"
destination = "/tmp/.env"
}
provisioner "file" {
source = "setup_app"
destination = "/tmp/setup_app"
}
provisioner "file" {
source = "postgrest.service"
destination = "/tmp/postgrest.service"
}
provisioner "file" {
source = "redis-server.service"
destination = "/tmp/redis-server.service"
}
provisioner "shell" {
script = "setup_machine.sh"
}
}

View file

@ -0,0 +1,33 @@
variable "ami_name" {
type = string
}
variable "instance_type" {
type = string
default = "t2.medium"
}
variable "ami_region" {
type = string
default = "us-west-2"
}
variable "ami_groups" {
type = list(string)
default = ["all"]
}
variable "ami_regions" {
type = list(string)
default = ["us-west-1","us-east-1", "us-east-2", "eu-central-1", "ap-northeast-1", "ca-central-1"]
}
variable "PACKER_BUILDER_TYPE" {
type = string
default = "amazon-ebs"
}
variable "PACKER_BUILD_NAME" {
type = string
default = "ubuntu"
}

View file

@ -0,0 +1,117 @@
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 npm install -g @nestjs/cli
RUN mkdir -p /app
WORKDIR /app
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME=modularisation/v3
# Clone and checkout the frontend repository
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'
COPY ./package.json ./package.json
# Building ToolJet plugins
COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/
RUN npm --prefix plugins install
COPY ./plugins/ ./plugins/
ENV NODE_ENV=production
RUN npm --prefix plugins run build
RUN npm --prefix plugins prune --production
# Building ToolJet server
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install --only=production
COPY ./server/ ./server/
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
# 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/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
# 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
USER appuser
WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
ENTRYPOINT ["./server/entrypoint.sh"]

View file

@ -137,6 +137,7 @@ const WidgetIcon = (props) => {
case 'map':
return <Map {...props} />;
case 'modal':
case 'modallegacy':
return <Modal {...props} />;
case 'multiselect':
case 'multiselectv2':

View file

@ -284,7 +284,7 @@
"userGroups": "User Groups",
"createNewGroup": "Create new group",
"updateGroup": "Update group",
"addNewGroup": "Create new group",
"addNewGroup": "Add new group",
"enterName": "Enter group name",
"createGroup": "Create Group",
"name": "Name"

View file

@ -131,9 +131,13 @@ class AppComponent extends React.Component {
}
return '';
};
render() {
const { updateAvailable, darkMode, isEditorOrViewer } = this.state;
const mergedProps = {
...this.props,
switchDarkMode: this.switchDarkMode,
darkMode: darkMode,
};
let toastOptions = {
style: {
wordBreak: 'break-all',
@ -256,7 +260,7 @@ class AppComponent extends React.Component {
}
/>
)}
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...this.props} />}></Route>
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...mergedProps} />}></Route>
<Route path="settings/*" element={<InstanceSettings {...this.props} />}></Route>
<Route path="/:workspaceId/settings/*" element={<Settings {...this.props} />}></Route>
@ -270,7 +274,7 @@ class AppComponent extends React.Component {
</PrivateRoute>
}
/>
{getDataSourcesRoutes(this.props)}
{getDataSourcesRoutes(mergedProps)}
<Route
exact
path="/applications/:id/versions/:versionId/:pageHandle?"

View file

@ -13,6 +13,7 @@ export const ConfigHandle = ({
customClassName = '',
showHandle,
componentType,
visibility,
}) => {
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow);
@ -28,16 +29,27 @@ export const ConfigHandle = ({
);
const setComponentToInspect = useStore((state) => state.setComponentToInspect);
const isModal = componentType === 'Modal' || componentType === 'ModalV2';
const _showHandle = useStore((state) => {
const isWidgetHovered = state.getHoveredComponentForGrid() === id || state.hoveredComponentBoundaryId === id;
const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== '';
// If one component is hovered and one is selected, show the handle for the hovered component
return (
isWidgetHovered ||
(showHandle &&
(!isMultipleComponentsSelected || (isModal && isModalOpen)) &&
!anyComponentHovered)
);
}, shallow);
let height = visibility === false ? 10 : widgetHeight;
return (
<div
className={`config-handle ${customClassName}`}
widget-id={id}
style={{
top: position === 'top' ? '-20px' : widgetTop + widgetHeight - (widgetTop < 10 ? 15 : 10),
visibility:
showHandle && (!isMultipleComponentsSelected || (componentType === 'Modal' && isModalOpen))
? 'visible'
: 'hidden',
top: position === 'top' ? '-20px' : widgetTop + height - (widgetTop < 10 ? 15 : 10),
visibility: _showHandle ? 'visible' : 'hidden',
left: '-1px',
}}
onClick={(e) => {
@ -51,7 +63,7 @@ export const ConfigHandle = ({
>
<span
style={{
background: componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
background: isModal && isModalOpen ? '#c6cad0' : '#4D72FA',
}}
className="badge handle-content"
>

View file

@ -65,9 +65,3 @@
}
}
}
.main-editor-canvas .widget-target:hover > .config-handle {
visibility: visible !important;
}

View file

@ -56,6 +56,11 @@ export const Container = React.memo(
const [{ isOverCurrent }, drop] = useDrop({
accept: 'box',
hover: (item) => {
item.canvasRef = realCanvasRef?.current;
item.canvasId = id;
item.canvasWidth = getContainerCanvasWidth();
},
drop: async ({ componentType }, monitor) => {
const didDrop = monitor.didDrop();
if (didDrop) return;
@ -89,14 +94,15 @@ export const Container = React.memo(
function getContainerCanvasWidth() {
if (canvasWidth !== undefined) {
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
return canvasWidth;
if (id === 'canvas') return canvasWidth;
return canvasWidth - 2;
}
return realCanvasRef?.current?.offsetWidth;
}
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
useEffect(() => {
useGridStore.getState().actions.setSubContainerWidths(id, (getContainerCanvasWidth() - 2) / NO_OF_GRIDS);
useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canvasWidth, listViewMode, columns]);
@ -137,8 +143,7 @@ export const Container = React.memo(
}}
style={{
height: id === 'canvas' ? `${canvasHeight}` : '100%',
// backgroundSize: '25.3953px 10px',
backgroundSize: `${gridWidth}px 10px`,
backgroundSize: `${gridWidth}px ${10}px`,
backgroundColor:
currentMode === 'view'
? computeViewerBackgroundColor(darkMode, canvasBgColor)
@ -169,6 +174,7 @@ export const Container = React.memo(
data-parentId={id}
canvas-height={canvasHeight}
onClick={handleCanvasClick}
component-type={componentType}
>
<div
className={cx('container-fluid rm-container p-0', {

View file

@ -186,5 +186,28 @@
display: none;
}
.moveable-guideline {
background: #97AEFC !important;
opacity: 0.8;
z-index: 9999;
}
/* */
.moveable-guideline.moveable-horizontal {
height: 1px !important;
width: 100% !important;
background: #97AEFC !important;
left: 0 !important;
}
.moveable-guideline.moveable-vertical {
width: 1px !important;
height: 100% !important;
background: #97AEFC !important;
top: 0 !important;
}
.moveable-guideline-group {
z-index: 9999;
}

View file

@ -17,9 +17,12 @@ import {
hasParentWithClass,
getPositionForGroupDrag,
adjustWidth,
hideGridLines,
showGridLines,
} from './gridUtils';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
import useStore from '@/AppBuilder/_stores/store';
import './Grid.css';
import { NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
@ -29,6 +32,7 @@ const RESIZABLE_CONFIG = {
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
};
export const GRID_HEIGHT = 10;
export default function Grid({ gridWidth, currentLayout }) {
const lastDraggedEventsRef = useRef(null);
@ -51,6 +55,49 @@ export default function Grid({ gridWidth, currentLayout }) {
const canvasWidth = NO_OF_GRIDS * gridWidth;
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow);
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
const [dragParentId, setDragParentId] = useState(null);
const [elementGuidelines, setElementGuidelines] = useState([]);
const componentsSnappedTo = useRef(null);
const prevDragParentId = useRef(null);
const newDragParentId = useRef(null);
const [isGroupDragging, setIsGroupDragging] = useState(false);
useEffect(() => {
const selectedSet = new Set(selectedComponents);
const draggingOrResizingId = draggingComponentId || resizingComponentId;
const isGrouped = findHighestLevelofSelection().length > 1;
const firstSelectedParent =
selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null;
const selectedParent = dragParentId || firstSelectedParent;
const guidelines = boxList
.filter((box) => {
const isVisible =
getResolvedValue(box?.component?.definition?.properties?.visibility?.value) ||
getResolvedValue(box?.component?.definition?.styles?.visibility?.value);
// Early return for non-visible elements
if (!isVisible) return false;
if (isGrouped) {
// If component is selected, don't show its guidelines
if (selectedSet.has(box.id)) return false;
return selectedParent ? box.parent === selectedParent : !box.parent;
}
if (draggingOrResizingId) {
if (box.id === draggingOrResizingId) return false;
return dragParentId ? box.parent === dragParentId : !box.parent;
}
return true;
})
.map((box) => `.ele-${box.id}`);
setElementGuidelines(guidelines);
}, [boxList, dragParentId, draggingComponentId, resizingComponentId, selectedComponents, getResolvedValue]);
useEffect(() => {
setBoxList(
@ -94,7 +141,7 @@ export default function Grid({ gridWidth, currentLayout }) {
boxList.forEach(({ id, height, width, x, y, gw }) => {
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
y = Math.round(y / 10) * 10;
y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
gw = gw ? gw : gridWidth;
const parent = transformedBoxes[id]?.component?.parent;
@ -117,7 +164,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
setComponentLayout({
[id]: {
height: height ? height : 10,
height: height ? height : GRID_HEIGHT,
width: newWidth ? newWidth : 1,
top: y,
left: Math.round(x / gw),
@ -319,7 +366,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
// Round y position
y = Math.max(0, Math.round(y / 10) * 10);
y = Math.max(0, Math.round(y / GRID_HEIGHT) * GRID_HEIGHT);
// Adjust height for certain parent components
if (parent) {
const parentElem = document.getElementById(`canvas-${parent}`);
@ -354,17 +401,16 @@ export default function Grid({ gridWidth, currentLayout }) {
);
// Add event listeners for config handle visibility when hovering over widget boundary
// This is needed even though we have hovered widget state because when hovered on boundary,
// the hovered widget state is empty, hence created a separate state for boundary
React.useEffect(() => {
const moveableBox = document.querySelector(`.moveable-control-box`);
const showConfigHandle = (e) => {
const targetId = e.target.offsetParent.getAttribute('target-id');
const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
configHandle.classList.add('config-handle-visible');
useStore.getState().setHoveredComponentBoundaryId(targetId);
};
const hideConfigHandle = (e) => {
const targetId = e.target.offsetParent.getAttribute('target-id');
const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
configHandle.classList.remove('config-handle-visible');
const hideConfigHandle = () => {
useStore.getState().setHoveredComponentBoundaryId('');
};
if (moveableBox) {
moveableBox.addEventListener('mouseover', showConfigHandle);
@ -376,49 +422,10 @@ export default function Grid({ gridWidth, currentLayout }) {
};
}, []);
const handleDragGridLinesVisibility = (e, events = []) => {
const { clientX, clientY } = e;
if (!document.elementFromPoint(clientX, clientY)) return;
const targetElems = document.elementsFromPoint(clientX, clientY);
const draggedOverElements = targetElems.filter(
(ele) =>
!events.some((event) => event.target.id === ele.id) &&
(ele.classList.contains('target') || ele.classList.contains('real-canvas'))
);
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
const appCanvas = document.getElementById('real-canvas');
// Show grid line for main canvas
draggedOverContainer?.classList.remove('hide-grid');
draggedOverContainer?.classList.add('show-grid');
// Remove 'show-grid' class from all sub-canvases
const canvasElms = document.getElementsByClassName('sub-canvas');
Array.from(canvasElms).forEach((element) => {
element.classList.remove('show-grid');
element.classList.add('hide-grid');
});
// Determine the potential new parent
const parentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
// Show grid for the appropriate canvas
if (parentId) {
const newParentCanvas = document.getElementById('canvas-' + parentId);
if (newParentCanvas) {
appCanvas?.classList?.remove('show-grid');
newParentCanvas?.classList.remove('hide-grid');
newParentCanvas?.classList.add('show-grid');
}
}
useGridStore.getState().actions.setDragTarget(parentId);
};
const handleDragGroupEnd = (e) => {
try {
hideGridLines();
setIsGroupDragging(false);
const { events, clientX, clientY } = e;
const initialParent = events[0].target.closest('.real-canvas');
// Get potential new parent using same logic as onDragEnd
@ -477,7 +484,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// Apply transform to return to original position
ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${
Math.round(_top / 10) * 10
Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT
}px)`;
}
});
@ -514,7 +521,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// Apply grid snapping and bounds
const snappedX = Math.round(posX / _gridWidth) * _gridWidth;
const snappedY = Math.round(posY / 10) * 10;
const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${snappedX}px, ${snappedY}px)`;
return {
@ -531,6 +538,18 @@ export default function Grid({ gridWidth, currentLayout }) {
}
};
React.useEffect(() => {
const components = Array.from(document.querySelectorAll('.active-target')).filter(
(component) => !selectedComponents.includes(component.getAttribute('widgetid'))
);
const draggingOrResizing = draggingComponentId || resizingComponentId;
if (!draggingOrResizing && components.length > 0) {
for (const component of components) {
component?.classList?.remove('active-target');
}
}
}, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]);
if (mode !== 'edit') return null;
return (
@ -557,7 +576,7 @@ export default function Grid({ gridWidth, currentLayout }) {
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
if (currentWidget.component?.parent) {
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent);
setDragParentId(currentWidget.component?.parent);
} else {
document.getElementById('real-canvas').classList.add('show-grid');
}
@ -584,9 +603,6 @@ export default function Grid({ gridWidth, currentLayout }) {
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${e.width}px`;
}
@ -612,12 +628,12 @@ export default function Grid({ gridWidth, currentLayout }) {
// When clicked on widget boundary/resizer, select the component
setSelectedComponents([e.target.id]);
}
showGridLines();
if (!isComponentVisible(e.target.id)) {
return false;
}
useGridStore.getState().actions.setResizingComponentId(e.target.id);
e.setMin([gridWidth, 10]);
e.setMin([gridWidth, GRID_HEIGHT]);
}}
onResizeEnd={(e) => {
try {
@ -629,7 +645,7 @@ export default function Grid({ gridWidth, currentLayout }) {
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid');
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth;
const height = Math.round(e?.lastEvent?.height / 10) * 10;
const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT;
const currentWidth = currentWidget.width * _gridWidth;
const diffWidth = e.lastEvent?.width - currentWidth;
@ -654,19 +670,17 @@ export default function Grid({ gridWidth, currentLayout }) {
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
const roundedTransformY = Math.round(transformY / 10) * 10;
transformY = transformY % 10 === 5 ? roundedTransformY - 10 : roundedTransformY;
const roundedTransformY = Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT;
transformY = transformY % GRID_HEIGHT === 5 ? roundedTransformY - GRID_HEIGHT : roundedTransformY;
e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${
Math.round(transformY / 10) * 10
Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT
}px)`;
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`;
}
if (!maxHeightHit || e.height < e.target.clientHeight) {
e.target.style.height = `${Math.round(e.lastEvent.height / 10) * 10}px`;
e.target.style.height = `${Math.round(e.lastEvent.height / GRID_HEIGHT) * GRID_HEIGHT}px`;
}
const resizeData = {
id: e.target.id,
@ -682,12 +696,11 @@ export default function Grid({ gridWidth, currentLayout }) {
} catch (error) {
console.error('ResizeEnd error ->', error);
}
useGridStore.getState().actions.setDragTarget();
setDragParentId(null);
toggleCanvasUpdater();
}}
onResizeGroupStart={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
parentElm.classList.add('show-grid');
showGridLines();
}}
onResizeGroup={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
@ -710,8 +723,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const { events } = e;
const newBoxs = [];
const parentElm = events[0].target.closest('.real-canvas');
parentElm.classList.remove('show-grid');
hideGridLines();
// TODO: Logic needs to be relooked post go live P2
groupResizeDataRef.current.forEach((ev) => {
@ -722,9 +734,9 @@ export default function Grid({ gridWidth, currentLayout }) {
let width = Math.round(ev.width / _gridWidth) * _gridWidth;
width = width < _gridWidth ? _gridWidth : width;
let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth;
let posY = Math.round(ev.drag.translate[1] / 10) * 10;
let height = Math.round(ev.height / 10) * 10;
height = height < 10 ? 10 : height;
let posY = Math.round(ev.drag.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
let height = Math.round(ev.height / GRID_HEIGHT) * GRID_HEIGHT;
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
@ -752,7 +764,7 @@ export default function Grid({ gridWidth, currentLayout }) {
let posX = currentWidget?.layouts[currentLayout].left * _gridWidth;
let posY = currentWidget?.layouts[currentLayout].top;
let height = currentWidget?.layouts[currentLayout].height;
height = height < 10 ? 10 : height;
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
@ -767,6 +779,11 @@ export default function Grid({ gridWidth, currentLayout }) {
}}
checkInput
onDragStart={(e) => {
// This is to prevent parent component from being dragged and the stop the propagation of the event
if (getHoveredComponentForGrid() !== e.target.id) {
return false;
}
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
const box = boxList.find((box) => box.id === e.target.id);
// Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS
@ -779,7 +796,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// to handle their own interactions like column resizing or card dragging
let isDragOnInnerElement = false;
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
Also user dont need to drag an calender from using popup */
if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) {
return false;
@ -809,10 +826,6 @@ export default function Grid({ gridWidth, currentLayout }) {
return false;
}
}
// This is to prevent parent component from being dragged and the stop the propagation of the event
if (getHoveredComponentForGrid() !== e.target.id) {
return false;
}
}}
onDragEnd={(e) => {
try {
@ -820,141 +833,103 @@ export default function Grid({ gridWidth, currentLayout }) {
useGridStore.getState().actions.setDraggingComponentId(null);
isDraggingRef.current = false;
}
prevDragParentId.current = null;
newDragParentId.current = null;
setDragParentId(null);
if (!e.lastEvent) {
return;
if (!e.lastEvent) return;
// Build the drag context from the event
const dragContext = dragContextBuilder({ event: e, widgets: boxList });
const { target, source, dragged } = dragContext;
const targetSlotId = target?.slotId;
const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth;
// const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || [];
// const draggedWidgetType = dragged.widgetType;
const isParentChangeAllowed = dragContext.isDroppable;
// Compute new position
let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged);
const isModalToCanvas = source.isModal && target.slotId === 'real-canvas';
if (isParentChangeAllowed && !isModalToCanvas) {
const parent = target.slotId === 'real-canvas' ? null : target.slotId;
// Special case for Modal; If source widget is modal, prevent drops to canvas
handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]);
} else {
const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth;
left = dragged.left * sourcegridWidth;
top = dragged.top;
!isModalToCanvas ??
toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
}
let draggedOverElemId = boxList.find((box) => box.id === e.target.id)?.parent;
let draggedOverElemIdType;
const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
let draggedOverElem;
if (document.elementFromPoint(e.clientX, e.clientY) && parentComponent?.component?.component !== 'Modal') {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
draggedOverElem = targetElems.find((ele) => {
const isOwnChild = e.target.contains(ele); // if the hovered element is a child of actual draged element its not considered
if (isOwnChild) return false;
// Apply transform for smooth transition
e.target.style.transform = `translate(${left}px, ${top}px)`;
let isDroppable = ele.id !== e.target.id && ele.classList.contains('drag-container-parent');
if (isDroppable) {
let widgetId = ele?.getAttribute('component-id') || ele.id;
let widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
if (!widgetType) {
widgetId = widgetId.split('-').slice(0, -1).join('-');
widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
}
if (
!['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(
widgetType
)
) {
isDroppable = false;
}
}
return isDroppable;
});
draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id;
draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type');
}
const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
const currentParentId = boxList.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent;
let left = e.lastEvent?.translate[0];
let top = e.lastEvent?.translate[1];
if (
['Listview', 'Kanban', 'Container'].includes(
boxList.find((box) => box.id === draggedOverElemId)?.component?.component
)
) {
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const maxY = containerHeight - e.target.clientHeight;
top = top > maxY ? maxY : top;
}
const currentWidget = boxList.find(({ id }) => id === e.target.id)?.component?.component;
const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId;
draggedOverElemIdType = getComponentTypeFromId(parentId);
const parentWidget = draggedOverElemIdType === 'Kanban' ? 'Kanban_card' : draggedOverElemIdType;
const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
if (draggedOverElemId !== currentParentId) {
if (isParentChangeAllowed) {
const draggedOverWidget = boxList.find((box) => box.id === draggedOverElemId);
let parentWidgetType = boxList.find((box) => box.id === draggedOverElemId)?.component?.component;
// @TODO - When dropping back to container from canvas, the boxList doesn't have canvas header,
// boxList will return null. But we need to tell getMouseDistanceFromParentDiv parentWidgetType is container
// As container id is like 'canvas-2375e23765e-123234'
if (parentId && !parentWidgetType && draggedOverElemId.includes('-header')) {
parentWidgetType = 'Container';
}
let { left: _left, top: _top } = getMouseDistanceFromParentDiv(
e,
draggedOverWidget?.component?.component === 'Kanban' ? draggedOverElem : draggedOverElemId,
parentWidgetType
);
left = _left;
top = _top;
} else {
const currBox = boxList.find((l) => l.id === e.target.id);
left = currBox.left * gridWidth;
top = currBox.top;
toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`);
}
}
e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${
Math.round(top / 10) * 10
}px)`;
if (draggedOverElemId === currentParentId || isParentChangeAllowed) {
handleDragEnd([
{
id: e.target.id,
x: left,
y: Math.round(top / 10) * 10,
parent: isParentChangeAllowed ? draggedOverElemId : undefined,
},
]);
}
const box = boxList.find((box) => box.id === e.target.id);
//
setTimeout(() => setSelectedComponents([box.id]));
// Select the dragged component after drop
setTimeout(() => setSelectedComponents([dragged.id]));
} catch (error) {
console.log('draggedOverElemId->error', error);
console.error('Error in onDragEnd:', error);
}
// Hide all sub-canvases
var canvasElms = document.getElementsByClassName('sub-canvas');
var elementsArray = Array.from(canvasElms);
elementsArray.forEach(function (element) {
element.classList.remove('show-grid');
element.classList.add('hide-grid');
});
document.getElementById('real-canvas')?.classList.remove('show-grid');
setCanvasBounds({ ...CANVAS_BOUNDS });
hideGridLines();
toggleCanvasUpdater();
}}
onDrag={(e) => {
// Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again
if (!isDraggingRef.current) {
useGridStore.getState().actions.setDraggingComponentId(e.target.id);
showGridLines();
isDraggingRef.current = true;
}
const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
const currentWidget = boxList.find((box) => box.id === e.target.id);
const currentParentId =
currentWidget?.component?.parent === null ? 'canvas' : currentWidget?.component?.parent;
const _gridWidth = useGridStore.getState().subContainerWidths[dragParentId] || gridWidth;
const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current;
let top = e.translate[1];
let left = e.translate[0];
// Snap to grid
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
// This logic is to handle the case when the dragged element is over a new canvas
if (_dragParentId !== currentParentId) {
left = e.translate[0];
top = e.translate[1];
}
// Special case for Modal
if (parentComponent?.component?.component === 'Modal') {
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const containerWidth = elemContainer.clientWidth;
const maxY = containerHeight - e.target.clientHeight;
const maxLeft = containerWidth - e.target.clientWidth;
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
const parentComponent = boxList.find((box) => box.id === parentId);
const parentWidgetType = parentComponent?.component?.component;
const isOnHeaderOrFooter = oldParentId
? oldParentId.includes('-header') || oldParentId.includes('-footer')
: false;
const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter;
const isParentNewModal = parentComponent?.component?.component === 'ModalV2';
const isParentLegacyModal = parentComponent?.component?.component === 'Modal';
const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
top = top < 0 ? 0 : top > maxY ? maxY : top;
left = left < 0 ? 0 : left > maxLeft ? maxLeft : left;
if (isParentModal) {
const modalContainer = e.target.closest('.tj-modal-widget-content');
const mainCanvas = document.getElementById('real-canvas');
const mainRect = mainCanvas.getBoundingClientRect();
const modalRect = modalContainer.getBoundingClientRect();
const relativePosition = {
top: modalRect.top - mainRect.top,
right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
bottom: modalRect.height + (modalRect.top - mainRect.top),
left: modalRect.left - mainRect.left,
};
setCanvasBounds({ ...relativePosition });
}
e.target.style.transform = `translate(${left}px, ${top}px)`;
@ -963,8 +938,32 @@ export default function Grid({ gridWidth, currentLayout }) {
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
);
handleDragGridLinesVisibility(e, [{ target: e.target }]);
// This block is to show grid lines on the canvas when the dragged element is over a new canvas
if (document.elementFromPoint(e.clientX, e.clientY)) {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
const draggedOverElements = targetElems.filter(
(ele) =>
(ele.id !== e.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas')
);
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
// Determine potential new parent
let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
if (newParentId === e.target.id) {
newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent;
} else if (parentComponent?.component?.component === 'Modal') {
// Never update parentId for Modal
newParentId = parentComponent?.id;
}
if (newParentId !== prevDragParentId.current) {
setDragParentId(newParentId === 'canvas' ? null : newParentId);
newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
prevDragParentId.current = newParentId;
}
}
// Postion ghost element exactly as same at dragged element
if (document.getElementById(`moveable-drag-ghost`)) {
document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`;
@ -979,31 +978,26 @@ export default function Grid({ gridWidth, currentLayout }) {
parentElm?.classList?.add('show-grid');
}
handleDragGridLinesVisibility(ev, events);
events.forEach((ev) => {
let left = ev.translate[0];
let top = ev.translate[1];
const currentWidget = boxList.find(({ id }) => id === ev.target.id);
const _gridWidth =
useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth;
let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${left}px, ${top}px)`;
});
updateNewPosition(events);
}}
onDragGroupStart={({ events }) => {
const parentElm = events[0]?.target?.closest('.real-canvas');
parentElm?.classList?.add('show-grid');
showGridLines();
setIsGroupDragging(true);
}}
onDragGroupEnd={(e) => {
handleDragGroupEnd(e);
toggleCanvasUpdater();
}}
//snap settgins
snappable={true}
snapThreshold={10}
isDisplaySnapDigit={false}
bounds={CANVAS_BOUNDS}
displayAroundControls={true}
controlPadding={20}
onClickGroup={(e) => {
const targetId =
e.inputEvent.target.id || e.inputEvent.target.closest('.moveable-box')?.getAttribute('widgetid');
@ -1019,6 +1013,43 @@ export default function Grid({ gridWidth, currentLayout }) {
}
}
}}
//snap settgins
snappable={true}
snapGap={false}
isDisplaySnapDigit={false}
snapThreshold={GRID_HEIGHT}
bounds={canvasBounds}
// Guidelines configuration
elementGuidelines={elementGuidelines}
snapDirections={{
top: true,
right: true,
bottom: true,
left: true,
center: false,
middle: false,
}}
elementSnapDirections={{
top: true,
left: true,
bottom: true,
right: true,
center: false,
middle: false,
}}
onSnap={(e) => {
const components = e.elements;
if (isArray(componentsSnappedTo.current)) {
for (const component of componentsSnappedTo.current) {
component?.element?.classList?.remove('active-target');
}
}
componentsSnappedTo.current = components;
for (const component of components) {
component.element.classList.add('active-target');
}
}}
snapGridAll={true}
/>
</>
);

View file

@ -291,6 +291,7 @@ export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
? document.getElementById(id)
: id
: document.getElementsByClassName('real-canvas')[0];
parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id);
if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') {
parentDiv = document.getElementById('canvas-' + id);
}
@ -391,3 +392,25 @@ export function hasParentWithClass(child, className) {
return false;
}
export function showGridLines() {
var canvasElms = document.getElementsByClassName('sub-canvas');
var elementsArray = Array.from(canvasElms);
elementsArray.forEach(function (element) {
element.classList.remove('hide-grid');
element.classList.add('show-grid');
});
document.getElementById('real-canvas')?.classList.remove('hide-grid');
document.getElementById('real-canvas')?.classList.add('show-grid');
}
export function hideGridLines() {
var canvasElms = document.getElementsByClassName('sub-canvas');
var elementsArray = Array.from(canvasElms);
elementsArray.forEach(function (element) {
element.classList.remove('show-grid');
element.classList.add('hide-grid');
});
document.getElementById('real-canvas')?.classList.remove('show-grid');
document.getElementById('real-canvas')?.classList.add('hide-grid');
}

View file

@ -0,0 +1,266 @@
/**
* Drag Context Breakdown:
*
* This object encapsulates all relevant details about a drag event,
* grouping the **source (where the widget came from)** and **target (where it's being dropped)**.
*
* Core Concepts:
* - `draggedWidget` The widget being dragged (`e.target`).
* - `sourceSlot` The original parent container of `draggedWidget`.
* - This could be a **header, footer, or a sub-container (like a container body)**.
* - `targetSlot` The new parent container where `draggedWidget` is dropped.
* - `sourceWidget` The **widget that owns** `sourceSlot` (its direct parent).
* - `targetWidget` The **widget that owns** `targetSlot` (its direct parent).
*
* These entities are structured into a **contextual grouping**, allowing for easy access:
*
* {
* source: {
* widget: sourceWidget, // The original widget that holds the source slot.
* slot: sourceSlot, // The slot where the widget was initially located.
* id: sourceWidget.id, // Unique identifier of the source widget.
* slotId: sourceSlot.id, // Unique identifier of the source slot.
*
* isModal: computed function, // Checks if sourceWidget is a Modal.
* slotType: computed function, // Determines if the slot is a header, footer, or body.
* widgetType: computed function, // Returns the type of the widget (e.g., Table, Form, etc.).
* },
*
* target: {
* widget: targetWidget, // The new widget where the dragged widget is being placed.
* slot: targetSlot, // The slot inside `targetWidget` where the drop is happening.
* id: targetWidget.id, // Unique identifier of the target widget.
* slotId: targetSlot.id, // Unique identifier of the target slot.
*
* isModal: computed function, // Checks if targetWidget is a Modal.
* slotType: computed function, // Determines if the slot is a header, footer, or body.
* widgetType: computed function, // Returns the type of the target widget.
* }
* }
*
* Additional Checks:
* - `isSourceModal` **Is the source inside a modal?**
* - `isTargetModal` **Is the target inside a modal?**
* - `isDraggingToModalSlots` **Is the widget being dragged into a modal slot (header/footer)?**
* - `targetSlotType` **Determines whether the drop is happening in a header, footer, or body.**
*
* Why This Matters?
* - This structure helps **validate and restrict movements**, ensuring widgets follow UI constraints.
* - Prevents invalid drops (e.g., putting a button inside a Table component).
* - Enables **modular and flexible** widget movement across different UI sections.
*/
import { getMouseDistanceFromParentDiv } from '../gridUtils';
import {
RESTRICTED_WIDGETS_CONFIG,
RESTRICTED_WIDGET_SLOTS_CONFIG,
} from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig';
const CANVAS_ID = 'canvas';
const REAL_CANVAS_ID = 'real-canvas';
/**
* Represents the widget being dragged.
*
* This class encapsulates all necessary information about the dragged widget,
* including its type, position, and whether it is allowed to move into certain areas.
*/
export class DragEntity {
constructor(widget) {
this.widget = widget; // The widget object being dragged
this.id = widget?.id || null; // Unique ID of the dragged widget
this.left = widget.left; // Initial X position (relative to grid)
this.top = widget.top; // Initial Y position (relative to grid)
}
get widgetType() {
return this.widget?.component?.component || null;
}
}
/**
* Defines a **droppable area** in the canvas.
*
* A droppable area is a container that can accept dragged widgets.
* This class helps determine if a slot is valid and handles various properties like modals.
*/
export class DropAreaEntity {
static dropAreaWidgets = ['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'ModalV2', 'Listview', 'Container', 'Table'];
constructor(widget, slotId) {
this.widget = widget; // The widget that owns this slot
this.id = widget?.id || CANVAS_ID; // ID of the widget
this.slotId = slotId || REAL_CANVAS_ID; // ID of the slot where the widget is located
}
// Checks if the widget is a modal
get isModal() {
return ['Modal', 'ModalV2'].includes(this.widget?.component?.component);
}
// Checks if the widget is the new version of modal
get isNewModal() {
return this.widget?.component?.component === 'ModalV2';
}
// Checks if the widget is the legacy modal
get isLegacyModal() {
return this.widget?.component?.component === 'Modal';
}
// Determines if the slot belongs to a modal's header/footer
get isInModalSlot() {
return this.isNewModal && this.isOnCustomSlot;
}
// Identifies if the slot is a custom slot (e.g., modal header/footer)
get isOnCustomSlot() {
return this.slotId.includes('-header') || this.slotId.includes('-footer');
}
// Determines if the slot is a valid drop target
get isDroppable() {
return DropAreaEntity.dropAreaWidgets.includes(this.widgetType);
}
// Returns the type of slot (header, footer, body, etc.)
get slotType() {
return this.slotId ? this.slotId.split('-').pop() : CANVAS_ID;
}
// Returns the type of the widget inside the slot
get widgetType() {
return this.widget?.component?.component || CANVAS_ID;
}
}
/**
* Represents the **dragging context**, encapsulating information
* about the source, target, and the dragged widget.
*
* This helps determine:
* - Whether the move is valid
* - Where the widget should be placed
* - Any restrictions based on parent-child relationships
*/
export class DragContext {
constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets }) {
const sourceWidgetId = sourceSlotId?.slice(0, 36);
const sourceWidget = getWidgetById(widgets, sourceWidgetId);
const targetWidgetId = targetSlotId?.slice(0, 36);
const targetWidget = getWidgetById(widgets, targetWidgetId);
const draggedWidget = getWidgetById(widgets, draggedWidgetId);
this.source = new DropAreaEntity(sourceWidget, sourceSlotId);
this.target = new DropAreaEntity(targetWidget, targetSlotId);
this.dragged = new DragEntity(draggedWidget);
this.widgets = widgets;
}
/**
* Updates the **target slot** dynamically as the drag event progresses.
*/
updateTarget(targetSlotId) {
const targetWidgetId = targetSlotId?.slice(0, 36);
const targetWidget = getWidgetById(this.widgets, targetWidgetId);
this.target = new DropAreaEntity(targetWidget, targetSlotId);
}
get isDroppable() {
const { dragged, target } = this;
const restrictedWidgetsOnTarget = RESTRICTED_WIDGETS_CONFIG?.[target.widgetType] || [];
const restrictedWidgetsOnTargetSlot = RESTRICTED_WIDGET_SLOTS_CONFIG?.[target.slotType] || [];
const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot];
return !restrictedWidgets.includes(dragged.widgetType);
ß;
}
}
/**
* Constructs the **dragging context** by gathering all relevant details from the event.
*/
export function dragContextBuilder({ event, widgets }) {
const draggedWidgetId = event.target.id;
const draggedWidget = getWidgetById(widgets, draggedWidgetId);
const sourceSlotId = draggedWidget.parent;
// Initialize drag context
const context = new DragContext({ widgets, draggedWidgetId, sourceSlotId, targetSlotId: sourceSlotId });
// Determine the potential drop target
const targetSlotId = getDroppableSlotIdOnScreen(event, widgets);
context.updateTarget(targetSlotId);
return context;
}
/**
* Given an event, finds the **nearest valid droppable slot**.
*/
export const getDroppableSlotIdOnScreen = (event, widgets) => {
const [slotId] = document
.elementsFromPoint(event.clientX, event.clientY)
.filter(
(ele) =>
!event.target.contains(ele) && ele.id !== event.target.id && ele.classList.contains('drag-container-parent')
)
.map((ele) => extractSlotId(ele))
.filter((slotId) => {
const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID;
return DropAreaEntity.dropAreaWidgets.includes(widgetType);
});
return slotId;
};
/**
* Finds a widget by its ID.
*/
export function getWidgetById(boxList, targetId) {
return boxList.find((box) => box.id === targetId) ?? null;
}
/**
* Extracts the **slot ID** from a given DOM element.
*/
const extractSlotId = (element) => {
return element?.getAttribute('component-id') || element.id.replace(/^canvas-/, '');
};
/**
* Computes the final (left, top) position for a dragged widget based on grid snapping and drop conditions.
*
* @param {Object} event - Drag event object containing movement data.
* @param {DropAreaEntity} target - The target drop area entity (where widget is dropped).
* @param {boolean} isParentChangeAllowed - Whether the widget can move to the target.
* @param {number} gridWidth - The width of the grid for alignment.
* @param {DragEntity} dragged - The entity being dragged.
* @returns {Object} { left, top } - The computed position.
*/
export const getAdjustedDropPosition = (event, target, isParentChangeAllowed, gridWidth, dragged) => {
let left = event.lastEvent?.translate[0];
let top = event.lastEvent?.translate[1];
if (isParentChangeAllowed) {
// Compute the relative position inside the new container
const { left: adjustedLeft, top: adjustedTop } = getMouseDistanceFromParentDiv(
event,
target.slotId,
target.widgetType
);
return {
left: Math.round(adjustedLeft / gridWidth) * gridWidth, // Snap to the nearest grid column
top: Math.round(adjustedTop / 10) * 10, // Snap to the nearest 10px
};
}
// If movement is restricted, revert to original position
return {
left: dragged.left * gridWidth,
top: dragged.top,
};
};

View file

@ -89,6 +89,7 @@ const WidgetWrapper = memo(
widgetHeight={layoutData.height}
showHandle={isWidgetActive}
componentType={componentType}
visibility={visibility}
/>
)}
<RenderWidget

View file

@ -6,7 +6,7 @@ export const CANVAS_WIDTHS = Object.freeze({
rightSideBarWidth: 300,
});
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container'];
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container', 'ModalV2'];
export const DEFAULT_CANVAS_WIDTH = 1292;

View file

@ -89,7 +89,8 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
const defaultChildren = deepClone(parentMeta)['defaultChildren'];
defaultChildren.forEach((child) => {
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child;
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles, slotName } =
child;
const componentMeta = deepClone(componentTypes.find((component) => component.component === componentName));
const componentData = JSON.parse(JSON.stringify(componentMeta));
@ -139,7 +140,12 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
}
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
const _parent = getParentComponentIdByType(child, parentMeta.component, parentId);
const _parent = getParentComponentIdByType({
child,
parentComponent: parentMeta.component,
parentId,
slotName,
});
const newChildComponent = {
id: uuidv4(),
@ -199,7 +205,8 @@ export const getAllChildComponents = (allComponents, parentId) => {
allComponents[parentId]?.component?.component === 'Tabs' ||
allComponents[parentId]?.component?.component === 'Calendar' ||
allComponents[parentId]?.component?.component === 'Kanban' ||
allComponents[parentId]?.component?.component === 'Container';
allComponents[parentId]?.component?.component === 'Container' ||
allComponents[parentId]?.component?.component === 'ModalV2';
if (componentParentId && isParentTabORCalendar) {
let childComponent = deepClone(allComponents[componentId]);
@ -249,7 +256,6 @@ export const copyComponents = ({ isCut = false, isCloning = false }) => {
const parentComponentId = isChildOfTabsOrCalendar(selectedComponent, allComponents)
? selectedComponent.component.parent.split('-').slice(0, -1).join('-')
: selectedComponent?.component?.parent;
if (parentComponentId) {
// Check if the parent component is also selected
const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId);
@ -320,7 +326,8 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
return (
parentComponent.component.component === 'Tabs' ||
parentComponent.component.component === 'Calendar' ||
parentComponent.component.component === 'Container'
parentComponent.component.component === 'Container' ||
parentComponent.component.component === 'ModalV2'
);
}
@ -483,11 +490,14 @@ export function pasteComponents(targetParentId, copiedComponentObj) {
// Prevent pasting if the parent subcontainer was deleted during a cut operation
if (
targetParentId &&
// Check if targetParentId is deleted from the components
!Object.keys(components).find(
(key) =>
targetParentId === key ||
(components?.[key]?.component.component === 'Tabs' &&
targetParentId?.split('-')?.slice(0, -1)?.join('-') === key)
targetParentId?.split('-')?.slice(0, -1)?.join('-') === key) ||
(['Container', 'Form', 'Modal'].includes(components?.[key]?.component.component) &&
['header', 'footer'].some((section) => targetParentId.includes(section)))
)
) {
return;
@ -655,10 +665,23 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
return canvasBgColor;
};
export const getParentComponentIdByType = (child, parentComponent, parentId) => {
export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => {
const { tab } = child;
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
else if (parentComponent === 'Container') return `${parentId}-header`;
else if (parentComponent === 'Container' || parentComponent === 'ModalV2') {
return `${parentId}-${slotName}`;
}
return parentId;
};
export const getParentWidgetFromId = (parentType, parentId) => {
const isAddingToSlot = parentId?.includes('-header') || parentId?.includes('-footer');
if (parentType === 'ModalV2' && isAddingToSlot) {
return 'ModalSlot';
} else if (parentType === 'Kanban') {
return 'Kanban_card';
}
return parentType;
};

View file

@ -45,15 +45,15 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
const queryName = selectedQuery?.name ?? '';
const sourcecomponentName = selectedDataSource?.kind?.charAt(0).toUpperCase() + selectedDataSource?.kind?.slice(1);
const ElementToRender = selectedDataSource?.pluginId ? source : allSources[sourcecomponentName];
const ElementToRender = selectedDataSource?.plugin_id ? source : allSources[sourcecomponentName];
const defaultOptions = useRef({});
const isFreezed = useStore((state) => state.getShouldFreeze());
useEffect(() => {
setDataSourceMeta(
selectedQuery?.pluginId
? selectedQuery?.manifestFile?.data?.source
selectedQuery?.plugin_id
? selectedQuery?.manifest_file?.data?.source
: DataSourceTypes.find((source) => source.kind === selectedQuery?.kind)
);
setSelectedQueryId(selectedQuery?.id);
@ -188,7 +188,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
<ElementToRender
renderCopilot={renderCopilot}
key={selectedQuery?.id}
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
pluginSchema={selectedDataSource?.plugin?.operations_file?.data}
selectedDataSource={selectedDataSource}
options={selectedQuery?.options}
optionsChanged={optionsChanged}
@ -281,7 +281,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE;
const docLink = isSampleDb
? 'https://docs.tooljet.com/docs/data-sources/sample-data-sources'
: selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
: selectedDataSource?.plugin_id && selectedDataSource.plugin_id.trim() !== ''
? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
: `https://docs.tooljet.com/docs/data-sources/${selectedDataSource?.kind}`;
return (

View file

@ -133,7 +133,7 @@ export const ComponentsManagerTab = ({ darkMode }) => {
'StarRating',
];
const integrationItems = ['Map'];
const layoutItems = ['Container', 'Listview', 'Tabs', 'Modal'];
const layoutItems = ['Container', 'Listview', 'Tabs', 'ModalV2'];
filteredComponents.forEach((f) => {
if (commonItems.includes(f)) commonSection.items.push(f);

View file

@ -2,12 +2,14 @@ import React, { useEffect } from 'react';
import { WidgetBox } from '../WidgetBox';
import { useDrag, useDragLayer } from 'react-dnd';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils';
import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants';
export const DragLayer = ({ index, component }) => {
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: 'box',
item: { componentType: component.component },
item: { componentType: component.component, component },
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
}),
[component.component]
@ -18,7 +20,6 @@ export const DragLayer = ({ index, component }) => {
}, []);
const size = component.defaultSize || { width: 30, height: 40 };
return (
<>
{isDragging && <CustomDragLayer size={size} />}
@ -30,32 +31,39 @@ export const DragLayer = ({ index, component }) => {
};
const CustomDragLayer = ({ size }) => {
const { currentOffset } = useDragLayer((monitor) => ({
const { currentOffset, item } = useDragLayer((monitor) => ({
currentOffset: monitor.getSourceClientOffset(),
item: monitor.getItem(),
}));
if (!currentOffset) return null;
const canvasWidth = document.getElementsByClassName('real-canvas')[0]?.getBoundingClientRect()?.width;
const canvasWidth = item?.canvasWidth;
const canvasBounds = item?.canvasRef?.getBoundingClientRect();
const height = size.height;
const width = (canvasWidth * size.width) / 43;
const width = (canvasWidth * size.width) / NO_OF_GRIDS;
// Calculate position relative to the current canvas (parent or child)
const left = currentOffset.x - (canvasBounds?.left || 0);
const top = currentOffset.y - (canvasBounds?.top || 0);
const [x, y] = snapToGrid(canvasWidth, left, top);
return (
<div
style={{
position: 'fixed',
pointerEvents: 'none',
zIndex: -1,
left: 0,
top: 0,
zIndex: 1000,
left: canvasBounds?.left || 0,
top: canvasBounds?.top || 0,
height: `${height}px`,
width: `${width}px`,
}}
>
<div
style={{
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
transform: `translate(${x}px, ${y}px)`,
background: '#D9E2FC',
opacity: '0.7',
height: '100%',

View file

@ -1 +1 @@
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];

View file

@ -23,6 +23,7 @@ const SHOW_ADDITIONAL_ACTIONS = [
'Button',
'RichTextEditor',
'Image',
'ModalV2',
];
const PROPERTIES_VS_ACCORDION_TITLE = {
Text: 'Data',
@ -34,6 +35,7 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
Button: 'Data',
Image: 'Data',
Container: 'Data',
ModalV2: 'Data',
};
export const DefaultComponent = ({ componentMeta, darkMode, ...restProps }) => {
@ -151,7 +153,8 @@ export const baseComponentProperties = (
'properties',
currentState,
allComponents,
darkMode
darkMode,
''
)
),
});

View file

@ -0,0 +1,110 @@
import React from 'react';
import Accordion from '@/_ui/Accordion';
import { renderElement } from '../Utils';
import { baseComponentProperties } from './DefaultComponent';
import { resolveReferences } from '@/_helpers/utils';
const INDEX_OF_TRIGGER = 2;
export const ModalV2 = ({ componentMeta, darkMode, ...restProps }) => {
const {
layoutPropertyChanged,
component,
paramUpdated,
dataQueries,
currentState,
eventsChanged,
apps,
allComponents,
} = restProps;
let properties = [];
let additionalActions = [];
let dataProperties = [];
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
for (const [key] of Object.entries(componentMeta?.properties)) {
if (componentMeta?.properties[key]?.section === 'additionalActions') {
additionalActions.push(key);
} else if (componentMeta?.properties[key]?.accordian === 'Data') {
dataProperties.push(key);
} else {
properties.push(key);
}
}
const renderCustomElement = (param, paramType = 'properties') => {
return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
};
const conditionalAccordionItems = (component) => {
const useDefaultButton = resolveReferences(
component.component.definition.properties.useDefaultButton?.value ?? false
);
const accordionItems = [];
let renderOptions = [];
const options = ['visibility', 'disabledTrigger', 'useDefaultButton'];
options.map((option) => renderOptions.push(renderCustomElement(option)));
const conditionalOptions = [{ name: 'triggerButtonLabel', condition: useDefaultButton }];
conditionalOptions.map(({ name, condition }) => {
if (condition) renderOptions.push(renderCustomElement(name));
});
accordionItems.push({
title: 'Trigger',
children: renderOptions,
});
return accordionItems;
};
if (component.component.definition.properties.size.value === 'fullscreen') {
component.component.properties.modalHeight = {
...component.component.properties.modalHeight,
isHidden: true,
};
}
if (component.component.definition.properties.showHeader.value === '{{false}}') {
component.component.properties.headerHeight = {
...component.component.properties.headerHeight,
isHidden: true,
};
}
if (component.component.definition.properties.showFooter.value === '{{false}}') {
component.component.properties.footerHeight = {
...component.component.properties.footerHeight,
isHidden: true,
};
}
const accordionItems = baseComponentProperties(
dataProperties,
events,
component,
componentMeta,
layoutPropertyChanged,
paramUpdated,
dataQueries,
currentState,
eventsChanged,
apps,
allComponents,
validations,
darkMode,
[],
additionalActions
);
const [optionsItems] = conditionalAccordionItems(component);
// Insert the Trigger option as the third item
accordionItems.splice(INDEX_OF_TRIGGER, 0, optionsItems);
return <Accordion items={accordionItems} />;
};

View file

@ -37,7 +37,6 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
if (!Array.isArray(optionsValue)) {
optionsValue = Object.values(optionsValue);
}
const valuesToResolve = ['label', 'value'];
let options = [];
if (isDynamicOptionsEnabled || typeof optionsValue === 'string') {
@ -202,9 +201,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
});
setOptions(_options);
updateAllOptionsParams(_options);
setMarkedAsDefault(_value);
paramUpdated({ name: 'value' }, 'value', _value, 'properties');
updateAllOptionsParams(_options);
}
};

View file

@ -19,6 +19,7 @@ export const Code = ({
accordian,
placeholder,
validationFn,
isHidden = false,
}) => {
const currentState = useCurrentState();
@ -43,6 +44,7 @@ export const Code = ({
onChange({ name: 'iconVisibility' }, 'value', value, 'styles');
}
if (isHidden) return null;
return (
<div className={`field ${options.className}`} style={{ marginBottom: '8px' }}>
<CodeEditor

View file

@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { DefaultComponent } from './Components/DefaultComponent';
import { FilePicker } from './Components/FilePicker';
import { Modal } from './Components/Modal';
import { ModalV2 } from './Components/ModalV2';
import { CustomComponent } from './Components/CustomComponent';
import { Icon } from './Components/Icon';
import useFocus from '@/_hooks/use-focus';
@ -78,6 +79,7 @@ const NEW_REVAMPED_COMPONENTS = [
'Icon',
'Image',
'Container',
'ModalV2',
];
export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => {
@ -703,6 +705,9 @@ const GetAccordion = React.memo(
case 'FilePicker':
return <FilePicker {...restProps} />;
case 'ModalV2':
return <ModalV2 {...restProps} />;
case 'Modal':
return <Modal {...restProps} />;

View file

@ -50,7 +50,8 @@ export function renderCustomStyles(
componentConfig.component == 'MultiselectV2' ||
componentConfig.component == 'RadioButtonV2' ||
componentConfig.component == 'Button' ||
componentConfig.component == 'Image'
componentConfig.component == 'Image' ||
componentConfig.component == 'ModalV2'
) {
const paramTypeConfig = componentMeta[paramType] || {};
const paramConfig = paramTypeConfig[param] || {};
@ -131,6 +132,7 @@ export function renderElement(
const paramTypeDefinition = componentDefinition[paramType] || {};
const definition = paramTypeDefinition[param] || {};
const meta = componentMeta[paramType][param];
const isHidden = component.component.properties[param]?.isHidden ?? false;
if (
componentConfig.component == 'DropDown' ||
@ -170,6 +172,7 @@ export function renderElement(
component={component}
placeholder={placeholder}
validationFn={validationFn}
isHidden={isHidden}
/>
);
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import WidgetIcon from '@/../assets/images/icons/widgets';
import { useTranslation } from 'react-i18next';
const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
const NEW_WIDGETS = [
'ToggleSwitchV2',
'DropdownV2',
@ -12,6 +12,7 @@ const NEW_WIDGETS = [
'DaterangePicker',
'DatePickerV2',
'TimePicker',
'ModalV2',
];
export const WidgetBox = ({ component, darkMode }) => {

View file

@ -4,7 +4,14 @@ export const RESTRICTED_WIDGETS_CONFIG = {
Calendar: ['Calendar', 'Kanban'],
Container: ['Calendar', 'Kanban'],
Modal: ['Calendar', 'Kanban'],
ModalV2: ['Calendar', 'Kanban'],
ModalSlot: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
Tabs: ['Calendar', 'Kanban'],
Kanban_popout: ['Calendar', 'Kanban'],
Listview: ['Calendar', 'Kanban'],
};
export const RESTRICTED_WIDGET_SLOTS_CONFIG = {
header: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
footer: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
};

View file

@ -3,6 +3,7 @@ import {
tableConfig,
chartConfig,
modalConfig,
modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
@ -64,6 +65,7 @@ export const widgets = [
buttonConfig,
chartConfig,
modalConfig,
modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,

View file

@ -299,7 +299,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },

View file

@ -2,6 +2,7 @@ import { buttonConfig } from './button';
import { tableConfig } from './table';
import { chartConfig } from './chart';
import { modalConfig } from './modal';
import { modalV2Config } from './modalV2';
import { formConfig } from './form';
import { textinputConfig } from './textinput';
import { numberinputConfig } from './numberinput';
@ -62,7 +63,8 @@ export {
buttonConfig,
tableConfig,
chartConfig,
modalConfig,
modalConfig, //Deprecated
modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,

View file

@ -48,8 +48,12 @@ export const listviewConfig = {
data: {
type: 'code',
displayName: 'List data',
validation: {
schema: { type: 'array', element: { type: 'object' } },
schema: {
type: 'union',
schemas: [
{ type: 'array', element: { type: 'object' } },
{ type: 'array', element: { type: 'string' } },
],
defaultValue: "[{text: 'Sample text 1'}]",
},
},

View file

@ -1,6 +1,6 @@
export const modalConfig = {
name: 'Modal',
displayName: 'Modal',
name: 'ModalLegacy',
displayName: 'Modal (Legacy)',
description: 'Show pop-up windows',
component: 'Modal',
defaultSize: {

View file

@ -0,0 +1,277 @@
export const modalV2Config = {
name: 'Modal',
displayName: 'Modal',
description: 'Show pop-up windows',
component: 'ModalV2',
defaultSize: {
width: 10,
height: 34,
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
properties: {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
section: 'additionalActions',
},
visibility: {
type: 'toggle',
displayName: 'Modal trigger visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledTrigger: {
type: 'toggle',
displayName: 'Disable modal trigger',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
disabledModal: {
type: 'toggle',
displayName: 'Disable modal window',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
section: 'additionalActions',
},
useDefaultButton: {
type: 'toggle',
displayName: 'Use default trigger button',
validation: {
schema: {
type: 'boolean',
},
defaultValue: true,
},
},
triggerButtonLabel: {
type: 'code',
displayName: 'Trigger button label',
validation: {
schema: {
type: 'string',
},
defaultValue: 'Launch Modal',
},
},
// Data Accordion
showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
size: {
type: 'select',
displayName: 'Width',
accordian: 'Data',
options: [
{ name: 'small', value: 'sm' },
{ name: 'medium', value: 'lg' },
{ name: 'large', value: 'xl' },
{ name: 'fullscreen', value: 'fullscreen' },
],
validation: {
schema: { type: 'string' },
defaultValue: 'lg',
},
},
modalHeight: {
type: 'numberInput',
displayName: 'Height',
accordian: 'Data',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
accordian: 'Data',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
footerHeight: {
type: 'numberInput',
displayName: 'Footer height',
accordian: 'Data',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
},
events: {
onOpen: { displayName: 'On open' },
onClose: { displayName: 'On close' },
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 21,
left: 1,
height: 40,
},
displayName: 'ModalHeaderTitle',
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Modal title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 24,
left: 22,
height: 36,
},
displayName: 'ModalFooterCancel',
properties: ['text'],
styles: ['type', 'borderColor', 'padding'],
defaultValue: {
text: 'Button1',
type: 'outline',
borderColor: '#CCD1D5',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 24,
left: 32,
height: 36,
},
displayName: 'ModalFooterConfirm',
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
],
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
bodyBackgroundColor: {
type: 'color',
displayName: 'Body background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
triggerButtonBackgroundColor: {
type: 'color',
displayName: 'Trigger button background color',
validation: {
schema: { type: 'string' },
defaultValue: false,
},
},
triggerButtonTextColor: {
type: 'color',
displayName: 'Trigger button text color',
validation: {
schema: { type: 'string' },
defaultValue: false,
},
},
},
exposedVariables: {
show: false,
isDisabledModal: false,
isDisabledTrigger: false,
isVisible: true,
isLoading: false,
},
actions: [
{
handle: 'open',
displayName: 'Open',
},
{
handle: 'close',
displayName: 'Close',
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
},
{
handle: 'setDisableTrigger',
displayName: 'Set disable trigger',
params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setDisableModal',
displayName: 'Set disable modal',
params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set loading',
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
properties: {
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledTrigger: { value: '{{false}}' },
disabledModal: { value: '{{false}}' },
useDefaultButton: { value: `{{true}}` },
triggerButtonLabel: { value: `Launch Modal` },
size: { value: 'lg' },
showHeader: { value: '{{true}}' },
showFooter: { value: '{{true}}' },
hideCloseButton: { value: '{{false}}' },
hideOnEsc: { value: '{{true}}' },
closeOnClickingOutside: { value: '{{false}}' },
modalHeight: { value: 400 },
headerHeight: { value: 80 },
footerHeight: { value: 80 },
},
events: [],
styles: {
headerBackgroundColor: { value: '#ffffffff' },
footerBackgroundColor: { value: '#ffffffff' },
bodyBackgroundColor: { value: '#ffffffff' },
triggerButtonBackgroundColor: { value: '#4D72FA' },
triggerButtonTextColor: { value: '#ffffffff' },
},
},
};

View file

@ -45,7 +45,7 @@ export const Container = ({
border: `1px solid ${borderColor}`,
height,
display: isVisible ? 'flex' : 'none',
overflow: 'hidden auto',
flexDirection: 'column',
position: 'relative',
boxShadow,
};
@ -66,9 +66,7 @@ export const Container = ({
return (
<div
className={`jet-container tw-flex tw-flex-col ${isLoading && 'jet-container-loading'} ${
properties.showHeader && 'jet-container--with-header'
}`}
className={`jet-container widget-type-container ${properties.loadingState && 'jet-container-loading'}`}
id={id}
data-disabled={isDisabled}
style={computedStyles}

View file

@ -0,0 +1,34 @@
import React from 'react';
import { default as BootstrapModal } from 'react-bootstrap/Modal';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode, width, footerHeight, onClick }) => {
const canvasFooterHeight = getCanvasHeight(footerHeight);
return (
<BootstrapModal.Footer style={{ ...customStyles.modalFooter }} data-cy={`modal-footer`} onClick={onClick}>
<SubContainer
id={`${id}-footer`}
canvasHeight={canvasFooterHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
margin: 0,
backgroundColor: 'transparent',
overflowX: 'hidden',
overflowY: isDisabled ? 'hidden' : 'auto',
}}
/>
{isDisabled && (
<div
id={`${id}-footer-disabled`}
className="tj-modal-disabled-overlay"
style={{ height: footerHeight || '100%' }}
onClick={onClick}
onDrop={(e) => e.stopPropagation()}
/>
)}
</BootstrapModal.Footer>
);
});

View file

@ -0,0 +1,69 @@
import React from 'react';
import { default as BootstrapModal } from 'react-bootstrap/Modal';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
export const ModalHeader = React.memo(
({ id, isDisabled, customStyles, hideCloseButton, darkMode, width, onHideModal, headerHeight, onClick }) => {
const canvasHeaderHeight = getCanvasHeight(headerHeight);
return (
<BootstrapModal.Header style={{ ...customStyles.modalHeader }} data-cy={`modal-header`} onClick={onClick}>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<SubContainer
id={`${id}-header`}
canvasHeight={canvasHeaderHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
backgroundColor: 'transparent',
overflowX: 'hidden',
overflowY: isDisabled ? 'hidden' : 'auto',
}}
/>
</div>
{isDisabled && (
<div
id={`${id}-header-disabled`}
className="tj-modal-disabled-overlay"
style={{ height: headerHeight || '100%' }}
onClick={onClick}
onDrop={(e) => e.stopPropagation()}
/>
)}
{!hideCloseButton && (
<div className="tw-w-14 tw-h-14 tw-flex tw-items-center tw-justify-center tw-pr-4 tw-relative">
<span
className={`tj-modal-close-button ${isDisabled ? 'is-disabled' : ''}`}
data-cy={`modal-close-button`}
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onHideModal();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icon-tabler-x"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</span>
</div>
)}
</BootstrapModal.Header>
);
}
);

View file

@ -0,0 +1,134 @@
import React, { useEffect } from 'react';
import { default as BootstrapModal } from 'react-bootstrap/Modal';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
import { ConfigHandle } from '@/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle';
import { ModalHeader } from '@/AppBuilder/Widgets/ModalV2/Components/Header';
import { ModalFooter } from '@/AppBuilder/Widgets/ModalV2/Components/Footer';
export const ModalWidget = ({ ...restProps }) => {
const {
customStyles,
parentRef,
id,
showConfigHandler,
isDisabled,
isLoading,
modalBodyHeight,
onHideModal,
hideCloseButton,
darkMode,
modalWidth,
showHeader,
hideOnEsc,
showFooter,
headerHeight,
footerHeight,
onSelectModal,
} = restProps['modalProps'];
// When the modal body is clicked capture it and use the callback to set the selected component as modal
const handleModalSlotClick = (event) => {
const clickedComponentId = event.target.getAttribute('component-id');
const clickedId = event.target.getAttribute('id');
// Check if the clicked element is part of the modal canvas & same widget with id
if (clickedComponentId?.includes(id)) {
onSelectModal(id);
} else if (clickedId?.includes(id)) {
onSelectModal(id);
}
};
useEffect(() => {
// When modal is active, prevent drop event on backdrop (else widgets droppped will get added to canvas)
const preventBackdropDrop = (e) => {
if (e.target.className === 'fade modal show') {
e.preventDefault();
e.stopPropagation();
}
};
document.addEventListener('drop', preventBackdropDrop);
return () => {
document.removeEventListener('drop', preventBackdropDrop);
};
}, []);
return (
<BootstrapModal
{...restProps}
contentClassName="modal-component tj-modal-widget-content"
animation={true}
onEscapeKeyDown={(e) => {
e.preventDefault();
if (hideOnEsc) {
onHideModal();
}
}}
onClick={handleModalSlotClick}
>
{showConfigHandler && (
<ConfigHandle
id={id}
customClassName={showHeader ? '' : 'modalWidget-config-handle tw-h-0'}
showHandle={showConfigHandler}
setSelectedComponentAsModal={onSelectModal}
componentType="Modal"
isModalOpen={true}
/>
)}
{showHeader && (
<ModalHeader
id={id}
isDisabled={isDisabled}
customStyles={customStyles}
hideCloseButton={hideCloseButton}
darkMode={darkMode}
width={modalWidth}
onHideModal={onHideModal}
headerHeight={headerHeight}
onClick={handleModalSlotClick}
/>
)}
<BootstrapModal.Body style={{ ...customStyles.modalBody }} ref={parentRef} id={id} data-cy={`modal-body`}>
{isDisabled && (
<div
id={`${id}-body-disabled`}
className="tj-modal-disabled-overlay"
style={{
height: modalBodyHeight || '100%',
}}
onDrop={(e) => e.stopPropagation()}
/>
)}
{!isLoading ? (
<>
<SubContainer
id={`${id}`}
canvasHeight={modalBodyHeight}
styles={{ backgroundColor: customStyles.modalBody.backgroundColor, height: 'inherit' }}
canvasWidth={modalWidth}
darkMode={darkMode}
/>
</>
) : (
<div className="p-2">
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
</div>
)}
</BootstrapModal.Body>
{showFooter && (
<ModalFooter
id={id}
isDisabled={isDisabled}
darkMode={darkMode}
customStyles={customStyles}
width={modalWidth}
footerHeight={footerHeight}
onClick={handleModalSlotClick}
/>
)}
</BootstrapModal>
);
};

View file

@ -0,0 +1,243 @@
import React, { useRef, useState, useEffect } from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { useExposeState } from '@/AppBuilder/Widgets/ModalV2/hooks/useModalCSA';
import { useResetZIndex } from '@/AppBuilder/Widgets/ModalV2/hooks/useModalZIndex';
import { useModalEventSideEffects } from '@/AppBuilder/Widgets/ModalV2/hooks/useResizeSideEffects';
import { useEventListener } from '@/_hooks/use-event-listener';
import { ModalWidget } from '@/AppBuilder/Widgets/ModalV2/Components/Modal';
import {
getModalBodyHeight,
getModalHeaderHeight,
getModalFooterHeight,
} from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
import { createModalStyles } from '@/AppBuilder/Widgets/ModalV2/helpers/stylesFactory';
import { onShowSideEffects, onHideSideEffects } from '@/AppBuilder/Widgets/ModalV2/helpers/sideEffects';
import '@/AppBuilder/Widgets/ModalV2/style.scss';
export const ModalV2 = function Modal({
id,
component,
darkMode,
properties,
styles,
setExposedVariable,
setExposedVariables,
fireEvent,
dataCy,
height,
}) {
const [showModal, setShowModal] = useState(false);
const {
closeOnClickingOutside = false,
hideOnEsc,
hideCloseButton,
hideTitleBar,
useDefaultButton,
triggerButtonLabel,
modalHeight,
showHeader,
showFooter,
headerHeight,
footerHeight,
} = properties;
const {
headerBackgroundColor,
footerBackgroundColor,
bodyBackgroundColor,
triggerButtonBackgroundColor,
triggerButtonTextColor,
boxShadow,
} = styles;
const isInitialRender = useRef(true);
const title = properties.title ?? '';
const titleAlignment = properties.titleAlignment ?? 'left';
const size = properties.size ?? 'lg';
const setSelectedComponentAsModal = useStore((state) => state.setSelectedComponentAsModal, shallow);
const mode = useStore((state) => state.currentMode, shallow);
const computedModalBodyHeight = getModalBodyHeight(modalHeight, showHeader, showFooter, headerHeight, footerHeight);
const headerHeightPx = getModalHeaderHeight(showHeader, headerHeight);
const footerHeightPx = getModalFooterHeight(showFooter, footerHeight);
const isFullScreen = properties.size === 'fullscreen';
const computedCanvasHeight = isFullScreen
? `calc(100vh - 48px - 40px - ${headerHeightPx} - ${footerHeightPx})`
: computedModalBodyHeight;
useEffect(() => {
const exposedVariables = {
open: async function () {
setExposedVariable('show', true);
setShowModal(true);
},
close: async function () {
setExposedVariable('show', false);
setShowModal(false);
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function hideModal() {
setExposedVariable('show', false);
setShowModal(false);
}
function openModal() {
setExposedVariable('show', true);
setShowModal(true);
}
useEventListener('resize', onShowSideEffects, window);
const onShowModal = () => {
openModal();
onShowSideEffects();
fireEvent('onOpen');
setSelectedComponentAsModal(id);
};
const onHideModal = () => {
onHideSideEffects(() => fireEvent('onOpen'));
hideModal();
setSelectedComponentAsModal(null);
};
useEffect(() => {
if (isInitialRender.current) {
isInitialRender.current = false;
return;
}
const inputRef = document?.getElementsByClassName('tj-text-input-widget')?.[0];
inputRef?.blur();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showModal]);
useEffect(() => {
// When modal is active, prevent drop event on backdrop (else widgets droppped will get added to canvas)
const preventBackdropDrop = (e) => {
if (e.target.className === 'fade modal show') {
e.preventDefault();
e.stopPropagation();
}
};
document.addEventListener('drop', preventBackdropDrop);
return () => {
document.removeEventListener('drop', preventBackdropDrop);
};
}, []);
const { controlBoxRef } = useResetZIndex({ showModal, id, mode });
const { isDisabledTrigger, isDisabledModal, isVisible, isLoading } = useExposeState({
loadingState: properties.loadingState,
visibleState: properties.visibility,
disabledModalState: properties.disabledModal,
disabledTriggerState: properties.disabledTrigger,
setExposedVariables,
setExposedVariable,
onHideModal,
onShowModal,
});
const customStyles = createModalStyles({
height,
modalHeight,
computedCanvasHeight,
bodyBackgroundColor,
darkMode,
isDisabledModal,
headerBackgroundColor,
headerHeightPx,
footerBackgroundColor,
footerHeightPx,
triggerButtonBackgroundColor,
triggerButtonTextColor,
isVisible,
boxShadow,
});
const { modalWidth, parentRef } = useModalEventSideEffects({
showModal,
size,
id,
onShowSideEffects,
closeOnClickingOutside,
onHideModal,
});
return (
<div
className="container d-flex align-items-center"
data-disabled={isDisabledTrigger}
data-cy={dataCy}
style={{ height }}
>
{useDefaultButton && isVisible && (
<button
disabled={isDisabledTrigger}
className="jet-button btn btn-primary p-1 overflow-hidden"
style={customStyles.buttonStyles}
onClick={(event) => {
/**** Start - Logic to reduce the zIndex of modal control box ****/
controlBoxRef.current = document.querySelector(`.selected-component.sc-${id}`)?.parentElement;
if (mode === 'edit' && controlBoxRef.current) {
controlBoxRef.current.classList.add('modal-moveable');
}
/**** End - Logic to reduce the zIndex of modal control box ****/
event.stopPropagation();
setShowModal(true);
}}
data-cy={`${dataCy}-launch-button`}
>
{triggerButtonLabel ?? 'Show Modal'}
</button>
)}
<ModalWidget
show={showModal}
contentClassName="modal-component"
container={document.getElementsByClassName('real-canvas')[0]}
size={size}
keyboard={true}
enforceFocus={false}
animation={false}
onShow={() => onShowModal()}
onHide={() => onHideModal()}
onEscapeKeyDown={() => hideOnEsc && onHideModal()}
id="modal-container"
component-id={id}
backdrop={'static'}
scrollable={true}
modalProps={{
customStyles,
parentRef,
id,
title,
titleAlignment,
hideTitleBar,
hideCloseButton,
onHideModal,
component,
hideOnEsc,
modalHeight,
isLoading,
isDisabled: isDisabledModal,
showConfigHandler: mode === 'edit',
fullscreen: isFullScreen,
showHeader,
showFooter,
headerHeight: headerHeightPx,
footerHeight: footerHeightPx,
modalBodyHeight: computedCanvasHeight,
modalWidth,
onSelectModal: setSelectedComponentAsModal,
}}
/>
</div>
);
};

View file

@ -0,0 +1,35 @@
// Side effects for modal, which include dom manipulation to hide overflow when opening
// And cleaning up dom when modal is closed
export const onShowSideEffects = () => {
const canvasElement = document.querySelector('.page-container.canvas-container');
const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
const allModalContainers = realCanvasEl.querySelectorAll('.modal');
const modalContainer = allModalContainers[allModalContainers.length - 1];
if (canvasElement && realCanvasEl && modalContainer) {
const currentScroll = canvasElement.scrollTop;
canvasElement.style.overflowY = 'hidden';
modalContainer.style.height = `${canvasElement.offsetHeight}px`;
modalContainer.style.top = `${currentScroll}px`;
}
};
export const onHideSideEffects = (callback = () => {}) => {
const canvasElement = document.querySelector('.page-container.canvas-container');
const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
const allModalContainers = realCanvasEl.querySelectorAll('.modal');
const modalContainer = allModalContainers[allModalContainers.length - 1];
const hasManyModalsOpen = allModalContainers.length > 1;
if (canvasElement && realCanvasEl && modalContainer) {
modalContainer.style.height = ``;
modalContainer.style.top = ``;
callback();
// fireEvent('onClose');
}
if (canvasElement && !hasManyModalsOpen) {
canvasElement.style.overflow = 'auto';
}
};

View file

@ -0,0 +1,49 @@
var tinycolor = require('tinycolor2');
export function createModalStyles({
height,
modalHeight,
computedCanvasHeight,
bodyBackgroundColor,
darkMode,
isDisabledModal,
headerBackgroundColor,
headerHeightPx,
footerBackgroundColor,
footerHeightPx,
triggerButtonBackgroundColor,
triggerButtonTextColor,
isVisible,
boxShadow,
}) {
const backwardCompatibilityCheck = height == '34' || modalHeight != undefined ? true : false;
return {
modalBody: {
height: backwardCompatibilityCheck ? computedCanvasHeight : height,
backgroundColor:
['#fff', '#ffffffff'].includes(bodyBackgroundColor) && darkMode ? '#1F2837' : bodyBackgroundColor,
overflowY: isDisabledModal ? 'hidden' : 'auto',
},
modalHeader: {
backgroundColor:
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
height: headerHeightPx,
overflowY: isDisabledModal ? 'hidden' : 'auto',
},
modalFooter: {
backgroundColor:
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
height: footerHeightPx,
overflowY: isDisabledModal ? 'hidden' : 'auto',
},
buttonStyles: {
backgroundColor: triggerButtonBackgroundColor,
color: triggerButtonTextColor,
width: '100%',
display: isVisible ? '' : 'none',
'--tblr-btn-color-darker': tinycolor(triggerButtonBackgroundColor).darken(8).toString(),
boxShadow,
},
};
}

View file

@ -0,0 +1,44 @@
const MODAL_HEADER = {
HEIGHT: 80,
};
const MODAL_FOOTER = {
HEIGHT: 80,
};
export const getCanvasHeight = (height) => {
const parsedHeight = height.includes('px') ? parseInt(height, 10) : height;
return Math.ceil(parsedHeight);
};
export const getModalBodyHeight = (
height,
showHeader,
showFooter,
headerHeight = MODAL_HEADER.HEIGHT,
footerHeight = MODAL_FOOTER.HEIGHT
) => {
let modalHeight = height ? parseInt(height, 10) : 0;
let parsedHeaderHeight = showHeader ? parseInt(headerHeight, 10) : 0;
let parsedFooterHeight = showFooter ? parseInt(footerHeight, 10) : 0;
if (showHeader) {
modalHeight = modalHeight - parsedHeaderHeight;
}
if (showFooter) {
modalHeight = modalHeight - parsedFooterHeight;
}
return `${Math.max(modalHeight, 40)}px`;
};
export const getModalHeaderHeight = (showHeader, headerHeight = MODAL_FOOTER.HEIGHT) => {
let parsedHeight = showHeader ? parseInt(headerHeight, 10) : 0;
return `${parsedHeight}px`;
};
export const getModalFooterHeight = (showFooter, footerHeight = MODAL_FOOTER.HEIGHT) => {
let parsedHeight = showFooter ? parseInt(footerHeight, 10) : 0;
return `${parsedHeight}px`;
};

View file

@ -0,0 +1,84 @@
import { useEffect, useState, useRef } from 'react';
export const useExposeState = ({
loadingState,
visibleState,
disabledModalState,
disabledTriggerState,
setExposedVariables,
setExposedVariable,
onHideModal,
onShowModal,
}) => {
const [isVisible, setVisibility] = useState(visibleState ?? true);
const [isLoading, setLoading] = useState(loadingState ?? false);
const [isDisabledModal, setDisabledModal] = useState(disabledModalState ?? false);
const [isDisabledTrigger, setDisabledTrigger] = useState(disabledTriggerState ?? false);
// Track previous values to prevent redundant updates
const prevValues = useRef({});
// Effect to sync state with props (only when props change)
useEffect(() => {
setDisabledModal(disabledModalState);
}, [disabledModalState]);
useEffect(() => {
setDisabledTrigger(disabledTriggerState);
}, [disabledTriggerState]);
useEffect(() => {
setVisibility(visibleState);
}, [visibleState]);
useEffect(() => {
setLoading(loadingState);
}, [loadingState]);
// Expose state-modifying functions only once
useEffect(() => {
setExposedVariables({
setDisableTrigger: async (value) => setDisabledTrigger(value),
setDisableModal: async (value) => setDisabledModal(value),
setVisibility: async (value) => setVisibility(value),
setLoading: async (value) => setLoading(value),
open: async () => onShowModal(),
close: async () => onHideModal(),
});
}, []);
// Prevent redundant updates to `setExposedVariable`
const updateExposedVariable = (key, value) => {
if (prevValues.current[key] !== value) {
prevValues.current[key] = value;
setExposedVariable(key, value);
}
};
useEffect(() => {
updateExposedVariable('isDisabledModal', isDisabledModal);
}, [isDisabledModal]);
useEffect(() => {
updateExposedVariable('isDisabledTrigger', isDisabledTrigger);
}, [isDisabledTrigger]);
useEffect(() => {
updateExposedVariable('isVisible', isVisible);
}, [isVisible]);
useEffect(() => {
updateExposedVariable('isLoading', isLoading);
}, [isLoading]);
return {
isDisabledTrigger,
setDisabledTrigger,
isDisabledModal,
setDisabledModal,
isVisible,
setVisibility,
isLoading,
setLoading,
};
};

View file

@ -0,0 +1,26 @@
import { useEffect, useRef } from 'react';
import { useGridStore } from '@/_stores/gridStore';
/**** Start - Logic to reset the zIndex of modal control box ****/
export const useResetZIndex = ({ showModal, id, mode }) => {
const controlBoxRef = useRef(null);
useEffect(() => {
if (!showModal && mode === 'edit') {
controlBoxRef.current?.classList?.remove('modal-moveable');
controlBoxRef.current = null;
}
if (showModal) {
useGridStore.getState().actions.setOpenModalWidgetId(id);
} else {
if (useGridStore.getState().openModalWidgetId === id) {
useGridStore.getState().actions.setOpenModalWidgetId(null);
}
}
}, [showModal, id, mode]);
/**** End - Logic to reset the zIndex of modal control box ****/
return {
controlBoxRef,
};
};

View file

@ -0,0 +1,80 @@
import { useState, useEffect, useRef } from 'react';
export function useModalEventSideEffects({
showModal,
size,
id,
onShowSideEffects,
closeOnClickingOutside,
onHideModal,
}) {
const [modalWidth, setModalWidth] = useState();
const parentRef = useRef(null);
// When query panel opens or closes, the modal container height should change to
// accomodate the new height of the canvas
useEffect(() => {
// Select the DOM element
const canvasElement = document.querySelector('.page-container.canvas-container');
if (!canvasElement) return; // Ensure the element exists
// Create a ResizeObserver
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
// Update the height state when the element's height changes
onShowSideEffects();
// When modal is in fullscreen and width of browser changes, update the modal width
if (size === 'fullscreen') {
const width = entry.target.offsetWidth;
setModalWidth(width);
}
}
});
// Observe the canvas element
resizeObserver.observe(canvasElement);
return () => {
// Cleanup observer on component unmount
resizeObserver.disconnect();
};
}, [size, onShowSideEffects]);
useEffect(() => {
if (showModal && parentRef.current) {
if (size === 'fullscreen') {
// First time modal is opened, the whole modal is part of the full body, later put into the canvas
// This delay is to get the correct width of the modal
const canvasElement = document.querySelector('.page-container.canvas-container');
const width = canvasElement.offsetWidth;
setModalWidth(width);
} else {
setModalWidth(parentRef.current.offsetWidth);
}
}
}, [showModal, size, id]);
useEffect(() => {
if (closeOnClickingOutside) {
const handleClickOutside = (event) => {
const modalRef = parentRef?.current?.parentElement?.parentElement?.parentElement;
if (modalRef && modalRef === event.target) {
onHideModal();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [closeOnClickingOutside, parentRef]);
return { modalWidth, parentRef };
}

View file

@ -0,0 +1,66 @@
.modal-open .modal.show {
/* To fix the padding issue */
scrollbar-gutter: auto;
// padding-right: 0 !important;
}
.modal-content.modal-component.tj-modal-widget-content {
border: 0;
.tj-modal-close-button {
padding: 8px;
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 4px;
transition: background-color 0.2s ease;
cursor: pointer;
&:hover {
background-color: var(--interactive-hover);
}
&.is-disabled {
position: absolute;
top: 8px;
bottom: 0;
display: inline-flex;
align-items: center;
justify-content: center;
left: 0;
z-index: 1;
}
}
.tj-modal-disabled-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
margin: 0;
}
.modal-header {
padding: 0;
position: relative;
}
.modal-body {
overflow-x: hidden;
position: relative;
}
.modal-footer {
position: relative;
padding: 0;
border-top: 1px solid var(--border-weak);
overflow-x: hidden;
width: 100%;
}
}

View file

@ -65,6 +65,7 @@ import { Tabs } from '@/AppBuilder/Widgets/Tabs';
import { Kanban } from '@/AppBuilder/Widgets/Kanban/Kanban';
import { Form } from '@/AppBuilder/Widgets/Form/Form';
import { Modal } from '@/AppBuilder/Widgets/Modal';
import { ModalV2 } from '@/AppBuilder/Widgets/ModalV2/ModalV2';
import { Calendar } from '@/AppBuilder/Widgets/Calendar/Calendar';
// import './requestIdleCallbackPolyfill';
@ -106,6 +107,7 @@ export const AllComponents = {
Multiselect,
MultiselectV2,
Modal,
ModalV2,
Chart,
Map: MapComponent,
QrScanner,

View file

@ -329,6 +329,11 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
setCurrentPageHandle(startingPage.handle);
updateFeatureAccess();
setCurrentPageId(startingPage.id, moduleId);
setResolvedPageConstants({
id: startingPage?.id,
handle: startingPage?.handle,
name: startingPage?.name,
});
setComponentNameIdMapping(moduleId);
updateEventsField('events', appData.events);
setCurrentVersionId(appData.editing_version?.id || appData.current_version_id);

View file

@ -112,7 +112,7 @@ export const createAppSlice = (set, get) => ({
setResolvedPageConstants,
setPageSwitchInProgress,
currentMode,
isLicenseValid,
license,
modules: {
canvas: { pages },
},
@ -127,7 +127,7 @@ export const createAppSlice = (set, get) => ({
const appId = get().app.appId;
const filteredQueryParams = queryParams.filter(([key, value]) => {
if (!value) return false;
if (key === 'env' && !isLicenseValid()) return false;
if (key === 'env' && !license.isLicenseValid()) return false;
return true;
});

View file

@ -10,7 +10,11 @@ import {
import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { cloneDeep, merge, set as lodashSet } from 'lodash';
import { computeComponentName, getAllChildComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils';
import {
computeComponentName,
getAllChildComponents,
getParentWidgetFromId,
} from '@/AppBuilder/AppCanvas/appCanvasUtils';
import { pageConfig } from '@/AppBuilder/RightSideBar/PageSettingsTab/pageConfig';
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
import { DEFAULT_COMPONENT_STRUCTURE } from './resolvedSlice';
@ -761,7 +765,7 @@ export const createComponentsSlice = (set, get) => ({
const { getComponentTypeFromId } = get();
const transformedParentId = parentId?.length > 36 ? parentId.slice(0, 36) : parentId;
let parentType = getComponentTypeFromId(transformedParentId, moduleId);
const parentWidget = parentType === 'Kanban' ? 'Kanban_card' : parentType;
const parentWidget = getParentWidgetFromId(parentType, parentId);
const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
if (!isParentChangeAllowed)
@ -1742,7 +1746,10 @@ export const createComponentsSlice = (set, get) => ({
getCustomResolvableReference: (value, parentId, moduleId) => {
const { getParentComponentType } = get();
const parentComponentType = getParentComponentType(parentId, moduleId);
if (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) {
if (
(parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) ||
value === '{{listItem}}'
) {
return { entityType: 'components', entityNameOrId: parentId, entityKey: 'listItem' };
} else if (
parentComponentType === 'Kanban' &&

View file

@ -3,6 +3,7 @@ import { debounce } from 'lodash';
const initialState = {
hoveredComponentForGrid: '',
hoveredComponentBoundaryId: '',
triggerCanvasUpdater: false,
lastCanvasIdClick: '',
lastCanvasClickPosition: null,
@ -13,6 +14,8 @@ export const createGridSlice = (set, get) => ({
setHoveredComponentForGrid: (id) =>
set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }),
getHoveredComponentForGrid: () => get().hoveredComponentForGrid,
setHoveredComponentBoundaryId: (id) =>
set(() => ({ hoveredComponentBoundaryId: id }), false, { type: 'setHoveredComponentBoundaryId', id }),
toggleCanvasUpdater: () =>
set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }),
debouncedToggleCanvasUpdater: debounce(() => {

View file

@ -61,7 +61,6 @@ export const DropdownV2 = ({
}) => {
const {
label,
value,
advanced,
schema,
placeholder,
@ -89,7 +88,7 @@ export const DropdownV2 = ({
padding,
} = styles;
const isInitialRender = useRef(true);
const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value));
const [currentValue, setCurrentValue] = useState(() => findDefaultItem(schema));
const isMandatory = validation?.mandatory ?? false;
const options = properties?.options;
const [validationStatus, setValidationStatus] = useState(validate(currentValue));
@ -168,18 +167,9 @@ export const DropdownV2 = ({
};
useEffect(() => {
if (advanced) {
setInputValue(findDefaultItem(schema));
}
setInputValue(findDefaultItem(advanced ? schema : options));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, JSON.stringify(schema)]);
useEffect(() => {
if (!advanced) {
setInputValue(value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, value]);
}, [advanced, JSON.stringify(schema), JSON.stringify(options)]);
useEffect(() => {
if (visibility !== properties.visibility) setVisibility(properties.visibility);

View file

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
export const RadioButton = function RadioButton({
@ -12,8 +12,8 @@ export const RadioButton = function RadioButton({
darkMode,
dataCy,
}) {
const isInitialRender = useRef(true);
const { label, value, values, display_values } = properties;
const { visibility, disabledState, activeColor, boxShadow } = styles;
const textColor = darkMode && styles.textColor === '#000' ? '#fff' : styles.textColor;
const [checkedValue, setValue] = useState(() => value);
@ -37,12 +37,6 @@ export const RadioButton = function RadioButton({
fireEvent('onSelectionChange');
}
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('value', value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const exposedVariables = {
value: value,
@ -51,9 +45,8 @@ export const RadioButton = function RadioButton({
},
};
setExposedVariables(exposedVariables);
isInitialRender.current = false;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [value]);
return (
<div

View file

@ -10,7 +10,7 @@ function deepEqualityCheckusingLoDash(obj1, obj2) {
export const shouldUpdate = (prevProps, nextProps) => {
const listToRender = getComponentsToRenders();
// evaluate change in exposedVariables only for Modal component, because open/close in modal relies on exposedVariables
const compareExposedVariables = nextProps.componentName === 'Modal';
const compareExposedVariables = nextProps.componentName === 'Modal' || nextProps.componentName === 'ModalV2';
let needToRender = false;

View file

@ -299,7 +299,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },

View file

@ -2,6 +2,7 @@ import { buttonConfig } from './button';
import { tableConfig } from './table';
import { chartConfig } from './chart';
import { modalConfig } from './modal';
import { modalV2Config } from './modalV2';
import { formConfig } from './form';
import { textinputConfig } from './textinput';
import { numberinputConfig } from './numberinput';
@ -59,7 +60,8 @@ export {
buttonConfig,
tableConfig,
chartConfig,
modalConfig,
modalConfig, //!Depreciated
modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,

View file

@ -49,7 +49,13 @@ export const listviewConfig = {
type: 'code',
displayName: 'List data',
validation: {
schema: { type: 'array', element: { type: 'object' } },
schema: {
type: 'union',
schemas: [
{ type: 'array', element: { type: 'object' } },
{ type: 'array', element: { type: 'string' } },
],
},
defaultValue: "[{text: 'Sample text 1'}]",
},
},

View file

@ -1,6 +1,6 @@
export const modalConfig = {
name: 'Modal',
displayName: 'Modal',
name: 'ModalLegacy',
displayName: 'Modal (Legacy)',
description: 'Show pop-up windows',
component: 'Modal',
defaultSize: {

View file

@ -0,0 +1,277 @@
export const modalV2Config = {
name: 'Modal',
displayName: 'Modal',
description: 'Show pop-up windows',
component: 'ModalV2',
defaultSize: {
width: 10,
height: 34,
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
properties: {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
section: 'additionalActions',
},
visibility: {
type: 'toggle',
displayName: 'Modal trigger visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledTrigger: {
type: 'toggle',
displayName: 'Disable modal trigger',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
disabledModal: {
type: 'toggle',
displayName: 'Disable modal window',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
section: 'additionalActions',
},
useDefaultButton: {
type: 'toggle',
displayName: 'Use default trigger button',
validation: {
schema: {
type: 'boolean',
},
defaultValue: true,
},
},
triggerButtonLabel: {
type: 'code',
displayName: 'Trigger button label',
validation: {
schema: {
type: 'string',
},
defaultValue: 'Launch Modal',
},
},
// Data Accordion
showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
size: {
type: 'select',
displayName: 'Width',
accordian: 'Data',
options: [
{ name: 'small', value: 'sm' },
{ name: 'medium', value: 'lg' },
{ name: 'large', value: 'xl' },
{ name: 'fullscreen', value: 'fullscreen' },
],
validation: {
schema: { type: 'string' },
defaultValue: 'lg',
},
},
modalHeight: {
type: 'numberInput',
displayName: 'Height',
accordian: 'Data',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
accordian: 'Data',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
footerHeight: {
type: 'numberInput',
displayName: 'Footer height',
accordian: 'Data',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
},
events: {
onOpen: { displayName: 'On open' },
onClose: { displayName: 'On close' },
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 21,
left: 1,
height: 40,
},
displayName: 'ModalHeaderTitle',
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Modal title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 24,
left: 22,
height: 36,
},
displayName: 'ModalFooterCancel',
properties: ['text'],
styles: ['type', 'borderColor', 'padding'],
defaultValue: {
text: 'Button1',
type: 'outline',
borderColor: '#CCD1D5',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 24,
left: 32,
height: 36,
},
displayName: 'ModalFooterConfirm',
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
],
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
bodyBackgroundColor: {
type: 'color',
displayName: 'Body background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
triggerButtonBackgroundColor: {
type: 'color',
displayName: 'Trigger button background color',
validation: {
schema: { type: 'string' },
defaultValue: false,
},
},
triggerButtonTextColor: {
type: 'color',
displayName: 'Trigger button text color',
validation: {
schema: { type: 'string' },
defaultValue: false,
},
},
},
exposedVariables: {
show: false,
isDisabledModal: false,
isDisabledTrigger: false,
isVisible: true,
isLoading: false,
},
actions: [
{
handle: 'open',
displayName: 'Open',
},
{
handle: 'close',
displayName: 'Close',
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
},
{
handle: 'setDisableTrigger',
displayName: 'Set disable trigger',
params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setDisableModal',
displayName: 'Set disable modal',
params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set loading',
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
properties: {
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledTrigger: { value: '{{false}}' },
disabledModal: { value: '{{false}}' },
useDefaultButton: { value: `{{true}}` },
triggerButtonLabel: { value: `Launch Modal` },
size: { value: 'lg' },
showHeader: { value: '{{true}}' },
showFooter: { value: '{{true}}' },
hideCloseButton: { value: '{{false}}' },
hideOnEsc: { value: '{{true}}' },
closeOnClickingOutside: { value: '{{false}}' },
modalHeight: { value: 400 },
headerHeight: { value: 80 },
footerHeight: { value: 80 },
},
events: [],
styles: {
headerBackgroundColor: { value: '#ffffffff' },
footerBackgroundColor: { value: '#ffffffff' },
bodyBackgroundColor: { value: '#ffffffff' },
triggerButtonBackgroundColor: { value: '#4D72FA' },
triggerButtonTextColor: { value: '#ffffffff' },
},
},
};

View file

@ -1 +1,7 @@
export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy', 'RadioButtonLegacy'];
export const LEGACY_ITEMS = [
'ToggleSwitchLegacy',
'DropdownLegacy',
'MultiselectLegacy',
'RadioButtonLegacy',
'ModalLegacy',
];

View file

@ -3,6 +3,7 @@ import {
tableConfig,
chartConfig,
modalConfig,
modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
@ -62,6 +63,7 @@ export const widgets = [
buttonConfig,
chartConfig,
modalConfig,
modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,

View file

@ -38,7 +38,6 @@ import { useLicenseStore } from '@/_stores/licenseStore';
import { shallow } from 'zustand/shallow';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
import HeaderSkeleton from '@/_ui/FolderSkeleton/HeaderSkeleton';
import { UserGroupMigrationModal } from './MigrationModal/UserGroupMigrationModal';
import {
ImportAppMenu,
AppActionModal,
@ -136,13 +135,7 @@ class HomePageComponent extends React.Component {
this.fetchWorkflowsWorkspaceLimit();
this.fetchOrgGit();
this.setQueryParameter();
const hasShownModal = localStorage.getItem('hasShownUserGroupMigrationModal');
const hasClosedBanner = localStorage.getItem('hasClosedGroupMigrationBanner');
//Only show the modal once
if (!hasShownModal) {
this.setState({ showUserGroupMigrationModal: true });
localStorage.setItem('hasShownUserGroupMigrationModal', 'true');
}
//Only show the banner once
if (hasClosedBanner) {
@ -790,6 +783,16 @@ class HomePageComponent extends React.Component {
handleCommitChange = (commitEnabled) => {
this.setState({ commitEnabled: commitEnabled });
};
shouldShowMigrationBanner = () => {
const { currentSessionValue } = authenticationService;
const { appType } = this.props;
return (
currentSessionValue?.admin &&
this.state.showGroupMigrationBanner &&
new Date(currentSessionValue?.current_user?.created_at) < new Date('2025-02-01') &&
appType !== 'workflow'
);
};
render() {
const {
apps,
@ -885,15 +888,6 @@ class HomePageComponent extends React.Component {
return (
<Layout switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode}>
<div className="wrapper home-page">
{authenticationService.currentSessionValue?.admin &&
showUserGroupMigrationModal &&
this.props.appType !== 'workflow' && (
<UserGroupMigrationModal
show={showUserGroupMigrationModal}
onHide={() => this.setShowUserGroupMigrationModal()}
darkMode={this.props.darkMode}
/>
)}
<AppActionModal
modalStates={{
showCreateAppModal,
@ -1240,14 +1234,12 @@ class HomePageComponent extends React.Component {
classes={`${this.props.darkMode ? 'theme-dark dark-theme m-3 trial-banner' : 'm-3 trial-banner'}`}
/>
)}
{authenticationService.currentSessionValue?.admin &&
showGroupMigrationBanner &&
this.props.appType !== 'workflow' && (
<UserGroupMigrationBanner
classes={`${this.props.darkMode ? 'theme-dark dark-theme m-3 trial-banner' : 'm-3 trial-banner'}`}
closeBanner={this.setShowGroupMigrationBanner}
/>
)}
{this.shouldShowMigrationBanner() && (
<UserGroupMigrationBanner
classes={`${this.props.darkMode ? 'theme-dark dark-theme m-3 trial-banner' : 'm-3 trial-banner'}`}
closeBanner={this.setShowGroupMigrationBanner}
/>
)}
<OrganizationList />
</div>

View file

@ -3,7 +3,6 @@ export const workspaceSettingsLinks = [
{ id: 'groups', name: 'Groups', route: 'groups', conditions: ['admin'] },
{ id: 'workspacelogin', name: 'Workspace login', route: 'workspace-login', conditions: ['admin', 'wsLoginEnabled'] },
{ id: 'workspace-variables', name: 'Workspace variables', route: 'workspace-variables', conditions: ['admin'] },
{ id: 'copilot', name: 'Copilot', route: 'copilot', conditions: ['admin'] },
{ id: 'custom-styles', name: 'Custom styles', route: 'custom-styles', conditions: ['admin'] },
{ id: 'configure-git', name: 'Configure Git', route: 'configure-git', conditions: ['admin'] },
];

View file

@ -8,11 +8,10 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { BreadCrumbContext } from '@/App/App';
import LicenseBanner from '@/modules/common/components/LicenseBanner';
export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) {
export default function CreateTableDrawer({ bannerVisible, setBannerVisible, tablesLimit, setTablesLimit }) {
const { organizationId, setSelectedTable, setTables, tables } = useContext(TooljetDatabaseContext);
const [isCreateTableDrawerOpen, setIsCreateTableDrawerOpen] = useState(false);
const { updateSidebarNAV } = useContext(BreadCrumbContext);
const [tablesLimit, setTablesLimit] = useState({});
setBannerVisible(tablesLimit?.current >= tablesLimit?.total - 1 || false);
useEffect(() => {
@ -42,15 +41,6 @@ export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) {
Create new table
</ButtonSolid>
</div>
<LicenseBanner
classes="mb-3 small"
limits={tablesLimit}
type="tables"
size="small"
style={{ marginTop: '20px' }}
z-index="10000"
/>
<Drawer
isOpen={isCreateTableDrawerOpen}
onClose={() => setIsCreateTableDrawerOpen(false)}

View file

@ -3,18 +3,40 @@ import List from '../TableList';
import CreateTableDrawer from '../Drawers/CreateTableDrawer';
import { OrganizationList } from '@/modules/dashboard/components';
import cx from 'classnames';
import LicenseBanner from '@/modules/common/components/LicenseBanner';
import { authenticationService } from '@/_services';
export default function Sidebar({ collapseSidebar }) {
const [bannerVisible, setBannerVisible] = useState(false);
const [tablesLimit, setTablesLimit] = useState({});
const isAdmin = authenticationService.currentSessionValue?.admin === true;
const isResourceLimitReached = tablesLimit?.percentage === 100;
return (
<div className={cx('tooljet-database-sidebar col d-flex flex-column', { 'visually-hidden': collapseSidebar })}>
<div className={`sidebar-container ${!bannerVisible ? '' : 'sidebar-container-with-banner'}`}>
<CreateTableDrawer bannerVisible={bannerVisible} setBannerVisible={setBannerVisible} />
<CreateTableDrawer
bannerVisible={bannerVisible}
setBannerVisible={setBannerVisible}
tablesLimit={tablesLimit}
setTablesLimit={setTablesLimit}
/>
</div>
<div className="col table-left-sidebar" data-cy="all-table-column">
<div className={`sidebar-list-wrap ${!bannerVisible ? '' : 'sidebar-list-wrap-with-banner'}`}>
<div
className={`sidebar-list-wrap ${!bannerVisible ? '' : 'sidebar-list-wrap-with-banner'} ${
isAdmin ? 'isAdmin' : ''
} ${isResourceLimitReached ? 'resource-limit-reached' : ''}`}
>
<List />
</div>
<LicenseBanner
classes="mb-3 small"
limits={tablesLimit}
type="tables"
size="small"
style={{ marginTop: '20px' }}
z-index="10000"
/>
<OrganizationList />
</div>
</div>

View file

@ -226,7 +226,7 @@ export const authorizeUserAndHandleErrors = (workspace_id, workspace_slug, callb
const unauthorized_organization_slug = workspace_slug;
/* get current session's workspace id */
authenticationService
sessionService
.validateSession()
.then(({ current_organization_id, ...restSessionData }) => {
/* change current organization id to valid one [current logged in organization] */

View file

@ -56,8 +56,9 @@ function deleteDataSource(id) {
}
function test(body) {
const id = body.dataSourceId;
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/data-sources/test-connection`, requestOptions).then(handleResponse);
return fetch(`${config.apiUrl}/data-sources/${id}/test-connection`, requestOptions).then(handleResponse);
}
function testSampleDb(body) {

View file

@ -15,7 +15,7 @@ export const orgEnvironmentConstantService = {
function getAll(type = null) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
const queryParams = type ? `?type=${type}` : '';
return fetch(`${config.apiUrl}/organization-constants${queryParams}`, requestOptions).then(handleResponse);
return fetch(`${config.apiUrl}/organization-constants/decrypted${queryParams}`, requestOptions).then(handleResponse);
}
function create(name, value, type, environments) {

View file

@ -493,4 +493,13 @@ $btn-dark-color: #FFFFFF;
}
}
}
}
//[Container-widget]Show scrollbar only on hover
.widget-type-container {
overflow: hidden auto;
scrollbar-width: none;
&:hover {
scrollbar-width: auto;
}
}

View file

@ -198,7 +198,7 @@
.message-wrapper {
display: flex;
flex-direction: column;
margin-left: 10px;
margin-left: 5px;
.heading {
display: unset !important;
@ -246,9 +246,9 @@
.upgrade-btn-new{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: auto;
overflow: hidden;
text-overflow: ellipsis;
width: auto;
margin-left: auto;
border: 1px solid var(--border-default, #CCD1D5);
background: var(--background-surface-layer-01);
@ -262,7 +262,9 @@
padding: 8px 16px 8px 16px;
}
.demo-btn-bg{
border: 1px solid var(--border-accent-weak, #97AEFC);
}
.start-trial-btn {
min-width: 131px;
}
@ -318,16 +320,14 @@
max-width: fit-content;
.upgrade-link {
background: var(--upgrade-default);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin-left: 2px;
color: var(--text-default, #1B1F24);
font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
line-height: 20px;
font-weight: 500;
width: 67px;
height: 20px;
line-height: 20px;
/* 166.667% */
}
}
@ -345,16 +345,13 @@
max-width: fit-content;
.upgrade-link {
background: linear-gradient(to right, #ff5f6d, #ffc371);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: var(--text-default, #1B1F24);
font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
line-height: 20px;
font-weight: 500;
width: 67px;
height: 20px;
line-height: 20px;
/* 166.667% */
}
}
@ -372,16 +369,13 @@
max-width: fit-content;
.upgrade-link {
background: linear-gradient(to right, #ff5f6d, #ffc371);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: var(--text-default, #1B1F24);
font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
line-height: 20px;
font-weight: 500;
width: 67px;
height: 20px;
line-height: 20px;
/* 166.667% */
}
}
@ -399,16 +393,13 @@
max-width: fit-content;
.upgrade-link {
background: var(--upgrade-default);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: var(--text-default, #1B1F24);
font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
line-height: 20px;
font-weight: 500;
width: 67px;
height: 20px;
line-height: 20px;
/* 166.667% */
}
}

View file

@ -1467,5 +1467,59 @@
}
.tj-table-tag-col-readonly {
margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
}
margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
}
.jet-data-table {
.table-bordered {
th,
td {
border-bottom: 1px solid var(--interactive-overlay-border-pressed) !important;
border-right: 1px solid var(--interactive-overlay-border-pressed) !important;
&:first-child {
border-left: none !important;
}
&:last-child {
border-right: none !important;
}
}
thead th {
border-top: none !important;
&:first-child {
border-left: none !important;
}
&:last-child {
border-right: none !important;
}
}
}
.table-striped {
tbody {
div[data-index]:nth-child(odd) {
background-color: transparent !important;
}
div[data-index]:nth-child(even) {
background-color: var(--slate2) !important;
}
}
}
}
@media (hover: none) and (pointer: coarse) {
.jet-data-table {
overflow: auto;
}
// hide scrollbar on touch devices
.jet-data-table::-webkit-scrollbar {
width: 0;
height: 0;
background: transparent;
}
}

View file

@ -5277,7 +5277,8 @@ fieldset:disabled .btn {
width: 100%;
height: 100%;
overflow: hidden;
outline: 0
outline: 0;
padding-left: 0 !important;
}
.modal-dialog {
@ -5311,7 +5312,7 @@ fieldset:disabled .btn {
}
.modal-dialog-scrollable .modal-content {
max-height: 100%;
max-height: 88%;
overflow: hidden
}
@ -5447,6 +5448,10 @@ fieldset:disabled .btn {
margin: 0
}
.real-canvas .modal-dialog.modal-fullscreen {
width: 100%;
}
.modal-fullscreen .modal-content {
height: 100%;
border: 0;
@ -5465,6 +5470,12 @@ fieldset:disabled .btn {
border-radius: 0
}
.modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
// Modal header height
padding-bottom: 56px;
max-height: 100%;
}
@media (max-width:575.98px) {
.modal-fullscreen-sm-down {
width: 100vw;
@ -19129,4 +19140,4 @@ img {
background: #1f2936;
border-color: #dadcde
}
}
}

View file

@ -2665,10 +2665,18 @@ hr {
overflow-y: initial !important
}
.modal-dialog-scrollable .modal-content {
.modal-dialog-scrollable:not(.modal-fullscreen) .modal-content {
max-height: 88% !important;
}
.modal-dialog-scrollable.modal-fullscreen .modal-content {
max-height: 100% !important;
}
.modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
// Modal header height
padding-bottom: 0;
}
}
@ -7924,7 +7932,7 @@ tbody {
}
.sidebar-container-with-banner {
height: 140px !important;
height: 40px !important;
padding-top: 1px !important;
margin: 0 auto;
display: flex;
@ -12085,13 +12093,19 @@ tbody {
.sidebar-list-wrap-with-banner {
margin-top: 24px;
padding: 0px 20px 20px 20px;
height: calc(100vh - 280px);
height: calc(100vh - 408px);
overflow: auto;
span {
letter-spacing: -0.02em;
}
}
.sidebar-list-wrap.sidebar-list-wrap-with-banner.isAdmin {
height: calc(100vh - 371px);
&.resource-limit-reached {
height: calc(100vh - 371px);
}
}
.drawer-footer-btn-wrap,
.variable-form-footer {
@ -14016,8 +14030,8 @@ tbody {
}
/*
* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
*/
.tj-dark-mode {
background-color: var(--base) !important;
@ -18562,4 +18576,52 @@ section.ai-message-prompt-input-wrapper {
margin-left: 8px;
flex-grow: 1;
}
}
}
.workspace-constant-value {
position: relative;
.fromEnv {
content: '.env';
border-radius: 6px;
background: var(--Indigo-50, #EEF4FF);
padding: 0px 8px;
width: 40px;
align-items: center;
position: absolute;
color: var(--Indigo-700, #3538CD);
text-align: center;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px;
margin-left: 24px;
}
.isDuplicate {
padding: 0px 8px;
border-radius: 6px;
background: var(--Error-50, #FEF3F2);
color: var(--Error-700, #B42318);
text-align: center;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px;
/* 166.667% */
margin-left: 24px;
}
.env-secret-hidden-message {
border-radius: 16px;
background: var(--Warning-50, #FFFAEB);
padding: 4px 12px;
color: var(--Warning-700, #B54708);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px;
&.dark {
background: #FFFAEB !important;
}
}
}

View file

@ -0,0 +1,22 @@
import React from 'react';
const AICrown = ({ className = '', fill = '#FCA23F', width = '40', height = '41', ...props }) => {
return (
<svg
width={width}
height={height}
viewBox="0 0 40 41"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
{...props}
>
<path
d="M36.6169 16.3861L33.2402 31.8226C33.0472 32.9321 32.0342 33.7522 30.9247 33.7522H9.07232C7.91457 33.7522 6.94979 32.9321 6.75683 31.8226L3.38008 16.4343C3.13889 15.1318 4.00719 13.9258 5.30965 13.6847C6.12972 13.5399 6.94979 13.8294 7.52866 14.4565L12.5455 19.8593L17.8519 7.8477C18.3825 6.64172 19.8297 6.11109 20.9874 6.68996C21.518 6.93116 21.904 7.31707 22.1452 7.8477L27.4515 19.811L32.4684 14.4082C33.3367 13.4435 34.8321 13.347 35.8451 14.2153C36.4722 14.7459 36.7617 15.6142 36.6169 16.3861Z"
fill={fill}
/>
</svg>
);
};
export default AICrown;

View file

@ -0,0 +1,24 @@
import React from 'react';
const AppLimitSvg = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25" fill="none">
<path
d="M2.5 4.64844C2.5 3.26773 3.61929 2.14844 5 2.14844H7.5C8.88071 2.14844 10 3.26773 10 4.64844V7.14844C10 8.52915 8.88071 9.64844 7.5 9.64844H5C3.61929 9.64844 2.5 8.52915 2.5 7.14844V4.64844Z"
fill="#CCD1D5"
/>
<path
d="M17.5 2.14844C16.1193 2.14844 15 3.26773 15 4.64844V7.14844C15 8.52915 16.1193 9.64844 17.5 9.64844H20C21.3807 9.64844 22.5 8.52915 22.5 7.14844V4.64844C22.5 3.26773 21.3807 2.14844 20 2.14844H17.5Z"
fill="#CCD1D5"
/>
<path
d="M18.75 22.1484C20.8211 22.1484 22.5 20.4695 22.5 18.3984C22.5 16.3274 20.8211 14.6484 18.75 14.6484C16.6789 14.6484 15 16.3274 15 18.3984C15 20.4695 16.6789 22.1484 18.75 22.1484Z"
fill="#CCD1D5"
/>
<path
d="M5 14.6484C3.61929 14.6484 2.5 15.7677 2.5 17.1484V19.6484C2.5 21.0291 3.61929 22.1484 5 22.1484H7.5C8.88071 22.1484 10 21.0291 10 19.6484V17.1484C10 15.7677 8.88071 14.6484 7.5 14.6484H5Z"
fill="#CCD1D5"
/>
</svg>
);
export default AppLimitSvg;

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