mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Merge branch 'modularisation/v3' into fix/codehinter-search-replace
This commit is contained in:
commit
e4aaaadf89
241 changed files with 10001 additions and 1252 deletions
|
|
@ -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"
|
||||
|
|
|
|||
36
.github/workflows/cypress-appbuilder.yml
vendored
36
.github/workflows/cypress-appbuilder.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
38
.github/workflows/cypress-marketplace.yml
vendored
38
.github/workflows/cypress-marketplace.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
44
.github/workflows/cypress-platform.yml
vendored
44
.github/workflows/cypress-platform.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
39
.github/workflows/doc-release.yml
vendored
39
.github/workflows/doc-release.yml
vendored
|
|
@ -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
232
.github/workflows/docker-release.yml
vendored
Normal 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 }}
|
||||
|
||||
|
||||
35
.github/workflows/packer-build.yml
vendored
35
.github/workflows/packer-build.yml
vendored
|
|
@ -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 }}
|
||||
8
.github/workflows/render-preview-deploy.yml
vendored
8
.github/workflows/render-preview-deploy.yml
vendored
|
|
@ -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"
|
||||
# },
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
40
.github/workflows/update-lts-test-system.yml
vendored
40
.github/workflows/update-lts-test-system.yml
vendored
|
|
@ -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
|
||||
132
.github/workflows/update-test-system.yml
vendored
132
.github/workflows/update-test-system.yml
vendored
|
|
@ -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
18
.vscode/launch.json
vendored
|
|
@ -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>/**"]
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
68
deploy/ec2/ee/.env
Normal 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=
|
||||
17
deploy/ec2/ee/nest.service
Normal file
17
deploy/ec2/ee/nest.service
Normal 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
|
||||
16
deploy/ec2/ee/postgrest.service
Normal file
16
deploy/ec2/ee/postgrest.service
Normal 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
|
||||
45
deploy/ec2/ee/redis-server.service
Normal file
45
deploy/ec2/ee/redis-server.service
Normal 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
175
deploy/ec2/ee/setup_app
Executable 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
|
||||
99
deploy/ec2/ee/setup_machine.sh
Normal file
99
deploy/ec2/ee/setup_machine.sh
Normal 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
|
||||
77
deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl
Normal file
77
deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl
Normal 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"
|
||||
}
|
||||
}
|
||||
33
deploy/ec2/ee/variables.pkr.hcl
Normal file
33
deploy/ec2/ee/variables.pkr.hcl
Normal 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"
|
||||
}
|
||||
117
docker/cloud/cloud-server.Dockerfile
Normal file
117
docker/cloud/cloud-server.Dockerfile
Normal 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"]
|
||||
|
|
@ -137,6 +137,7 @@ const WidgetIcon = (props) => {
|
|||
case 'map':
|
||||
return <Map {...props} />;
|
||||
case 'modal':
|
||||
case 'modallegacy':
|
||||
return <Modal {...props} />;
|
||||
case 'multiselect':
|
||||
case 'multiselectv2':
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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?"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -65,9 +65,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-editor-canvas .widget-target:hover > .config-handle {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
266
frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
Normal file
266
frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* Drag Context Breakdown:
|
||||
*
|
||||
* This object encapsulates all relevant details about a drag event,
|
||||
* grouping the **source (where the widget came from)** and **target (where it's being dropped)**.
|
||||
*
|
||||
* Core Concepts:
|
||||
* - `draggedWidget` → The widget being dragged (`e.target`).
|
||||
* - `sourceSlot` → The original parent container of `draggedWidget`.
|
||||
* - This could be a **header, footer, or a sub-container (like a container body)**.
|
||||
* - `targetSlot` → The new parent container where `draggedWidget` is dropped.
|
||||
* - `sourceWidget` → The **widget that owns** `sourceSlot` (its direct parent).
|
||||
* - `targetWidget` → The **widget that owns** `targetSlot` (its direct parent).
|
||||
*
|
||||
* These entities are structured into a **contextual grouping**, allowing for easy access:
|
||||
*
|
||||
* {
|
||||
* source: {
|
||||
* widget: sourceWidget, // The original widget that holds the source slot.
|
||||
* slot: sourceSlot, // The slot where the widget was initially located.
|
||||
* id: sourceWidget.id, // Unique identifier of the source widget.
|
||||
* slotId: sourceSlot.id, // Unique identifier of the source slot.
|
||||
*
|
||||
* isModal: computed function, // Checks if sourceWidget is a Modal.
|
||||
* slotType: computed function, // Determines if the slot is a header, footer, or body.
|
||||
* widgetType: computed function, // Returns the type of the widget (e.g., Table, Form, etc.).
|
||||
* },
|
||||
*
|
||||
* target: {
|
||||
* widget: targetWidget, // The new widget where the dragged widget is being placed.
|
||||
* slot: targetSlot, // The slot inside `targetWidget` where the drop is happening.
|
||||
* id: targetWidget.id, // Unique identifier of the target widget.
|
||||
* slotId: targetSlot.id, // Unique identifier of the target slot.
|
||||
*
|
||||
* isModal: computed function, // Checks if targetWidget is a Modal.
|
||||
* slotType: computed function, // Determines if the slot is a header, footer, or body.
|
||||
* widgetType: computed function, // Returns the type of the target widget.
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Additional Checks:
|
||||
* - `isSourceModal` → **Is the source inside a modal?**
|
||||
* - `isTargetModal` → **Is the target inside a modal?**
|
||||
* - `isDraggingToModalSlots` → **Is the widget being dragged into a modal slot (header/footer)?**
|
||||
* - `targetSlotType` → **Determines whether the drop is happening in a header, footer, or body.**
|
||||
*
|
||||
* Why This Matters?
|
||||
* - This structure helps **validate and restrict movements**, ensuring widgets follow UI constraints.
|
||||
* - Prevents invalid drops (e.g., putting a button inside a Table component).
|
||||
* - Enables **modular and flexible** widget movement across different UI sections.
|
||||
*/
|
||||
import { getMouseDistanceFromParentDiv } from '../gridUtils';
|
||||
import {
|
||||
RESTRICTED_WIDGETS_CONFIG,
|
||||
RESTRICTED_WIDGET_SLOTS_CONFIG,
|
||||
} from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig';
|
||||
|
||||
const CANVAS_ID = 'canvas';
|
||||
const REAL_CANVAS_ID = 'real-canvas';
|
||||
|
||||
/**
|
||||
* Represents the widget being dragged.
|
||||
*
|
||||
* This class encapsulates all necessary information about the dragged widget,
|
||||
* including its type, position, and whether it is allowed to move into certain areas.
|
||||
*/
|
||||
export class DragEntity {
|
||||
constructor(widget) {
|
||||
this.widget = widget; // The widget object being dragged
|
||||
this.id = widget?.id || null; // Unique ID of the dragged widget
|
||||
this.left = widget.left; // Initial X position (relative to grid)
|
||||
this.top = widget.top; // Initial Y position (relative to grid)
|
||||
}
|
||||
|
||||
get widgetType() {
|
||||
return this.widget?.component?.component || null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a **droppable area** in the canvas.
|
||||
*
|
||||
* A droppable area is a container that can accept dragged widgets.
|
||||
* This class helps determine if a slot is valid and handles various properties like modals.
|
||||
*/
|
||||
export class DropAreaEntity {
|
||||
static dropAreaWidgets = ['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'ModalV2', 'Listview', 'Container', 'Table'];
|
||||
|
||||
constructor(widget, slotId) {
|
||||
this.widget = widget; // The widget that owns this slot
|
||||
this.id = widget?.id || CANVAS_ID; // ID of the widget
|
||||
this.slotId = slotId || REAL_CANVAS_ID; // ID of the slot where the widget is located
|
||||
}
|
||||
|
||||
// Checks if the widget is a modal
|
||||
get isModal() {
|
||||
return ['Modal', 'ModalV2'].includes(this.widget?.component?.component);
|
||||
}
|
||||
|
||||
// Checks if the widget is the new version of modal
|
||||
get isNewModal() {
|
||||
return this.widget?.component?.component === 'ModalV2';
|
||||
}
|
||||
|
||||
// Checks if the widget is the legacy modal
|
||||
get isLegacyModal() {
|
||||
return this.widget?.component?.component === 'Modal';
|
||||
}
|
||||
|
||||
// Determines if the slot belongs to a modal's header/footer
|
||||
get isInModalSlot() {
|
||||
return this.isNewModal && this.isOnCustomSlot;
|
||||
}
|
||||
|
||||
// Identifies if the slot is a custom slot (e.g., modal header/footer)
|
||||
get isOnCustomSlot() {
|
||||
return this.slotId.includes('-header') || this.slotId.includes('-footer');
|
||||
}
|
||||
|
||||
// Determines if the slot is a valid drop target
|
||||
get isDroppable() {
|
||||
return DropAreaEntity.dropAreaWidgets.includes(this.widgetType);
|
||||
}
|
||||
|
||||
// Returns the type of slot (header, footer, body, etc.)
|
||||
get slotType() {
|
||||
return this.slotId ? this.slotId.split('-').pop() : CANVAS_ID;
|
||||
}
|
||||
|
||||
// Returns the type of the widget inside the slot
|
||||
get widgetType() {
|
||||
return this.widget?.component?.component || CANVAS_ID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the **dragging context**, encapsulating information
|
||||
* about the source, target, and the dragged widget.
|
||||
*
|
||||
* This helps determine:
|
||||
* - Whether the move is valid
|
||||
* - Where the widget should be placed
|
||||
* - Any restrictions based on parent-child relationships
|
||||
*/
|
||||
export class DragContext {
|
||||
constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets }) {
|
||||
const sourceWidgetId = sourceSlotId?.slice(0, 36);
|
||||
const sourceWidget = getWidgetById(widgets, sourceWidgetId);
|
||||
|
||||
const targetWidgetId = targetSlotId?.slice(0, 36);
|
||||
const targetWidget = getWidgetById(widgets, targetWidgetId);
|
||||
|
||||
const draggedWidget = getWidgetById(widgets, draggedWidgetId);
|
||||
|
||||
this.source = new DropAreaEntity(sourceWidget, sourceSlotId);
|
||||
this.target = new DropAreaEntity(targetWidget, targetSlotId);
|
||||
this.dragged = new DragEntity(draggedWidget);
|
||||
this.widgets = widgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the **target slot** dynamically as the drag event progresses.
|
||||
*/
|
||||
updateTarget(targetSlotId) {
|
||||
const targetWidgetId = targetSlotId?.slice(0, 36);
|
||||
const targetWidget = getWidgetById(this.widgets, targetWidgetId);
|
||||
this.target = new DropAreaEntity(targetWidget, targetSlotId);
|
||||
}
|
||||
|
||||
get isDroppable() {
|
||||
const { dragged, target } = this;
|
||||
|
||||
const restrictedWidgetsOnTarget = RESTRICTED_WIDGETS_CONFIG?.[target.widgetType] || [];
|
||||
const restrictedWidgetsOnTargetSlot = RESTRICTED_WIDGET_SLOTS_CONFIG?.[target.slotType] || [];
|
||||
|
||||
const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot];
|
||||
return !restrictedWidgets.includes(dragged.widgetType);
|
||||
ß;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the **dragging context** by gathering all relevant details from the event.
|
||||
*/
|
||||
export function dragContextBuilder({ event, widgets }) {
|
||||
const draggedWidgetId = event.target.id;
|
||||
const draggedWidget = getWidgetById(widgets, draggedWidgetId);
|
||||
const sourceSlotId = draggedWidget.parent;
|
||||
|
||||
// Initialize drag context
|
||||
const context = new DragContext({ widgets, draggedWidgetId, sourceSlotId, targetSlotId: sourceSlotId });
|
||||
|
||||
// Determine the potential drop target
|
||||
const targetSlotId = getDroppableSlotIdOnScreen(event, widgets);
|
||||
context.updateTarget(targetSlotId);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event, finds the **nearest valid droppable slot**.
|
||||
*/
|
||||
export const getDroppableSlotIdOnScreen = (event, widgets) => {
|
||||
const [slotId] = document
|
||||
.elementsFromPoint(event.clientX, event.clientY)
|
||||
.filter(
|
||||
(ele) =>
|
||||
!event.target.contains(ele) && ele.id !== event.target.id && ele.classList.contains('drag-container-parent')
|
||||
)
|
||||
.map((ele) => extractSlotId(ele))
|
||||
.filter((slotId) => {
|
||||
const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID;
|
||||
return DropAreaEntity.dropAreaWidgets.includes(widgetType);
|
||||
});
|
||||
|
||||
return slotId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a widget by its ID.
|
||||
*/
|
||||
export function getWidgetById(boxList, targetId) {
|
||||
return boxList.find((box) => box.id === targetId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the **slot ID** from a given DOM element.
|
||||
*/
|
||||
const extractSlotId = (element) => {
|
||||
return element?.getAttribute('component-id') || element.id.replace(/^canvas-/, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the final (left, top) position for a dragged widget based on grid snapping and drop conditions.
|
||||
*
|
||||
* @param {Object} event - Drag event object containing movement data.
|
||||
* @param {DropAreaEntity} target - The target drop area entity (where widget is dropped).
|
||||
* @param {boolean} isParentChangeAllowed - Whether the widget can move to the target.
|
||||
* @param {number} gridWidth - The width of the grid for alignment.
|
||||
* @param {DragEntity} dragged - The entity being dragged.
|
||||
* @returns {Object} { left, top } - The computed position.
|
||||
*/
|
||||
export const getAdjustedDropPosition = (event, target, isParentChangeAllowed, gridWidth, dragged) => {
|
||||
let left = event.lastEvent?.translate[0];
|
||||
let top = event.lastEvent?.translate[1];
|
||||
|
||||
if (isParentChangeAllowed) {
|
||||
// Compute the relative position inside the new container
|
||||
const { left: adjustedLeft, top: adjustedTop } = getMouseDistanceFromParentDiv(
|
||||
event,
|
||||
target.slotId,
|
||||
target.widgetType
|
||||
);
|
||||
|
||||
return {
|
||||
left: Math.round(adjustedLeft / gridWidth) * gridWidth, // Snap to the nearest grid column
|
||||
top: Math.round(adjustedTop / 10) * 10, // Snap to the nearest 10px
|
||||
};
|
||||
}
|
||||
|
||||
// If movement is restricted, revert to original position
|
||||
return {
|
||||
left: dragged.left * gridWidth,
|
||||
top: dragged.top,
|
||||
};
|
||||
};
|
||||
|
|
@ -89,6 +89,7 @@ const WidgetWrapper = memo(
|
|||
widgetHeight={layoutData.height}
|
||||
showHandle={isWidgetActive}
|
||||
componentType={componentType}
|
||||
visibility={visibility}
|
||||
/>
|
||||
)}
|
||||
<RenderWidget
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export const ComponentsManagerTab = ({ darkMode }) => {
|
|||
'StarRating',
|
||||
];
|
||||
const integrationItems = ['Map'];
|
||||
const layoutItems = ['Container', 'Listview', 'Tabs', 'Modal'];
|
||||
const layoutItems = ['Container', 'Listview', 'Tabs', 'ModalV2'];
|
||||
|
||||
filteredComponents.forEach((f) => {
|
||||
if (commonItems.includes(f)) commonSection.items.push(f);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import React, { useEffect } from 'react';
|
|||
import { WidgetBox } from '../WidgetBox';
|
||||
import { useDrag, useDragLayer } from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils';
|
||||
import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants';
|
||||
|
||||
export const DragLayer = ({ index, component }) => {
|
||||
const [{ isDragging }, drag, preview] = useDrag(
|
||||
() => ({
|
||||
type: 'box',
|
||||
item: { componentType: component.component },
|
||||
item: { componentType: component.component, component },
|
||||
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
||||
}),
|
||||
[component.component]
|
||||
|
|
@ -18,7 +20,6 @@ export const DragLayer = ({ index, component }) => {
|
|||
}, []);
|
||||
|
||||
const size = component.defaultSize || { width: 30, height: 40 };
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDragging && <CustomDragLayer size={size} />}
|
||||
|
|
@ -30,32 +31,39 @@ export const DragLayer = ({ index, component }) => {
|
|||
};
|
||||
|
||||
const CustomDragLayer = ({ size }) => {
|
||||
const { currentOffset } = useDragLayer((monitor) => ({
|
||||
const { currentOffset, item } = useDragLayer((monitor) => ({
|
||||
currentOffset: monitor.getSourceClientOffset(),
|
||||
item: monitor.getItem(),
|
||||
}));
|
||||
|
||||
if (!currentOffset) return null;
|
||||
|
||||
const canvasWidth = document.getElementsByClassName('real-canvas')[0]?.getBoundingClientRect()?.width;
|
||||
|
||||
const canvasWidth = item?.canvasWidth;
|
||||
const canvasBounds = item?.canvasRef?.getBoundingClientRect();
|
||||
const height = size.height;
|
||||
const width = (canvasWidth * size.width) / 43;
|
||||
|
||||
const width = (canvasWidth * size.width) / NO_OF_GRIDS;
|
||||
|
||||
// Calculate position relative to the current canvas (parent or child)
|
||||
const left = currentOffset.x - (canvasBounds?.left || 0);
|
||||
const top = currentOffset.y - (canvasBounds?.top || 0);
|
||||
|
||||
const [x, y] = snapToGrid(canvasWidth, left, top);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 1000,
|
||||
left: canvasBounds?.left || 0,
|
||||
top: canvasBounds?.top || 0,
|
||||
height: `${height}px`,
|
||||
width: `${width}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
|
||||
transform: `translate(${x}px, ${y}px)`,
|
||||
background: '#D9E2FC',
|
||||
opacity: '0.7',
|
||||
height: '100%',
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
|
||||
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const SHOW_ADDITIONAL_ACTIONS = [
|
|||
'Button',
|
||||
'RichTextEditor',
|
||||
'Image',
|
||||
'ModalV2',
|
||||
];
|
||||
const PROPERTIES_VS_ACCORDION_TITLE = {
|
||||
Text: 'Data',
|
||||
|
|
@ -34,6 +35,7 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
|
|||
Button: 'Data',
|
||||
Image: 'Data',
|
||||
Container: 'Data',
|
||||
ModalV2: 'Data',
|
||||
};
|
||||
|
||||
export const DefaultComponent = ({ componentMeta, darkMode, ...restProps }) => {
|
||||
|
|
@ -151,7 +153,8 @@ export const baseComponentProperties = (
|
|||
'properties',
|
||||
currentState,
|
||||
allComponents,
|
||||
darkMode
|
||||
darkMode,
|
||||
''
|
||||
)
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const Code = ({
|
|||
accordian,
|
||||
placeholder,
|
||||
validationFn,
|
||||
isHidden = false,
|
||||
}) => {
|
||||
const currentState = useCurrentState();
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ export const Code = ({
|
|||
onChange({ name: 'iconVisibility' }, 'value', value, 'styles');
|
||||
}
|
||||
|
||||
if (isHidden) return null;
|
||||
return (
|
||||
<div className={`field ${options.className}`} style={{ marginBottom: '8px' }}>
|
||||
<CodeEditor
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||
import { DefaultComponent } from './Components/DefaultComponent';
|
||||
import { FilePicker } from './Components/FilePicker';
|
||||
import { Modal } from './Components/Modal';
|
||||
import { ModalV2 } from './Components/ModalV2';
|
||||
import { CustomComponent } from './Components/CustomComponent';
|
||||
import { Icon } from './Components/Icon';
|
||||
import useFocus from '@/_hooks/use-focus';
|
||||
|
|
@ -78,6 +79,7 @@ const NEW_REVAMPED_COMPONENTS = [
|
|||
'Icon',
|
||||
'Image',
|
||||
'Container',
|
||||
'ModalV2',
|
||||
];
|
||||
|
||||
export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => {
|
||||
|
|
@ -703,6 +705,9 @@ const GetAccordion = React.memo(
|
|||
case 'FilePicker':
|
||||
return <FilePicker {...restProps} />;
|
||||
|
||||
case 'ModalV2':
|
||||
return <ModalV2 {...restProps} />;
|
||||
|
||||
case 'Modal':
|
||||
return <Modal {...restProps} />;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ export function renderCustomStyles(
|
|||
componentConfig.component == 'MultiselectV2' ||
|
||||
componentConfig.component == 'RadioButtonV2' ||
|
||||
componentConfig.component == 'Button' ||
|
||||
componentConfig.component == 'Image'
|
||||
componentConfig.component == 'Image' ||
|
||||
componentConfig.component == 'ModalV2'
|
||||
) {
|
||||
const paramTypeConfig = componentMeta[paramType] || {};
|
||||
const paramConfig = paramTypeConfig[param] || {};
|
||||
|
|
@ -131,6 +132,7 @@ export function renderElement(
|
|||
const paramTypeDefinition = componentDefinition[paramType] || {};
|
||||
const definition = paramTypeDefinition[param] || {};
|
||||
const meta = componentMeta[paramType][param];
|
||||
const isHidden = component.component.properties[param]?.isHidden ?? false;
|
||||
|
||||
if (
|
||||
componentConfig.component == 'DropDown' ||
|
||||
|
|
@ -170,6 +172,7 @@ export function renderElement(
|
|||
component={component}
|
||||
placeholder={placeholder}
|
||||
validationFn={validationFn}
|
||||
isHidden={isHidden}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import WidgetIcon from '@/../assets/images/icons/widgets';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
|
||||
const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
|
||||
const NEW_WIDGETS = [
|
||||
'ToggleSwitchV2',
|
||||
'DropdownV2',
|
||||
|
|
@ -12,6 +12,7 @@ const NEW_WIDGETS = [
|
|||
'DaterangePicker',
|
||||
'DatePickerV2',
|
||||
'TimePicker',
|
||||
'ModalV2',
|
||||
];
|
||||
|
||||
export const WidgetBox = ({ component, darkMode }) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,14 @@ export const RESTRICTED_WIDGETS_CONFIG = {
|
|||
Calendar: ['Calendar', 'Kanban'],
|
||||
Container: ['Calendar', 'Kanban'],
|
||||
Modal: ['Calendar', 'Kanban'],
|
||||
ModalV2: ['Calendar', 'Kanban'],
|
||||
ModalSlot: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
|
||||
Tabs: ['Calendar', 'Kanban'],
|
||||
Kanban_popout: ['Calendar', 'Kanban'],
|
||||
Listview: ['Calendar', 'Kanban'],
|
||||
};
|
||||
|
||||
export const RESTRICTED_WIDGET_SLOTS_CONFIG = {
|
||||
header: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
|
||||
footer: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
tableConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
@ -64,6 +65,7 @@ export const widgets = [
|
|||
buttonConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
|
|||
|
|
@ -299,7 +299,6 @@ export const dropdownV2Config = {
|
|||
],
|
||||
},
|
||||
label: { value: 'Select' },
|
||||
value: { value: '{{"2"}}' },
|
||||
optionsLoadingState: { value: '{{false}}' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
visibility: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const modalConfig = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
name: 'ModalLegacy',
|
||||
displayName: 'Modal (Legacy)',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'Modal',
|
||||
defaultSize: {
|
||||
|
|
|
|||
277
frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
Normal file
277
frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
export const modalV2Config = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'ModalV2',
|
||||
defaultSize: {
|
||||
width: 10,
|
||||
height: 34,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Modal trigger visibility',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
disabledTrigger: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal trigger',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
disabledModal: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal window',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
useDefaultButton: {
|
||||
type: 'toggle',
|
||||
displayName: 'Use default trigger button',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'boolean',
|
||||
},
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
triggerButtonLabel: {
|
||||
type: 'code',
|
||||
displayName: 'Trigger button label',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
defaultValue: 'Launch Modal',
|
||||
},
|
||||
},
|
||||
|
||||
// Data Accordion
|
||||
showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
|
||||
showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
|
||||
|
||||
size: {
|
||||
type: 'select',
|
||||
displayName: 'Width',
|
||||
accordian: 'Data',
|
||||
options: [
|
||||
{ name: 'small', value: 'sm' },
|
||||
{ name: 'medium', value: 'lg' },
|
||||
{ name: 'large', value: 'xl' },
|
||||
{ name: 'fullscreen', value: 'fullscreen' },
|
||||
],
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'lg',
|
||||
},
|
||||
},
|
||||
modalHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
footerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Footer height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
|
||||
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
|
||||
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
|
||||
},
|
||||
events: {
|
||||
onOpen: { displayName: 'On open' },
|
||||
onClose: { displayName: 'On close' },
|
||||
},
|
||||
defaultChildren: [
|
||||
{
|
||||
componentName: 'Text',
|
||||
slotName: 'header',
|
||||
layout: {
|
||||
top: 21,
|
||||
left: 1,
|
||||
height: 40,
|
||||
},
|
||||
displayName: 'ModalHeaderTitle',
|
||||
properties: ['text'],
|
||||
accessorKey: 'text',
|
||||
styles: ['fontWeight', 'textSize', 'textColor'],
|
||||
defaultValue: {
|
||||
text: 'Modal title',
|
||||
textSize: 20,
|
||||
textColor: '#000',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 22,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterCancel',
|
||||
properties: ['text'],
|
||||
styles: ['type', 'borderColor', 'padding'],
|
||||
defaultValue: {
|
||||
text: 'Button1',
|
||||
type: 'outline',
|
||||
borderColor: '#CCD1D5',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 32,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterConfirm',
|
||||
properties: ['text'],
|
||||
defaultValue: {
|
||||
text: 'Button2',
|
||||
padding: 'none',
|
||||
},
|
||||
},
|
||||
],
|
||||
styles: {
|
||||
headerBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Header background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
footerBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Footer background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
bodyBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Body background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
triggerButtonBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
triggerButtonTextColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button text color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
show: false,
|
||||
isDisabledModal: false,
|
||||
isDisabledTrigger: false,
|
||||
isVisible: true,
|
||||
isLoading: false,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'open',
|
||||
displayName: 'Open',
|
||||
},
|
||||
{
|
||||
handle: 'close',
|
||||
displayName: 'Close',
|
||||
},
|
||||
{
|
||||
handle: 'setVisibility',
|
||||
displayName: 'Set visibility',
|
||||
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableTrigger',
|
||||
displayName: 'Set disable trigger',
|
||||
params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableModal',
|
||||
displayName: 'Set disable modal',
|
||||
params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setLoading',
|
||||
displayName: 'Set loading',
|
||||
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
],
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: { value: `{{false}}` },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledTrigger: { value: '{{false}}' },
|
||||
disabledModal: { value: '{{false}}' },
|
||||
useDefaultButton: { value: `{{true}}` },
|
||||
triggerButtonLabel: { value: `Launch Modal` },
|
||||
size: { value: 'lg' },
|
||||
showHeader: { value: '{{true}}' },
|
||||
showFooter: { value: '{{true}}' },
|
||||
hideCloseButton: { value: '{{false}}' },
|
||||
hideOnEsc: { value: '{{true}}' },
|
||||
closeOnClickingOutside: { value: '{{false}}' },
|
||||
modalHeight: { value: 400 },
|
||||
headerHeight: { value: 80 },
|
||||
footerHeight: { value: 80 },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
headerBackgroundColor: { value: '#ffffffff' },
|
||||
footerBackgroundColor: { value: '#ffffffff' },
|
||||
bodyBackgroundColor: { value: '#ffffffff' },
|
||||
triggerButtonBackgroundColor: { value: '#4D72FA' },
|
||||
triggerButtonTextColor: { value: '#ffffffff' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
134
frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
Normal file
134
frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
243
frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx
Normal file
243
frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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';
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
44
frontend/src/AppBuilder/Widgets/ModalV2/helpers/utils.js
Normal file
44
frontend/src/AppBuilder/Widgets/ModalV2/helpers/utils.js
Normal 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`;
|
||||
};
|
||||
84
frontend/src/AppBuilder/Widgets/ModalV2/hooks/useModalCSA.js
Normal file
84
frontend/src/AppBuilder/Widgets/ModalV2/hooks/useModalCSA.js
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 };
|
||||
}
|
||||
66
frontend/src/AppBuilder/Widgets/ModalV2/style.scss
Normal file
66
frontend/src/AppBuilder/Widgets/ModalV2/style.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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' &&
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -299,7 +299,6 @@ export const dropdownV2Config = {
|
|||
],
|
||||
},
|
||||
label: { value: 'Select' },
|
||||
value: { value: '{{"2"}}' },
|
||||
optionsLoadingState: { value: '{{false}}' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
visibility: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const modalConfig = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
name: 'ModalLegacy',
|
||||
displayName: 'Modal (Legacy)',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'Modal',
|
||||
defaultSize: {
|
||||
|
|
|
|||
277
frontend/src/Editor/WidgetManager/configs/modalV2.js
Normal file
277
frontend/src/Editor/WidgetManager/configs/modalV2.js
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
export const modalV2Config = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'ModalV2',
|
||||
defaultSize: {
|
||||
width: 10,
|
||||
height: 34,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Modal trigger visibility',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
disabledTrigger: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal trigger',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
disabledModal: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal window',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
useDefaultButton: {
|
||||
type: 'toggle',
|
||||
displayName: 'Use default trigger button',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'boolean',
|
||||
},
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
triggerButtonLabel: {
|
||||
type: 'code',
|
||||
displayName: 'Trigger button label',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
defaultValue: 'Launch Modal',
|
||||
},
|
||||
},
|
||||
|
||||
// Data Accordion
|
||||
showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
|
||||
showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
|
||||
|
||||
size: {
|
||||
type: 'select',
|
||||
displayName: 'Width',
|
||||
accordian: 'Data',
|
||||
options: [
|
||||
{ name: 'small', value: 'sm' },
|
||||
{ name: 'medium', value: 'lg' },
|
||||
{ name: 'large', value: 'xl' },
|
||||
{ name: 'fullscreen', value: 'fullscreen' },
|
||||
],
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'lg',
|
||||
},
|
||||
},
|
||||
modalHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
footerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Footer height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
|
||||
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
|
||||
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
|
||||
},
|
||||
events: {
|
||||
onOpen: { displayName: 'On open' },
|
||||
onClose: { displayName: 'On close' },
|
||||
},
|
||||
defaultChildren: [
|
||||
{
|
||||
componentName: 'Text',
|
||||
slotName: 'header',
|
||||
layout: {
|
||||
top: 21,
|
||||
left: 1,
|
||||
height: 40,
|
||||
},
|
||||
displayName: 'ModalHeaderTitle',
|
||||
properties: ['text'],
|
||||
accessorKey: 'text',
|
||||
styles: ['fontWeight', 'textSize', 'textColor'],
|
||||
defaultValue: {
|
||||
text: 'Modal title',
|
||||
textSize: 20,
|
||||
textColor: '#000',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 22,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterCancel',
|
||||
properties: ['text'],
|
||||
styles: ['type', 'borderColor', 'padding'],
|
||||
defaultValue: {
|
||||
text: 'Button1',
|
||||
type: 'outline',
|
||||
borderColor: '#CCD1D5',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 32,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterConfirm',
|
||||
properties: ['text'],
|
||||
defaultValue: {
|
||||
text: 'Button2',
|
||||
padding: 'none',
|
||||
},
|
||||
},
|
||||
],
|
||||
styles: {
|
||||
headerBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Header background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
footerBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Footer background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
bodyBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Body background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
triggerButtonBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
triggerButtonTextColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button text color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
show: false,
|
||||
isDisabledModal: false,
|
||||
isDisabledTrigger: false,
|
||||
isVisible: true,
|
||||
isLoading: false,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'open',
|
||||
displayName: 'Open',
|
||||
},
|
||||
{
|
||||
handle: 'close',
|
||||
displayName: 'Close',
|
||||
},
|
||||
{
|
||||
handle: 'setVisibility',
|
||||
displayName: 'Set visibility',
|
||||
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableTrigger',
|
||||
displayName: 'Set disable trigger',
|
||||
params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableModal',
|
||||
displayName: 'Set disable modal',
|
||||
params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setLoading',
|
||||
displayName: 'Set loading',
|
||||
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
],
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: { value: `{{false}}` },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledTrigger: { value: '{{false}}' },
|
||||
disabledModal: { value: '{{false}}' },
|
||||
useDefaultButton: { value: `{{true}}` },
|
||||
triggerButtonLabel: { value: `Launch Modal` },
|
||||
size: { value: 'lg' },
|
||||
showHeader: { value: '{{true}}' },
|
||||
showFooter: { value: '{{true}}' },
|
||||
hideCloseButton: { value: '{{false}}' },
|
||||
hideOnEsc: { value: '{{true}}' },
|
||||
closeOnClickingOutside: { value: '{{false}}' },
|
||||
modalHeight: { value: 400 },
|
||||
headerHeight: { value: 80 },
|
||||
footerHeight: { value: 80 },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
headerBackgroundColor: { value: '#ffffffff' },
|
||||
footerBackgroundColor: { value: '#ffffffff' },
|
||||
bodyBackgroundColor: { value: '#ffffffff' },
|
||||
triggerButtonBackgroundColor: { value: '#4D72FA' },
|
||||
triggerButtonTextColor: { value: '#ffffffff' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1 +1,7 @@
|
|||
export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy', 'RadioButtonLegacy'];
|
||||
export const LEGACY_ITEMS = [
|
||||
'ToggleSwitchLegacy',
|
||||
'DropdownLegacy',
|
||||
'MultiselectLegacy',
|
||||
'RadioButtonLegacy',
|
||||
'ModalLegacy',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'] },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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] */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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% */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
frontend/src/_ui/Icon/solidIcons/AICrown.jsx
Normal file
22
frontend/src/_ui/Icon/solidIcons/AICrown.jsx
Normal 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;
|
||||
24
frontend/src/_ui/Icon/solidIcons/AppLimitSvg.jsx
Normal file
24
frontend/src/_ui/Icon/solidIcons/AppLimitSvg.jsx
Normal 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
Loading…
Reference in a new issue