diff --git a/.gitconfig b/.gitconfig index 3a59d51cdd..5d7b70339d 100644 --- a/.gitconfig +++ b/.gitconfig @@ -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" diff --git a/.github/workflows/cypress-appbuilder.yml b/.github/workflows/cypress-appbuilder.yml index acd5d9c08e..47c7bda175 100644 --- a/.github/workflows/cypress-appbuilder.yml +++ b/.github/workflows/cypress-appbuilder.yml @@ -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 diff --git a/.github/workflows/cypress-marketplace.yml b/.github/workflows/cypress-marketplace.yml index 7d2141c41c..c24fe5ae72 100644 --- a/.github/workflows/cypress-marketplace.yml +++ b/.github/workflows/cypress-marketplace.yml @@ -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 diff --git a/.github/workflows/cypress-platform.yml b/.github/workflows/cypress-platform.yml index 622bd43ec9..0d15c01f9c 100644 --- a/.github/workflows/cypress-platform.yml +++ b/.github/workflows/cypress-platform.yml @@ -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: | diff --git a/.github/workflows/doc-release.yml b/.github/workflows/doc-release.yml deleted file mode 100644 index 2574d105a8..0000000000 --- a/.github/workflows/doc-release.yml +++ /dev/null @@ -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 - - - diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000000..54774c1d7a --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -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 }} + + diff --git a/.github/workflows/packer-build.yml b/.github/workflows/packer-build.yml index 16a6b656c0..aa60c6444f 100644 --- a/.github/workflows/packer-build.yml +++ b/.github/workflows/packer-build.yml @@ -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 }} \ No newline at end of file + curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/render-preview-deploy.yml b/.github/workflows/render-preview-deploy.yml index 6f24e3b95f..a025290d39 100644 --- a/.github/workflows/render-preview-deploy.yml +++ b/.github/workflows/render-preview-deploy.yml @@ -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" # }, diff --git a/.github/workflows/tooljet-docker-develop-build.yml b/.github/workflows/tooljet-docker-develop-build.yml deleted file mode 100644 index 7dab1342c3..0000000000 --- a/.github/workflows/tooljet-docker-develop-build.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/update-lts-test-system.yml b/.github/workflows/update-lts-test-system.yml deleted file mode 100644 index c0757dcd5f..0000000000 --- a/.github/workflows/update-lts-test-system.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/update-test-system.yml b/.github/workflows/update-test-system.yml deleted file mode 100644 index 529f21a917..0000000000 --- a/.github/workflows/update-test-system.yml +++ /dev/null @@ -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'] - }) diff --git a/.vscode/launch.json b/.vscode/launch.json index 29aec59b0b..fb142c19f3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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": ["/**"] + }, + { + "type": "node", + "request": "launch", + "name": "Server (Original)", "args": [ "${workspaceFolder}/server/src/main.ts" ], @@ -49,7 +63,7 @@ "remoteRoot": "/app/server", "sourceMaps": true, "skipFiles": ["/**"] - }, + } } ] } diff --git a/deploy/ec2/.env b/deploy/ec2/ce/.env similarity index 100% rename from deploy/ec2/.env rename to deploy/ec2/ce/.env diff --git a/deploy/ec2/nest.service b/deploy/ec2/ce/nest.service similarity index 100% rename from deploy/ec2/nest.service rename to deploy/ec2/ce/nest.service diff --git a/deploy/ec2/postgrest.service b/deploy/ec2/ce/postgrest.service similarity index 100% rename from deploy/ec2/postgrest.service rename to deploy/ec2/ce/postgrest.service diff --git a/deploy/ec2/setup_app b/deploy/ec2/ce/setup_app similarity index 94% rename from deploy/ec2/setup_app rename to deploy/ec2/ce/setup_app index bc106fc4b4..b07a1299d5 100755 --- a/deploy/ec2/setup_app +++ b/deploy/ec2/ce/setup_app @@ -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 diff --git a/deploy/ec2/setup_machine.sh b/deploy/ec2/ce/setup_machine.sh similarity index 99% rename from deploy/ec2/setup_machine.sh rename to deploy/ec2/ce/setup_machine.sh index b5c3f632df..bb65d1c11c 100644 --- a/deploy/ec2/setup_machine.sh +++ b/deploy/ec2/ce/setup_machine.sh @@ -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 diff --git a/deploy/ec2/tooljet_ubuntu_focal.pkr.hcl b/deploy/ec2/ce/tooljet_ubuntu_focal.pkr.hcl similarity index 100% rename from deploy/ec2/tooljet_ubuntu_focal.pkr.hcl rename to deploy/ec2/ce/tooljet_ubuntu_focal.pkr.hcl diff --git a/deploy/ec2/variables.pkr.hcl b/deploy/ec2/ce/variables.pkr.hcl similarity index 100% rename from deploy/ec2/variables.pkr.hcl rename to deploy/ec2/ce/variables.pkr.hcl diff --git a/deploy/ec2/ee/.env b/deploy/ec2/ee/.env new file mode 100644 index 0000000000..c28115183f --- /dev/null +++ b/deploy/ec2/ee/.env @@ -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= diff --git a/deploy/ec2/ee/nest.service b/deploy/ec2/ee/nest.service new file mode 100644 index 0000000000..61a1127e2f --- /dev/null +++ b/deploy/ec2/ee/nest.service @@ -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 diff --git a/deploy/ec2/ee/postgrest.service b/deploy/ec2/ee/postgrest.service new file mode 100644 index 0000000000..806c6c8ee1 --- /dev/null +++ b/deploy/ec2/ee/postgrest.service @@ -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 \ No newline at end of file diff --git a/deploy/ec2/ee/redis-server.service b/deploy/ec2/ee/redis-server.service new file mode 100644 index 0000000000..c1a83a7581 --- /dev/null +++ b/deploy/ec2/ee/redis-server.service @@ -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 \ No newline at end of file diff --git a/deploy/ec2/ee/setup_app b/deploy/ec2/ee/setup_app new file mode 100755 index 0000000000..3dad6ebeef --- /dev/null +++ b/deploy/ec2/ee/setup_app @@ -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 \ No newline at end of file diff --git a/deploy/ec2/ee/setup_machine.sh b/deploy/ec2/ee/setup_machine.sh new file mode 100644 index 0000000000..23aa5bf911 --- /dev/null +++ b/deploy/ec2/ee/setup_machine.sh @@ -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 \ No newline at end of file diff --git a/deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl b/deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl new file mode 100644 index 0000000000..144803d55c --- /dev/null +++ b/deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl @@ -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" + } +} diff --git a/deploy/ec2/ee/variables.pkr.hcl b/deploy/ec2/ee/variables.pkr.hcl new file mode 100644 index 0000000000..39dcdfd3cd --- /dev/null +++ b/deploy/ec2/ee/variables.pkr.hcl @@ -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" +} diff --git a/docker/cloud/cloud-server.Dockerfile b/docker/cloud/cloud-server.Dockerfile new file mode 100644 index 0000000000..a808506ffb --- /dev/null +++ b/docker/cloud/cloud-server.Dockerfile @@ -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"] diff --git a/frontend/assets/images/icons/widgets/index.jsx b/frontend/assets/images/icons/widgets/index.jsx index 058bf838e1..7ecb678d1b 100644 --- a/frontend/assets/images/icons/widgets/index.jsx +++ b/frontend/assets/images/icons/widgets/index.jsx @@ -137,6 +137,7 @@ const WidgetIcon = (props) => { case 'map': return ; case 'modal': + case 'modallegacy': return ; case 'multiselect': case 'multiselectv2': diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index 63e37e8fd1..b76668a062 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -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" diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index 5fcedc2237..00fde7c9f2 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -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 { } /> )} - }> + }> }> }> @@ -270,7 +274,7 @@ class AppComponent extends React.Component { } /> - {getDataSourcesRoutes(this.props)} + {getDataSourcesRoutes(mergedProps)} { 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 (
{ @@ -51,7 +63,7 @@ export const ConfigHandle = ({ > diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss index 7f20210c10..e7322959e5 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss @@ -65,9 +65,3 @@ } } } - -.main-editor-canvas .widget-target:hover > .config-handle { - visibility: visible !important; -} - - diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index fed1a3db34..fc163939f6 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -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} >
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} /> ); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index 817f9a5ca9..2889fc06db 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -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'); +} diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js new file mode 100644 index 0000000000..a9405d043e --- /dev/null +++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js @@ -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, + }; +}; diff --git a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx index 34c172292d..f583ebb53a 100644 --- a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx +++ b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx @@ -89,6 +89,7 @@ const WidgetWrapper = memo( widgetHeight={layoutData.height} showHandle={isWidgetActive} componentType={componentType} + visibility={visibility} /> )} { - 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; +}; diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx index d40dc44f74..9e6737f41c 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx @@ -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 = () = { '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); diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx index 7450011b93..77274cd658 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx @@ -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 && } @@ -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 (
{ @@ -151,7 +153,8 @@ export const baseComponentProperties = ( 'properties', currentState, allComponents, - darkMode + darkMode, + '' ) ), }); diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx new file mode 100644 index 0000000000..4131217386 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx @@ -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 ; +}; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx index a57c879121..db52205467 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx @@ -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); } }; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx index 7ee512ee20..c74e023c0b 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx @@ -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 (
{ @@ -703,6 +705,9 @@ const GetAccordion = React.memo( case 'FilePicker': return ; + case 'ModalV2': + return ; + case 'Modal': return ; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js index 0ddda9f572..62ee032172 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js @@ -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} /> ); } diff --git a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx index 1226bfba3e..69ded14971 100644 --- a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx +++ b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx @@ -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 }) => { diff --git a/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js index 4ad52da372..6933f4a5a5 100644 --- a/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js +++ b/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js @@ -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'], +}; diff --git a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js index 13832857fb..be2855c476 100644 --- a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js +++ b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js @@ -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, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js index 247af3ccef..de90dbd0bf 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js @@ -299,7 +299,6 @@ export const dropdownV2Config = { ], }, label: { value: 'Select' }, - value: { value: '{{"2"}}' }, optionsLoadingState: { value: '{{false}}' }, placeholder: { value: 'Select an option' }, visibility: { value: '{{true}}' }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/index.js b/frontend/src/AppBuilder/WidgetManager/widgets/index.js index 2540cdeeef..ce0e73fdf5 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/index.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/index.js @@ -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, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js index a813bb5a0b..fec2e812b4 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js @@ -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'}]", }, }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js index 42740ad9c1..8f0c34b566 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js @@ -1,6 +1,6 @@ export const modalConfig = { - name: 'Modal', - displayName: 'Modal', + name: 'ModalLegacy', + displayName: 'Modal (Legacy)', description: 'Show pop-up windows', component: 'Modal', defaultSize: { diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js new file mode 100644 index 0000000000..e7e96c4398 --- /dev/null +++ b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js @@ -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' }, + }, + }, +}; diff --git a/frontend/src/AppBuilder/Widgets/Container.jsx b/frontend/src/AppBuilder/Widgets/Container.jsx index 3ccedd869b..1334098423 100644 --- a/frontend/src/AppBuilder/Widgets/Container.jsx +++ b/frontend/src/AppBuilder/Widgets/Container.jsx @@ -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 (
{ + const canvasFooterHeight = getCanvasHeight(footerHeight); + return ( + + + {isDisabled && ( +