diff --git a/.env.example b/.env.example index dd5dd755f9..3d49db3358 100644 --- a/.env.example +++ b/.env.example @@ -93,6 +93,10 @@ ENABLE_PRIVATE_APP_EMBED= #Enable cors else restricted to TOOLJET_HOST. Set the value true if you are serving front end from diffrent host ENABLE_CORS= +# cloud specific variables +ORGANIZATION_LICENSE_URL= +ORGANIZATION_LICENSE_API_KEY= + #pat session expiry in minutes PAT_SESSION_EXPIRY= diff --git a/.github/workflows/cloud-frontend-gcp.yml b/.github/workflows/cloud-frontend-gcp.yml new file mode 100644 index 0000000000..12deb9c653 --- /dev/null +++ b/.github/workflows/cloud-frontend-gcp.yml @@ -0,0 +1,133 @@ +name: Deploy to cloud frontend stage + +on: + workflow_dispatch: + inputs: + branch: + description: 'Git branch to deploy (must start with "lts-", e.g., lts-3.6)' + required: true + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: ✅ Check user authorization + run: | + allowed_user1=${{ secrets.ALLOWED_USER1_USERNAME }} + allowed_user2=${{ secrets.ALLOWED_USER2_USERNAME }} + allowed_user3=${{ secrets.ALLOWED_USER3_USERNAME }} + + if [[ "${{ github.actor }}" != "$allowed_user1" && \ + "${{ github.actor }}" != "$allowed_user2" && \ + "${{ github.actor }}" != "$allowed_user3" ]]; then + echo "❌ User '${{ github.actor }}' is not authorized to trigger this workflow." + exit 1 + else + echo "✅ User '${{ github.actor }}' is authorized." + fi + + - name: 📥 Manual Git checkout with submodules + run: | + set -e + + BRANCH="${{ github.event.inputs.branch }}" + REPO="https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/${{ github.repository }}" + + git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/" + git config --global http.version HTTP/1.1 + git config --global http.postBuffer 524288000 + + echo "👉 Cloning $REPO (branch: $BRANCH)" + git clone --recurse-submodules --depth=1 --branch "$BRANCH" "$REPO" repo + cd repo + + echo "🔁 Updating submodules" + git submodule update --init --recursive + + echo "🔀 Attempting to checkout '$BRANCH' in each submodule and validating" + + BRANCH="$BRANCH" git submodule foreach --recursive bash -c ' + name="$sm_path" + echo "" + echo "Entering '\''$name'\''" + echo "↪ $name: trying to checkout branch '\''$BRANCH'\''" + + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null; then + git fetch origin "$BRANCH:$BRANCH" || { + echo "❌ $name: fetch failed for $BRANCH" + exit 1 + } + + PREV=$(git rev-parse --short HEAD || echo "unknown") + git checkout "$BRANCH" || { + echo "❌ $name: checkout failed for $BRANCH" + exit 1 + } + + echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')" + echo "✅ $name: checked out branch $BRANCH" + else + echo "⚠️ $name: branch '$BRANCH' not found on origin. Falling back to 'main'" + PREV=$(git rev-parse --short HEAD || echo "unknown") + git checkout main && git pull origin main || { + echo "❌ $name: fallback to main failed" + exit 1 + } + echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')" + echo "✅ $name: now on branch main" + fi + + CURRENT=$(git rev-parse --abbrev-ref HEAD) + echo "🔎 $name: current branch = $CURRENT" + if [ "$CURRENT" != "$BRANCH" ] && [ "$CURRENT" != "main" ]; then + echo "❌ $name: unexpected branch state — wanted '$BRANCH' or fallback 'main', got '$CURRENT'" + exit 1 + fi + ' + + - name: 🧰 Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 22.15.1 + + - name: 📦 Install dependencies + run: npm install + working-directory: repo + + - name: 🛠️ Build the project + run: npm run build:plugins:prod && npm run build:frontend + working-directory: repo + env: + GOOGLE_MAPS_API_KEY: ${{ secrets.CLOUD_GOOGLE_MAPS_API_KEY }} + NODE_ENV: ${{ secrets.CLOUD_NODE_ENV }} + NODE_OPTIONS: ${{ secrets.CLOUD_NODE_OPTIONS }} + SENTRY_AUTH_TOKEN: ${{ secrets.CLOUD_SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.CLOUD_SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.CLOUD_SENTRY_PROJECT }} + SERVE_CLIENT: ${{ secrets.CLOUD_SERVE_CLIENT }} + SERVER_IP: ${{ secrets.CLOUD_SERVER_IP }} + TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }} + TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }} + TOOLJET_EDITION: cloud + WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/ai-create-account + + - name: 🚀 Deploy to Netlify + run: | + npm install -g netlify-cli + netlify deploy --prod --dir=frontend/build --auth=$NETLIFY_AUTH_TOKEN --site=${{ secrets.CLOUD_NETLIFY_SITE_ID }} + working-directory: repo + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + GOOGLE_MAPS_API_KEY: ${{ secrets.CLOUD_GOOGLE_MAPS_API_KEY }} + NODE_ENV: ${{ secrets.CLOUD_NODE_ENV }} + NODE_OPTIONS: ${{ secrets.CLOUD_NODE_OPTIONS }} + SENTRY_AUTH_TOKEN: ${{ secrets.CLOUD_SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.CLOUD_SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.CLOUD_SENTRY_PROJECT }} + SERVE_CLIENT: ${{ secrets.CLOUD_SERVE_CLIENT }} + SERVER_IP: ${{ secrets.CLOUD_SERVER_IP }} + TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }} + TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }} + WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/ai-create-account + TOOLJET_EDITION: cloud diff --git a/.github/workflows/cloud-frontend.yml b/.github/workflows/cloud-frontend.yml new file mode 100644 index 0000000000..35d13aaa40 --- /dev/null +++ b/.github/workflows/cloud-frontend.yml @@ -0,0 +1,133 @@ +name: Deploy to cloud frontend + +on: + workflow_dispatch: + inputs: + branch: + description: 'Git branch to deploy (must start with "lts-", e.g., lts-3.6)' + required: true + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: ✅ Check user authorization + run: | + allowed_user1=${{ secrets.ALLOWED_USER1_USERNAME }} + allowed_user2=${{ secrets.ALLOWED_USER2_USERNAME }} + allowed_user3=${{ secrets.ALLOWED_USER3_USERNAME }} + + if [[ "${{ github.actor }}" != "$allowed_user1" && \ + "${{ github.actor }}" != "$allowed_user2" && \ + "${{ github.actor }}" != "$allowed_user3" ]]; then + echo "❌ User '${{ github.actor }}' is not authorized to trigger this workflow." + exit 1 + else + echo "✅ User '${{ github.actor }}' is authorized." + fi + + - name: 📥 Manual Git checkout with submodules + run: | + set -e + + BRANCH="${{ github.event.inputs.branch }}" + REPO="https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/${{ github.repository }}" + + git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/" + git config --global http.version HTTP/1.1 + git config --global http.postBuffer 524288000 + + echo "👉 Cloning $REPO (branch: $BRANCH)" + git clone --recurse-submodules --depth=1 --branch "$BRANCH" "$REPO" repo + cd repo + + echo "🔁 Updating submodules" + git submodule update --init --recursive + + echo "🔀 Attempting to checkout '$BRANCH' in each submodule and validating" + + BRANCH="$BRANCH" git submodule foreach --recursive bash -c ' + name="$sm_path" + echo "" + echo "Entering '\''$name'\''" + echo "↪ $name: trying to checkout branch '\''$BRANCH'\''" + + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null; then + git fetch origin "$BRANCH:$BRANCH" || { + echo "❌ $name: fetch failed for $BRANCH" + exit 1 + } + + PREV=$(git rev-parse --short HEAD || echo "unknown") + git checkout "$BRANCH" || { + echo "❌ $name: checkout failed for $BRANCH" + exit 1 + } + + echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')" + echo "✅ $name: checked out branch $BRANCH" + else + echo "⚠️ $name: branch '$BRANCH' not found on origin. Falling back to 'main'" + PREV=$(git rev-parse --short HEAD || echo "unknown") + git checkout main && git pull origin main || { + echo "❌ $name: fallback to main failed" + exit 1 + } + echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')" + echo "✅ $name: now on branch main" + fi + + CURRENT=$(git rev-parse --abbrev-ref HEAD) + echo "🔎 $name: current branch = $CURRENT" + if [ "$CURRENT" != "$BRANCH" ] && [ "$CURRENT" != "main" ]; then + echo "❌ $name: unexpected branch state — wanted '$BRANCH' or fallback 'main', got '$CURRENT'" + exit 1 + fi + ' + + - name: 🧰 Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 22.15.1 + + - name: 📦 Install dependencies + run: npm install + working-directory: repo + + - name: 🛠️ Build the project + run: npm run build:plugins:prod && npm run build:frontend + working-directory: repo + env: + GOOGLE_MAPS_API_KEY: ${{ secrets.CLOUD_PROD_CLOUD_GOOGLE_MAPS_API_KEY }} + NODE_ENV: ${{ secrets.CLOUD_NODE_ENV }} + NODE_OPTIONS: ${{ secrets.CLOUD_NODE_OPTIONS }} + SENTRY_AUTH_TOKEN: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_PROJECT }} + SERVE_CLIENT: ${{ secrets.CLOUD_PROD_CLOUD_SERVE_CLIENT }} + SERVER_IP: ${{ secrets.CLOUD_PROD_CLOUD_SERVER_IP }} + TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }} + TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }} + WEBSITE_SIGNUP_URL: https://tooljet.ai/ai-create-account + TOOLJET_EDITION: cloud + + - name: 🚀 Deploy to Netlify + run: | + npm install -g netlify-cli + netlify deploy --prod --dir=frontend/build --auth=$NETLIFY_AUTH_TOKEN --site=${{ secrets.CLOUD_PROD_NETLIFY_SITE_ID }} + working-directory: repo + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + GOOGLE_MAPS_API_KEY: ${{ secrets.CLOUD_PROD_CLOUD_GOOGLE_MAPS_API_KEY }} + NODE_ENV: ${{ secrets.CLOUD_NODE_ENV }} + NODE_OPTIONS: ${{ secrets.CLOUD_NODE_OPTIONS }} + SENTRY_AUTH_TOKEN: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_PROJECT }} + SERVE_CLIENT: ${{ secrets.CLOUD_PROD_CLOUD_SERVE_CLIENT }} + SERVER_IP: ${{ secrets.CLOUD_PROD_CLOUD_SERVER_IP }} + TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }} + TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }} + WEBSITE_SIGNUP_URL: https://tooljet.ai/ai-create-account + TOOLJET_EDITION: cloud diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index b5f3acd0d5..7afca64ea7 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -21,7 +21,7 @@ jobs: if: "contains(github.event.release.tag_name, '-ce-lts')" uses: actions/checkout@v2 with: - ref: refs/heads/lts-4.0 + ref: refs/heads/lts-3.6 # Create Docker Buildx builder with platform configuration - name: Set up Docker Buildx @@ -99,7 +99,7 @@ jobs: steps: - name: Checkout code to main for pre-release EE edition - if: "!contains(github.event.release.tag_name, 'ee-lts')" + if: "!contains(github.event.release.tag_name, '-lts')" uses: actions/checkout@v2 with: ref: refs/heads/main @@ -108,7 +108,7 @@ jobs: if: "contains(github.event.release.tag_name, '-ee-lts')" uses: actions/checkout@v2 with: - ref: refs/heads/lts-4.0 + ref: refs/heads/lts-3.6 # Create Docker Buildx builder with platform configuration - name: Set up Docker Buildx @@ -139,15 +139,15 @@ jobs: context: . build-args: | CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }} + BRANCH_NAME=main file: docker/ee/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 }} + tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-latest,tooljet/tooljet:ee-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 @@ -155,6 +155,7 @@ jobs: context: . build-args: | CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }} + BRANCH_NAME=lts-3.6 file: docker/ee/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 }} @@ -174,64 +175,64 @@ jobs: curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} -# commented out for now, since cloud modularisation is not yet ready + build-tooljet-image-for-cloud-edtion: - # build-tooljet-image-for-cloud-edtion: + runs-on: ubuntu-latest + if: "${{ github.event.release }}" - # 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-3.6 - # 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 - # # 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: Set DOCKER_CLI_EXPERIMENTAL - # run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV + - name: use mybuilder buildx + run: docker buildx use mybuilder - # - 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: 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: . + build-args: | + CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }} + BRANCH_NAME=lts-3.6 + 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: 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 - # - 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 }} + curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} try-tooljet-image-build: diff --git a/.github/workflows/manual-docker-build.yml b/.github/workflows/manual-docker-build.yml new file mode 100644 index 0000000000..1a1e0c954d --- /dev/null +++ b/.github/workflows/manual-docker-build.yml @@ -0,0 +1,57 @@ +name: Manual Docker Build and Push + +on: + workflow_dispatch: + inputs: + branch_name: + description: 'Git branch to build from' + required: true + default: 'main' + dockerfile_path: + description: 'Path to Dockerfile' + required: true + docker_tag: + description: 'Docker tag suffix (e.g., pre-release-14)' + required: true + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch_name }} + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Generate full Docker tag + id: taggen + run: | + input_tag="${{ github.event.inputs.docker_tag }}" + if [[ "$input_tag" == *"/"* ]]; then + echo "tag=$input_tag" >> $GITHUB_OUTPUT + else + echo "tag=tooljet/tj-osv:$input_tag" >> $GITHUB_OUTPUT + fi + + - name: Build and Push Docker image + uses: docker/build-push-action@v4 + with: + context: . + file: ${{ github.event.inputs.dockerfile_path }} + push: true + tags: ${{ steps.taggen.outputs.tag }} + platforms: linux/amd64 + build-args: | + CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }} + BRANCH_NAME=${{ github.event.inputs.branch_name }} diff --git a/.github/workflows/merging-pr.yml b/.github/workflows/merging-pr.yml index f091baf0ec..63c76b46a3 100644 --- a/.github/workflows/merging-pr.yml +++ b/.github/workflows/merging-pr.yml @@ -47,7 +47,6 @@ jobs: - name: Checkout base repo uses: actions/checkout@v4 with: - repository: ToolJet/ToolJet token: ${{ secrets.TOKEN_PR }} ref: main submodules: recursive @@ -63,13 +62,30 @@ jobs: git add frontend/ee server/ee if git diff --cached --quiet; then - echo "No submodule updates found." - else - git commit -m "🔄 chore: update submodules to latest main after auto-merge" - git push origin main + echo "No submodule updates found." && exit 0 fi env: - GH_TOKEN: ${{ secrets.TOKEN_PR }} + GH_TOKEN: ${{ secrets.TOKEN_PR }} + + - name: Create PR for submodule update + id: cpr + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.TOKEN_PR }} + commit-message: "🚀 chore: update submodules to latest main after auto-merge" + title: "🚀 chore: update submodules" + body: "Auto-generated PR to update submodules after base PR merge" + branch: auto/update-submodules-${{ github.run_id }} + base: main + + - name: Auto-merge PR + if: steps.cpr.outputs.pull-request-number != '' + run: | + echo "Merging submodule update PR #${PR_NUMBER}" + gh pr merge --squash --admin "$PR_NUMBER" --repo ToolJet/ToolJet + env: + GH_TOKEN: ${{ secrets.TOKEN_PR }} + PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }} check-submodule-prs: if: github.event.action == 'labeled' && github.event.label.name == 'ready-to-merge' diff --git a/.github/workflows/packer-build.yml b/.github/workflows/packer-build.yml index aa60c6444f..7c1dfbb583 100644 --- a/.github/workflows/packer-build.yml +++ b/.github/workflows/packer-build.yml @@ -16,11 +16,11 @@ jobs: name: packer-ee steps: - - name: Checkout code to lts-4.0 + - name: Checkout code to lts-3.6 branch if: contains(github.event.release.tag_name, '-ee-lts') uses: actions/checkout@v2 with: - ref: refs/heads/lts-4.0 + ref: refs/heads/lts-3.6 - name: Setting tag if: "${{ github.event.inputs.version != '' }}" @@ -69,7 +69,7 @@ jobs: 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 + arguments: -color=false -on-error=abort -var ami_name=tooljet_${{ env.RELEASE_VERSION }}.ubuntu_jammy target: . working_directory: deploy/ec2/ee env: @@ -78,9 +78,9 @@ jobs: - name: Send Slack Notification run: | if [[ "${{ job.status }}" == "success" ]]; then - message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`" + message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu-jammy\`" else - message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`" + message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu-jammy\`" fi curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 68c98aa7a7..16a4acdae1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.18.2 \ No newline at end of file +v22.15.1 \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 63a9ee034f..d2c5181abc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,6 +17,9 @@ /package.json @shah21 @gsmithun4 @adishm98 /package-lock.json @shah21 @gsmithun4 @adishm98 -# Server service files -/server/src/services/email.service.ts @shah21 @gsmithun4 -/server/src/mails @shah21 @gsmithun4 +# Code owners for all module.ts files +**/module.ts @shah21 @gsmithun4 + +# Server migration directories +/server/migrations/* @shah21 @gsmithun4 +/server/data-migrations/* @shah21 @gsmithun4 diff --git a/cypress-tests/cypress-ee-platform.config.js b/cypress-tests/cypress-ee-platform.config.js index 25aa7f6f15..fb4f45faeb 100644 --- a/cypress-tests/cypress-ee-platform.config.js +++ b/cypress-tests/cypress-ee-platform.config.js @@ -98,8 +98,10 @@ module.exports = defineConfig({ configFile: environment.configFile, specPattern: [ "cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js", + "cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js", + "cypress/e2e/happyPath/platform/ceTestcases/apps/!(*appSlug).cy.js", "cypress/e2e/happyPath/platform/commonTestcases/userManagment/*.cy.js", - "cypress/e2e/happyPath/platform/eeTestcases/**/*.cy.js", + "cypress/e2e/happyPath/platform/eeTestcases/workspace/*.cy.js", ], numTestsKeptInMemory: 1, redirectionLimit: 15, diff --git a/cypress-tests/cypress-marketplace.config.js b/cypress-tests/cypress-marketplace.config.js index b8ffbeaa26..ce955b3c66 100644 --- a/cypress-tests/cypress-marketplace.config.js +++ b/cypress-tests/cypress-marketplace.config.js @@ -77,7 +77,7 @@ module.exports = defineConfig({ baseUrl: "http://localhost:8082", specPattern: [ "cypress/e2e/happyPath/marketplace/commonTestcases/**/*.cy.js", - ], + ] numTestsKeptInMemory: 1, redirectionLimit: 7, experimentalRunAllSpecs: true, diff --git a/cypress-tests/cypress.Dockerfile b/cypress-tests/cypress.Dockerfile index 32607825e7..3537f4fa7d 100644 --- a/cypress-tests/cypress.Dockerfile +++ b/cypress-tests/cypress.Dockerfile @@ -22,7 +22,7 @@ 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' +RUN git submodule foreach 'git checkout ${BRANCH_NAME}' # Scripts for building COPY ./package.json ./package.json @@ -54,7 +54,7 @@ RUN npm install -g @nestjs/cli RUN npm install -g copyfiles RUN npm --prefix server run build -FROM node:22.15.1 +FROM node:22.15.1-bullseye RUN apt-get update -yq \ && apt-get install curl wget gnupg zip -yq \ diff --git a/cypress-tests/cypress/commands/apiCommands.js b/cypress-tests/cypress/commands/apiCommands.js index c8ca26fd2f..60e747a48c 100644 --- a/cypress-tests/cypress/commands/apiCommands.js +++ b/cypress-tests/cypress/commands/apiCommands.js @@ -479,24 +479,22 @@ Cypress.Commands.add("apiMakeAppPublic", (appId = Cypress.env("appId")) => { }); }); -Cypress.Commands.add("apiDeleteGranularPermission", (groupName) => { +Cypress.Commands.add("apiDeleteGranularPermission", (groupName, typesToDelete = []) => { cy.getAuthHeaders().then((headers) => { - // Fetch group permissions + // Step 1: Get the group by name cy.request({ method: "GET", url: `${Cypress.env("server_host")}/api/v2/group-permissions`, - headers: headers, + headers, log: false, }).then((response) => { expect(response.status).to.equal(200); - const group = response.body.groupPermissions.find( - (g) => g.name === groupName - ); + const group = response.body.groupPermissions.find((g) => g.name === groupName); if (!group) throw new Error(`Group with name ${groupName} not found`); const groupId = group.id; - // Fetch granular permissions for the specific group + // Step 2: Get all granular permissions for the group cy.request({ method: "GET", url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions`, @@ -504,22 +502,31 @@ Cypress.Commands.add("apiDeleteGranularPermission", (groupName) => { log: false, }).then((granularResponse) => { expect(granularResponse.status).to.equal(200); - const granularPermissionId = granularResponse.body[0].id; + const granularPermissions = granularResponse.body; - // Delete the granular permission - cy.request({ - method: "DELETE", - url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${granularPermissionId}`, - headers, - log: false, - }).then((deleteResponse) => { - expect(deleteResponse.status).to.equal(200); + // Step 3: Filter if typesToDelete is specified + const permissionsToDelete = typesToDelete.length + ? granularPermissions.filter((perm) => typesToDelete.includes(perm.type)) + : granularPermissions; + + // Step 4: Delete each granular permission + permissionsToDelete.forEach((permission) => { + cy.request({ + method: "DELETE", + url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${permission.id}`, + headers, + log: false, + }).then((deleteResponse) => { + expect(deleteResponse.status).to.equal(200); + cy.log(`Deleted granular permission: ${permission.name}`); + }); }); }); }); }); }); + Cypress.Commands.add( "apiCreateGranularPermission", ( diff --git a/cypress-tests/cypress/commands/commands.js b/cypress-tests/cypress/commands/commands.js index 39e68da18a..79fea8849f 100644 --- a/cypress-tests/cypress/commands/commands.js +++ b/cypress-tests/cypress/commands/commands.js @@ -84,7 +84,20 @@ Cypress.Commands.add( const dataTransfer = new DataTransfer(); cy.forceClickOnCanvas(); - cy.clearAndType(commonSelectors.searchField, widgetName); + cy.get("body") + .then(($body) => { + const isSearchVisible = $body + .find(commonSelectors.searchField) + .is(":visible"); + + if (!isSearchVisible) { + cy.get('[data-cy="right-sidebar-plus-button"]').click(); + } + }) + .then(() => { + cy.clearAndType(commonSelectors.searchField, widgetName); + }); + cy.get(commonWidgetSelector.widgetBox(widgetName2)).trigger( "dragstart", { dataTransfer }, @@ -226,9 +239,9 @@ Cypress.Commands.add( .invoke("text") .then((text) => { cy.wrap(subject).realType(createBackspaceText(text)), - { - delay: 0, - }; + { + delay: 0, + }; }); } ); @@ -548,7 +561,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => { } }); - function installPlugin (pluginName) { + function installPlugin(pluginName) { cy.get('[data-cy="-list-item"]').eq(1).click(); cy.wait(1000); @@ -608,6 +621,7 @@ Cypress.Commands.add("uninstallMarketplacePlugin", (pluginName) => { Cypress.Commands.add( "verifyRequiredFieldValidation", (fieldName, expectedColor) => { + cy.get(commonSelectors.textField(fieldName)).type("some text").clear(); cy.get(commonSelectors.textField(fieldName)).should( "have.css", "border-color", @@ -622,11 +636,11 @@ Cypress.Commands.add( } ); -Cypress.Commands.add('ifEnv', (expectedEnvs, callback) => { +Cypress.Commands.add("ifEnv", (expectedEnvs, callback) => { const actualEnv = Cypress.env("environment"); const envArray = Array.isArray(expectedEnvs) ? expectedEnvs : [expectedEnvs]; if (envArray.includes(actualEnv)) { callback(); } -}); \ No newline at end of file +}); diff --git a/cypress-tests/cypress/constants/selectors/common.js b/cypress-tests/cypress/constants/selectors/common.js index 06f34baa99..464d0c3cf1 100644 --- a/cypress-tests/cypress/constants/selectors/common.js +++ b/cypress-tests/cypress/constants/selectors/common.js @@ -177,7 +177,7 @@ export const commonSelectors = { breadcrumbPageTitle: '[data-cy="breadcrumb-page-title"]', labelFullNameInput: '[data-cy="name-label"]', duplicateOption: '[data-cy="duplicate-group-card-option"]', - confirmDuplicateButton: '[data-cy="confim-button"]', + confirmDuplicateButton: '[data-cy="confirm-button"]', inputFieldFullName: '[data-cy="name-input"]', labelEmailInput: '[data-cy="email-label"]', inputFieldEmailAddress: '[data-cy="email-input"]', @@ -288,6 +288,7 @@ export const commonSelectors = { labelFieldAlert: (fieldName) => { return `[data-cy="${cyParamName(fieldName)}-is-required-field-alert-text"]`; }, + pageLogo: '[data-cy="page-logo"]', }; export const commonWidgetSelector = { diff --git a/cypress-tests/cypress/constants/selectors/manageGroups.js b/cypress-tests/cypress/constants/selectors/manageGroups.js index 5066342ff6..9ed4273ba5 100644 --- a/cypress-tests/cypress/constants/selectors/manageGroups.js +++ b/cypress-tests/cypress/constants/selectors/manageGroups.js @@ -133,7 +133,7 @@ export const groupsSelector = { usersCheckInput: '[data-cy="users-check-input"]', permissionCheckInput: '[data-cy="permissions-check-input"]', appsCheckInput: '[data-cy="apps-check-input"]', - confimButton: '[data-cy="confim-button"]', + confimButton: '[data-cy="confirm-button"]', duplicatedGroupLink: (groupName) => { return `[data-cy="${cyParamName(groupName)}_copy-list-item"]` }, diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTableHappyPath.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTableHappyPath.cy.js index 0f3cf9c7a5..9e383b041d 100644 --- a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTableHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTableHappyPath.cy.js @@ -202,10 +202,10 @@ describe("Data source Airtable", () => { ); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.dsName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.dsName}) completed.` + // ); // Verfiy Retrieve record operation @@ -225,10 +225,10 @@ describe("Data source Airtable", () => { ); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.dsName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.dsName}) completed.` + // ); // Verfiy Create record operation @@ -251,10 +251,10 @@ describe("Data source Airtable", () => { .realType('": {}', { force: true, delay: 0 }); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.dsName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.dsName}) completed.` + // ); // Verfiy Update record operation @@ -285,10 +285,10 @@ describe("Data source Airtable", () => { .realType('"Phone Number": "555_98"', { force: true, delay: 0 }); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.queryName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.queryName}) completed.` + // ); // Verify Delete record operation @@ -337,10 +337,10 @@ describe("Data source Airtable", () => { ); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.queryName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.queryName}) completed.` + // ); cy.apiDeleteApp(`${data.dsName}-airtable-app`); cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/postgresHappyPath.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/postgresHappyPath.cy.js index b86ca7cb17..962baeb991 100644 --- a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/postgresHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/postgresHappyPath.cy.js @@ -254,7 +254,7 @@ describe("Data sources", () => { .and("be.disabled"); cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement( "have.text", - "connect ECONNREFUSED 127.0.0.1:5432" + postgreSqlText.serverNotSuppotSsl ); cy.apiDeleteGDS(`cypress-${data.dataSourceName}-postgresql`); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js index 2bd1ccf51e..f75edd64ce 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js @@ -22,7 +22,7 @@ describe("App Import Functionality", () => { let data; beforeEach(() => { - cy.viewport(1200, 1300); + cy.viewport(1400, 1400); data = { workspaceName: fake.firstName, workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"), @@ -34,7 +34,7 @@ describe("App Import Functionality", () => { cy.apiLogin(); cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); cy.apiLogout(); - cy.skipWalkthrough() + cy.skipWalkthrough(); }); it("should verify app import functionality", () => { @@ -151,23 +151,49 @@ describe("App Import Functionality", () => { cy.visit(`${data.workspaceSlug}/data-sources`); cy.get('[data-cy="postgresql-button"]').should("be.visible"); - cy.apiUpdateDataSource("postgresql", "production", { - options: [ - { - key: "password", - value: `${Cypress.env("pg_password")}`, - encrypted: true, - }, - ], + + cy.ifEnv("Community", () => { + cy.apiUpdateDataSource("postgresql", "production", { + options: [ + { + key: "password", + value: `${Cypress.env("pg_password")}`, + encrypted: true, + }, + ], + }); + }); + cy.ifEnv("Enterprise", () => { + cy.apiUpdateDataSource("postgresql", "development", { + options: [ + { + key: "password", + value: `${Cypress.env("pg_password")}`, + encrypted: true, + }, + ], + }); }); - cy.apiCreateWsConstant( - "pageHeader", - "Import and Export", - ["Global"], - ["production"] - ); - cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["production"]); + cy.ifEnv("Community", () => { + cy.apiCreateWsConstant( + "pageHeader", + "Import and Export", + ["Global"], + ["production"] + ); + cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["production"]); + }); + + cy.ifEnv("Enterprise", () => { + cy.apiCreateWsConstant( + "pageHeader", + "Import and Export", + ["Global"], + ["development"] + ); + cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["development"]); + }); // Verify app after setup cy.wait("@importApp").then((interception) => { diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js index 796ac009b3..c4a2c674ff 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js @@ -7,6 +7,7 @@ import { verifyURLs, resolveHost, } from "Support/utils/apps"; +import { appPromote } from "Support/utils/platform/multiEnv"; describe("App Slug", () => { const data = {}; @@ -153,6 +154,7 @@ describe("App Slug", () => { cy.visit("/my-workspace"); cy.apiCreateApp(data.slug); cy.openApp("my-workspace"); + releaseApp(); cy.get(commonWidgetSelector.shareAppButton).click(); cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js index 19b87c6efe..171f5fc4b0 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js @@ -12,6 +12,8 @@ import { verifyRestrictedAccess, onboardUserFromAppLink, } from "Support/utils/apps"; +import { appPromote } from "Support/utils/platform/multiEnv"; +import { InstanceSSO } from "Support/utils/platform/eeCommon"; describe( "Private and Public apps", @@ -98,7 +100,7 @@ describe( ); // Test public access - cy.get(commonSelectors.viewerPageLogo).click(); + // cy.get(commonSelectors.viewerPageLogo).click(); cy.openApp( "appSlug", Cypress.env("workspaceId"), @@ -150,7 +152,7 @@ describe( "be.visible" ); - cy.get(commonSelectors.viewerPageLogo).click(); + // cy.get(commonSelectors.viewerPageLogo).click(); // Test public access cy.defaultWorkspaceLogin(); @@ -183,6 +185,9 @@ describe( setupAppWithSlug(data.appName, data.slug); cy.apiLogout(); + cy.ifEnv("Enterprise", () => { + InstanceSSO(true, true, true); + }); userSignUp(data.firstName, data.email, data.workspaceName); cy.wait(1000); cy.visitSlug({ @@ -253,7 +258,9 @@ describe( "be.visible" ); - cy.get('[data-cy="viewer-page-logo"]').click(); + // cy.get('[data-cy="viewer-page-logo"]').click(); + cy.visit("/my-workspace"); + cy.wait(2000); logout(); cy.wait(1000); cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should( @@ -312,7 +319,9 @@ describe( cy.apiLogout(); cy.apiLogin(); cy.visit(`${data.workspaceSlug}`); - cy.apiDeleteGranularPermission("end-user"); + + cy.apiDeleteGranularPermission("end-user", ["app", "workflow"]); + setSignupStatus(true, data.workspaceName); setupAppWithSlug(data.appName, data.slug); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js index 3432744fc3..433d74b05e 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js @@ -1,7 +1,6 @@ import { commonSelectors, commonWidgetSelector } from "Selectors/common"; import { fake } from "Fixtures/fake"; import { commonText } from "Texts/common"; - import { editVersionAndVerify, deleteVersionAndVerify, @@ -13,25 +12,20 @@ import { navigateToEditVersionModal, switchVersionAndVerify, } from "Support/utils/version"; - import { appVersionSelectors } from "Selectors/exportImport"; import { editVersionSelectors } from "Selectors/version"; import { editVersionText } from "Texts/version"; import { createNewVersion } from "Support/utils/exportImport"; - import { verifyModal, closeModal } from "Support/utils/common"; - import { verifyComponent, verifyComponentinrightpannel, deleteComponentAndVerify, } from "Support/utils/basicComponents"; - import { deleteVersionText, onlydeleteVersionText } from "Texts/version"; - import { createRestAPIQuery } from "Support/utils/dataSource"; import { deleteQuery } from "Support/utils/queries"; - +import { selectEnv, appPromote } from "Support/utils/platform/multiEnv"; describe("App Version", () => { let data; @@ -50,6 +44,8 @@ describe("App Version", () => { cy.defaultWorkspaceLogin(); cy.apiCreateApp(data.appName); cy.openApp(); + cy.viewport(1400, 1400); + }); it("should verify basic version management operations", () => { @@ -120,7 +116,15 @@ describe("App Version", () => { // Preview and release verification cy.openInCurrentTab(commonWidgetSelector.previewButton); - cy.url().should("include", "/home?version=v2"); + + cy.ifEnv("Community", () => { + cy.url().should("include", "/home?version=v2"); + }); + + cy.ifEnv("Enterprise", () => { + cy.url().should("include", "/home?env=development&version=v2"); + }); + cy.openApp( "", Cypress.env("workspaceId"), @@ -149,7 +153,11 @@ describe("App Version", () => { createRestAPIQuery(data.query1, data.datasourceName, "", "", "/1", true); - // Version v2 creation and verification + cy.ifEnv("Enterprise", () => { + appPromote("development", "production"); + }); + + // Version v2 creation and verification and v2 is created from v1 production environment navigateToCreateNewVersionModal("v1"); createNewVersion(["v2"], "v1"); cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement( @@ -201,7 +209,8 @@ describe("App Version", () => { versionChecks.forEach((check) => { navigateToCreateNewVersionModal(check.create.from); createNewVersion([check.create.version], check.create.from); - + cy.waitForAutoSave(); + cy.wait(1000); if (check.verify.component.value) { cy.get( commonWidgetSelector.draggableWidget(check.verify.component.selector) @@ -224,6 +233,9 @@ describe("App Version", () => { ); // Version switching and component verification + cy.ifEnv("Enterprise", () => { + selectEnv("development"); + }); cy.get(appVersionSelectors.currentVersionField("v5")).click(); cy.contains(`[id*="react-select-"]`, "v4").click(); cy.get(appVersionSelectors.currentVersionField("v4")).should( @@ -238,7 +250,14 @@ describe("App Version", () => { // Preview and version switching verification cy.openInCurrentTab(commonWidgetSelector.previewButton); - cy.url().should("include", "/home?version=v4"); + + cy.ifEnv("Community", () => { + cy.url().should("include", "/home?version=v4"); + }); + cy.ifEnv("Enterprise", () => { + cy.url().should("include", "/home?env=development&version=v4"); + }); + cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement( "have.text", "Leanne Graham" @@ -250,8 +269,74 @@ describe("App Version", () => { cy.get( commonWidgetSelector.draggableWidget("textInput") ).verifyVisibleElement("have.value", "Ervin Howell"); - //url validation should be added after bug fix - // cy.url().should("include", "/home?version=v5"); + cy.ifEnv("Enterprise", () => { + cy.openApp( + "", + Cypress.env("workspaceId"), + Cypress.env("appId"), + commonWidgetSelector.draggableWidget("textInput") + ); + + navigateToCreateNewVersionModal("v5"); + createNewVersion(["v6"], "v5"); + cy.waitForAutoSave(); + cy.wait(1000); + + appPromote("development", "staging"); + cy.get( + commonWidgetSelector.draggableWidget("textInput") + ).verifyVisibleElement("have.value", "Ervin Howell"); + cy.get(`[data-cy="list-query-${data.query2}"]`).should("be.visible"); + + appPromote("staging", "production"); + + cy.get( + commonWidgetSelector.draggableWidget("textInput") + ).verifyVisibleElement("have.value", "Ervin Howell"); + cy.get(`[data-cy="list-query-${data.query2}"]`).should("be.visible"); + + cy.openInCurrentTab(commonWidgetSelector.previewButton); + cy.get( + commonWidgetSelector.draggableWidget("textInput") + ).verifyVisibleElement("have.value", "Ervin Howell"); + cy.url().should("include", "/home?env=production&version=v6"); + + cy.wait(1000); + + cy.get('[data-cy="preview-settings"]').click(); + switchVersionAndVerify("v6", "v1"); + + cy.get( + commonWidgetSelector.draggableWidget("text1") + ).verifyVisibleElement("have.text", "Leanne Graham"); + // url bug + // cy.url().should("include", "/home?env=production&version=v1"); + cy.wait(1000); + cy.get('[data-cy="preview-settings"]').click(); + switchVersionAndVerify("v1", "v6"); + + cy.wait(1000); + cy.get('[data-cy="preview-settings"]').click(); + selectEnv("staging"); + + cy.get( + commonWidgetSelector.draggableWidget("textInput") + ).verifyVisibleElement("have.value", "Ervin Howell"); + // cy.url().should("include", "/home?env=staging&version=v6"); + + + cy.wait(1000); + cy.get('[data-cy="preview-settings"]').click(); + selectEnv("development"); + + cy.wait(1000); + cy.get('[data-cy="preview-settings"]').click(); + switchVersionAndVerify("v6", "v1"); + + cy.get( + commonWidgetSelector.draggableWidget("text1") + ).verifyVisibleElement("have.text", "Leanne Graham"); + }); }); }); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/dataSources/dataSourcePermissions.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/dataSources/dataSourcePermissions.cy.js index cc0cee6f6d..7ddd4e937d 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/dataSources/dataSourcePermissions.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/dataSources/dataSourcePermissions.cy.js @@ -47,8 +47,8 @@ describe("Datasource Manager", () => { data.dsName1 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); data.dsName2 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); - const allDataSources = host.includes("8082") ? "All data sources (43)" : "All data sources (45)"; - const allDatabase = host.includes("8082") ? "Databases (18)" : "Databases (20)"; + const allDataSources = host.includes("8082") ? "All data sources (45)" : "All data sources (45)"; + const allDatabase = host.includes("8082") ? "Databases (20)" : "Databases (20)"; cy.get(commonSelectors.globalDataSourceIcon).click(); cy.get(commonSelectors.pageSectionHeader).verifyVisibleElement( diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js index ddd3f3d91e..33d377459b 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js @@ -39,6 +39,8 @@ describe("Workspace constants", () => { beforeEach(() => { cy.defaultWorkspaceLogin(); cy.skipWalkthrough(); + cy.viewport(1800, 1800); + }); it("Verify workspace constants UI and CRUD operations", () => { @@ -66,12 +68,11 @@ describe("Workspace constants", () => { }); }); - it("Verify global and secret constants in the editor, inspector, data sources, static queries, query preview, and preview", () => { + it.only("Verify global and secret constants in the editor, inspector, data sources, static queries, query preview, and preview", () => { data.workspaceName = fake.firstName; data.workspaceSlug = fake.firstName.toLowerCase().replace(/[^A-Za-z]/g, ""); cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); cy.visit(data.workspaceSlug); - cy.viewport(1440, 960); data.appName = `${fake.companyName}-App`; // create global constants @@ -102,8 +103,8 @@ describe("Workspace constants", () => { .eq(0) .selectFile('cypress/fixtures/templates/workspace_constants.json', { force: true }); cy.get(importSelectors.importAppButton).click(); - cy.wait(5000); - + cy.wait(6000); + cy.get(commonWidgetSelector.draggableWidget('textinput1')).should('be.visible'); //Verify global constant value is resolved in component cy.get(commonWidgetSelector.draggableWidget('textinput1')) .verifyVisibleElement("have.value", "customHeader"); @@ -115,9 +116,10 @@ describe("Workspace constants", () => { cy.get(commonWidgetSelector.alertInfoText).contains( "secrets cannot be used in apps" ); - //Verify all static and datasource queries output in components + cy.wait(8000); for (let i = 3; i <= 16; i++) { + cy.wait(1000); cy.log("Verifying textinput" + i); cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`)) .verifyVisibleElement("have.value", "Production environment testing"); @@ -151,16 +153,20 @@ describe("Workspace constants", () => { //Preview app and verify components cy.openInCurrentTab(commonWidgetSelector.previewButton); - cy.wait(6000); - for (let i = 3; i <= 16; i++) { + cy.wait(8000); + cy.get(commonWidgetSelector.draggableWidget('textinput1')).should('be.visible'); + for (let i = 16; i >= 3; i--) { + cy.wait(1000); + cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`)).should('be.visible'); cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`)) - .verifyVisibleElement("have.value", "Production environment testing"); + .verifyVisibleElement("have.value", "Production environment testing", { timeout: 10000 }); } - //back to dashboard and open app again - cy.get(commonSelectors.viewerPageLogo).click(); - cy.wait(2000); + + cy.visit('/'); + cy.wait(4000); cy.get(commonSelectors.appEditButton).click({ force: true }); + cy.wait(4000); cy.releaseApp(); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/UserInviteFlow.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/UserInviteFlow.cy.js index 9883fef40d..be286792a5 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/UserInviteFlow.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/UserInviteFlow.cy.js @@ -369,7 +369,7 @@ describe("user invite flow cases", () => { "have.text", "Cancel" ); - cy.get('[data-cy="confim-button"]').verifyVisibleElement( + cy.get('[data-cy="confirm-button"]').verifyVisibleElement( "have.text", "Continue" ); @@ -407,7 +407,7 @@ describe("user invite flow cases", () => { cy.get('[data-cy="group-check-input"]').eq(0).check(); cy.get(usersSelector.buttonInviteUsers).click(); - cy.get('[data-cy="confim-button"]').click(); + cy.get('[data-cy="confirm-button"]').click(); cy.verifyToastMessage( commonSelectors.toastMessage, @@ -426,7 +426,7 @@ describe("user invite flow cases", () => { cy.get('[data-cy="group-check-input"]').eq(0).check(); cy.get(usersSelector.buttonInviteUsers).click(); - cy.get('[data-cy="confim-button"]').click(); + cy.get('[data-cy="confirm-button"]').click(); cy.verifyToastMessage( commonSelectors.toastMessage, diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js index fb8e932973..dc0ba39e61 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js @@ -24,6 +24,7 @@ import { logout } from "Support/utils/common"; describe("dashboard", () => { let data = {}; + beforeEach(() => { data = { appName: `${fake.companyName}-App`, @@ -44,164 +45,6 @@ describe("dashboard", () => { cy.visit(`${data.workspaceSlug}`); }); - // it("Should verify app card elements and app card operations", () => { - // const customLayout = { - // desktop: { top: 100, left: 20 }, - // mobile: { width: 8, height: 50 }, - // }; - - // cy.apiCreateApp(data.appName); - // cy.visit(`${data.workspaceSlug}`); - - // cy.wait(2000); - // cy.get(commonSelectors.appCreationDetails).should("be.visible"); - // cy.get(commonSelectors.appCard(data.appName)).should("be.visible"); - // cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement( - // "have.text", - // data.appName - // ); - - // viewAppCardOptions(data.appName); - // cy.get( - // commonSelectors.appCardOptions(commonText.changeIconOption) - // ).verifyVisibleElement("have.text", commonText.changeIconOption); - // cy.get( - // commonSelectors.appCardOptions(commonText.addToFolderOption) - // ).verifyVisibleElement("have.text", commonText.addToFolderOption); - // cy.get( - // commonSelectors.appCardOptions(commonText.cloneAppOption) - // ).verifyVisibleElement("have.text", commonText.cloneAppOption); - // cy.get( - // commonSelectors.appCardOptions(commonText.exportAppOption) - // ).verifyVisibleElement("have.text", commonText.exportAppOption); - // cy.get( - // commonSelectors.appCardOptions(commonText.deleteAppOption) - // ).verifyVisibleElement("have.text", commonText.deleteAppOption); - - // modifyAndVerifyAppCardIcon(data.appName); - // createFolder(data.folderName); - - // viewAppCardOptions(data.appName); - // cy.get( - // commonSelectors.appCardOptions(commonText.addToFolderOption) - // ).click(); - // verifyModal( - // dashboardText.addToFolderTitle, - // dashboardText.addToFolderButton, - // dashboardSelector.selectFolder - // ); - // cy.get(dashboardSelector.moveAppText).verifyVisibleElement( - // "have.text", - // dashboardText.moveAppText(data.appName) - // ); - - // cy.get(dashboardSelector.selectFolder).click(); - // cy.get(commonSelectors.folderList).contains(data.folderName).click(); - // cy.get(dashboardSelector.addToFolderButton).click(); - // cy.verifyToastMessage( - // commonSelectors.toastMessage, - // commonText.AddedToFolderToast, - // false - // ); - - // cy.get(dashboardSelector.folderName(data.folderName)).verifyVisibleElement( - // "have.text", - // dashboardText.folderName(`${data.folderName} (1)`) - // ); - - // cy.get(dashboardSelector.folderName(data.folderName)).click(); - // cy.get(commonSelectors.appCard(data.appName)) - // .contains(data.appName) - // .should("be.visible"); - - // viewAppCardOptions(data.appName); - - // cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption)) - // .verifyVisibleElement("have.text", commonText.removeFromFolderOption) - // .click(); - // verifyConfirmationModal(commonText.appRemovedFromFolderMessage); - - // cancelModal(commonText.cancelButton); - - // viewAppCardOptions(data.appName); - // cy.get( - // commonSelectors.appCardOptions(commonText.removeFromFolderOption) - // ).click(); - // cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click(); - // cy.verifyToastMessage( - // commonSelectors.toastMessage, - // commonText.appRemovedFromFolderTaost, - // false - // ); - // cy.get(commonSelectors.modalComponent).should("not.exist"); - // cy.get(commonSelectors.empytyFolderImage).should("be.visible"); - // cy.get(commonSelectors.emptyFolderText).verifyVisibleElement( - // "have.text", - // commonText.emptyFolderText - // ); - // cy.get(commonSelectors.allApplicationsLink).click(); - // deleteFolder(data.folderName); - - // cy.get(commonSelectors.allApplicationsLink).click(); - - // cy.wait(1000); - // viewAppCardOptions(data.appName); - // cy.wait(2000); - // cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click(); - // cy.get(commonSelectors.exportAllButton).click(); - - // cy.exec("ls ./cypress/downloads/").then((result) => { - // const downloadedAppExportFileName = result.stdout.split("\n")[0]; - // expect(downloadedAppExportFileName).to.contain.string("app"); - // }); - - // viewAppCardOptions(data.appName); - // cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click(); - // cy.get('[data-cy="clone-app"]').click(); - // cy.get(".go3958317564") - // .should("be.visible") - // .and("have.text", dashboardText.appClonedToast); - // cy.wait(3000); - - // cy.renameApp(data.cloneAppName); - // cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25); - // cy.backToApps(); - // cy.wait("@appLibrary"); - // cy.wait(1000); - - // cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible"); - - // cy.wait(1000); - - // viewAppCardOptions(data.cloneAppName); - // cy.get(commonSelectors.deleteAppOption).click(); - // cy.get(commonSelectors.modalMessage).verifyVisibleElement( - // "have.text", - // commonText.deleteAppModalMessage(data.cloneAppName) - // ); - // cy.get( - // commonSelectors.buttonSelector(commonText.cancelButton) - // ).verifyVisibleElement("have.text", commonText.cancelButton); - // cy.get( - // commonSelectors.buttonSelector(commonText.modalYesButton) - // ).verifyVisibleElement("have.text", commonText.modalYesButton); - // cancelModal(commonText.cancelButton); - - // viewAppCardOptions(data.cloneAppName); - // cy.get(commonSelectors.deleteAppOption).click(); - // cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click(); - // cy.verifyToastMessage( - // commonSelectors.toastMessage, - // commonText.appDeletedToast, - // false - // ); - // verifyAppDelete(data.cloneAppName); - // cy.wait("@appLibrary"); - - // cy.deleteApp(data.appName); - // verifyAppDelete(data.appName); - // }); - it("should verify the elements on empty dashboard", () => { cy.intercept("GET", "/api/metadata", { body: { @@ -259,9 +102,6 @@ describe("dashboard", () => { .should("have.attr", "class") .and("contain", "theme-dark"); cy.get(dashboardSelector.modeToggle).click(); - cy.get(dashboardSelector.homePageContent) - .should("have.attr", "class") - .and("contain", "bg-light-gray"); cy.wait(500); cy.get(commonSelectors.settingsIcon).click(); @@ -329,6 +169,169 @@ describe("dashboard", () => { verifyTooltip(dashboardSelector.modeToggle, "Mode"); }); + it("Should verify app card elements and app card operations", () => { + cy.exec("mkdir -p ./cypress/downloads/"); + cy.exec("cd ./cypress/downloads/ && rm -rf *"); + + const customLayout = { + desktop: { top: 100, left: 20 }, + mobile: { width: 8, height: 50 }, + }; + + cy.apiCreateApp(data.appName); + cy.visit(`${data.workspaceSlug}`); + + cy.wait(2000); + cy.get(commonSelectors.appCreationDetails).should("be.visible"); + cy.get(commonSelectors.appCard(data.appName)).should("be.visible"); + cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement( + "have.text", + data.appName + ); + + viewAppCardOptions(data.appName); + cy.get( + commonSelectors.appCardOptions(commonText.changeIconOption) + ).verifyVisibleElement("have.text", commonText.changeIconOption); + cy.get( + commonSelectors.appCardOptions(commonText.addToFolderOption) + ).verifyVisibleElement("have.text", commonText.addToFolderOption); + cy.get( + commonSelectors.appCardOptions(commonText.cloneAppOption) + ).verifyVisibleElement("have.text", commonText.cloneAppOption); + cy.get( + commonSelectors.appCardOptions(commonText.exportAppOption) + ).verifyVisibleElement("have.text", commonText.exportAppOption); + cy.get( + commonSelectors.appCardOptions(commonText.deleteAppOption) + ).verifyVisibleElement("have.text", commonText.deleteAppOption); + + modifyAndVerifyAppCardIcon(data.appName); + createFolder(data.folderName); + + viewAppCardOptions(data.appName); + cy.get( + commonSelectors.appCardOptions(commonText.addToFolderOption) + ).click(); + verifyModal( + dashboardText.addToFolderTitle, + dashboardText.addToFolderButton, + dashboardSelector.selectFolder + ); + cy.get(dashboardSelector.moveAppText).verifyVisibleElement( + "have.text", + dashboardText.moveAppText(data.appName) + ); + + cy.get(dashboardSelector.selectFolder).click(); + cy.get(commonSelectors.folderList).contains(data.folderName).click(); + cy.get(dashboardSelector.addToFolderButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + commonText.AddedToFolderToast, + false + ); + + cy.get(dashboardSelector.folderName(data.folderName)).verifyVisibleElement( + "have.text", + dashboardText.folderName(`${data.folderName} (1)`) + ); + + cy.get(dashboardSelector.folderName(data.folderName)).click(); + cy.get(commonSelectors.appCard(data.appName)) + .contains(data.appName) + .should("be.visible"); + + viewAppCardOptions(data.appName); + + cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption)) + .verifyVisibleElement("have.text", commonText.removeFromFolderOption) + .click(); + verifyConfirmationModal(commonText.appRemovedFromFolderMessage); + + cancelModal(commonText.cancelButton); + + viewAppCardOptions(data.appName); + cy.get( + commonSelectors.appCardOptions(commonText.removeFromFolderOption) + ).click(); + cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + commonText.appRemovedFromFolderTaost, + false + ); + cy.get(commonSelectors.modalComponent).should("not.exist"); + cy.get(commonSelectors.empytyFolderImage).should("be.visible"); + cy.get(commonSelectors.emptyFolderText).verifyVisibleElement( + "have.text", + commonText.emptyFolderText + ); + cy.get(commonSelectors.allApplicationsLink).click(); + deleteFolder(data.folderName); + + cy.get(commonSelectors.allApplicationsLink).click(); + + cy.wait(1000); + viewAppCardOptions(data.appName); + cy.wait(2000); + cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click(); + cy.get(commonSelectors.exportAllButton).click(); + + + cy.exec("ls ./cypress/downloads/").then((result) => { + const downloadedAppExportFileName = result.stdout.split("\n")[0]; + expect(downloadedAppExportFileName).to.contain.string("app"); + }); + + viewAppCardOptions(data.appName); + cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click(); + cy.get('[data-cy="clone-app"]').click(); + cy.get(".go3958317564") + .should("be.visible") + .and("have.text", dashboardText.appClonedToast); + cy.wait(3000); + + cy.renameApp(data.cloneAppName); + cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25); + cy.backToApps(); + cy.wait("@appLibrary"); + cy.wait(1000); + + cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible"); + + cy.wait(1000); + + viewAppCardOptions(data.cloneAppName); + cy.get(commonSelectors.deleteAppOption).click(); + cy.get(commonSelectors.modalMessage).verifyVisibleElement( + "have.text", + commonText.deleteAppModalMessage(data.cloneAppName) + ); + cy.get( + commonSelectors.buttonSelector(commonText.cancelButton) + ).verifyVisibleElement("have.text", commonText.cancelButton); + cy.get( + commonSelectors.buttonSelector(commonText.modalYesButton) + ).verifyVisibleElement("have.text", commonText.modalYesButton); + cancelModal(commonText.cancelButton); + + viewAppCardOptions(data.cloneAppName); + cy.get(commonSelectors.deleteAppOption).click(); + cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + commonText.appDeletedToast, + false + ); + + cy.get(commonSelectors.appCard(data.cloneAppName)).should("not.exist"); + cy.wait("@appLibrary"); + + cy.deleteApp(data.appName); + cy.get(commonSelectors.appCard(data.appName)).should("not.exist"); + }); + it("Should verify the app CRUD operation", () => { const customLayout = { desktop: { top: 100, left: 20 }, @@ -353,7 +356,7 @@ describe("dashboard", () => { cy.deleteApp(data.appName); - verifyAppDelete(data.appName); + cy.get(commonSelectors.appCard(data.appName)).should("not.exist"); }); it("Should verify the folder CRUD operation", () => { @@ -474,7 +477,7 @@ describe("dashboard", () => { cy.get(commonSelectors.allApplicationsLink).click(); cy.deleteApp(data.appName); - verifyAppDelete(data.appName); + cy.get(commonSelectors.appCard(data.appName)).should("not.exist"); logout(); }); }); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js index 96b87dd9b7..681a2d93e6 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js @@ -76,11 +76,11 @@ describe("Manage Groups", () => { // App operations cy.createApp(data.appName); - cy.verifyToastMessage( - commonSelectors.toastMessage, - commonText.appCreatedToast, - false - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // commonText.appCreatedToast, + // false + // ); cy.backToApps(); cy.deleteApp(data.appName); @@ -178,11 +178,11 @@ describe("Manage Groups", () => { // App operations cy.createApp(data.appName); - cy.verifyToastMessage( - commonSelectors.toastMessage, - commonText.appCreatedToast, - false - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // commonText.appCreatedToast, + // false + // ); cy.backToApps(); cy.deleteApp(data.appName); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js index bfa5806939..e9a4f5c222 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js @@ -196,10 +196,10 @@ describe("Manage Groups", () => { // App operations cy.createApp(data.appName); - cy.verifyToastMessage( - commonSelectors.toastMessage, - commonText.appCreatedToast - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // commonText.appCreatedToast + // ); cy.backToApps(); cy.wait(2500); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/multi-env/multiEnv.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/multi-env/multiEnv.cy.js new file mode 100644 index 0000000000..4197213244 --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/multi-env/multiEnv.cy.js @@ -0,0 +1,662 @@ +import { fake } from "Fixtures/fake"; +import { commonSelectors, commonWidgetSelector } from "Selectors/common"; +import { commonEeText, ssoEeText } from "Texts/eeCommon"; +import { commonEeSelectors, multiEnvSelector } from "Selectors/eeCommon"; +import { + verifyPromoteModalUI, + verifyTooltipDisabled, +} from "Support/utils/platform/eeCommon"; +import { dataSourceSelector } from "Selectors/dataSource"; +import { + navigateToAppEditor, + pinInspector, + verifyTooltip, +} from "Support/utils/common"; +import { addQuery, selectDatasource } from "Support/utils/dataSource"; +import { + appPromote, + createNewVersion, + selectVersion, + selectEnv, +} from "Support/utils/platform/multiEnv"; +import { appVersionSelectors } from "Selectors/exportImport"; + +import { editAndVerifyWidgetName } from "Support/utils/commonWidget"; +import { deleteVersionAndVerify } from "Support/utils/version"; +import { deleteVersionText } from "Texts/version"; + +describe("Multi env", () => { + const data = {}; + data.appName = `${fake.companyName} App`; + data.ds = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); + data.constName = fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", ""); + const slug = data.appName.toLowerCase().replace(/\s+/g, "-"); + let currentVersion = ""; + let newVersion = []; + let versionFrom = ""; + + beforeEach(() => { + cy.apiLogin(); + cy.viewport(1800, 1800); + cy.skipWalkthrough(); + }); + + it.only("Verify the datasource configuration and data on each env", () => { + cy.apiCreateGDS( + `${Cypress.env("server_host")}/api/data-sources`, + data.ds, + "restapi", + [ + { key: "url", value: "" }, + { key: "auth_type", value: "none" }, + { key: "grant_type", value: "authorization_code" }, + { key: "add_token_to", value: "header" }, + { key: "header_prefix", value: "Bearer " }, + { key: "access_token_url", value: "" }, + { key: "client_ide", value: "" }, + { key: "client_secret", value: "", encrypted: true }, + { key: "scopes", value: "read, write" }, + { key: "username", value: "", encrypted: false }, + { key: "password", value: "", encrypted: true }, + { key: "bearer_token", value: "", encrypted: true }, + { key: "auth_url", value: "" }, + { key: "client_auth", value: "header" }, + { key: "headers", value: [["", ""]] }, + { key: "custom_query_params", value: [["", ""]], encrypted: false }, + { key: "custom_auth_params", value: [["", ""]] }, + { + key: "access_token_custom_headers", + value: [["", ""]], + encrypted: false, + }, + { key: "multiple_auth_enabled", value: false, encrypted: false }, + { key: "ssl_certificate", value: "none", encrypted: false }, + ] + ); + cy.apiCreateApp(data.appName); + cy.visit("/my-workspace"); + cy.get(commonSelectors.globalDataSourceIcon).click(); + selectDatasource(data.ds); + cy.get('[data-cy="development-label"]').click(); + cy.clearAndType( + '[data-cy="base-url-text-field"]', + "https://reqres.in/api/users?page=1" + ); + cy.get(dataSourceSelector.buttonSave).click(); + cy.wait(2000); + + cy.get(commonSelectors.dashboardIcon).click(); + + cy.openApp(); + // cy.waitForAppLoad(); + cy.wait(2000); + cy.get(`[data-cy="${data.ds}-add-query-card"] > .text-truncate`).click(); + cy.wait(1000); + cy.get(dataSourceSelector.queryCreateAndRunButton).click(); + cy.get('[data-cy="query-tab-settings"]').click(); + cy.get(':nth-child(1) > .custom-toggle-switch > .switch > .slider').click(); + cy.waitForAutoSave(); + + cy.dragAndDropWidget("Text Input", 550, 650); + editAndVerifyWidgetName(data.constName, []); + cy.waitForAutoSave(); + + cy.get( + '[data-cy="default-value-input-field"]' + ).clearAndTypeOnCodeMirror(`{{queries.restapi1.data.data[0].email`); + cy.wait(1000); + cy.forceClickOnCanvas(); + cy.waitForAutoSave(); + cy.get(dataSourceSelector.queryCreateAndRunButton).click(); + cy.get( + commonWidgetSelector.draggableWidget(data.constName) + ).verifyVisibleElement("have.value", "george.bluth@reqres.in"); + + pinInspector(); + cy.get(commonWidgetSelector.sidebarinspector).click(); + cy.get(commonWidgetSelector.inspectorNodeComponents).click(); + cy.get(commonWidgetSelector.nodeComponent(data.constName)).click(); + cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement( + "have.text", + `"george.bluth@reqres.in"` + ); + cy.get('[style="height: 13px; width: 13px;"] > img').should("exist"); + cy.get('[data-cy="inspector-node-globals"] > .node-key').click(); + cy.get('[data-cy="inspector-node-environment"] > .node-key').click(); + cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement( + "have.text", + `"development"` + ); + + cy.openInCurrentTab(commonWidgetSelector.previewButton); + cy.wait(4000); + + cy.get( + commonWidgetSelector.draggableWidget(data.constName) + ).verifyVisibleElement("have.value", "george.bluth@reqres.in"); + + cy.go("back"); + cy.waitForAppLoad(); + cy.wait(3000); + cy.get(commonEeSelectors.promoteButton).click(); + cy.get(commonEeSelectors.promoteButton).eq(1).click(); + cy.waitForAppLoad(); + cy.wait(3000); + + cy.get(dataSourceSelector.queryCreateAndRunButton, { + timeout: 20000, + }).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + "Query could not be completed" + ); + + cy.backToApps(); + cy.get(commonSelectors.globalDataSourceIcon).click(); + selectDatasource(data.ds); + cy.get('[data-cy="staging-label"]').click(); + cy.clearAndType( + '[data-cy="base-url-text-field"]', + "https://reqres.in/api/users?page=2" + ); + cy.get(dataSourceSelector.buttonSave).click(); + cy.wait(2000); + + cy.get(commonSelectors.dashboardIcon).click(); + navigateToAppEditor(data.appName); + cy.get(dataSourceSelector.queryCreateAndRunButton).click(); + cy.get( + commonWidgetSelector.draggableWidget(data.constName) + ).verifyVisibleElement("have.value", "michael.lawson@reqres.in"); + + cy.get(commonWidgetSelector.sidebarinspector).click(); + cy.get(commonWidgetSelector.inspectorNodeComponents).click(); + cy.get(commonWidgetSelector.nodeComponent(data.constName)).click(); + cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement( + "have.text", + `"michael.lawson@reqres.in"` + ); + cy.get('[style="height: 13px; width: 13px;"] > img').should("not.exist"); + cy.get('[data-cy="inspector-node-globals"] > .node-key').click(); + cy.get('[data-cy="inspector-node-environment"] > .node-key').click(); + cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement( + "have.text", + `"staging"` + ); + + cy.openInCurrentTab(commonWidgetSelector.previewButton); + cy.wait(4000); + + cy.get( + commonWidgetSelector.draggableWidget(data.constName) + ).verifyVisibleElement("have.value", "michael.lawson@reqres.in"); + + cy.go("back"); + cy.waitForAppLoad(); + cy.wait(3000); + cy.get(commonEeSelectors.promoteButton).click(); + cy.get(commonEeSelectors.promoteButton).eq(1).click(); + cy.waitForAppLoad(); + cy.wait(3000); + + cy.get(dataSourceSelector.queryCreateAndRunButton, { + timeout: 20000, + }).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + "Query could not be completed" + ); + + cy.backToApps(); + cy.get(commonSelectors.globalDataSourceIcon).click(); + selectDatasource(data.ds); + cy.get('[data-cy="production-label"]').click(); + cy.clearAndType( + '[data-cy="base-url-text-field"]', + "https://reqres.in/api/users?page=1" + ); + cy.get(dataSourceSelector.buttonSave).click(); + cy.wait(2000); + + cy.get(commonSelectors.dashboardIcon).click(); + navigateToAppEditor(data.appName); + cy.get(dataSourceSelector.queryCreateAndRunButton).click(); + cy.get( + commonWidgetSelector.draggableWidget(data.constName) + ).verifyVisibleElement("have.value", "george.bluth@reqres.in"); + + cy.get(commonWidgetSelector.sidebarinspector).click(); + cy.get(commonWidgetSelector.inspectorNodeComponents).click(); + cy.get(commonWidgetSelector.nodeComponent(data.constName)).click(); + cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement( + "have.text", + `"george.bluth@reqres.in"` + ); + cy.get('[style="height: 13px; width: 13px;"] > img').should("not.exist"); + cy.get('[data-cy="inspector-node-globals"] > .node-key').click(); + cy.get('[data-cy="inspector-node-environment"] > .node-key').click(); + cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement( + "have.text", + `"production"` + ); + + cy.openInCurrentTab(commonWidgetSelector.previewButton); + cy.wait(4000); + + cy.get( + commonWidgetSelector.draggableWidget(data.constName) + ).verifyVisibleElement("have.value", "george.bluth@reqres.in"); + + cy.go("back"); + cy.waitForAppLoad(); + cy.wait(3000); + cy.get(commonSelectors.releaseButton).click(); + cy.get(commonSelectors.yesButton).click(); + cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released"); + cy.wait(4000); + + cy.get(commonWidgetSelector.shareAppButton).click(); + cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`); + cy.wait(2000); + cy.get(commonWidgetSelector.modalCloseButton).click(); + + cy.visit(`/applications/${slug}`); + cy.get( + commonWidgetSelector.draggableWidget(data.constName) + ).verifyVisibleElement("have.value", "george.bluth@reqres.in"); + }); + + it("should verify edit privilages of a promoted version", () => { + data.appName = `${fake.companyName} App`; + cy.apiCreateApp(data.appName); + cy.openApp(); + cy.waitForAppLoad(); + cy.dragAndDropWidget("Text", 550, 650); + appPromote("development", "production"); + + createNewVersion( + (currentVersion = "v1"), + (newVersion = ["v2"]), + (versionFrom = "v1") + ); + appPromote("development", "release"); + + createNewVersion( + (currentVersion = "v2"), + (newVersion = ["v3"]), + (versionFrom = "v2") + ); + appPromote("development", "staging"); + + selectVersion((currentVersion = "v3"), (newVersion = ["v1"])); + cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement( + "have.text", + "App cannot be edited after promotion. Please create a new version from Development to make any changes." + ); + + cy.forceClickOnCanvas(); + cy.get(".datasource-picker").should("have.class", "disabled"); + cy.get(commonEeSelectors.AddQueryButton).should("be.disabled"); + cy.get(".components-container").should("have.class", "disabled"); + + cy.wait(1000); + selectEnv("development"); + cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement( + "have.text", + "App cannot be edited after promotion. Please create a new version from Development to make any changes." + ); + cy.get(".datasource-picker").should("have.class", "disabled"); + cy.get(commonEeSelectors.AddQueryButton).should("be.disabled"); + cy.get(".components-container").should("have.class", "disabled"); + + selectVersion((currentVersion = "v1"), (newVersion = ["v2"])); + cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement( + "have.text", + "This version of the app is released. Please create a new version in development to make any changes." + ); + cy.get(".datasource-picker").should("have.class", "disabled"); + cy.get(commonEeSelectors.AddQueryButton).should("be.disabled"); + cy.get(".components-container").should("have.class", "disabled"); + + cy.wait(1000); + selectEnv("staging"); + cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement( + "have.text", + "This version of the app is released. Please create a new version in development to make any changes." + ); + cy.get(".datasource-picker").should("have.class", "disabled"); + cy.get(commonEeSelectors.AddQueryButton).should("be.disabled"); + cy.get(".components-container").should("have.class", "disabled"); + + cy.wait(1000); + selectEnv("production"); + cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement( + "have.text", + "This version of the app is released. Please create a new version in development to make any changes." + ); + cy.get(".datasource-picker").should("have.class", "disabled"); + cy.get(commonEeSelectors.AddQueryButton).should("be.disabled"); + cy.get(".components-container").should("have.class", "disabled"); + cy.get(commonSelectors.releaseButton).should("be.disabled"); + }); + + it("Should verify last exisiting version", () => { + data.appName = `${fake.companyName} App`; + cy.apiCreateApp(data.appName); + cy.openApp(); + cy.waitForAppLoad(); + cy.dragAndDropWidget("Text", 550, 650); + + appPromote("development", "staging"); + createNewVersion( + (currentVersion = "v1"), + (newVersion = ["v2"]), + (versionFrom = "v1") + ); + + selectVersion((currentVersion = "v2"), (newVersion = ["v1"])); + + cy.wait(1000); + selectEnv("staging"); + + cy.get(appVersionSelectors.currentVersionField(newVersion[0])) + .should("be.visible") + .and("have.text", "v1"); + + appPromote("staging", "production"); + + cy.wait(3000) + deleteVersionAndVerify( + (currentVersion = "v1"), + deleteVersionText.deleteToastMessage((currentVersion = "v1")) + ); + + cy.wait(2000); + cy.get('[data-cy="list-current-env-name"]').click(); + verifyTooltip( + '[data-cy="env-name-dropdown"]:eq(1)', + "There are no versions in this environment" + ); + verifyTooltip( + '[data-cy="env-name-dropdown"]:eq(2)', + "There are no versions in this environment" + ); + }); + + it("Should verify version deletion", () => { + data.appName = `${fake.companyName} App`; + cy.apiCreateApp(data.appName); + cy.openApp(); + cy.waitForAppLoad(); + cy.dragAndDropWidget("Text", 550, 650); + + appPromote("development", "staging"); + createNewVersion( + (currentVersion = "v1"), + (newVersion = ["v2"]), + (versionFrom = "v1") + ); + appPromote("development", "staging"); + + createNewVersion( + (currentVersion = "v2"), + (newVersion = ["v3"]), + (versionFrom = "v2") + ); + appPromote("development", "production"); + + selectEnv("staging"); + selectVersion((currentVersion = "v3"), (newVersion = ["v2"])); + deleteVersionAndVerify( + (currentVersion = "v2"), + deleteVersionText.deleteToastMessage((currentVersion = "v2")) + ); + + cy.get('[data-cy="v3-current-version-text"]') + .should("be.visible") + .and("have.text", "v3"); + + cy.get('[data-cy="list-current-env-name"]').should( + "have.text", + "Staging" + ); + }) + + it("Verify the multi env components UI", () => { + data.appName = `${fake.companyName} App`; + cy.apiCreateApp(data.appName); + cy.openApp(); + cy.waitForAppLoad(); + cy.dragAndDropWidget("Text", 550, 650); + cy.get(multiEnvSelector.envContainer).should("be.visible"); + cy.get(multiEnvSelector.currentEnvName) + .verifyVisibleElement("have.text", "Development") + .click(); + cy.get(multiEnvSelector.envArrow).should("be.visible"); + cy.get(multiEnvSelector.selectedEnvName).verifyVisibleElement( + "have.text", + " Development" + ); + cy.get(multiEnvSelector.envNameList) + .eq(0) + .verifyVisibleElement("have.text", "Development"); + cy.get(multiEnvSelector.envNameList) + .eq(1) + .verifyVisibleElement("have.text", "Staging"); + cy.get(multiEnvSelector.envNameList) + .eq(2) + .verifyVisibleElement("have.text", "Production"); + + verifyTooltip( + '[data-cy="env-name-dropdown"]:eq(1)', + "There are no versions in this environment" + ); + verifyTooltip( + '[data-cy="env-name-dropdown"]:eq(2)', + "There are no versions in this environment" + ); + + cy.get(multiEnvSelector.appVersionLabel).should("be.visible"); + cy.get('[data-cy="v1-current-version-text"]') + .verifyVisibleElement("have.text", "v1") + .click(); + cy.get(multiEnvSelector.currentVersion).verifyVisibleElement( + "have.text", + "v1" + ); + cy.get(".col-10 > .app-version-name").verifyVisibleElement( + "have.text", + "v1" + ); + cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement( + "have.text", + "Create new version" + ); + + verifyPromoteModalUI("v1", "Development", "Staging"); + cy.get('[data-cy="env-change-info-text"]').verifyVisibleElement( + "have.text", + "You won’t be able to edit this version after promotion. Are you sure you want to continue?" + ); + cy.get(commonSelectors.closeButton).click(); + cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement( + "have.text", + "Development" + ); + + cy.get(commonEeSelectors.promoteButton).click(); + cy.get(commonSelectors.cancelButton).click(); + cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement( + "have.text", + "Development" + ); + + cy.get(commonEeSelectors.promoteButton).click(); + cy.get(commonEeSelectors.promoteButton).eq(1).click(); + + cy.waitForAppLoad(); + cy.wait(3000); + + cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement( + "have.text", + "App cannot be edited after promotion. Please create a new version from Development to make any changes." + ); + cy.get(multiEnvSelector.envContainer).should("be.visible"); + cy.get(multiEnvSelector.currentEnvName) + .verifyVisibleElement("have.text", "Staging") + .click(); + cy.get(multiEnvSelector.envArrow).should("be.visible"); + cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement( + "have.text", + "Staging" + ); + cy.get(multiEnvSelector.envNameList) + .eq(0) + .verifyVisibleElement("have.text", "Development"); + cy.get(multiEnvSelector.envNameList) + .eq(1) + .verifyVisibleElement("have.text", "Staging"); + cy.get(multiEnvSelector.envNameList) + .eq(2) + .verifyVisibleElement("have.text", "Production"); + cy.wait(2000) + verifyTooltip( + '[data-cy="env-name-dropdown"]:eq(2)', + "There are no versions in this environment" + ); + + cy.get(multiEnvSelector.appVersionLabel).should("be.visible"); + cy.get('[data-cy="v1-current-version-text"]') + .verifyVisibleElement("have.text", "v1") + .click(); + cy.get(multiEnvSelector.currentVersion).verifyVisibleElement( + "have.text", + "v1" + ); + cy.get(".col-10 > .app-version-name").verifyVisibleElement( + "have.text", + "v1" + ); + cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement( + "have.text", + "Create new version" + ); + + verifyTooltip( + multiEnvSelector.createNewVersionButton, + "New versions can only be created in development" + ); + cy.forceClickOnCanvas(); + cy.get(".datasource-picker").should("have.class", "disabled"); + cy.get(commonEeSelectors.AddQueryButton).should("be.disabled"); + cy.get(".components-container").should("have.class", "disabled"); + + verifyPromoteModalUI("v1", "Staging", "Production"); + cy.get(commonSelectors.closeButton).click(); + cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement( + "have.text", + "Staging" + ); + + cy.get(commonEeSelectors.promoteButton).click(); + cy.get(commonSelectors.cancelButton).click(); + cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement( + "have.text", + "Staging" + ); + + cy.get(commonEeSelectors.promoteButton).click(); + cy.get(commonEeSelectors.promoteButton).eq(1).click(); + cy.waitForAppLoad(); + cy.wait(3000); + + cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement( + "have.text", + "App cannot be edited after promotion. Please create a new version from Development to make any changes." + ); + cy.get(multiEnvSelector.envContainer).should("be.visible"); + cy.get(multiEnvSelector.currentEnvName) + .verifyVisibleElement("have.text", "Production") + .click(); + cy.get(multiEnvSelector.envArrow).should("be.visible"); + cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement( + "have.text", + "Production" + ); + cy.get(multiEnvSelector.envNameList) + .eq(0) + .verifyVisibleElement("have.text", "Development"); + cy.get(multiEnvSelector.envNameList) + .eq(1) + .verifyVisibleElement("have.text", "Staging"); + cy.get(multiEnvSelector.envNameList) + .eq(2) + .verifyVisibleElement("have.text", "Production"); + + cy.get(multiEnvSelector.appVersionLabel).should("be.visible"); + cy.get('[data-cy="v1-current-version-text"]') + .verifyVisibleElement("have.text", "v1") + .click(); + cy.get(multiEnvSelector.currentVersion).verifyVisibleElement( + "have.text", + "v1" + ); + cy.get(".col-10 > .app-version-name").verifyVisibleElement( + "have.text", + "v1" + ); + cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement( + "have.text", + "Create new version" + ); + + cy.get(commonSelectors.releaseButton) + .verifyVisibleElement("have.text", "Release") + .click(); + cy.get('[data-cy="modal-title"]').verifyVisibleElement( + "have.text", + "Release Version" + ); + cy.get(commonSelectors.closeButton).should("be.visible"); + cy.get('[data-cy="confirm-dialogue-box-text"]').verifyVisibleElement( + "have.text", + "Are you sure you want to release this version?" + ); + cy.get(commonSelectors.cancelButton).verifyVisibleElement( + "have.text", + "Cancel" + ); + cy.get(commonSelectors.yesButton).verifyVisibleElement("have.text", "Yes"); + + cy.get(commonSelectors.closeButton).click(); + cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement( + "have.text", + "Production" + ); + + cy.get(commonSelectors.releaseButton).click(); + cy.get(commonSelectors.cancelButton).click(); + cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement( + "have.text", + "Production" + ); + + cy.get(commonSelectors.releaseButton).click(); + cy.get(commonSelectors.yesButton).click(); + cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released"); + cy.wait(500); + cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement( + "have.text", + "This version of the app is released. Please create a new version in development to make any changes." + ); + cy.get('[data-cy="v1-current-version-text"]').click(); + verifyTooltip( + multiEnvSelector.createNewVersionButton, + "New versions can only be created in development" + ); + cy.get(".datasource-picker").should("have.class", "disabled"); + cy.get(commonEeSelectors.AddQueryButton).should("be.disabled"); + cy.get(".components-container").should("have.class", "disabled"); + cy.get(commonSelectors.releaseButton).should("be.disabled"); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/ldapOnboarding.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/workspace/ldapOnboarding.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/ldapOnboarding.cy.js rename to cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/workspace/ldapOnboarding.cy.js diff --git a/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/openId.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/workspace/openId.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/openId.cy.js rename to cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/workspace/openId.cy.js diff --git a/cypress-tests/cypress/support/utils/apps.js b/cypress-tests/cypress/support/utils/apps.js index ff791d6d6c..4041c3ce46 100644 --- a/cypress-tests/cypress/support/utils/apps.js +++ b/cypress-tests/cypress/support/utils/apps.js @@ -1,112 +1,132 @@ import { commonSelectors, commonWidgetSelector } from "Selectors/common"; +import { appPromote } from "Support/utils/platform/multiEnv"; const slugValidations = [ - { input: "", error: "App slug can't be empty" }, - { input: "_2#", error: "Special characters are not accepted." }, - { input: "t ", error: "Cannot contain spaces" }, - { input: "T", error: "Only lowercase letters are accepted." }, + { input: "", error: "App slug can't be empty" }, + { input: "_2#", error: "Special characters are not accepted." }, + { input: "t ", error: "Cannot contain spaces" }, + { input: "T", error: "Only lowercase letters are accepted." }, ]; export const verifySlugValidations = (inputSelector) => { - slugValidations.forEach(({ input, error }) => { - cy.get(inputSelector).clear(); - if (input) cy.clearAndType(inputSelector, input); - cy.wait(500); - cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement( - "have.text", - error - ); - }); + slugValidations.forEach(({ input, error }) => { + cy.get(inputSelector).clear(); + if (input) cy.clearAndType(inputSelector, input); + cy.wait(500); + cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement( + "have.text", + error + ); + }); }; export const verifySuccessfulSlugUpdate = (workspaceId, slug) => { - const host = resolveHost(); - cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement( - "have.text", - "Slug accepted!" - ); + const host = resolveHost(); + cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement( + "have.text", + "Slug accepted!" + ); - cy.wait(500); - // cy.get(commonWidgetSelector.appLinkSucessLabel).should('be.visible'); - cy.get(commonWidgetSelector.appLinkSucessLabel).should( - "have.text", - "Link updated successfully!" - ); - cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement( - "have.text", - `${host}/${workspaceId}/apps/${slug}` - ); + cy.wait(500); + // cy.get(commonWidgetSelector.appLinkSucessLabel).should('be.visible'); + cy.get(commonWidgetSelector.appLinkSucessLabel).should( + "have.text", + "Link updated successfully!" + ); + cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement( + "have.text", + `${host}/${workspaceId}/apps/${slug}` + ); }; export const verifyURLs = (workspaceId, slug, page) => { - const baseUrl = Cypress.config("baseUrl"); + const baseUrl = Cypress.config("baseUrl"); - cy.url().should( - "eq", - page - ? `${baseUrl}/${workspaceId}/apps/${slug}/home` - : `${baseUrl}/${workspaceId}/apps/${slug}` - ); + cy.url().should( + "eq", + page + ? `${baseUrl}/${workspaceId}/apps/${slug}/home` + : `${baseUrl}/${workspaceId}/apps/${slug}` + ); - cy.openInCurrentTab(commonWidgetSelector.previewButton); + cy.openInCurrentTab(commonWidgetSelector.previewButton); + cy.ifEnv("Community", () => { cy.url().should("eq", `${baseUrl}/applications/${slug}/home?version=v1`); + }); + cy.ifEnv("Enterprise", () => { + cy.url().should( + "eq", + `${baseUrl}/applications/${slug}/home?env=production&version=v1` + ); + }); - cy.visit("/my-workspace"); - cy.visitSlug({ - actualUrl: `${baseUrl}/applications/${slug}`, - }); - cy.url().should("eq", `${baseUrl}/applications/${slug}`); + cy.visit("/my-workspace"); + cy.visitSlug({ + actualUrl: `${baseUrl}/applications/${slug}`, + }); + cy.url().should("eq", `${baseUrl}/applications/${slug}`); }; export const setUpSlug = (slug) => { - cy.get(commonWidgetSelector.shareAppButton).click(); - cy.clearAndType(commonWidgetSelector.appNameSlugInput, slug); - cy.get('[data-cy="app-slug-accepted-label"]') - .should("be.visible") - .and("have.text", "Slug accepted!"); - cy.get(commonWidgetSelector.modalCloseButton).click(); + cy.get(commonWidgetSelector.shareAppButton).click(); + cy.clearAndType(commonWidgetSelector.appNameSlugInput, slug); + cy.get('[data-cy="app-slug-accepted-label"]') + .should("be.visible") + .and("have.text", "Slug accepted!"); + cy.get(commonWidgetSelector.modalCloseButton).click(); }; export const setupAppWithSlug = (appName, slug) => { - cy.apiCreateApp(appName); - cy.apiAddComponentToApp(appName, "text1"); - cy.apiReleaseApp(appName); - cy.apiAddAppSlug(appName, slug); + cy.apiCreateApp(appName); + cy.apiAddComponentToApp(appName, "text1"); + + cy.ifEnv("Enterprise", () => { + cy.openApp( + "", + Cypress.env("workspaceId"), + Cypress.env("appId"), + commonWidgetSelector.draggableWidget("text1") + ); + appPromote("development", "production"); + }); + + cy.apiReleaseApp(appName); + cy.apiAddAppSlug(appName, slug); }; export const verifyRestrictedAccess = () => { - cy.get('[data-cy="modal-header"]').should("have.text", "Restricted access"); - cy.get('[data-cy="modal-description"]') - .invoke("text") - .then((text) => { - const normalizedText = text.replace(/’/g, "'"); - expect(normalizedText).to.equal( - "You don't have access to this app. Kindly contact admin to know more." - ); - }); - cy.get('[data-cy="back-to-home-button"]').verifyVisibleElement( - "have.text", - "Back to home page" - ); + cy.get('[data-cy="modal-header"]').should("have.text", "Restricted access"); + cy.get('[data-cy="modal-description"]') + .invoke("text") + .then((text) => { + const normalizedText = text.replace(/’/g, "'"); + expect(normalizedText).to.equal( + "You don't have access to this app. Kindly contact admin to know more." + ); + }); + cy.get('[data-cy="back-to-home-button"]').verifyVisibleElement( + "have.text", + "Back to home page" + ); }; export const onboardUserFromAppLink = ( - email, - slug, - workspaceName = "My workspace", - isNonExistingUser = true + email, + slug, + workspaceName = "My workspace", + isNonExistingUser = true ) => { - const dbConfig = Cypress.env("app_db"); + const dbConfig = Cypress.env("app_db"); - const query = isNonExistingUser - ? ` + const query = isNonExistingUser + ? ` SELECT u.invitation_token, o.id AS workspace_id, ou.invitation_token AS organization_token FROM users u JOIN organization_users ou ON u.id = ou.user_id JOIN organizations o ON ou.organization_id = o.id WHERE u.email = '${email}' AND o.name = '${workspaceName}'; ` - : ` + : ` SELECT ou.invitation_token, o.id AS workspace_id FROM users u JOIN organization_users ou ON u.id = ou.user_id @@ -114,33 +134,33 @@ export const onboardUserFromAppLink = ( WHERE u.email = '${email}' AND o.name = '${workspaceName}'; `; - cy.task("dbConnection", { dbconfig: dbConfig, sql: query }).then((resp) => { - if (!resp.rows || resp.rows.length === 0) { - throw new Error( - `No records found for email: ${email} and workspace: ${workspaceName}` - ); - } + cy.task("dbConnection", { dbconfig: dbConfig, sql: query }).then((resp) => { + if (!resp.rows || resp.rows.length === 0) { + throw new Error( + `No records found for email: ${email} and workspace: ${workspaceName}` + ); + } - const { invitation_token, workspace_id, organization_token } = resp.rows[0]; - const token = isNonExistingUser ? organization_token : invitation_token; - const url = isNonExistingUser - ? `${Cypress.config("baseUrl")}/invitations/${invitation_token}/workspaces/${organization_token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}` - : `${Cypress.config("baseUrl")}/organization-invitations/${token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`; + const { invitation_token, workspace_id, organization_token } = resp.rows[0]; + const token = isNonExistingUser ? organization_token : invitation_token; + const url = isNonExistingUser + ? `${Cypress.config("baseUrl")}/invitations/${invitation_token}/workspaces/${organization_token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}` + : `${Cypress.config("baseUrl")}/organization-invitations/${token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`; - cy.visit(url); - }); + cy.visit(url); + }); }; export const resolveHost = () => { - const baseUrl = Cypress.config("baseUrl"); + const baseUrl = Cypress.config("baseUrl"); - const urlMapping = { - "http://localhost:8082": "http://localhost:8082", - "http://localhost:3000": "http://localhost:3000", - "http://localhost:3000/apps": "http://localhost:3000/apps", - "http://localhost:4001": "http://localhost:3000", - "http://localhost:4001/apps": "http://localhost:3000/apps", - }; + const urlMapping = { + "http://localhost:8082": "http://localhost:8082", + "http://localhost:3000": "http://localhost:3000", + "http://localhost:3000/apps": "http://localhost:3000/apps", + "http://localhost:4001": "http://localhost:3000", + "http://localhost:4001/apps": "http://localhost:3000/apps", + }; - return urlMapping[baseUrl]; + return urlMapping[baseUrl]; }; diff --git a/cypress-tests/cypress/support/utils/basicComponents.js b/cypress-tests/cypress/support/utils/basicComponents.js index cae63f493d..9577dee022 100644 --- a/cypress-tests/cypress/support/utils/basicComponents.js +++ b/cypress-tests/cypress/support/utils/basicComponents.js @@ -14,9 +14,21 @@ export const verifyComponent = (widgetName) => { }; export const verifyComponentinrightpannel = (widgetName) => { - cy.get(commonWidgetSelector.widgetBox(widgetName), { - timeout: 10000, - }).should("be.visible"); + cy.get("body") + .then(($body) => { + const isSearchVisible = $body + .find(commonSelectors.searchField) + .is(":visible"); + + if (!isSearchVisible) { + cy.get('[data-cy="right-sidebar-plus-button"]').click(); + } + }) + .then(() => { + cy.get(commonWidgetSelector.widgetBox(widgetName), { + timeout: 10000, + }).should("be.visible"); + }); }; export const deleteComponentAndVerify = (widgetName) => { diff --git a/cypress-tests/cypress/support/utils/common.js b/cypress-tests/cypress/support/utils/common.js index 7a8c55ffcf..63f188db61 100644 --- a/cypress-tests/cypress/support/utils/common.js +++ b/cypress-tests/cypress/support/utils/common.js @@ -6,6 +6,7 @@ import moment from "moment"; import { dashboardSelector } from "Selectors/dashboard"; import { groupsSelector } from "Selectors/manageGroups"; import { groupsText } from "Texts/manageGroups"; +import { appPromote } from "Support/utils/platform/multiEnv"; export const navigateToProfile = () => { cy.get(commonSelectors.settingsIcon).click(); @@ -48,7 +49,7 @@ export const randomDateOrTime = (format = "DD/MM/YYYY") => { let startDate = new Date(2018, 0, 1); startDate = new Date( startDate.getTime() + - Math.random() * (endDate.getTime() - startDate.getTime()) + Math.random() * (endDate.getTime() - startDate.getTime()) ); return moment(startDate).format(format); }; @@ -104,7 +105,7 @@ export const viewAppCardOptions = (appName) => { cy.get(commonSelectors.appCard(appName)) .realHover() .find(commonSelectors.appCardOptionsButton) - .realHover() + .realHover(); cy.contains("div", appName) .parent() .within(() => { @@ -230,6 +231,9 @@ export const navigateToworkspaceConstants = () => { }; export const releaseApp = () => { + cy.ifEnv("Enterprise", () => { + appPromote("development", "production"); + }); cy.get(commonSelectors.releaseButton).click(); cy.get(commonSelectors.yesButton).click(); cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released"); diff --git a/cypress-tests/cypress/support/utils/onboarding.js b/cypress-tests/cypress/support/utils/onboarding.js index 182e890acd..74d5c77d3d 100644 --- a/cypress-tests/cypress/support/utils/onboarding.js +++ b/cypress-tests/cypress/support/utils/onboarding.js @@ -92,7 +92,7 @@ export const userSignUp = (fullName, email, workspaceName = "test") => { cy.visit(invitationLink); cy.wait(2500); }); - if (Cypress.env("environment") !== "Community") { + if (Cypress.env("environment") == "Cloud") { cy.clearAndType( '[data-cy="onboarding-workspace-name-input"]', workspaceName diff --git a/cypress-tests/cypress/support/utils/platform/multiEnv.js b/cypress-tests/cypress/support/utils/platform/multiEnv.js new file mode 100644 index 0000000000..be72c3ae53 --- /dev/null +++ b/cypress-tests/cypress/support/utils/platform/multiEnv.js @@ -0,0 +1,120 @@ +import { multiEnvSelector, commonEeSelectors } from "Selectors/eeCommon"; +import { commonSelectors, commonWidgetSelector } from "Selectors/common"; +import { appVersionSelectors } from "Selectors/exportImport"; +import { appVersionText } from "Texts/exportImport"; + +export const promoteApp = () => { + cy.get(commonEeSelectors.promoteButton).click(); + cy.get(commonEeSelectors.promoteButton).eq(1).click(); + cy.waitForAppLoad(); + cy.wait(3000); +}; + +export const releaseApp = () => { + cy.get(commonSelectors.releaseButton).click(); + cy.get(commonSelectors.yesButton).click(); + cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released"); + cy.wait(500); +}; + +export const launchApp = () => { + cy.url().then((url) => { + const parts = url.split("/"); + const value = parts[parts.length - 1]; + cy.log(`Extracted value: ${value}`); + cy.visit(`/applications/${value}`); + cy.wait(3000); + }); +}; + +export const appPromote = (fromEnv, toEnv) => { + const commonActions = () => { + cy.get(commonEeSelectors.promoteButton).click(); + cy.get(commonEeSelectors.promoteButton).eq(1).click(); + cy.waitForAppLoad(); + cy.wait(2000); + }; + + const transitions = { + development: { + staging: commonActions, + production: () => { + commonActions(); + appPromote("staging", "production"); + }, + release: () => { + commonActions(); + commonActions(); + cy.get(commonSelectors.releaseButton).click(); + cy.get(commonSelectors.yesButton).click(); + cy.wait(500); + }, + }, + staging: { + production: commonActions, + release: () => { + commonActions(); + cy.get(commonSelectors.releaseButton).click(); + cy.get(commonSelectors.yesButton).click(); + cy.wait(500); + }, + }, + }; + + const transition = transitions[fromEnv]?.[toEnv]; + + transition(); +}; + +export const createNewVersion = (value, newVersion = [], version) => { + cy.get('[data-cy="list-current-env-name"]').click(); + cy.get(multiEnvSelector.envNameList).eq(0).click(); + cy.get(appVersionSelectors.currentVersionField(value)).click(); + cy.get(appVersionSelectors.createNewVersionButton).click(); + cy.get(appVersionSelectors.createVersionInputField).click(); + cy.contains(`[id*="react-select-"]`, version).click(); + cy.get(appVersionSelectors.versionNameInputField).click().type(newVersion[0]); + cy.get(appVersionSelectors.createNewVersionButton).click(); + cy.waitForAppLoad(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + appVersionText.createdToastMessage + ); + cy.get(appVersionSelectors.currentVersionField(newVersion[0])).should( + "be.visible" + ); +}; + +export const selectVersion = (value, newVersion = []) => { + cy.get(appVersionSelectors.currentVersionField(value)).click(); + cy.get(".react-select__menu-list .app-version-name") + .contains(newVersion[0]) + .click(); + cy.waitForAppLoad(); +}; + +export const selectEnv = (envName) => { + const envIndex = { + development: 0, + staging: 1, + production: 2, + }[envName]; + + const isValidEnvName = (envName) => { + return ( + envName === "development" || + envName === "staging" || + envName === "production" + ); + }; + + if (isValidEnvName(envName)) { + cy.get('[data-cy="list-current-env-name"]').click(); + cy.wait(500) + const envSelector = `${multiEnvSelector.envNameList}:eq(${envIndex})`; + cy.get(envSelector).click(); + cy.waitForAppLoad(); + } +}; + + diff --git a/cypress-tests/cypress/support/utils/queries.js b/cypress-tests/cypress/support/utils/queries.js index 478d4498fd..83e8df0e2e 100644 --- a/cypress-tests/cypress/support/utils/queries.js +++ b/cypress-tests/cypress/support/utils/queries.js @@ -11,8 +11,9 @@ export const selectQueryFromLandingPage = (dbName, label) => { }; export const deleteQuery = (queryName) => { - cy.get(`[data-cy="list-query-${queryName}"]`).realHover(); + cy.get(`[data-cy="list-query-${queryName}"]`).click(); cy.get(`[data-cy="delete-query-${queryName}"]`).click(); + cy.get('[data-cy="component-inspector-delete-button"]').click() }; export const query = (action) => { diff --git a/cypress-tests/cypress/support/utils/version.js b/cypress-tests/cypress/support/utils/version.js index b15d84f45b..ffb1ad32a0 100644 --- a/cypress-tests/cypress/support/utils/version.js +++ b/cypress-tests/cypress/support/utils/version.js @@ -9,6 +9,7 @@ import { } from "Selectors/version"; import { deleteVersionText, releasedVersionText } from "Texts/version"; import { verifyComponent } from "Support/utils/basicComponents"; +import { appPromote } from "./platform/multiEnv"; export const navigateToCreateNewVersionModal = (value) => { cy.get(appVersionSelectors.appVersionLabel).click(); @@ -121,6 +122,9 @@ export const verifyDuplicateVersion = (newVersion = [], version) => { }; export const releasedVersionAndVerify = (currentVersion) => { + cy.ifEnv("Enterprise", () => { + appPromote("development", "production"); + }); cy.contains("Release").click(); cy.get(confirmVersionModalSelectors.yesButton).click(); @@ -146,7 +150,8 @@ export const verifyVersionAfterPreview = (currentVersion) => { cy.wait(2000); cy.get('[data-cy^="draggable-widget-table"]').should("be.visible"); cy.url().should("include", `version=${currentVersion}`); - cy.get('[data-cy="viewer-page-logo"]').click(); + // cy.get('[data-cy="viewer-page-logo"]').click(); + cy.go("back"); cy.wait(8000); }; diff --git a/deploy/ec2/ce/.env b/deploy/ec2/ce/.env deleted file mode 100644 index 8668d3d824..0000000000 --- a/deploy/ec2/ce/.env +++ /dev/null @@ -1,60 +0,0 @@ -# https://docs.tooljet.io/docs/setup/env-vars -TOOLJET_HOST=__required__ -LOCKBOX_MASTER_KEY=__required__ -SECRET_KEY_BASE=__required__ -PG_USER=__required__ -PG_HOST=__required__ -PG_PASS=__required__ -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 - -# 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= -ENABLE_MULTIPLAYER_EDITING=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/ce/nest.service b/deploy/ec2/ce/nest.service deleted file mode 100644 index 61a1127e2f..0000000000 --- a/deploy/ec2/ce/nest.service +++ /dev/null @@ -1,17 +0,0 @@ -[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/ce/postgrest.service b/deploy/ec2/ce/postgrest.service deleted file mode 100644 index 806c6c8ee1..0000000000 --- a/deploy/ec2/ce/postgrest.service +++ /dev/null @@ -1,16 +0,0 @@ -[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/ce/setup_app b/deploy/ec2/ce/setup_app deleted file mode 100755 index b07a1299d5..0000000000 --- a/deploy/ec2/ce/setup_app +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -if grep __required__ .env -then - echo "Please set the required 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 openresty -then - echo "Successfully started reverse proxy!" -else - echo "Failed to start reverse proxy" - exit 1 -fi - -if $ENABLE_TOOLJET_DB == "true" -then - if sudo systemctl start postgrest - then - echo "Successfully started PostgREST server!" - else - echo "Failed to start PostgREST server" - exit 1 - fi -fi - -TOOLJET_EDTION=ce npm --prefix server run db:setup:prod - -if sudo systemctl start nest -then - echo "The app will be served at ${TOOLJET_HOST}" -else - echo "Failed to start the server!" - exit 1 -fi diff --git a/deploy/ec2/ce/setup_machine.sh b/deploy/ec2/ce/setup_machine.sh deleted file mode 100644 index 8e23853c7c..0000000000 --- a/deploy/ec2/ce/setup_machine.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/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 22.15.1 -sudo ln -s "$(which node)" /usr/bin/node -sudo ln -s "$(which npm)" /usr/bin/npm - -sudo npm i -g npm@10.9.2 - -# Setup openresty -wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add - -echo "deb http://openresty.org/package/ubuntu bionic main" > openresty.list -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.2.0/postgrest-v12.2.0-linux-static-x64.tar.xz -tar xJf postgrest-v12.2.0-linux-static-x64.tar.xz -sudo mv ./postgrest /bin/postgrest -sudo rm postgrest-v12.2.0-linux-static-x64.tar.xz - -# Setup app and postgrest as systemd service -sudo cp /tmp/nest.service /lib/systemd/system/nest.service -sudo cp /tmp/postgrest.service /lib/systemd/system/postgrest.service - -# Setup app directory -mkdir -p ~/app -git clone -b main https://github.com/ToolJet/ToolJet.git ~/app && cd ~/app - - -mv /tmp/.env ~/app/.env -mv /tmp/setup_app ~/app/setup_app -sudo chmod +x ~/app/setup_app - -npm install -g npm@10.9.2 - -# Building ToolJet app -npm install -g @nestjs/cli -TOOLJET_EDTION=ce npm run build diff --git a/deploy/ec2/ce/tooljet_ubuntu_focal.pkr.hcl b/deploy/ec2/ce/tooljet_ubuntu_focal.pkr.hcl deleted file mode 100644 index 9c61b0d554..0000000000 --- a/deploy/ec2/ce/tooljet_ubuntu_focal.pkr.hcl +++ /dev/null @@ -1,63 +0,0 @@ -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" -} - - -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 "shell" { - script = "setup_machine.sh" - } -} diff --git a/deploy/ec2/ce/variables.pkr.hcl b/deploy/ec2/ce/variables.pkr.hcl deleted file mode 100644 index fcd6254505..0000000000 --- a/deploy/ec2/ce/variables.pkr.hcl +++ /dev/null @@ -1,23 +0,0 @@ -variable "ami_name" { - type = string -} - -variable "instance_type" { - type = string - default = "t2.medium" -} - -variable "ami_region" { - type = string - default = "us-west-1" -} - -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-west-2", "eu-central-1", "ap-northeast-1", "ap-southeast-1","ap-northeast-3", "ap-south-1", "ap-northeast-2", "ap-southeast-2", "ca-central-1", "eu-west-1", "eu-north-1", "sa-east-1", "ap-east-1"] -} diff --git a/deploy/ec2/ee/setup_app b/deploy/ec2/ee/setup_app index 3dad6ebeef..deec307c1d 100755 --- a/deploy/ec2/ee/setup_app +++ b/deploy/ec2/ee/setup_app @@ -161,6 +161,21 @@ else exit 1 fi +if [[ "$WORKFLOW_WORKER" == "true" ]]; then + echo "WORKER is true. Running the worker..." + npm run worker:prod & +else + echo "WORKER is not true. Skipping the worker execution." +fi + +if sudo systemctl start neo4j && sudo systemctl enable neo4j +then + echo "Successfully started Neo4j!" +else + echo "Failed to start and enable Neo4j" + exit 1 +fi + TOOLJET_EDTION=ee npm --prefix server run db:setup:prod if sudo -E systemctl start nest @@ -172,4 +187,4 @@ else fi sudo systemctl restart nest -sudo -E systemctl restart postgrest \ No newline at end of file +sudo -E systemctl restart postgrest diff --git a/deploy/ec2/ee/setup_machine.sh b/deploy/ec2/ee/setup_machine.sh index 7c8427b9e2..5dd6c635fd 100644 --- a/deploy/ec2/ee/setup_machine.sh +++ b/deploy/ec2/ee/setup_machine.sh @@ -78,6 +78,28 @@ sudo cp /tmp/redis-server.service /lib/systemd/system/redis-server.service # Start and enable Redis service sudo systemctl daemon-reload + +# Setup Neo4j with APOC plugin +wget -O - https://debian.neo4j.com/neotechnology.gpg.key | sudo apt-key add - +echo "deb https://debian.neo4j.com stable 5" | sudo tee /etc/apt/sources.list.d/neo4j.list +sudo apt-get update +sudo apt-get install -y neo4j=1:5.26.6 +sudo apt-mark hold neo4j + +# Setup APOC plugin +sudo mkdir -p /var/lib/neo4j/plugins +sudo wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar + +# Update Neo4j config +echo "dbms.security.procedures.unrestricted=apoc.*" | sudo tee -a /etc/neo4j/neo4j.conf +echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" | sudo tee -a /etc/neo4j/neo4j.conf +echo "dbms.directories.plugins=/var/lib/neo4j/plugins" | sudo tee -a /etc/neo4j/neo4j.conf +echo "dbms.security.auth_enabled=true" | sudo tee -a /etc/neo4j/neo4j.conf + +# Clean up APT cache +sudo apt-get clean +sudo rm -rf /var/lib/apt/lists/* + # Setup app directory mkdir -p ~/app @@ -96,4 +118,5 @@ npm install -g npm@10.9.2 # Building ToolJet app npm install -g @nestjs/cli -TOOLJET_EDTION=ee npm run build \ No newline at end of file +export NODE_OPTIONS='--max-old-space-size=8000' +TOOLJET_EDTION=ee npm run build diff --git a/deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl b/deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl index 144803d55c..675c67f4b5 100644 --- a/deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl +++ b/deploy/ec2/ee/tooljet_ubuntu_focal.pkr.hcl @@ -16,7 +16,7 @@ source "amazon-ebs" "ubuntu" { source_ami_filter { filters = { - name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*" + name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*" root-device-type = "ebs" virtualization-type = "hvm" } @@ -30,7 +30,7 @@ source "amazon-ebs" "ubuntu" { launch_block_device_mappings { device_name = "/dev/sda1" - volume_size = 10 + volume_size = 30 delete_on_termination = true } @@ -47,7 +47,7 @@ build { } provisioner "file" { - source = "../../frontend/config/nginx.conf.template" + source = "../../../frontend/config/nginx.conf.template" destination = "/tmp/nginx.conf" } diff --git a/deploy/ec2/ee/variables.pkr.hcl b/deploy/ec2/ee/variables.pkr.hcl index 39dcdfd3cd..f18073b73b 100644 --- a/deploy/ec2/ee/variables.pkr.hcl +++ b/deploy/ec2/ee/variables.pkr.hcl @@ -4,7 +4,7 @@ variable "ami_name" { variable "instance_type" { type = string - default = "t2.medium" + default = "t2.large" } variable "ami_region" { diff --git a/docker/cloud/cloud-entrypoint.sh b/docker/cloud/cloud-entrypoint.sh old mode 100644 new mode 100755 diff --git a/docker/cloud/cloud-production.Dockerfile b/docker/cloud/cloud-production.Dockerfile index a2e36bedb3..f3e1f09d0b 100644 --- a/docker/cloud/cloud-production.Dockerfile +++ b/docker/cloud/cloud-production.Dockerfile @@ -1,14 +1,40 @@ -FROM node:18.18.2-buster AS builder +FROM node:22.15.1 AS builder # Fix for JS heap limit allocation issue ENV NODE_OPTIONS="--max-old-space-size=4096" -RUN npm i -g npm@9.8.1 +RUN npm i -g npm@10.9.2 RUN mkdir -p /app -# RUN npm cache clean --force +RUN npm cache clean --force WORKDIR /app +# Set GitHub token and branch as build arguments +ARG CUSTOM_GITHUB_TOKEN +ARG BRANCH_NAME + +# 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 " \ + if git show-ref --verify --quiet refs/heads/${BRANCH_NAME} || \ + git ls-remote --exit-code --heads origin ${BRANCH_NAME}; then \ + git checkout ${BRANCH_NAME}; \ + else \ + echo 'Branch ${BRANCH_NAME} not found in submodule \$name, falling back to main'; \ + git checkout main; \ + fi" + # Scripts for building COPY ./package.json ./package.json @@ -19,6 +45,8 @@ COPY ./plugins/ ./plugins/ RUN NODE_ENV=production npm --prefix plugins run build RUN npm --prefix plugins prune --production +ENV TOOLJET_EDITION=cloud + # Build frontend COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/ RUN npm --prefix frontend install @@ -26,32 +54,34 @@ COPY ./frontend/ ./frontend/ RUN npm --prefix frontend run build --production RUN npm --prefix frontend prune --production +ENV TOOLJET_EDITION=cloud ENV NODE_ENV=production # Build server COPY ./server/package.json ./server/package-lock.json ./server/ RUN npm --prefix server install COPY ./server/ ./server/ -RUN npm install -g @nestjs/cli +RUN npm install -g @nestjs/cli +RUN npm install -g copyfiles RUN npm --prefix server run build -FROM debian:11 +FROM debian:12 RUN apt-get update -yq \ && apt-get install curl gnupg zip -yq \ && apt-get install -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 \ +RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \ + && tar -xf node-v22.15.1-linux-x64.tar.xz \ + && mv node-v22.15.1-linux-x64 /usr/local/lib/nodejs \ && echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \ && /bin/bash -c "source /etc/profile.d/nodejs.sh" \ - && rm node-v18.18.2-linux-x64.tar.xz + && rm node-v22.15.1-linux-x64.tar.xz ENV PATH=/usr/local/lib/nodejs/bin:$PATH ENV NODE_ENV=production +ENV TOOLJET_EDITION=cloud ENV NODE_OPTIONS="--max-old-space-size=4096" RUN apt-get update && \ apt-get install -y postgresql-client freetds-dev libaio1 wget && \ @@ -79,21 +109,23 @@ 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 frontend build +COPY --from=builder /app/frontend/build ./app/frontend/build # copy server build COPY --from=builder /app/server/package.json ./app/server/package.json COPY --from=builder /app/server/.version ./app/server/.version +COPY --from=builder /app/server/ee/keys ./app/server/ee/keys COPY --from=builder /app/server/node_modules ./app/server/node_modules COPY --from=builder /app/server/templates ./app/server/templates COPY --from=builder /app/server/scripts ./app/server/scripts COPY --from=builder /app/server/dist ./app/server/dist +COPY --from=builder --chown=appuser:0 /app/server/ee/ai/assets ./app/server/ee/ai/assets COPY ./docker/cloud/cloud-entrypoint.sh ./app/server/cloud-entrypoint.sh @@ -118,4 +150,4 @@ WORKDIR /app # Dependencies for scripts outside nestjs RUN npm install dotenv@10.0.0 joi@17.4.1 -ENTRYPOINT ["./server/cloud-entrypoint.sh"] +ENTRYPOINT ["./server/cloud-entrypoint.sh"] \ No newline at end of file diff --git a/docker/cloud/cloud-server.Dockerfile b/docker/cloud/cloud-server.Dockerfile index 96a9fe0f01..ce591f0898 100644 --- a/docker/cloud/cloud-server.Dockerfile +++ b/docker/cloud/cloud-server.Dockerfile @@ -1,16 +1,18 @@ -FROM node:18.18.2-buster as builder +FROM node:22.15.1 AS builder # Fix for JS heap limit allocation issue ENV NODE_OPTIONS="--max-old-space-size=4096" -RUN npm i -g npm@9.8.1 +RUN npm i -g npm@10.9.2 +RUN npm cache clean --force RUN npm install -g @nestjs/cli RUN mkdir -p /app WORKDIR /app +# Set GitHub token and branch as build arguments ARG CUSTOM_GITHUB_TOKEN -ARG BRANCH_NAME=main +ARG BRANCH_NAME # Clone and checkout the frontend repository RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" @@ -25,8 +27,17 @@ 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' +RUN git submodule foreach " \ + if git show-ref --verify --quiet refs/heads/${BRANCH_NAME} || \ + git ls-remote --exit-code --heads origin ${BRANCH_NAME}; then \ + git checkout ${BRANCH_NAME}; \ + else \ + echo 'Branch ${BRANCH_NAME} not found in submodule \$name, falling back to main'; \ + git checkout main; \ + fi" + +# Scripts for building COPY ./package.json ./package.json # Building ToolJet plugins @@ -37,28 +48,34 @@ ENV NODE_ENV=production RUN npm --prefix plugins run build RUN npm --prefix plugins prune --production +ENV TOOLJET_EDITION=cloud +ENV NODE_ENV=production + # Building ToolJet server COPY ./server/package.json ./server/package-lock.json ./server/ -RUN npm --prefix server install --only=production +RUN npm --prefix server install COPY ./server/ ./server/ +RUN npm install -g @nestjs/cli +RUN npm install -g copyfiles RUN npm --prefix server run build -FROM debian:11 +FROM debian:12 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 \ +RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \ + && tar -xf node-v22.15.1-linux-x64.tar.xz \ + && mv node-v22.15.1-linux-x64 /usr/local/lib/nodejs \ && echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \ && /bin/bash -c "source /etc/profile.d/nodejs.sh" \ - && rm node-v18.18.2-linux-x64.tar.xz + && rm node-v22.15.1-linux-x64.tar.xz ENV PATH=/usr/local/lib/nodejs/bin:$PATH ENV NODE_ENV=production +ENV TOOLJET_EDITION=cloud ENV NODE_OPTIONS="--max-old-space-size=4096" RUN apt-get update && apt-get install -y postgresql-client freetds-dev libaio1 wget @@ -91,10 +108,12 @@ 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/ee/keys ./app/server/ee/keys COPY --from=builder /app/server/node_modules ./app/server/node_modules COPY --from=builder /app/server/templates ./app/server/templates COPY --from=builder /app/server/scripts ./app/server/scripts COPY --from=builder /app/server/dist ./app/server/dist +COPY --from=builder --chown=appuser:0 /app/server/ee/ai/assets ./app/server/ee/ai/assets COPY ./docker/cloud/cloud-entrypoint.sh ./app/server/cloud-entrypoint.sh @@ -105,18 +124,25 @@ RUN useradd --create-home --home-dir /home/appuser appuser \ && chmod u+x /app \ && chmod -R g=u /app +RUN mkdir -p /home/appuser/.npm/_cacache \ + mkdir -p /home/appuser/.npm_cache_tmp \ + mkdir -p /home/appuser/.npm/_logs \ + && chown -R appuser:0 /home/appuser/.npm \ + && chmod g+s /home/appuser/.npm_cache_tmp + # Set npm cache directory -ENV npm_config_cache /home/appuser/.npm +RUN npm config set cache /tmp/npm-cache --global +ENV npm_config_cache /tmp/npm-cache ENV HOME=/home/appuser -# Installing git for simple git commands -RUN apt-get update && apt-get install -y git && apt-get clean - +# Switch back to appuser USER appuser WORKDIR /app # Dependencies for scripts outside nestjs RUN npm install dotenv@10.0.0 joi@17.4.1 +RUN npm cache clean --force + ENTRYPOINT ["./server/cloud-entrypoint.sh"] diff --git a/docker/ee/ee-entrypoint.sh b/docker/ee/ee-entrypoint.sh index f9319e16be..23cd04ab0e 100755 --- a/docker/ee/ee-entrypoint.sh +++ b/docker/ee/ee-entrypoint.sh @@ -42,6 +42,20 @@ else echo "Using external PostgREST at $PGRST_HOST." fi + +# Check WORKLOW_WORKER and skip SETUP_CMD if true +if [ "${WORKFLOW_WORKER}" == "true" ]; then + echo "WORKFLOW_WORKER is set to true. Running worker process." + npm run worker:prod +else + # Determine setup command based on the presence of ./server/dist + if [ -d "./server/dist" ]; then + SETUP_CMD='npm run db:setup:prod' + else + SETUP_CMD='npm run db:setup' + fi +fi + # Neo4j configuration # ---------------------------------- # Default Neo4j environment values @@ -63,14 +77,14 @@ if [ -n "$NEO4J_AUTH" ]; then export NEO4J_USERNAME export NEO4J_PASSWORD - echo "Neo4j authentication configured with username: $NEO4J_USERNAME" + echo "Neo4j authentication configured with username: $NEO4J_USERNAME" >/dev/null 2>&1 else - echo "NEO4J_AUTH not set, using default authentication" + echo "NEO4J_AUTH not set, using default authentication" >/dev/null 2>&1 fi # Check if Neo4j is already initialized and set password if necessary if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then - echo "Setting Neo4j initial password..." + echo "Setting Neo4j initial password..." >/dev/null 2>&1 # Ensure Neo4j is not running before setting the initial password neo4j stop || true @@ -78,27 +92,27 @@ if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then # Set the initial password using the correct command format for Neo4j 5.x NEO4J_ADMIN_CMD=$(which neo4j-admin) NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1) - echo "Detected Neo4j version: $NEO4J_VERSION" + echo "Detected Neo4j version: $NEO4J_VERSION" >/dev/null 2>&1 # Use version-specific command format MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1) if [ "$MAJOR_VERSION" -ge "5" ]; then # For Neo4j 5.x and higher - echo "Using Neo4j 5.x+ password command format" + echo "Using Neo4j 5.x+ password command format" >/dev/null 2>&1 $NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || { - echo "Warning: Could not set Neo4j password, it may already be set" + echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1 } else # For Neo4j 4.x and lower echo "Using Neo4j 4.x password command format" >/dev/null 2>&1 $NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || { - echo "Warning: Could not set Neo4j password, it may already be set" + echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1 } fi fi # Update Neo4j configuration -echo "Configuring Neo4j..." +echo "Configuring Neo4j..." >/dev/null 2>&1 cat > /etc/neo4j/neo4j.conf << EOF # Neo4j configuration dbms.security.auth_enabled=true @@ -124,12 +138,12 @@ echo "Starting Neo4j service..." neo4j console >/dev/null 2>&1 & # Add a wait for Neo4j to be ready with more robust checking -echo "Waiting for Neo4j to be ready..." +echo "Waiting for Neo4j to be ready..." >/dev/null 2>&1 NEO4J_READY=false for i in {1..60}; do # First try standard status check if neo4j status >/dev/null 2>&1; then - echo "Neo4j is ready (via status check)" + echo "Neo4j is ready 🚀" NEO4J_READY=true break fi @@ -143,7 +157,7 @@ for i in {1..60}; do fi fi - echo "Waiting for Neo4j to start... ($i/60)" + echo "Waiting for Neo4j to start... ($i/60)" >/dev/null 2>&1 sleep 2 done @@ -151,19 +165,6 @@ if [ "$NEO4J_READY" = false ]; then echo "WARNING: Neo4j may not be fully started yet, but continuing..." fi -# Check WORKLOW_WORKER and skip SETUP_CMD if true -if [ "${WORKFLOW_WORKER}" == "true" ]; then - echo "WORKFLOW_WORKER is set to true. Running worker process." - npm run worker:prod -else - # Determine setup command based on the presence of ./server/dist - if [ -d "./server/dist" ]; then - SETUP_CMD='npm run db:setup:prod' - else - SETUP_CMD='npm run db:setup' - fi -fi - # Wait for PostgreSQL connection if [ -z "$DATABASE_URL" ]; then ./server/scripts/wait-for-it.sh $PG_HOST:${PG_PORT:-5432} --strict --timeout=300 -- echo "PostgreSQL is up" diff --git a/docker/ee/ee-production.Dockerfile b/docker/ee/ee-production.Dockerfile index 337bafb476..bf9386d262 100644 --- a/docker/ee/ee-production.Dockerfile +++ b/docker/ee/ee-production.Dockerfile @@ -3,15 +3,14 @@ FROM node:22.15.1 AS builder # Fix for JS heap limit allocation issue ENV NODE_OPTIONS="--max-old-space-size=4096" -RUN npm i -g npm@10.9.2 -RUN mkdir -p /app -RUN npm cache clean --force +RUN npm i -g npm@10.9.2 && npm cache clean --force +RUN mkdir -p /app WORKDIR /app # Set GitHub token and branch as build arguments ARG CUSTOM_GITHUB_TOKEN -ARG BRANCH_NAME=main +ARG BRANCH_NAME # Clone and checkout the frontend repository RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" @@ -21,22 +20,28 @@ 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 main +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' +# Checkout the same branch in submodules if it exists, otherwise fallback to main +RUN git submodule foreach " \ + if git show-ref --verify --quiet refs/heads/${BRANCH_NAME} || \ + git ls-remote --exit-code --heads origin ${BRANCH_NAME}; then \ + git checkout ${BRANCH_NAME}; \ + else \ + echo 'Branch ${BRANCH_NAME} not found in submodule \$name, falling back to main'; \ + git checkout main; \ + fi" # Scripts for building COPY ./package.json ./package.json # Build plugins COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/ -RUN npm --prefix plugins install +RUN npm --prefix plugins ci --omit=dev COPY ./plugins/ ./plugins/ -RUN NODE_ENV=production npm --prefix plugins run build -RUN npm --prefix plugins prune --production +RUN NODE_ENV=production npm --prefix plugins run build && npm --prefix plugins prune --omit=dev ENV TOOLJET_EDITION=ee @@ -44,74 +49,50 @@ ENV TOOLJET_EDITION=ee COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/ RUN npm --prefix frontend install COPY ./frontend/ ./frontend/ -RUN npm --prefix frontend run build --production -RUN npm --prefix frontend prune --production +RUN npm --prefix frontend run build --production && npm --prefix frontend prune --production ENV NODE_ENV=production ENV TOOLJET_EDITION=ee # Build server COPY ./server/package.json ./server/package-lock.json ./server/ -RUN npm --prefix server install +RUN npm --prefix server ci --omit=dev COPY ./server/ ./server/ -RUN npm install -g @nestjs/cli -RUN npm install -g copyfiles -RUN npm --prefix server run build +RUN npm install -g @nestjs/cli && npm install -g copyfiles +RUN npm --prefix server run build && npm prune --production --prefix server -FROM debian:12 - -RUN apt-get update -yq \ - && apt-get install curl wget gnupg zip -yq \ - && apt-get install -yq build-essential \ - && apt -y install redis \ - && apt-get clean -y - -# Install required dependencies for downloading and extracting files +# Install dependencies for PostgREST, curl, tar, etc. RUN apt-get update && apt-get install -y \ - curl tar xz-utils postgresql postgresql-contrib postgresql-client && \ - apt-get clean && rm -rf /var/lib/apt/lists/* + curl ca-certificates tar \ + && rm -rf /var/lib/apt/lists/* -# Install PostgREST from official Docker image -COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin +ENV POSTGREST_VERSION=v12.2.0 -RUN apt-get update && apt-get install -y supervisor +RUN curl -Lo postgrest.tar.xz https://github.com/PostgREST/postgrest/releases/download/${POSTGREST_VERSION}/postgrest-v12.2.0-linux-static-x64.tar.xz && \ + tar -xf postgrest.tar.xz && \ + mv postgrest /postgrest && \ + rm postgrest.tar.xz && \ + chmod +x /postgrest -# Create supervisord configuration file -RUN echo "[supervisord]\n" \ - "nodaemon=true\n" \ - "\n" \ - "[program:postgrest]\n" \ - "command=/bin/postgrest\n" \ - "autostart=true\n" \ - "autorestart=true\n" \ - "stdout_logfile=/dev/stdout\n" \ - "stderr_logfile=/dev/stderr\n" \ - "stdout_logfile_maxbytes=0\n" \ - "stderr_logfile_maxbytes=0\n" \ - "\n" \ - "[program:neo4j]\n" \ - "command=neo4j console\n" \ - "autostart=true\n" \ - "autorestart=unexpected\n" \ - "startsecs=30\n" \ - "startretries=999\n" \ - "priority=90\n" \ - "exitcodes=0,1,2\n" \ - "stopsignal=SIGTERM\n" \ - "stopasgroup=true\n" \ - "killasgroup=true\n" \ - "redirect_stderr=true\n" \ - "stdout_logfile=/var/log/neo4j/neo4j.log\n" \ - "stdout_logfile_backups=10\n" \ - "stderr_capture_maxbytes=20MB\n" \ - "\n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf +FROM debian:12-slim -# Create a wrapper for PostgREST to prefix its logs -RUN mv /bin/postgrest /bin/postgrest-original && \ - echo '#!/bin/bash\n\ -exec /bin/postgrest-original "$@" 2>&1 | sed "s/^/[PostgREST] /"\n\ -' > /bin/postgrest && \ - chmod +x /bin/postgrest +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + wget \ + gnupg \ + unzip \ + ca-certificates \ + xz-utils \ + tar \ + postgresql-client \ + redis \ + libaio1 \ + git \ + freetds-dev \ + && apt-get upgrade -y -o Dpkg::Options::="--force-confold" \ + && apt-get autoremove -y \ + && apt-get clean && rm -rf /var/lib/apt/lists/* RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \ @@ -125,53 +106,18 @@ ENV PATH=/usr/local/lib/nodejs/bin:$PATH ENV NODE_ENV=production ENV TOOLJET_EDITION=ee ENV NODE_OPTIONS="--max-old-space-size=4096" -RUN apt-get update && \ - apt-get install -y postgresql-client freetds-dev libaio1 wget && \ - apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes && \ - apt-get -y autoremove && \ - apt-get -y autoclean -# Install Neo4j +# Install Neo4j + APOC RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \ echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \ - apt-get update && \ - apt-get install -y neo4j=1:5.26.6 && \ - apt-mark hold neo4j && \ - apt-get clean && rm -rf /var/lib/apt/lists/* - -# Set the necessary Neo4j environment variables -ENV NEO4J_HOME=/opt/neo4j -ENV NEO4J_CONF=/etc/neo4j -ENV NEO4J_DATA=/var/lib/neo4j/data -ENV NEO4J_LOG=/var/log/neo4j -ENV NEO4J_PLUGIN=/var/lib/neo4j/plugins -ENV NEO4J_IMPORT=/var/lib/neo4j/import - -# Create the necessary directories for Neo4j -RUN mkdir -p /data/db /data/logs /data/plugins -RUN mkdir -p /opt/neo4j/plugins - -# Configure APOC plugin for Neo4j -ENV NEO4J_dbms_active_plugins=apoc - -# Download and install APOC plugin for Neo4j 5.x (BEFORE creating user) -RUN mkdir -p /var/lib/neo4j/plugins && \ + apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \ + mkdir -p /var/lib/neo4j/plugins && \ wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \ - # Try to download extended version - (wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-extended.jar || \ - wget -P /var/lib/neo4j/plugins https://neo4j-contrib.github.io/neo4j-apoc-procedures/5.26.6/apoc-5.26.6-extended.jar || \ - echo "Extended JAR not available, continuing with core only") - -# Configure Neo4j with APOC -RUN echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \ echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \ - echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf - -# Configure Neo4j to use authentication -RUN if [ -f "/etc/neo4j/neo4j.conf" ]; then \ - sed -i '/dbms.security.auth_enabled/d' /etc/neo4j/neo4j.conf && \ - echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf; \ -fi + echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \ + apt-get clean && rm -rf /var/lib/apt/lists/* # Install Instantclient Basic Light Oracle and Dependencies WORKDIR /opt/oracle @@ -186,40 +132,39 @@ RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketpla # Set the Instant Client library paths ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}" +RUN rm -f *.zip *.key && apt-get clean && rm -rf /var/lib/apt/lists/* 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 frontend build -COPY --from=builder /app/frontend/build ./app/frontend/build -# copy server build -COPY --from=builder /app/server/package.json ./app/server/package.json -COPY --from=builder /app/server/.version ./app/server/.version -COPY --from=builder /app/server/ee/keys ./app/server/ee/keys -COPY --from=builder /app/server/node_modules ./app/server/node_modules -COPY --from=builder /app/server/templates ./app/server/templates -COPY --from=builder /app/server/scripts ./app/server/scripts -COPY --from=builder /app/server/dist ./app/server/dist -COPY --from=builder /app/server/src/assets ./app/server/src/assets -COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh +RUN useradd --create-home --home-dir /home/appuser appuser -# Define non-sudo user -RUN useradd --create-home --home-dir /home/appuser appuser \ - && chown -R appuser:0 /app \ - && chown -R appuser:0 /home \ - && chmod u+x /app \ - && chmod u+x /home \ - && chmod -R g=u /app \ - && chmod -R g=u /home +# Use the PostgREST binary from the builder stage +COPY --from=builder --chown=appuser:0 /postgrest /usr/local/bin/postgrest + +RUN mv /usr/local/bin/postgrest /usr/local/bin/postgrest-original && \ + echo '#!/bin/bash\nexec /usr/local/bin/postgrest-original "$@" 2>&1 | sed "s/^/[PostgREST] /"' > /usr/local/bin/postgrest && \ + chmod +x /usr/local/bin/postgrest + + +# Copy application with ownership set directly to avoid chown -R +COPY --from=builder --chown=appuser:0 /app/package.json ./app/package.json +COPY --from=builder --chown=appuser:0 /app/plugins/dist ./app/plugins/dist +COPY --from=builder --chown=appuser:0 /app/plugins/client.js ./app/plugins/client.js +COPY --from=builder --chown=appuser:0 /app/plugins/node_modules ./app/plugins/node_modules +COPY --from=builder --chown=appuser:0 /app/plugins/packages/common ./app/plugins/packages/common +COPY --from=builder --chown=appuser:0 /app/plugins/package.json ./app/plugins/package.json +COPY --from=builder --chown=appuser:0 /app/frontend/build ./app/frontend/build +COPY --from=builder --chown=appuser:0 /app/server/package.json ./app/server/package.json +COPY --from=builder --chown=appuser:0 /app/server/.version ./app/server/.version +COPY --from=builder --chown=appuser:0 /app/server/ee/keys ./app/server/ee/keys +COPY --from=builder --chown=appuser:0 /app/server/node_modules ./app/server/node_modules +COPY --from=builder --chown=appuser:0 /app/server/templates ./app/server/templates +COPY --from=builder --chown=appuser:0 /app/server/scripts ./app/server/scripts +COPY --from=builder --chown=appuser:0 /app/server/dist ./app/server/dist +COPY --from=builder --chown=appuser:0 /app/server/ee/ai/assets ./app/server/ee/ai/assets +COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh RUN mkdir -p /var/lib/neo4j/data/databases /var/lib/neo4j/data/transactions /var/log/neo4j /opt/neo4j/run && \ chown -R appuser:0 /var/lib/neo4j /var/log/neo4j /etc/neo4j /opt/neo4j/run && \ @@ -258,31 +203,11 @@ RUN mkdir -p /var/lib/redis /var/log/redis /etc/redis \ && chmod g+s /var/lib/redis /var/log/redis /etc/redis \ && chmod -R g=u /var/lib/redis /var/log/redis /etc/redis -# Set permissions for PostgREST binary -RUN chown appuser:0 /bin/postgrest && chmod u+x /bin/postgrest && chmod g=u /bin/postgrest - -RUN touch /tmp/postgrest.conf \ - && chown appuser:0 /tmp/postgrest.conf \ - && chmod 640 /tmp/postgrest.conf - -# Create PostgREST data, log, and configuration directories -RUN mkdir -p /var/lib/postgrest /var/log/postgrest /etc/postgrest \ - && chown -R appuser:0 /var/lib/postgrest /var/log/postgrest /etc/postgrest \ - && chmod g+s /var/lib/postgrest /var/log/postgrest /etc/postgrest \ - && chmod -R g=u /var/lib/postgrest /var/log/postgrest /etc/postgrest - ENV HOME=/home/appuser - -# Installing git for simple git commands -RUN apt-get update && apt-get install -y git && apt-get clean - # Switch back to appuser USER appuser - WORKDIR /app -# Dependencies for scripts outside nestjs -RUN npm install dotenv@10.0.0 joi@17.4.1 -RUN npm cache clean --force +RUN npm install --prefix server --no-save dotenv@10.0.0 joi@17.4.1 && npm cache clean --force ENTRYPOINT ["./server/ee-entrypoint.sh"] diff --git a/docker/ee/ee-try-entrypoint-lts.sh b/docker/ee/ee-try-entrypoint-lts.sh index 27590534d0..c46d799a5b 100755 --- a/docker/ee/ee-try-entrypoint-lts.sh +++ b/docker/ee/ee-try-entrypoint-lts.sh @@ -1,15 +1,227 @@ #!/bin/bash set -e -# Start Redis -# service redis-server start -# redis-server /etc/redis/redis.conf +echo "🚀 Starting Try ToolJet container initialization..." -# Start Postgres +# Neo4j configuration +# ---------------------------------- +# Default Neo4j environment values +# ---------------------------------- +export NEO4J_USER=${NEO4J_USER:-"neo4j"} +export NEO4J_PASSWORD=${NEO4J_PASSWORD:-"appaqvyvRLbeukhFE"} +export NEO4J_AUTH=${NEO4J_AUTH:-"neo4j/appaqvyvRLbeukhFE"} +export NEO4J_URI=${NEO4J_URI:-"bolt://localhost:7687"} +export NEO4J_PLUGINS=${NEO4J_PLUGINS:-'["apoc"]'} +export NEO4J_AUTH + +# Extract username and password from NEO4J_AUTH if set +if [ -n "$NEO4J_AUTH" ]; then + # Extract username and password from NEO4J_AUTH (format: username/password) + NEO4J_USERNAME=$(echo "$NEO4J_AUTH" | cut -d'/' -f1) + NEO4J_PASSWORD=$(echo "$NEO4J_AUTH" | cut -d'/' -f2) + + # Export these for application use + export NEO4J_USERNAME + export NEO4J_PASSWORD + + echo "Neo4j authentication configured with username: $NEO4J_USERNAME" >/dev/null 2>&1 +else + echo "NEO4J_AUTH not set, using default authentication" >/dev/null 2>&1 +fi + +# Check if Neo4j is already initialized and set password if necessary +if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then + echo "Setting Neo4j initial password..." >/dev/null 2>&1 + + # Ensure Neo4j is not running before setting the initial password + neo4j stop || true + + # Set the initial password using the correct command format for Neo4j 5.x + NEO4J_ADMIN_CMD=$(which neo4j-admin) + NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1) + echo "Detected Neo4j version: $NEO4J_VERSION" >/dev/null 2>&1 + + # Use version-specific command format + MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1) + if [ "$MAJOR_VERSION" -ge "5" ]; then + # For Neo4j 5.x and higher + echo "Using Neo4j 5.x+ password command format" >/dev/null 2>&1 + $NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || { + echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1 + } + else + # For Neo4j 4.x and lower + echo "Using Neo4j 4.x password command format" >/dev/null 2>&1 + $NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || { + echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1 + } + fi +fi + +# Update Neo4j configuration +echo "Configuring Neo4j..." >/dev/null 2>&1 +cat > /etc/neo4j/neo4j.conf << EOF +# Neo4j configuration +dbms.security.auth_enabled=true +server.bolt.enabled=true +server.bolt.listen_address=0.0.0.0:7687 +server.directories.data=/var/lib/neo4j/data +server.directories.logs=/var/log/neo4j +initial.dbms.default_database=neo4j +server.directories.plugins=/var/lib/neo4j/plugins +server.directories.import=/var/lib/neo4j/import + +# APOC Settings +dbms.security.procedures.unrestricted=apoc.* +dbms.security.procedures.allowlist=apoc.*,algo.*,gds.* +EOF + +if [ -w "$NEO4J_LOG_DIR" ]; then + chmod -R 770 "$NEO4J_LOG_DIR" || echo "Warning: Could not set log directory permissions" >/dev/null 2>&1 +fi + +# Start Neo4j +echo "Starting Neo4j service..." +neo4j console >/dev/null 2>&1 & + +# Add a wait for Neo4j to be ready with more robust checking +echo "Waiting for Neo4j to be ready..." >/dev/null 2>&1 +NEO4J_READY=false +for i in {1..60}; do + # First try standard status check + if neo4j status >/dev/null 2>&1; then + echo "Neo4j is ready 🚀" + NEO4J_READY=true + break + fi + + # Also try connecting to the bolt port as a fallback + if command -v nc >/dev/null 2>&1; then + if nc -z localhost 7687 >/dev/null 2>&1; then + echo "Neo4j is ready (port 7687 is open)" + NEO4J_READY=true + break + fi + fi + + echo "Waiting for Neo4j to start... ($i/60)" >/dev/null 2>&1 + sleep 2 +done + +if [ "$NEO4J_READY" = false ]; then + echo "WARNING: Neo4j may not be fully started yet, but continuing..." +fi + + +# Configure PostgreSQL authentication +echo "🔧 Configuring PostgreSQL authentication..." +sed -i 's/^local\s\+all\s\+postgres\s\+\(peer\|md5\)/local all postgres trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1 +sed -i 's/^local\s\+all\s\+all\s\+\(peer\|md5\)/local all all trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1 + +# Start PostgreSQL +echo "📈 Starting PostgreSQL..." service postgresql start -# Export the PORT variable to be used by the application +# Wait until PostgreSQL is ready +echo "⏳ Waiting for PostgreSQL..." +until pg_isready -h localhost -p 5432; do + echo "PostgreSQL not ready yet, retrying..." + sleep 2 +done + +# Create user and databases for Temporal +echo "🔧 Creating Temporal DBs and user if needed..." +psql -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='tooljet'" | grep -q 1 || \ +psql -U postgres -c "CREATE USER tooljet WITH PASSWORD 'postgres' SUPERUSER;" >/dev/null 2>&1 + +psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'temporal'" | grep -q 1 || \ +psql -U postgres -c "CREATE DATABASE temporal OWNER tooljet;" >/dev/null 2>&1 + +psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'temporal_visibility'" | grep -q 1 || \ +psql -U postgres -c "CREATE DATABASE temporal_visibility OWNER tooljet;" >/dev/null 2>&1 + +# Generate Temporal config +echo "🔧 Generating Temporal config..." +mkdir -p /etc/temporal/config +if [ -f /etc/temporal/temporal-server.template.yaml ]; then + envsubst < /etc/temporal/temporal-server.template.yaml > /etc/temporal/config/temporal-server.yaml >/dev/null 2>&1 +else + echo "❌ Missing template: /etc/temporal/temporal-server.template.yaml" + exit 1 +fi + +# Download schema files if not present +if [ ! -d "/etc/temporal/schema/postgresql" ]; then + echo "📥 Downloading Temporal schema files..." + mkdir -p /etc/temporal/schema + cd /tmp + curl -sOL https://github.com/temporalio/temporal/archive/refs/tags/v1.28.0.tar.gz + tar -xzf v1.28.0.tar.gz + cp -r temporal-1.28.0/schema/postgresql /etc/temporal/schema/ + rm -rf temporal-1.28.0 v1.28.0.tar.gz + cd / +fi + +rm -f /etc/temporal/temporal-sql-tool.yaml ~/.temporal/config.yaml +mkdir -p /tmp/temporal + +# Set up schemas +echo "🔧 Setting up Temporal schemas..." +for db in temporal temporal_visibility; do + PGPASSWORD=postgres /usr/bin/temporal-sql-tool --plugin postgres12 \ + --ep "localhost" --port 5432 --user tooljet --password postgres \ + --database $db setup-schema -v 0.0 >/dev/null 2>&1 + + schema_dir="/etc/temporal/schema/postgresql/v12" + schema_type=$([ "$db" = "temporal" ] && echo "temporal" || echo "visibility") + + PGPASSWORD=postgres /usr/bin/temporal-sql-tool --plugin postgres12 \ + --ep "localhost" --port 5432 --user tooljet --password postgres \ + --database $db update-schema -d "$schema_dir/$schema_type/versioned" >/dev/null 2>&1 +done + +echo "✅ Schema setup complete" + +# Export default port if not set export PORT=${PORT:-80} +# Start Temporal Server +echo "🚀 Starting Temporal Server..." +/usr/bin/temporal-server start >/dev/null 2>&1 & +TEMPORAL_PID=$! + # Start Supervisor -exec supervisord -c /etc/supervisor/conf.d/supervisord.conf +echo "🚀 Starting Supervisor..." +supervisord -c /etc/supervisor/conf.d/supervisord.conf & +SUPERVISOR_PID=$! + +# Wait for Temporal to become ready +echo "⏳ Waiting for Temporal..." +for i in {1..30}; do + if grpcurl -plaintext localhost:7233 grpc.health.v1.Health/Check >/dev/null 2>&1; then + echo "✅ Temporal is ready" + break + fi + sleep 2 +done + +# Check if namespace already exists +echo "Checking if Temporal namespace exists..." +if grpcurl -plaintext localhost:7233 temporal.api.workflowservice.v1.WorkflowService/ListNamespaces | grep -q '"name": "default"'; then + echo "Namespace 'default' already exists." +else + # Register the namespace if it doesn't exist + echo "Registering Temporal namespace..." + grpcurl -plaintext -d '{ + "namespace": "default", + "description": "Default namespace", + "workflowExecutionRetentionPeriod": "259200s" + }' localhost:7233 temporal.api.workflowservice.v1.WorkflowService/RegisterNamespace +fi + +# Wait on background processes +wait $TEMPORAL_PID $SUPERVISOR_PID + +# Start worker (last step) +echo "🚀 Starting ToolJet worker..." +npm run worker:prod diff --git a/docker/ee/ee-try-entrypoint.sh b/docker/ee/ee-try-entrypoint.sh index 5143e10e75..df6128f9da 100755 --- a/docker/ee/ee-try-entrypoint.sh +++ b/docker/ee/ee-try-entrypoint.sh @@ -1,32 +1,208 @@ #!/bin/bash set -e -# Install grpcurl if not already installed -if ! command -v grpcurl &> /dev/null; then - echo "grpcurl not found, installing..." - apt update && apt install -y curl \ - && curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl +echo "🚀 Starting Try ToolJet container initialization..." + +# Neo4j configuration +# ---------------------------------- +# Default Neo4j environment values +# ---------------------------------- +export NEO4J_USER=${NEO4J_USER:-"neo4j"} +export NEO4J_PASSWORD=${NEO4J_PASSWORD:-"appaqvyvRLbeukhFE"} +export NEO4J_AUTH=${NEO4J_AUTH:-"neo4j/appaqvyvRLbeukhFE"} +export NEO4J_URI=${NEO4J_URI:-"bolt://localhost:7687"} +export NEO4J_PLUGINS=${NEO4J_PLUGINS:-'["apoc"]'} +export NEO4J_AUTH + +# Extract username and password from NEO4J_AUTH if set +if [ -n "$NEO4J_AUTH" ]; then + # Extract username and password from NEO4J_AUTH (format: username/password) + NEO4J_USERNAME=$(echo "$NEO4J_AUTH" | cut -d'/' -f1) + NEO4J_PASSWORD=$(echo "$NEO4J_AUTH" | cut -d'/' -f2) + + # Export these for application use + export NEO4J_USERNAME + export NEO4J_PASSWORD + + echo "Neo4j authentication configured with username: $NEO4J_USERNAME" >/dev/null 2>&1 +else + echo "NEO4J_AUTH not set, using default authentication" >/dev/null 2>&1 fi -# Start Redis -service redis-server start +# Check if Neo4j is already initialized and set password if necessary +if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then + echo "Setting Neo4j initial password..." >/dev/null 2>&1 + + # Ensure Neo4j is not running before setting the initial password + neo4j stop || true -# Start Postgres + # Set the initial password using the correct command format for Neo4j 5.x + NEO4J_ADMIN_CMD=$(which neo4j-admin) + NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1) + echo "Detected Neo4j version: $NEO4J_VERSION" >/dev/null 2>&1 + + # Use version-specific command format + MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1) + if [ "$MAJOR_VERSION" -ge "5" ]; then + # For Neo4j 5.x and higher + echo "Using Neo4j 5.x+ password command format" >/dev/null 2>&1 + $NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || { + echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1 + } + else + # For Neo4j 4.x and lower + echo "Using Neo4j 4.x password command format" >/dev/null 2>&1 + $NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || { + echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1 + } + fi +fi + +# Update Neo4j configuration +echo "Configuring Neo4j..." >/dev/null 2>&1 +cat > /etc/neo4j/neo4j.conf << EOF +# Neo4j configuration +dbms.security.auth_enabled=true +server.bolt.enabled=true +server.bolt.listen_address=0.0.0.0:7687 +server.directories.data=/var/lib/neo4j/data +server.directories.logs=/var/log/neo4j +initial.dbms.default_database=neo4j +server.directories.plugins=/var/lib/neo4j/plugins +server.directories.import=/var/lib/neo4j/import + +# APOC Settings +dbms.security.procedures.unrestricted=apoc.* +dbms.security.procedures.allowlist=apoc.*,algo.*,gds.* +EOF + +if [ -w "$NEO4J_LOG_DIR" ]; then + chmod -R 770 "$NEO4J_LOG_DIR" || echo "Warning: Could not set log directory permissions" >/dev/null 2>&1 +fi + +# Start Neo4j +echo "Starting Neo4j service..." +neo4j console >/dev/null 2>&1 & + +# Add a wait for Neo4j to be ready with more robust checking +echo "Waiting for Neo4j to be ready..." >/dev/null 2>&1 +NEO4J_READY=false +for i in {1..60}; do + # First try standard status check + if neo4j status >/dev/null 2>&1; then + echo "Neo4j is ready 🚀" + NEO4J_READY=true + break + fi + + # Also try connecting to the bolt port as a fallback + if command -v nc >/dev/null 2>&1; then + if nc -z localhost 7687 >/dev/null 2>&1; then + echo "Neo4j is ready (port 7687 is open)" + NEO4J_READY=true + break + fi + fi + + echo "Waiting for Neo4j to start... ($i/60)" >/dev/null 2>&1 + sleep 2 +done + +if [ "$NEO4J_READY" = false ]; then + echo "WARNING: Neo4j may not be fully started yet, but continuing..." +fi + +# Configure PostgreSQL authentication +echo "🔧 Configuring PostgreSQL authentication..." +sed -i 's/^local\s\+all\s\+postgres\s\+\(peer\|md5\)/local all postgres trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1 +sed -i 's/^local\s\+all\s\+all\s\+\(peer\|md5\)/local all all trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1 + +# Start PostgreSQL +echo "📈 Starting PostgreSQL..." service postgresql start -# Start Temporal Server (SQLite configuration) -echo "Starting Temporal Server..." -/usr/bin/temporal-server -r / -c /etc/temporal/ -e temporal-server start & +# Wait until PostgreSQL is ready +echo "⏳ Waiting for PostgreSQL..." +until pg_isready -h localhost -p 5432; do + echo "PostgreSQL not ready yet, retrying..." + sleep 2 +done -# Export the PORT variable to be used by the application +# Create user and databases for Temporal +echo "🔧 Creating Temporal DBs and user if needed..." +psql -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='tooljet'" | grep -q 1 || \ +psql -U postgres -c "CREATE USER tooljet WITH PASSWORD 'postgres' SUPERUSER;" >/dev/null 2>&1 + +psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'temporal'" | grep -q 1 || \ +psql -U postgres -c "CREATE DATABASE temporal OWNER tooljet;" >/dev/null 2>&1 + +psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'temporal_visibility'" | grep -q 1 || \ +psql -U postgres -c "CREATE DATABASE temporal_visibility OWNER tooljet;" >/dev/null 2>&1 + +# Generate Temporal config +echo "🔧 Generating Temporal config..." +mkdir -p /etc/temporal/config +if [ -f /etc/temporal/temporal-server.template.yaml ]; then + envsubst < /etc/temporal/temporal-server.template.yaml > /etc/temporal/config/temporal-server.yaml >/dev/null 2>&1 +else + echo "❌ Missing template: /etc/temporal/temporal-server.template.yaml" + exit 1 +fi + +# Download schema files if not present +if [ ! -d "/etc/temporal/schema/postgresql" ]; then + echo "📥 Downloading Temporal schema files..." + mkdir -p /etc/temporal/schema + cd /tmp + curl -sOL https://github.com/temporalio/temporal/archive/refs/tags/v1.28.0.tar.gz + tar -xzf v1.28.0.tar.gz + cp -r temporal-1.28.0/schema/postgresql /etc/temporal/schema/ + rm -rf temporal-1.28.0 v1.28.0.tar.gz + cd / +fi + +rm -f /etc/temporal/temporal-sql-tool.yaml ~/.temporal/config.yaml +mkdir -p /tmp/temporal + +# Set up schemas +echo "🔧 Setting up Temporal schemas..." +for db in temporal temporal_visibility; do + PGPASSWORD=postgres /usr/bin/temporal-sql-tool --plugin postgres12 \ + --ep "localhost" --port 5432 --user tooljet --password postgres \ + --database $db setup-schema -v 0.0 >/dev/null 2>&1 + + schema_dir="/etc/temporal/schema/postgresql/v12" + schema_type=$([ "$db" = "temporal" ] && echo "temporal" || echo "visibility") + + PGPASSWORD=postgres /usr/bin/temporal-sql-tool --plugin postgres12 \ + --ep "localhost" --port 5432 --user tooljet --password postgres \ + --database $db update-schema -d "$schema_dir/$schema_type/versioned" >/dev/null 2>&1 +done + +echo "✅ Schema setup complete" + +# Export default port if not set export PORT=${PORT:-80} -# Start Supervisor -exec supervisord -c /etc/supervisor/conf.d/supervisord.conf & +# Start Temporal Server +echo "🚀 Starting Temporal Server..." +/usr/bin/temporal-server start >/dev/null 2>&1 & +TEMPORAL_PID=$! -# Wait for Temporal Server to be ready -echo "Waiting for Temporal Server to be ready..." -sleep 10 +# Start Supervisor +echo "🚀 Starting Supervisor..." +supervisord -c /etc/supervisor/conf.d/supervisord.conf & +SUPERVISOR_PID=$! + +# Wait for Temporal to become ready +echo "⏳ Waiting for Temporal..." +for i in {1..30}; do + if grpcurl -plaintext localhost:7233 grpc.health.v1.Health/Check >/dev/null 2>&1; then + echo "✅ Temporal is ready" + break + fi + sleep 2 +done # Check if namespace already exists echo "Checking if Temporal namespace exists..." @@ -42,6 +218,9 @@ else }' localhost:7233 temporal.api.workflowservice.v1.WorkflowService/RegisterNamespace fi -# Run the worker process (last step) -echo "Starting worker process..." -npm run worker:prod +# Wait on background processes +wait $TEMPORAL_PID $SUPERVISOR_PID + +# Start worker (last step) +echo "🚀 Starting ToolJet worker..." +npm --prefix server run worker:prod diff --git a/docker/ee/ee-try-tooljet-lts.Dockerfile b/docker/ee/ee-try-tooljet-lts.Dockerfile index 5eb10b938a..c9fa440db2 100644 --- a/docker/ee/ee-try-tooljet-lts.Dockerfile +++ b/docker/ee/ee-try-tooljet-lts.Dockerfile @@ -1,20 +1,19 @@ FROM tooljet/tooljet:ee-lts-latest -# Copy PostgREST executable +# Copy postgrest executable COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin -# Install PostgreSQL +# Install Postgres USER root RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list +RUN echo "deb http://deb.debian.org/debian" RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor - USER postgres RUN service postgresql start && \ psql -c "create role tooljet with login superuser password 'postgres';" USER root -# Install Redis RUN apt update && apt -y install redis # Create appuser home & ensure permission for supervisord and services @@ -22,6 +21,49 @@ RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/li chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \ chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql +# Install Temporal Server Binaries +RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.28.0/temporal_1.28.0_linux_amd64.tar.gz \ + && tar -xzf temporal_1.28.0_linux_amd64.tar.gz \ + && mv temporal-server /usr/bin/temporal-server \ + && mv temporal-sql-tool /usr/bin/temporal-sql-tool \ + && chmod +x /usr/bin/temporal-server /usr/bin/temporal-sql-tool \ + && rm temporal_1.28.0_linux_amd64.tar.gz + +# Install Temporal UI Server Binaries +RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/ui-server_2.28.0_linux_amd64.tar.gz && \ + tar -xzf ui-server_2.28.0_linux_amd64.tar.gz && \ + mv ui-server /usr/bin/temporal-ui-server && \ + chmod +x /usr/bin/temporal-ui-server && \ + rm ui-server_2.28.0_linux_amd64.tar.gz + + +# Install Git for schema extraction +RUN apt update && apt install -y git && \ + git clone --depth 1 --branch v1.28.0 https://github.com/temporalio/temporal.git /tmp/temporal && \ + mkdir -p /etc/temporal/schema/postgresql && \ + cp -r /tmp/temporal/schema/postgresql/v12 /etc/temporal/schema/postgresql/ && \ + rm -rf /tmp/temporal + +# Install envsubst and grpcurl +RUN apt update && apt install -y gettext-base curl \ + && curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl + +# Copy Temporal configuration files +COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.template.yaml +COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml + +# Install Neo4j + APOC +RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \ + echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \ + apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \ + mkdir -p /var/lib/neo4j/plugins && \ + wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \ + echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + # Configure Supervisor to manage PostgREST, ToolJet, and Redis RUN echo "[supervisord] \n" \ "nodaemon=true \n" \ @@ -54,6 +96,7 @@ RUN echo "[supervisord] \n" \ # ENV defaults ENV TOOLJET_HOST=http://localhost \ + TOOLJET_SERVER_URL=http://localhost \ PORT=80 \ NODE_ENV=production \ LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \ @@ -62,6 +105,7 @@ ENV TOOLJET_HOST=http://localhost \ PG_USER=tooljet \ PG_PASS=postgres \ PG_HOST=localhost \ + PG_PORT=5432 \ ENABLE_TOOLJET_DB=true \ TOOLJET_DB_HOST=localhost \ TOOLJET_DB_USER=tooljet \ @@ -78,7 +122,18 @@ ENV TOOLJET_HOST=http://localhost \ REDIS_PORT=6379 \ REDIS_USER=default \ REDIS_PASS= \ - TERM=xterm + ENABLE_MARKETPLACE_FEATURE=true \ + TERM=xterm \ + ENABLE_WORKFLOW_SCHEDULING=true \ + TEMPORAL_SERVER_ADDRESS=localhost:7233 \ + TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS=tooljet-workflows \ + TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE=default \ + TEMPORAL_ADDRESS=localhost:7233 \ + TEMPORAL_DB_HOST=localhost \ + TEMPORAL_DB_PORT=5432 \ + TEMPORAL_DB_USER=tooljet \ + TEMPORAL_DB_PASS=postgres \ + TEMPORAL_CORS_ORIGINS=http://localhost:8080 # Set the entrypoint COPY ./docker/ee/ee-try-entrypoint-lts.sh /ee-try-entrypoint-lts.sh diff --git a/docker/ee/ee-try-tooljet.Dockerfile b/docker/ee/ee-try-tooljet.Dockerfile index 11cbe88be3..a108f30691 100644 --- a/docker/ee/ee-try-tooljet.Dockerfile +++ b/docker/ee/ee-try-tooljet.Dockerfile @@ -14,7 +14,6 @@ RUN service postgresql start && \ psql -c "create role tooljet with login superuser password 'postgres';" USER root - RUN apt update && apt -y install redis # Create appuser home & ensure permission for supervisord and services @@ -23,11 +22,12 @@ RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/li chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql # Install Temporal Server Binaries -RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.24.2/temporal_1.24.2_linux_amd64.tar.gz && \ - tar -xzf temporal_1.24.2_linux_amd64.tar.gz && \ - mv temporal-server /usr/bin/temporal-server && \ - chmod +x /usr/bin/temporal-server && \ - rm temporal_1.24.2_linux_amd64.tar.gz +RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.28.0/temporal_1.28.0_linux_amd64.tar.gz \ + && tar -xzf temporal_1.28.0_linux_amd64.tar.gz \ + && mv temporal-server /usr/bin/temporal-server \ + && mv temporal-sql-tool /usr/bin/temporal-sql-tool \ + && chmod +x /usr/bin/temporal-server /usr/bin/temporal-sql-tool \ + && rm temporal_1.28.0_linux_amd64.tar.gz # Install Temporal UI Server Binaries RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/ui-server_2.28.0_linux_amd64.tar.gz && \ @@ -36,13 +36,33 @@ RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/u chmod +x /usr/bin/temporal-ui-server && \ rm ui-server_2.28.0_linux_amd64.tar.gz + +# Install Git for schema extraction +RUN apt update && apt install -y git && \ + git clone --depth 1 --branch v1.28.0 https://github.com/temporalio/temporal.git /tmp/temporal && \ + mkdir -p /etc/temporal/schema/postgresql && \ + cp -r /tmp/temporal/schema/postgresql/v12 /etc/temporal/schema/postgresql/ && \ + rm -rf /tmp/temporal + +# Install envsubst and grpcurl +RUN apt update && apt install -y gettext-base curl \ + && curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl + # Copy Temporal configuration files -COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.yaml +COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.template.yaml COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml -# Install grpcurl -RUN apt update && apt install -y curl \ - && curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl +# Install Neo4j + APOC +RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \ + echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \ + apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \ + mkdir -p /var/lib/neo4j/plugins && \ + wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \ + echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \ + apt-get clean && rm -rf /var/lib/apt/lists/* # Configure Supervisor to manage PostgREST, ToolJet, and Redis RUN echo "[supervisord] \n" \ @@ -74,7 +94,6 @@ RUN echo "[supervisord] \n" \ "stdout_logfile=/dev/stdout \n" \ "stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf - # ENV defaults ENV TOOLJET_HOST=http://localhost \ TOOLJET_SERVER_URL=http://localhost \ @@ -86,6 +105,7 @@ ENV TOOLJET_HOST=http://localhost \ PG_USER=tooljet \ PG_PASS=postgres \ PG_HOST=localhost \ + PG_PORT=5432 \ ENABLE_TOOLJET_DB=true \ TOOLJET_DB_HOST=localhost \ TOOLJET_DB_USER=tooljet \ @@ -109,9 +129,13 @@ ENV TOOLJET_HOST=http://localhost \ TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS=tooljet-workflows \ TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE=default \ TEMPORAL_ADDRESS=localhost:7233 \ + TEMPORAL_DB_HOST=localhost \ + TEMPORAL_DB_PORT=5432 \ + TEMPORAL_DB_USER=tooljet \ + TEMPORAL_DB_PASS=postgres \ TEMPORAL_CORS_ORIGINS=http://localhost:8080 # Set the entrypoint COPY ./docker/ee/ee-try-entrypoint.sh /ee-try-entrypoint.sh RUN chmod +x /ee-try-entrypoint.sh -ENTRYPOINT ["/ee-try-entrypoint.sh"] +ENTRYPOINT ["/ee-try-entrypoint.sh"] \ No newline at end of file diff --git a/docker/ee/temporal-server.yaml b/docker/ee/temporal-server.yaml index bc17ed934f..45324165a2 100644 --- a/docker/ee/temporal-server.yaml +++ b/docker/ee/temporal-server.yaml @@ -3,29 +3,24 @@ log: level: info persistence: - defaultStore: sqlite-default - visibilityStore: sqlite-visibility + defaultStore: postgres-default + visibilityStore: postgres-visibility numHistoryShards: 4 - datastores: - sqlite-default: + dataStores: + postgres-default: sql: - pluginName: "sqlite" - databaseName: "/etc/temporal/default.db" - connectAddr: "localhost" - connectProtocol: "tcp" - connectAttributes: - cache: "private" - setup: true - - sqlite-visibility: + pluginName: "postgres12" + databaseName: "temporal" + connectAddr: "localhost:5432" + user: "tooljet" + password: "postgres" + postgres-visibility: sql: - pluginName: "sqlite" - databaseName: "/etc/temporal/visibility.db" - connectAddr: "localhost" - connectProtocol: "tcp" - connectAttributes: - cache: "private" - setup: true + pluginName: "postgres12" + databaseName: "temporal_visibility" + connectAddr: "localhost:5432" + user: "tooljet" + password: "postgres" global: membership: @@ -41,7 +36,7 @@ services: membershipPort: 6933 bindOnLocalHost: true httpPort: 7243 - + matching: rpc: grpcPort: 7235 @@ -68,8 +63,8 @@ clusterMetadata: enabled: true initialFailoverVersion: 1 rpcName: "frontend" - rpcAddress: "localhost:7236" + rpcAddress: "localhost:7233" httpAddress: "localhost:7243" dcRedirectionPolicy: - policy: "noop" + policy: "noop" \ No newline at end of file diff --git a/frontend/assets/images/icons/empty-modules.svg b/frontend/assets/images/icons/empty-modules.svg new file mode 100644 index 0000000000..4a93de1815 --- /dev/null +++ b/frontend/assets/images/icons/empty-modules.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index c464a81a65..8233824dd6 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -43,7 +43,9 @@ "page": "Page", "searchItem": "Search apps in this workspace", "workflowsSearchItem": "Search workflows in this workspace", - "searchComponents": "Search components" + "searchComponents": "Search components", + "promote": "Promote", + "release": "Release" }, "errorBoundary": "Something went wrong.", "viewer": "Sorry!. This app is under maintenance", diff --git a/frontend/ee b/frontend/ee index 9da4f77691..dbb130bfd8 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 9da4f776915e328120c3024e551ef6b8032f9f63 +Subproject commit dbb130bfd859ab795557a36dc26936aa2252e248 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 483cb1dfcf..b165f8fb62 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "@dnd-kit/utilities": "^3.2.1", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", + "@mdxeditor/editor": "^3.38.0", "@microsoft/fetch-event-source": "^2.0.1", "@radix-ui/colors": "^0.1.8", "@radix-ui/react-avatar": "^1.0.4", @@ -85,6 +86,7 @@ "papaparse": "^5.3.2", "path-browserify": "^1.0.1", "plotly.js-dist-min": "^2.29.1", + "posthog-js": "^1.255.1", "process": "^0.11.10", "psl": "^1.9.0", "query-string": "^8.1.0", @@ -160,6 +162,7 @@ "@babel/plugin-transform-runtime": "^7.19.6", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", "@storybook/addon-essentials": "^7.2.1", "@storybook/addon-interactions": "^7.2.1", "@storybook/addon-links": "^7.2.1", @@ -196,6 +199,7 @@ "postcss": "^8.4.35", "postcss-loader": "^8.1.0", "prettier": "^2.8.4", + "react-refresh": "^0.17.0", "sass": "^1.78.0", "sass-loader": "^13.2.0", "storybook": "^7.2.1", @@ -2589,6 +2593,30 @@ "@lezer/common": "^1.1.0" } }, + "node_modules/@codemirror/lang-angular": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz", + "integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", + "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, "node_modules/@codemirror/lang-css": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", @@ -2602,6 +2630,46 @@ "@lezer/css": "^1.1.7" } }, + "node_modules/@codemirror/lang-go": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/go": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", + "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz", + "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, "node_modules/@codemirror/lang-javascript": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz", @@ -2617,6 +2685,73 @@ "@lezer/javascript": "^1.0.0" } }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", + "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-liquid": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.2.3.tgz", + "integrity": "sha512-yeN+nMSrf/lNii3FJxVVEGQwFG0/2eDyH6gNOj+TGCa0hlNO4bhQnoO5ISnd7JOG+7zTEcI/GOoyraisFVY7jQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.3.tgz", + "integrity": "sha512-1fn1hQAPWlSSMCvnF810AkhWpNLkJpl66CRfIy3vVl20Sl4NwChkorCHqpMtNbXr1EuMJsrDnhEpjZxKZ2UX3A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, "node_modules/@codemirror/lang-python": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.0.tgz", @@ -2630,6 +2765,16 @@ "@lezer/python": "^1.1.4" } }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, "node_modules/@codemirror/lang-sass": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", @@ -2657,6 +2802,61 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", + "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", + "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz", @@ -2671,6 +2871,45 @@ "style-mod": "^4.0.0" } }, + "node_modules/@codemirror/language-data": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.1.tgz", + "integrity": "sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-go": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.4.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz", + "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, "node_modules/@codemirror/lint": { "version": "6.8.5", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", @@ -2682,6 +2921,19 @@ "crelt": "^1.0.5" } }, + "node_modules/@codemirror/merge": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.10.2.tgz", + "integrity": "sha512-rmHzVkt5FnCtsi0IgvDIDjh/J4LmbfOboB7FMvVl21IHO0p1QM6jSwjkBjBD3D+c+T79OabEqoduCqvJCBV8Yg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/highlight": "^1.0.0", + "style-mod": "^4.1.0" + } + }, "node_modules/@codemirror/search": { "version": "6.5.10", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz", @@ -2725,6 +2977,73 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@codesandbox/nodebox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@codesandbox/nodebox/-/nodebox-0.1.8.tgz", + "integrity": "sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg==", + "license": "SEE LICENSE IN ./LICENSE", + "dependencies": { + "outvariant": "^1.4.0", + "strict-event-emitter": "^0.4.3" + } + }, + "node_modules/@codesandbox/sandpack-client": { + "version": "2.19.8", + "resolved": "https://registry.npmjs.org/@codesandbox/sandpack-client/-/sandpack-client-2.19.8.tgz", + "integrity": "sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ==", + "license": "Apache-2.0", + "dependencies": { + "@codesandbox/nodebox": "0.1.8", + "buffer": "^6.0.3", + "dequal": "^2.0.2", + "mime-db": "^1.52.0", + "outvariant": "1.4.0", + "static-browser-server": "1.0.3" + } + }, + "node_modules/@codesandbox/sandpack-react": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@codesandbox/sandpack-react/-/sandpack-react-2.20.0.tgz", + "integrity": "sha512-takd1YpW/PMQ6KPQfvseWLHWklJovGY8QYj8MtWnskGKbjOGJ6uZfyZbcJ6aCFLQMpNyjTqz9AKNbvhCOZ1TUQ==", + "license": "Apache-2.0", + "dependencies": { + "@codemirror/autocomplete": "^6.4.0", + "@codemirror/commands": "^6.1.3", + "@codemirror/lang-css": "^6.0.1", + "@codemirror/lang-html": "^6.4.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.3.2", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.7.1", + "@codesandbox/sandpack-client": "^2.19.8", + "@lezer/highlight": "^1.1.3", + "@react-hook/intersection-observer": "^3.1.1", + "@stitches/core": "^1.2.6", + "anser": "^2.1.1", + "clean-set": "^1.1.2", + "dequal": "^2.0.2", + "escape-carriage": "^1.3.1", + "lz-string": "^1.4.4", + "react-devtools-inline": "4.4.0", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19", + "react-dom": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@codesandbox/sandpack-react/node_modules/anser": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.2.tgz", + "integrity": "sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw==", + "license": "MIT" + }, + "node_modules/@codesandbox/sandpack-react/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -4313,12 +4632,278 @@ "dev": true, "license": "MIT" }, + "node_modules/@lexical/clipboard": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.32.1.tgz", + "integrity": "sha512-oO7CuMVh3EFEqtE6+7Ccf7jMD5RNUmSdTnFm/X4kYNGqs9lgGt8j5PgSk7oP9OuAjxKNdBTbltSlh54CX3AUIg==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.32.1", + "@lexical/list": "0.32.1", + "@lexical/selection": "0.32.1", + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/code": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.32.1.tgz", + "integrity": "sha512-2rXj8s/CG32XKQ2EpORpACfpzyAxB+/SrQW2cjwczarLs5Fxnx6u6HwahZnxaF0z5UHIPUy90qDiOiRExc74Yg==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.32.1", + "lexical": "0.32.1", + "prismjs": "^1.30.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.32.1.tgz", + "integrity": "sha512-3WnZQo6Qig7ccjDu2b8s1Kb5CCXowxnK0i8CCRG9mHAw7i6XpZUYAbk4rmcK/qbhLHrc7LwUrAMFzGtfLEH3XA==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.32.1", + "@lexical/link": "0.32.1", + "@lexical/mark": "0.32.1", + "@lexical/table": "0.32.1", + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/dragon": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.32.1.tgz", + "integrity": "sha512-Dlx8P2b/O7gZLmXnoanmDkFL5RgA8Vvix4ZuSvT0apblqySzgi8l3NHHwwqXy1g2nfSupvpr7Dsf10Lu3l0Hlw==", + "license": "MIT", + "dependencies": { + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.32.1.tgz", + "integrity": "sha512-S63bb7uIB4hO2V0UmzUiKlwAGegQlyFKqrOw9NJwOb8O96gHRxr27FUsEb8ToWLM8TSm2aw1WsZXs7CJQqGtCg==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/history": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.32.1.tgz", + "integrity": "sha512-IRsKllumYEWxmzR2evN30MFY+JBM723lSyzm2PAQcgHCeBxi8t0Vc3EdyJRay+YdN65JgrohQi1WbktbK923uQ==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/html": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.32.1.tgz", + "integrity": "sha512-uctCdC9gVzx/Sw9CimT4C2IDfSbfEGYunyIrJBpsfcdqp0rroGNizjIoZNBH3xcgkk9UDboSADo+wimbzEoy8A==", + "license": "MIT", + "dependencies": { + "@lexical/selection": "0.32.1", + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/link": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.32.1.tgz", + "integrity": "sha512-atdwNpWjZ0U2/kgS0ATTkZ8lJLHiv3TsJgqJL33BuV9Gn7advJokd4faM79Y8XxkhiPi1lVTBSHgI8V4hs+c+Q==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/list": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.32.1.tgz", + "integrity": "sha512-3zShCfEdAvodR6mQ5CNN1gcEwfV341LXJzWCIkZzG1cPwaiBHUlT7TynQtKTPn1sATCEMmxoDG0/T+itsRNZgA==", + "license": "MIT", + "dependencies": { + "@lexical/selection": "0.32.1", + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/mark": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.32.1.tgz", + "integrity": "sha512-AXF2wmUvvSI45y+sgZKnU0pnUdttd9v75DDQgdplqtCkyDqHVGxVCNCrLE+PJtzIrwJxtA2UyC8yFZMBM92HpA==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/markdown": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.32.1.tgz", + "integrity": "sha512-AmUTRRx6Je0AOiQqp48Xn92/71AzhFgi4nO1EtPW5eae1CihrtiEh5UQr48mV6EyjvH9D3OlOLU8XrzS+J9a+w==", + "license": "MIT", + "dependencies": { + "@lexical/code": "0.32.1", + "@lexical/link": "0.32.1", + "@lexical/list": "0.32.1", + "@lexical/rich-text": "0.32.1", + "@lexical/text": "0.32.1", + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/offset": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.32.1.tgz", + "integrity": "sha512-zfHqoLlQ0lq1akFHy81xnDaRRE5KkqFa7OovOxKPBpALQCiJIAb2ykqj/Woc2oUeYaEcnkaFU9+kEWMK9yY0fQ==", + "license": "MIT", + "dependencies": { + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/overflow": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.32.1.tgz", + "integrity": "sha512-wjcFGjzkbugds2Q5Wag59WrcxJwMUACEXms1FtFdu1/YcBPqNAKJSyfo8Z/5LGfstQEb2nPtSuEQZUb7v+XYyA==", + "license": "MIT", + "dependencies": { + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.32.1.tgz", + "integrity": "sha512-uFS3xoETB3phnYHZXfMKvl8gh6YRW7rpokuJmQVMHNNBklORmMpN00rRQ/zsc/jt/nPzaPpE5cLwSHXeJdqJUg==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.32.1", + "@lexical/selection": "0.32.1", + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/react": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.32.1.tgz", + "integrity": "sha512-PCiAiwGIGfkYb2o9Kx+gGGqXwxqb7/W4cGSnw1nzmNtCerJ3S64WZs87Lgcow0RlDSwqzpH534+eCyIddueSqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.8", + "@lexical/devtools-core": "0.32.1", + "@lexical/dragon": "0.32.1", + "@lexical/hashtag": "0.32.1", + "@lexical/history": "0.32.1", + "@lexical/link": "0.32.1", + "@lexical/list": "0.32.1", + "@lexical/mark": "0.32.1", + "@lexical/markdown": "0.32.1", + "@lexical/overflow": "0.32.1", + "@lexical/plain-text": "0.32.1", + "@lexical/rich-text": "0.32.1", + "@lexical/table": "0.32.1", + "@lexical/text": "0.32.1", + "@lexical/utils": "0.32.1", + "@lexical/yjs": "0.32.1", + "lexical": "0.32.1", + "react-error-boundary": "^3.1.4" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.32.1.tgz", + "integrity": "sha512-SnmpZ7boTLxeYfNezNLvchDiJOAALA2nD0Uq/SpkIOJ6R01R7m1aPdLv55LGKoBT9UxCRdo0HWXytwiVZI+ehQ==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.32.1", + "@lexical/selection": "0.32.1", + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/selection": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.32.1.tgz", + "integrity": "sha512-X1aXJdq/5EOuSuMOqK3t+rEVmpqLf+vc2Kl5YuP8+gGWUbXuxR6iryrQuy1mAViZpF/5qw4HO/Sb+9JjubaZEg==", + "license": "MIT", + "dependencies": { + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/table": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.32.1.tgz", + "integrity": "sha512-sGk2jUbQHj5hatpxRyl6IE2oWsjRnYhmaP94THzn95/uK69o8eSizcnd148WzYsX8Zz+L9PTLS1xjvCbfLTP+A==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.32.1", + "@lexical/utils": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/text": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.32.1.tgz", + "integrity": "sha512-0Ek8F3KC4d16b2YaTHdyYFqDSBZ5KRtGrqU3GBog+VOGxucGaEbXEK1/ypX5CTe/wwkQDrH0FKWPQbd3l5t5YQ==", + "license": "MIT", + "dependencies": { + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/utils": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.32.1.tgz", + "integrity": "sha512-ZaqZZksNIHJd+g8GXc11D1ESi8JzsdLVQZ+9odXVaNxtwDIaGIqMSccFyuZ9VSoJDde4JXZkgp/0PShbAZDkyw==", + "license": "MIT", + "dependencies": { + "@lexical/list": "0.32.1", + "@lexical/selection": "0.32.1", + "@lexical/table": "0.32.1", + "lexical": "0.32.1" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.32.1.tgz", + "integrity": "sha512-VHGTg5z4wcDkPe8NnhzA5+CoOKJ5tVmTmMvoiZ91rtNUFQPxWRky88Gjt1e3yXldYp4pImNEgtAjlWqvaJBYGA==", + "license": "MIT", + "dependencies": { + "@lexical/offset": "0.32.1", + "@lexical/selection": "0.32.1", + "lexical": "0.32.1" + }, + "peerDependencies": { + "yjs": ">=13.5.22" + } + }, "node_modules/@lezer/common": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", "license": "MIT" }, + "node_modules/@lezer/cpp": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.3.tgz", + "integrity": "sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/css": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.11.tgz", @@ -4330,6 +4915,17 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/go": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", + "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, "node_modules/@lezer/highlight": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", @@ -4339,6 +4935,28 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/html": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", + "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/javascript": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz", @@ -4350,6 +4968,17 @@ "@lezer/lr": "^1.3.0" } }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/lr": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", @@ -4359,6 +4988,27 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/markdown": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.3.tgz", + "integrity": "sha512-kfw+2uMrQ/wy/+ONfrH83OkdFNM0ye5Xq96cLlaCy7h5UT9FO54DU4oRoIc0CSBh5NWmWuiIJA7NGLMJbQ+Oxg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.2.tgz", + "integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, "node_modules/@lezer/python": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", @@ -4370,6 +5020,17 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/sass": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.0.7.tgz", @@ -4381,6 +5042,28 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@mapbox/geojson-rewind": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", @@ -4521,6 +5204,190 @@ "react": ">=16" } }, + "node_modules/@mdxeditor/editor": { + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/@mdxeditor/editor/-/editor-3.38.0.tgz", + "integrity": "sha512-m7vK7TQ4P5TUWvRr1yHgzoJm2kupmR2PWKGPRzl3+pPL10bwnVtGhTL2l3PrLMlnRhNqpaBK23XmqdLSq4tvzg==", + "license": "MIT", + "dependencies": { + "@codemirror/commands": "^6.2.4", + "@codemirror/lang-markdown": "^6.2.3", + "@codemirror/language-data": "^6.5.1", + "@codemirror/merge": "^6.4.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.23.0", + "@codesandbox/sandpack-react": "^2.20.0", + "@lexical/clipboard": "^0.32.1", + "@lexical/link": "^0.32.1", + "@lexical/list": "^0.32.1", + "@lexical/markdown": "^0.32.1", + "@lexical/plain-text": "^0.32.1", + "@lexical/react": "^0.32.1", + "@lexical/rich-text": "^0.32.1", + "@lexical/selection": "^0.32.1", + "@lexical/utils": "^0.32.1", + "@mdxeditor/gurx": "^1.1.4", + "@radix-ui/colors": "^3.0.0", + "@radix-ui/react-dialog": "^1.1.11", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-popover": "^1.1.11", + "@radix-ui/react-popper": "^1.2.4", + "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-toggle-group": "^1.1.7", + "@radix-ui/react-toolbar": "^1.1.7", + "@radix-ui/react-tooltip": "^1.2.4", + "classnames": "^2.3.2", + "cm6-theme-basic-light": "^0.2.0", + "codemirror": "^6.0.1", + "downshift": "^7.6.0", + "js-yaml": "4.1.0", + "lexical": "^0.32.1", + "mdast-util-directive": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-mdx": "^3.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-to-markdown": "^2.1.0", + "micromark-extension-directive": "^3.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.1", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs": "^3.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.1", + "micromark-util-symbol": "^2.0.0", + "react-hook-form": "^7.56.1", + "unidiff": "^1.0.2" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">= 18 || >= 19", + "react-dom": ">= 18 || >= 19" + } + }, + "node_modules/@mdxeditor/editor/node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", + "license": "MIT" + }, + "node_modules/@mdxeditor/editor/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@mdxeditor/editor/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@mdxeditor/editor/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@mdxeditor/editor/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mdxeditor/gurx": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@mdxeditor/gurx/-/gurx-1.2.3.tgz", + "integrity": "sha512-5DQOlEx46oN9spggrC8husAGAhVoEFBGIYKN48es08XhRUbSU6l5bcIQYwRrQaY8clU1tExIcXzw8/fNnoxjpg==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">= 18 || >= 19", + "react-dom": ">= 18 || >= 19" + } + }, "node_modules/@microsoft/fetch-event-source": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", @@ -4789,6 +5656,12 @@ "node": ">= 8" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4902,32 +5775,31 @@ } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz", - "integrity": "sha512-kLQc9xz6QIqd2oIYyXRUiAp79kGpFBm3fEM9ahfG1HI0WI5gdZ2OVHWdmZYnwODt7ISck+QuQ6sBPrtvUBML7Q==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.0.tgz", + "integrity": "sha512-AAc+QWfZ1KQ/e1C6OHWVlxU+ks6zFGOA44IJUlvju7RlDS8nsX6poPFOIlsg/rTofO9vKov12+WCjMhKkRKD5g==", "dev": true, "license": "MIT", "dependencies": { - "ansi-html": "^0.0.9", + "anser": "^2.1.1", "core-js-pure": "^3.23.3", "error-stack-parser": "^2.0.6", "html-entities": "^2.1.0", - "loader-utils": "^2.0.4", "schema-utils": "^4.2.0", "source-map": "^0.7.3" }, "engines": { - "node": ">= 10.13" + "node": ">=18.12" }, "peerDependencies": { - "@types/webpack": "4.x || 5.x", + "@types/webpack": "5.x", "react-refresh": ">=0.10.0 <1.0.0", "sockjs-client": "^1.4.0", "type-fest": ">=0.17.0 <5.0.0", - "webpack": ">=4.43.0 <6.0.0", - "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack": "^5.0.0", + "webpack-dev-server": "^4.8.0 || 5.x", "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "0.x || 1.x" + "webpack-plugin-serve": "1.x" }, "peerDependenciesMeta": { "@types/webpack": { @@ -4950,6 +5822,13 @@ } } }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/anser": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.2.tgz", + "integrity": "sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw==", + "dev": true, + "license": "MIT" + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -5068,30 +5947,6 @@ } } }, - "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collection": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", @@ -5148,6 +6003,159 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -5430,6 +6438,15 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -5616,30 +6633,6 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-popper": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", @@ -5959,6 +6952,30 @@ } } }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-primitive": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", @@ -5982,6 +6999,104 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz", @@ -6156,6 +7271,70 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slider": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.4.tgz", @@ -6236,18 +7415,43 @@ } } }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz", + "integrity": "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.9.tgz", - "integrity": "sha512-HJ6gXdYVN38q/5KDdCcd+JTuXUyFZBMJbwXaU/82/Gi+V2ps6KpiZ2sQecAeZCV80POGRfkUBdUIj6hIdF6/MQ==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz", + "integrity": "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-roving-focus": "1.1.9", - "@radix-ui/react-toggle": "1.1.8", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-toggle": "1.1.9", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -6265,21 +7469,13 @@ } } }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", - "integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==", + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.6", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -6296,15 +7492,31 @@ } } }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-toggle": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.8.tgz", - "integrity": "sha512-hrpa59m3zDnsa35LrTOH5s/a3iGv/VD+KKQjjiCTo/W4r0XwPpiWQvAv6Xl1nupSoaZeNNxW6sJH9ZydsjKdYQ==", + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -6321,6 +7533,94 @@ } } }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.10.tgz", + "integrity": "sha512-jiwQsduEL++M4YBIurjSa+voD86OIytCod0/dbIxFZDLD8NfO1//keXYMfsW8BPcfqwoNjt+y06XcJqAb4KR7A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.6.tgz", @@ -6438,30 +7738,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz", @@ -6797,6 +8073,28 @@ "integrity": "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==", "license": "MIT" }, + "node_modules/@react-hook/intersection-observer": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-hook/intersection-observer/-/intersection-observer-3.1.2.tgz", + "integrity": "sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==", + "license": "MIT", + "dependencies": { + "@react-hook/passive-layout-effect": "^1.2.0", + "intersection-observer": "^0.10.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@react-hook/passive-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz", + "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.2", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.2.tgz", @@ -7663,6 +8961,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@stitches/core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz", + "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==", + "license": "MIT" + }, "node_modules/@storybook/addon-actions": { "version": "7.6.20", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.20.tgz", @@ -8893,241 +10197,6 @@ } } }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.9.tgz", - "integrity": "sha512-qqGkE9h018CSbpO4ag4rR6ZuOc/A9wM3dUv2jHrkfwUqspuvZmPegBPElVimH0FPWrYn4Alt4QTOptRjbwJnKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-roving-focus": "1.1.9", - "@radix-ui/react-separator": "1.1.6", - "@radix-ui/react-toggle-group": "1.1.9" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", - "integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.6", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", - "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-separator": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.6.tgz", - "integrity": "sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@storybook/components/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", @@ -10102,6 +11171,55 @@ } } }, + "node_modules/@storybook/preset-react-webpack/node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz", + "integrity": "sha512-kLQc9xz6QIqd2oIYyXRUiAp79kGpFBm3fEM9ahfG1HI0WI5gdZ2OVHWdmZYnwODt7ISck+QuQ6sBPrtvUBML7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, "node_modules/@storybook/preset-react-webpack/node_modules/@types/node": { "version": "18.19.100", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.100.tgz", @@ -10112,6 +11230,26 @@ "undici-types": "~5.26.4" } }, + "node_modules/@storybook/preset-react-webpack/node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@storybook/preset-react-webpack/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, "node_modules/@storybook/preset-react-webpack/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -12952,7 +14090,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -14939,6 +16076,12 @@ "node": ">=0.10.0" } }, + "node_modules/clean-set": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/clean-set/-/clean-set-1.1.2.tgz", + "integrity": "sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug==", + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -15052,6 +16195,18 @@ "node": ">=6" } }, + "node_modules/cm6-theme-basic-light": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cm6-theme-basic-light/-/cm6-theme-basic-light-0.2.0.tgz", + "integrity": "sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -15371,6 +16526,12 @@ "node": ">= 0.6" } }, + "node_modules/compute-scroll-into-view": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz", + "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -15996,7 +17157,6 @@ "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "license": "ISC", - "peer": true, "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" @@ -16685,6 +17845,15 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -16921,6 +18090,28 @@ "node": ">=12" } }, + "node_modules/downshift": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.2.tgz", + "integrity": "sha512-iOv+E1Hyt3JDdL9yYcOgW7nZ7GQ2Uz6YbggwXvKUSleetYhU2nXD482Rz6CzvM4lvI1At34BYruKAL4swRGxaA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.14.8", + "compute-scroll-into-view": "^2.0.4", + "prop-types": "^15.7.2", + "react-is": "^17.0.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/downshift/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, "node_modules/draft-js": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz", @@ -16953,6 +18144,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz", "integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==", + "license": "ISC", "dependencies": { "draft-js-utils": "^1.4.0", "synthetic-dom": "^1.4.0" @@ -16966,6 +18158,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz", "integrity": "sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg==", + "license": "ISC", "dependencies": { "draft-js-import-element": "^1.4.0" }, @@ -17513,7 +18706,6 @@ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, "license": "ISC", - "peer": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", @@ -17529,7 +18721,6 @@ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "license": "MIT", - "peer": true, "dependencies": { "d": "1", "es5-ext": "^0.10.35", @@ -17541,7 +18732,6 @@ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "license": "ISC", - "peer": true, "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" @@ -17702,6 +18892,12 @@ "node": ">=6" } }, + "node_modules/escape-carriage": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz", + "integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==", + "license": "MIT" + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -18262,7 +19458,6 @@ "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", "license": "ISC", - "peer": true, "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", @@ -18361,6 +19556,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -18390,7 +19599,6 @@ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "license": "MIT", - "peer": true, "dependencies": { "d": "1", "es5-ext": "~0.10.14" @@ -18564,7 +19772,6 @@ "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "license": "ISC", - "peer": true, "dependencies": { "type": "^2.7.2" } @@ -18741,6 +19948,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -19456,6 +20676,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -21302,6 +22530,12 @@ "node": ">= 0.10" } }, + "node_modules/intersection-observer": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz", + "integrity": "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==", + "license": "W3C-20150513" + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -23767,6 +25001,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lexical": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.32.1.tgz", + "integrity": "sha512-Rvr9p00zUwzjXIqElIjMDyl/24QHw68yaqmXUWIT3lSdSAr8OpjSJK3iWBLZwVZwwpVhwShZRckomc+3vSb/zw==", + "license": "MIT" + }, "node_modules/lib0": { "version": "0.2.107", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.107.tgz", @@ -24041,7 +25281,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, "license": "MIT", "bin": { "lz-string": "bin/bin.js" @@ -24293,6 +25532,16 @@ "license": "ISC", "peer": true }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/markdown-to-jsx": { "version": "7.7.6", "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.6.tgz", @@ -24401,6 +25650,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", @@ -24425,6 +25695,101 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -25201,6 +26566,213 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -25244,6 +26816,39 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, "node_modules/micromark-factory-space": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", @@ -25445,6 +27050,37 @@ ], "license": "MIT" }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-events-to-acorn/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", @@ -25925,8 +27561,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/no-case": { "version": "3.0.4", @@ -26464,6 +28099,12 @@ "node": ">= 6" } }, + "node_modules/outvariant": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz", + "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==", + "license": "MIT" + }, "node_modules/overlap-area": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/overlap-area/-/overlap-area-1.1.0.tgz", @@ -27442,6 +29083,36 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/posthog-js": { + "version": "1.255.1", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.255.1.tgz", + "integrity": "sha512-KMh0o9MhORhEZVjXpktXB5rJ8PfDk+poqBoTSoLzWgNjhJf6D8jcyB9jUMA6vVPfn4YeepVX5NuclDRqOwr5Mw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "core-js": "^3.38.1", + "fflate": "^0.4.8", + "preact": "^10.19.3", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@rrweb/types": "2.0.0-alpha.17", + "rrweb-snapshot": "2.0.0-alpha.17" + }, + "peerDependenciesMeta": { + "@rrweb/types": { + "optional": true + }, + "rrweb-snapshot": { + "optional": true + } + } + }, + "node_modules/posthog-js/node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/potpack": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", @@ -27449,6 +29120,16 @@ "license": "ISC", "peer": true }, + "node_modules/preact": { + "version": "10.26.9", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz", + "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -27544,6 +29225,15 @@ "node": ">= 0.8" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/probe-image-size": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz", @@ -28519,6 +30209,15 @@ } } }, + "node_modules/react-devtools-inline": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/react-devtools-inline/-/react-devtools-inline-4.4.0.tgz", + "integrity": "sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ==", + "license": "MIT", + "dependencies": { + "es6-symbol": "^3" + } + }, "node_modules/react-dnd": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", @@ -28673,6 +30372,22 @@ "dev": true, "license": "MIT" }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-fit": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/react-fit/-/react-fit-1.7.1.tgz", @@ -28718,6 +30433,22 @@ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz", "integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==" }, + "node_modules/react-hook-form": { + "version": "7.59.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.59.0.tgz", + "integrity": "sha512-kmkek2/8grqarTJExFNjy+RXDIP8yM+QTl3QL6m6Q8b2bih4ltmiXxH7T9n+yXNK477xPh5yZT/6vD8sYGzJTA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-hot-toast": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", @@ -29197,9 +30928,10 @@ "license": "MIT" }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29655,6 +31387,16 @@ } } }, + "node_modules/react-spring/node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-spring/node_modules/scheduler": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", @@ -31884,6 +33626,18 @@ "node": ">=8" } }, + "node_modules/static-browser-server": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/static-browser-server/-/static-browser-server-1.0.3.tgz", + "integrity": "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA==", + "license": "Apache-2.0", + "dependencies": { + "@open-draft/deferred-promise": "^2.1.0", + "dotenv": "^16.0.3", + "mime-db": "^1.52.0", + "outvariant": "^1.3.0" + } + }, "node_modules/static-eval": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", @@ -31975,6 +33729,12 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "license": "MIT" }, + "node_modules/strict-event-emitter": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz", + "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==", + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -32674,7 +34434,8 @@ "node_modules/synthetic-dom": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz", - "integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg==" + "integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg==", + "license": "ISC" }, "node_modules/tabbable": { "version": "6.2.0", @@ -33445,8 +35206,7 @@ "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/type-check": { "version": "0.4.0", @@ -33733,6 +35493,15 @@ "node": ">=4" } }, + "node_modules/unidiff": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unidiff/-/unidiff-1.0.4.tgz", + "integrity": "sha512-ynU0vsAXw0ir8roa+xPCUHmnJ5goc5BTM2Kuc3IJd8UwgaeRs7VSD5+eeaQL+xp1JtB92hu/Zy/Lgy7RZcr1pQ==", + "license": "MIT", + "dependencies": { + "diff": "^5.1.0" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -33803,6 +35572,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", @@ -34328,6 +36110,12 @@ "node": ">= 8" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webgl-context": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/webgl-context/-/webgl-context-2.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4dc4be5289..6a65305bb8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@dnd-kit/utilities": "^3.2.1", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", + "@mdxeditor/editor": "^3.38.0", "@microsoft/fetch-event-source": "^2.0.1", "@radix-ui/colors": "^0.1.8", "@radix-ui/react-avatar": "^1.0.4", @@ -80,6 +81,7 @@ "papaparse": "^5.3.2", "path-browserify": "^1.0.1", "plotly.js-dist-min": "^2.29.1", + "posthog-js": "^1.255.1", "process": "^0.11.10", "psl": "^1.9.0", "query-string": "^8.1.0", @@ -155,6 +157,7 @@ "@babel/plugin-transform-runtime": "^7.19.6", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", "@storybook/addon-essentials": "^7.2.1", "@storybook/addon-interactions": "^7.2.1", "@storybook/addon-links": "^7.2.1", @@ -191,6 +194,7 @@ "postcss": "^8.4.35", "postcss-loader": "^8.1.0", "prettier": "^2.8.4", + "react-refresh": "^0.17.0", "sass": "^1.78.0", "sass-loader": "^13.2.0", "storybook": "^7.2.1", @@ -232,7 +236,7 @@ } }, "scripts": { - "start": "webpack serve --port 8082 --host 0.0.0.0", + "start": "webpack serve --hot --port 8082 --host 0.0.0.0", "build": "webpack --mode=production && cp -a ./assets/. ./build/assets/", "lint": "eslint . '**/*.{js,jsx}'", "format": "eslint . --fix '**/*.{js,jsx}'", diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index 4a463d93ae..e7a5b21f11 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -38,6 +38,7 @@ import { getDataSourcesRoutes, getAuditLogsRoutes, } from '@/modules'; +import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils'; import { shallow } from 'zustand/shallow'; import useStore from '@/AppBuilder/_stores/store'; import { checkIfToolJetCloud } from '@/_helpers/utils'; @@ -112,6 +113,7 @@ class AppComponent extends React.Component { const featureAccess = await licenseService.getFeatureAccess(); const isBasicPlan = !featureAccess?.licenseStatus?.isLicenseValid || featureAccess?.licenseStatus?.isExpired; this.setState({ showBanner: isBasicPlan }); + this.updateColorScheme(); } // check if its getting routed from editor checkPreviousRoute = (route) => { @@ -121,7 +123,7 @@ class AppComponent extends React.Component { return false; }; - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps, prevState) { // Check if the current location is the dashboard (homepage) if ( this.props.location.pathname === `/${getWorkspaceIdOrSlugFromURL()}` && @@ -134,18 +136,24 @@ class AppComponent extends React.Component { } // Update margin when showBanner changes this.updateMargin(); + // Update color scheme if darkMode changed + if (prevState.darkMode !== this.state.darkMode) { + this.updateColorScheme(); + } } switchDarkMode = (newMode) => { this.setState({ darkMode: newMode }); this.props.updateIsTJDarkMode(newMode); localStorage.setItem('darkMode', newMode); + this.updateColorScheme(newMode); }; isEditorOrViewerFromPath = () => { const pathname = this.props.location.pathname; if (pathname.includes('/apps/')) { return 'editor'; - } else if (pathname.includes('/applications/') || pathname.includes('/embed-apps/')) { + } + if (pathname.includes('/applications/') || pathname.includes('/embed-apps/')) { return 'viewer'; } return ''; @@ -156,6 +164,14 @@ class AppComponent extends React.Component { isExistingPlanUser = (date) => { return new Date(date) < new Date('2025-04-24'); //show banner if user created before 2 april (24 for testing) }; + updateColorScheme = (darkModeValue) => { + const isDark = darkModeValue !== undefined ? darkModeValue : this.state.darkMode; + if (isDark) { + document.documentElement.style.setProperty('color-scheme', 'dark'); + } else { + document.documentElement.style.removeProperty('color-scheme'); + } + }; render() { const { updateAvailable, darkMode, isEditorOrViewer, showBanner } = this.state; const mergedProps = { @@ -278,23 +294,30 @@ class AppComponent extends React.Component { } /> - {window.public_config?.ENABLE_WORKFLOWS_FEATURE === 'true' && ( + {isWorkflowsFeatureEnabled() && ( - + } /> )} + } /> } - > - }> - }> + path="settings/*" + element={ + + } + /> + + } + /> - + diff --git a/frontend/src/AppBuilder/AppBuilder.jsx b/frontend/src/AppBuilder/AppBuilder.jsx index fa12e0d1d9..b300329eef 100644 --- a/frontend/src/AppBuilder/AppBuilder.jsx +++ b/frontend/src/AppBuilder/AppBuilder.jsx @@ -17,6 +17,8 @@ import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext'; import RightSidebarToggle from '@/AppBuilder/RightSideBar/RightSidebarToggle'; import { shallow } from 'zustand/shallow'; +import ArtifactPreview from './ArtifactPreview'; + // const EditorHeader = lazy(() => import('@/AppBuilder/Header')); // const LeftSidebar = lazy(() => import('@/AppBuilder/LeftSidebar')); // const AppCanvas = lazy(() => import('@/AppBuilder/AppCanvas')); @@ -32,6 +34,9 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod const isModuleEditor = appType === 'module'; const updateIsTJDarkMode = useStore((state) => state.updateIsTJDarkMode, shallow); + const appBuilderMode = useStore((state) => state.appStore.modules[moduleId]?.app?.appBuilderMode ?? 'visual'); + + const isUserInZeroToOneFlow = appBuilderMode === 'ai'; const changeToDarkMode = (newMode) => { updateIsTJDarkMode(newMode); @@ -51,17 +56,29 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod Loading...}> - - + + + - {window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && } - - - - - {isRightSidebarOpen && }{' '} - - + + {isUserInZeroToOneFlow ? ( + + ) : ( + <> + {window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && } + + + + + {isRightSidebarOpen && }{' '} + + + + )} diff --git a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx index d82fb93386..2cd94143c7 100644 --- a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx +++ b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx @@ -8,7 +8,13 @@ import './appCanvas.scss'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { computeViewerBackgroundColor, getCanvasWidth } from './appCanvasUtils'; -import { NO_OF_GRIDS } from './appCanvasConstants'; +import { + LEFT_SIDEBAR_WIDTH, + NO_OF_GRIDS, + PAGES_SIDEBAR_WIDTH_COLLAPSED, + PAGES_SIDEBAR_WIDTH_EXPANDED, + RIGHT_SIDEBAR_WIDTH, +} from './appCanvasConstants'; import cx from 'classnames'; import FreezeVersionInfo from '@/AppBuilder/Header/FreezeVersionInfo'; import { computeCanvasContainerHeight } from '../_helpers/editorHelpers'; @@ -109,15 +115,15 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode }) return () => window.removeEventListener('resize', handleResize); }, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId, isRightSidebarOpen]); - useEffect(() => { }, [isViewerSidebarPinned]); + useEffect(() => {}, [isViewerSidebarPinned]); const canvasContainerStyles = useMemo(() => { const canvasBgColor = currentMode === 'view' ? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor) : !isAppDarkMode - ? '#EBEBEF' - : '#2F3C4C'; + ? '#EBEBEF' + : '#2F3C4C'; if (isModuleMode) { return { @@ -134,7 +140,7 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode }) width: currentMode === 'edit' ? `calc(100% - 96px)` : '100%', alignItems: 'unset', justifyContent: 'unset', - borderRight: currentMode === 'edit' && isRightSidebarOpen && '299' + 'px solid', + borderRight: currentMode === 'edit' && isRightSidebarOpen && `300px solid ${canvasBgColor}`, padding: currentMode === 'edit' && '8px', paddingBottom: currentMode === 'edit' && '2px', }; @@ -152,15 +158,34 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode }) const shouldAdjust = isSidebarOpen || (isRightSidebarOpen && currentMode === 'edit'); if (!shouldAdjust) return ''; - let offset; - if (isViewerSidebarPinned) { - offset = position === 'side' ? '352px' : '126px'; + if (isViewerSidebarPinned && !isPagesSidebarHidden) { + if (position === 'side' && isSidebarOpen && isRightSidebarOpen && !isPagesSidebarHidden) { + offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`; + } else if (position === 'side' && isSidebarOpen && !isRightSidebarOpen && !isPagesSidebarHidden) { + offset = `${LEFT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`; + } else if (position === 'side' && isRightSidebarOpen && !isSidebarOpen && !isPagesSidebarHidden) { + offset = `${RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`; + } } else { - offset = position === 'side' ? '171px' : '126px'; + if (position === 'side' && isSidebarOpen && isRightSidebarOpen && !isPagesSidebarHidden) { + offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`; + } else if (position === 'side' && isSidebarOpen && !isRightSidebarOpen && !isPagesSidebarHidden) { + offset = `${LEFT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`; + } else if (position === 'side' && isRightSidebarOpen && !isSidebarOpen && !isPagesSidebarHidden) { + offset = `${RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`; + } } - return `calc(100vw - ${offset})`; + if ((position === 'top' || isPagesSidebarHidden) && isSidebarOpen && isRightSidebarOpen) { + offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH}px`; + } else if ((position === 'top' || isPagesSidebarHidden) && isSidebarOpen && !isRightSidebarOpen) { + offset = `${LEFT_SIDEBAR_WIDTH}px`; + } else if ((position === 'top' || isPagesSidebarHidden) && isRightSidebarOpen && !isSidebarOpen) { + offset = `${RIGHT_SIDEBAR_WIDTH}px`; + } + + return `calc(100% + ${offset})`; } return ( @@ -177,12 +202,12 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode }) 'canvas-container d-flex page-container', { 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned }, { 'overflow-x-auto': currentMode === 'edit' }, - { 'position-top': position === 'top' }, + { 'position-top': position === 'top' || isPagesSidebarHidden }, { 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode )} style={canvasContainerStyles} > - {showOnDesktop && ( + {showOnDesktop && appType !== 'module' && ( @@ -213,7 +239,7 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode }) {environmentLoadingState !== 'loading' && (
{ const { moduleId } = useModuleContext(); + const isLicenseValid = useStore((state) => state.isLicenseValid(), shallow); const shouldFreeze = useStore((state) => state.getShouldFreeze()); const componentName = useStore((state) => state.getComponentDefinition(id, moduleId)?.component?.name || '', shallow); const isMultipleComponentsSelected = useStore( @@ -111,6 +113,9 @@ export const ConfigHandle = ({ } } }} + data-tooltip-id={`invalid-license-modules-${componentName?.toLowerCase()}`} + data-tooltip-html="Your plan is expired.
Renew to use the modules." + data-tooltip-place="right" > {licenseValid && isRestricted && ( @@ -201,6 +206,15 @@ export const ConfigHandle = ({
)} + {/* Tooltip for invalid license on ModuleViewer */} + {!isLicenseValid && componentType === 'ModuleViewer' && ( + + )} ); }; diff --git a/frontend/src/AppBuilder/AppCanvas/HotkeyProvider.jsx b/frontend/src/AppBuilder/AppCanvas/HotkeyProvider.jsx index cf2c3471ad..018383c61f 100644 --- a/frontend/src/AppBuilder/AppCanvas/HotkeyProvider.jsx +++ b/frontend/src/AppBuilder/AppCanvas/HotkeyProvider.jsx @@ -136,7 +136,7 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth } style={{ width: currentLayout == 'mobile' ? '450px' : '100%', maxWidth: canvasMaxWidth, - // margin: '0 auto', + margin: '0 auto', transform: 'translateZ(0)', }} > diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvas.scss b/frontend/src/AppBuilder/AppCanvas/appCanvas.scss index 66e5d622dd..fea79195c5 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvas.scss +++ b/frontend/src/AppBuilder/AppCanvas/appCanvas.scss @@ -26,6 +26,7 @@ .empty-box-cont{ display: flex; justify-content: center; + margin: unset !important; .dotted-cont{ border: 1px dashed var(--indigo8); diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js index 9553504441..0d3fa570e3 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js @@ -14,9 +14,13 @@ export const DEFAULT_CANVAS_WIDTH = 1292; export const APP_HEADER_HEIGHT = 47; -export const LEFT_SIDEBAR_WIDTH = 348; // exclusive of border +export const LEFT_SIDEBAR_WIDTH = 350; -export const RIGHT_SIDEBAR_WIDTH = 299; +export const RIGHT_SIDEBAR_WIDTH = 300; + +export const PAGES_SIDEBAR_WIDTH_EXPANDED = 226; + +export const PAGES_SIDEBAR_WIDTH_COLLAPSED = 44; export const SUBCONTAINER_WIDGETS = ['Container', 'Tabs', 'Listview', 'Kanban', 'Form']; diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js index 6b4eb6536b..c689adb51c 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js @@ -66,10 +66,10 @@ export const addNewWidgetToTheEditor = ( componentData.definition.properties.moduleVersionId = { value: moduleInfo.versionId }; componentData.definition.properties.moduleEnvironmentId = { value: moduleInfo.environmentId }; componentData.definition.properties.visibility = { value: true }; - customLayouts = moduleInfo.moduleContainer.layouts; + customLayouts = moduleInfo?.moduleContainer?.layouts; const inputItems = Object.values( - moduleInfo.moduleContainer.component.definition.properties?.input_items?.value ?? {} + moduleInfo.moduleContainer?.component.definition.properties?.input_items?.value ?? {} ); for (const { name, default_value } of inputItems) { diff --git a/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js b/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js index d80cbd76cc..df94532289 100644 --- a/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js +++ b/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js @@ -9,6 +9,7 @@ const useSidebarMargin = (canvasContainerRef) => { const { moduleId } = useModuleContext(); const [editorMarginLeft, setEditorMarginLeft] = useState(0); const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow); + const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow); const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); useEffect(() => { @@ -17,10 +18,10 @@ const useSidebarMargin = (canvasContainerRef) => { }, [isSidebarOpen, mode]); useEffect(() => { - if (!isEmpty(canvasContainerRef?.current)) { + if (!isEmpty(canvasContainerRef?.current) && isSidebarOpen && canvasContainerRef.current.scrollLeft === 0) { canvasContainerRef.current.scrollLeft += editorMarginLeft; } - }, [editorMarginLeft, canvasContainerRef]); + }, [editorMarginLeft, canvasContainerRef, isSidebarOpen]); return editorMarginLeft; }; diff --git a/frontend/src/AppBuilder/ArtifactPreview/index.jsx b/frontend/src/AppBuilder/ArtifactPreview/index.jsx new file mode 100644 index 0000000000..103119d9bc --- /dev/null +++ b/frontend/src/AppBuilder/ArtifactPreview/index.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +function ArtifactPreview() { + return <>; +} + +export default withEditionSpecificComponent(ArtifactPreview, 'AiBuilder'); diff --git a/frontend/src/AppBuilder/CodeEditor/FixWithAi.jsx b/frontend/src/AppBuilder/CodeEditor/FixWithAi.jsx new file mode 100644 index 0000000000..c56c2368e0 --- /dev/null +++ b/frontend/src/AppBuilder/CodeEditor/FixWithAi.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +function FixWithAi() { + return <>; +} + +export default withEditionSpecificComponent(FixWithAi, 'AiBuilder'); diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx index ce3c310f24..c6a024eaf7 100644 --- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx @@ -56,6 +56,8 @@ const MultiLineCodeEditor = (props) => { renderCopilot, setCodeEditorView, } = props; + const editorRef = useRef(null); + const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow); const wrapperRef = useRef(null); const getSuggestions = useStore((state) => state.getSuggestions, shallow); @@ -330,6 +332,11 @@ const MultiLineCodeEditor = (props) => { } } + const onAiSuggestionAccept = (newValue) => { + currentValueRef.current = newValue; + onChange(newValue); + }; + return (
{ ref={wrapperRef} >
- + + renderCopilot?.({ + darkMode, + language: lang, + editorRef, + onAiSuggestionAccept, + }) + } + /> + {
{ const SIZE_LIMIT_KB = 5 * 1024; // 5 KB in bytes @@ -143,7 +147,7 @@ export const PreviewBox = ({ useEffect(() => { if (error) { setErrorStateActive(true); - setErrorMessage(error.message); + setErrorMessage(error); } else { setErrorStateActive(false); setErrorMessage(null); @@ -159,6 +163,8 @@ export const PreviewBox = ({ validationFn ); + const completeErrMessage = Array.isArray(_error) ? _error.join('.') : _error; + const resolvedValue = typeof rawResolvedValue === 'function' ? undefined : rawResolvedValue; const newValue = typeof rawNewValue === 'function' ? undefined : rawNewValue; @@ -185,7 +191,7 @@ export const PreviewBox = ({ setError(null); } else if (!valid && !newValue && !resolvedValue && !isSecretError) { const err = !error ? `Invalid value for ${validationSchema?.schema?.type}` : `${_error}`; - setError({ message: err, value: resolvedValue, type: 'Invalid' }); + setError({ message: err, value: resolvedValue, type: 'Invalid', completeErrorMessage: completeErrMessage }); } else { const jsErrorType = isSecretError ? 'Error' @@ -211,6 +217,7 @@ export const PreviewBox = ({ ? JSON.stringify(errValue, reservedKeywordReplacer) : resolvedValue, type: isSecretError ? 'Error' : jsErrorType, + completeErrorMessage: completeErrMessage, }); setCoersionData(null); } @@ -309,13 +316,118 @@ const PreviewContainer = ({ isPortalOpen, previewRef, showPreview, + onAiSuggestionAccept, ...restProps }) => { - const { validationSchema, isWorkspaceVariable, errorStateActive, previewPlacement, validationFn } = restProps; - const [errorMessage, setErrorMessage] = useState(''); - const typeofError = getCurrentNodeType(errorMessage); - const errorMsg = typeofError === 'Array' ? errorMessage[0] : errorMessage; + const { + validationSchema, + isWorkspaceVariable, + errorStateActive, + previewPlacement, + validationFn, + componentId, + paramName, + fieldMeta, + setIsFocused, + currentValue, + } = restProps; + + const aiFeaturesEnabled = useStore((state) => state.ai?.aiFeaturesEnabled ?? false); + const fetchErrorFixUsingAi = useStore((state) => state.fetchErrorFixUsingAi); + const clearChatHistory = useStore((state) => state.clearChatHistory); + const componentDefinition = useStore((state) => state.getComponentDefinition(componentId), shallow); // TODO: check if moduleId needs to be passed here + + const componentName = componentDefinition?.component?.name; + const componentKey = `${componentName} - ${fieldMeta?.displayName}`; + + const chatList = useStore((state) => state.fixWithAiSlice?.[componentId]?.[componentKey]?.chatHistory ?? []); + + const [errorMessage, setErrorMessage] = useState(null); + + const [popoverToShow, setPopoverToShow] = useState('preview'); // preview | fixWithAI + + const errMsg = errorMessage?.message ?? null; + + const typeofError = getCurrentNodeType(errMsg); + + const errorMsg = typeofError === 'Array' ? errMsg[0] : errMsg; + const darkMode = localStorage.getItem('darkMode') === 'true'; + + useEffect(() => { + !showPreview && setPopoverToShow('preview'); + }, [showPreview]); + + useEffect(() => { + setPopoverToShow('preview'); + + if (chatList?.length) { + clearChatHistory(componentId, componentKey); + } + }, [currentValue]); + + const fetchFixUsingAi = () => { + const defaultValue = validationSchema?.defaultValue + ? validationSchema?.defaultValue + : validationSchema + ? findDefault(validationSchema?.schema ?? {}, errorMessage?.value) + : undefined; + + const errorData = { + key: componentKey, + componentId: componentId, + message: errorMessage?.completeErrorMessage, + error: { + resolvedProperty: { [paramName]: errorMessage?.value }, + effectiveProperty: { [paramName]: defaultValue }, + componentId, + }, + }; + + fetchErrorFixUsingAi(errorData, { + componentDisplayName: + componentDefinition?.component?.displayName ?? componentDefinition?.component?.component ?? componentName, + errorPropertyDisplayName: fieldMeta?.displayName, + customErrMessage: errorMessage?.message, + }); + }; + + const handleFixErrorWithAI = () => { + setPopoverToShow('fixWithAI'); + + if (!componentId || chatList?.length) { + return; + } + + fetchFixUsingAi(); + }; + + const fixWithAIPopover = ( + setCursorInsidePreview(true)} + onMouseLeave={() => setCursorInsidePreview(false)} + > + + setIsFocused(false)} + /> + + + ); + const popover = (
{errorMsg !== 'null' ? errorMsg : 'Invalid'}
+ + {aiFeaturesEnabled && ( + + )}
)} @@ -479,7 +603,7 @@ const PreviewContainer = ({ }, }} > - {(props) => React.cloneElement(popover, props)} + {(props) => React.cloneElement(popoverToShow === 'fixWithAI' ? fixWithAIPopover : popover, props)} )} diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index c3d0bb798c..ed07b6da70 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -37,7 +37,7 @@ import Icon from '@/_ui/Icon/solidIcons/index'; const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => { const { moduleId } = useModuleContext(); - const { initialValue, onChange, enablePreview = true, portalProps } = restProps; + const { initialValue, onChange, enablePreview = true, portalProps, paramName } = restProps; const { validation = {} } = fieldMeta; const [showPreview, setShowPreview] = useState(false); const [isFocused, setIsFocused] = useState(false); @@ -146,17 +146,24 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r enablePreview={enablePreview} currentValue={currentValue} isFocused={isFocused} + setIsFocused={setIsFocused} setCursorInsidePreview={setCursorInsidePreview} componentName={componentName} validationSchema={validation} setErrorStateActive={setErrorStateActive} ignoreValidation={restProps?.ignoreValidation || isEmpty(validation)} - componentId={restProps?.componentId ?? null} + componentId={componentId ?? null} + fieldMeta={fieldMeta} + paramName={paramName} isWorkspaceVariable={isWorkspaceVariable} errorStateActive={errorStateActive} previewPlacement={restProps?.cyLabel === 'canvas-bg-colour' ? 'top' : 'left-start'} isPortalOpen={restProps?.portalProps?.isOpen} validationFn={validationFn} + onAiSuggestionAccept={(newValue) => { + setCurrentValue(newValue); + onChange(newValue); + }} >
@@ -176,6 +183,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r showPreview={showPreview} wrapperRef={wrapperRef} showSuggestions={showSuggestions} + cursorInsidePreview={cursorInsidePreview} {...restProps} />
@@ -211,6 +219,7 @@ const EditorInput = ({ wrapperRef, showSuggestions, setCodeEditorView = null, // Function to set the CodeMirror view + cursorInsidePreview = false, }) => { const codeHinterContext = useContext(CodeHinterContext); const { suggestionList: paramHints } = createReferencesLookup(codeHinterContext, true); @@ -333,7 +342,8 @@ const EditorInput = ({ }, []); const handleOnBlur = () => { - setShowPreview(false); + !cursorInsidePreview && setShowPreview(false); + if (!delayOnChange) { setFirstTimeFocus(false); return onBlurUpdate(currentValue); diff --git a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx index 66e62fcd8b..dd2fb8c916 100644 --- a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx +++ b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx @@ -16,11 +16,15 @@ const CreateVersionModal = ({ canCommit, orgGit, fetchingOrgGit, - handleCommitOnVersionCreation = () => { }, + handleCommitOnVersionCreation = () => {}, }) => { const { moduleId } = useModuleContext(); const [isCreatingVersion, setIsCreatingVersion] = useState(false); const [versionName, setVersionName] = useState(''); + const isGitSyncEnabled = + orgGit?.org_git?.git_ssh?.is_enabled || + orgGit?.org_git?.git_https?.is_enabled || + orgGit?.org_git?.git_lab?.is_enabled; const { createNewVersionAction, @@ -102,8 +106,8 @@ const CreateVersionModal = ({ }); }, (error) => { - if (error?.data?.code === "23505") { - toast.error("Version name already exists."); + if (error?.data?.code === '23505') { + toast.error('Version name already exists.'); } else { toast.error(error?.error); } @@ -172,7 +176,7 @@ const CreateVersionModal = ({
- {orgGit?.org_git?.is_enabled && ( + {isGitSyncEnabled && (
{ {props?.selectProps?.value?.appVersionName && decodeEntities(props?.selectProps?.value?.appVersionName)}
-
+
diff --git a/frontend/src/AppBuilder/Header/EditorHeader.jsx b/frontend/src/AppBuilder/Header/EditorHeader.jsx index 5e1e639819..e4d5e16850 100644 --- a/frontend/src/AppBuilder/Header/EditorHeader.jsx +++ b/frontend/src/AppBuilder/Header/EditorHeader.jsx @@ -15,13 +15,16 @@ import UpdatePresenceMultiPlayer from './UpdatePresenceMultiPlayer'; import { ModuleEditorBanner } from '@/modules/Modules/components'; import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; -export const EditorHeader = ({ darkMode }) => { +import Steps from './Steps'; + +export const EditorHeader = ({ darkMode, isUserInZeroToOneFlow }) => { const { moduleId, isModuleEditor } = useModuleContext(); - const { isSaving, saveError, isVersionReleased } = useStore( + const { isSaving, saveError, isVersionReleased, aiGenerationMetadata } = useStore( (state) => ({ isSaving: state.appStore.modules[moduleId].app.isSaving, saveError: state.appStore.modules[moduleId].app.saveError, isVersionReleased: state.isVersionReleased, + aiGenerationMetadata: state.appStore.modules[moduleId].app?.aiGenerationMetadata, }), shallow ); @@ -80,44 +83,64 @@ export const EditorHeader = ({ darkMode }) => {
- -
-
- - {getSaveIndicator()} - -
- {shouldEnableMultiplayer && ( -
- -
- )} - {shouldEnableMultiplayer && } -
- - {!isModuleEditor &&
} - -
-
- {!isModuleEditor && ( + + {isUserInZeroToOneFlow && ( + ({ label: step.name, value: step.id })) ?? []} + activeStep={aiGenerationMetadata?.active_step} + /> + )} + + {!isUserInZeroToOneFlow && ( <> - - - + +
+
+ + {getSaveIndicator()} + +
+ {shouldEnableMultiplayer && ( +
+ +
+ )} + {shouldEnableMultiplayer && } +
)}
+ {!isModuleEditor && !isUserInZeroToOneFlow &&
}
+ + {!isUserInZeroToOneFlow && ( +
+
+ {!isModuleEditor && ( + <> + + + + + )} +
+
+ )} - - + + {!isUserInZeroToOneFlow && ( + <> + + + + )} diff --git a/frontend/src/AppBuilder/Header/Steps.jsx b/frontend/src/AppBuilder/Header/Steps.jsx new file mode 100644 index 0000000000..3abb9a0953 --- /dev/null +++ b/frontend/src/AppBuilder/Header/Steps.jsx @@ -0,0 +1,64 @@ +import React, { Children } from 'react'; + +import { cn } from '@/lib/utils'; +import CheckCircle from '@/_ui/Icon/solidIcons/CheckCircle'; +import SolidArrow from '@/_ui/Icon/solidIcons/SolidArrow'; +import DottedArrow from '@/_ui/Icon/solidIcons/DottedArrow'; + +function Step({ stepNo, label, active, completed }) { + return ( +
+ {completed ? ( + + ) : ( + + {stepNo} + + )} + +

+ {label} +

+
+ ); +} + +function Connector({ completed }) { + if (completed) return ; + + return ; +} + +// sequential steps +export default function Steps({ steps, activeStep }) { + const activeStepIndex = steps.findIndex((step) => step.value === activeStep); + const currentStepIdx = activeStepIndex === -1 ? 0 : activeStepIndex; + + return ( +
+ {Children.toArray( + steps.map((step, index) => { + const isActive = index === currentStepIdx; + const isCompleted = index < currentStepIdx; + + return ( + <> + + + {index < steps.length - 1 && } + + ); + }) + )} +
+ ); +} diff --git a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/index.jsx b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/index.jsx index 9f8379f85c..341ed07652 100644 --- a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/index.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/index.jsx @@ -31,7 +31,6 @@ const GlobalSettings = ({ darkMode }) => {
-
Canvas Styles diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx index bcd90bee33..6d2a15a501 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx @@ -24,6 +24,7 @@ export const BaseLeftSidebar = ({ switchDarkMode, renderAISideBarTrigger = () => null, renderAIChat = () => null, + isUserInZeroToOneFlow, }) => { const { moduleId, isModuleEditor, appType } = useModuleContext(); const [ @@ -72,6 +73,11 @@ export const BaseLeftSidebar = ({ }; useEffect(() => { + if (isUserInZeroToOneFlow) { + setPopoverContentHeight(((window.innerHeight - 48) / window.innerHeight) * 100); + return; + } + if (!isDraggingQueryPane) { setPopoverContentHeight( ((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100 @@ -80,7 +86,7 @@ export const BaseLeftSidebar = ({ setPopoverContentHeight(100); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [queryPanelHeight, isDraggingQueryPane]); + }, [isUserInZeroToOneFlow, queryPanelHeight, isDraggingQueryPane]); const renderPopoverContent = () => { if (selectedSidebarItem === null || !isSidebarOpen) return null; @@ -111,7 +117,7 @@ export const BaseLeftSidebar = ({ /> ); case 'tooljetai': - return renderAIChat({ darkMode }); + return renderAIChat({ darkMode, isUserInZeroToOneFlow }); // case 'datasource': // return ( // handleSelectedSidebarItem('settings')} - className={`left-sidebar-item left-sidebar-layout`} - badge={true} - tip="Settings" - ref={setSideBarBtnRefs('settings')} - isModuleEditor={isModuleEditor} - /> + + {!isUserInZeroToOneFlow && ( + <> + {renderCommonItems()} + handleSelectedSidebarItem('settings')} + className={`left-sidebar-item left-sidebar-layout`} + badge={true} + tip="Settings" + ref={setSideBarBtnRefs('settings')} + isModuleEditor={isModuleEditor} + /> + + )} ); }; diff --git a/frontend/src/AppBuilder/QueryManager/Components/DataSourcePicker.jsx b/frontend/src/AppBuilder/QueryManager/Components/DataSourcePicker.jsx index 1c6ebe9ebd..30f2828ae8 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/DataSourcePicker.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/DataSourcePicker.jsx @@ -14,6 +14,7 @@ import { useQueryPanelActions } from '@/_stores/queryPanelStore'; import { Tooltip } from 'react-tooltip'; import { canCreateDataSource } from '@/_helpers'; import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils'; import '../queryManager.theme.scss'; import useStore from '@/AppBuilder/_stores/store'; import { staticDataSources } from '../constants'; @@ -80,7 +81,7 @@ function DataSourcePicker({ darkMode }) { navigate(`/${workspaceId}/data-sources`); }; - const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true'; + const workflowsEnabled = isWorkflowsFeatureEnabled(); return ( <> diff --git a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx index f0a3bbe811..52a9e8c1ea 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx @@ -15,6 +15,7 @@ import { DataBaseSources, ApiSources, CloudStorageSources } from '@/modules/comm import { canCreateDataSource } from '@/_helpers'; import './../queryManager.theme.scss'; import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; +import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, defaultDataSources }) { @@ -34,13 +35,12 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc const createDataQuery = useStore((state) => state.dataQuery.createDataQuery); const setPreviewData = useStore((state) => state.queryPanel.setPreviewData); const handleChangeDataSource = (source) => { - console.log({ source }); createDataQuery(source); setPreviewData(null); closePopup(); }; - const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true'; + const workflowsEnabled = isWorkflowsFeatureEnabled(); const staticDataSources = workflowsEnabled ? staticDatasources : staticDatasources.filter((ds) => ds?.kind !== 'workflows'); diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx index 547aa5452c..e5a9849c35 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx @@ -9,6 +9,7 @@ import { BaseUrl } from './BaseUrl'; import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles'; import CodeHinter from '@/AppBuilder/CodeEditor'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; +import './styles.css'; class Restapi extends React.Component { constructor(props) { @@ -287,14 +288,15 @@ class Restapi extends React.Component { const { options } = this.state; const dataSourceURL = this.props.selectedDataSource?.options?.url?.value; const queryName = this.props.queryName; + const isWorkflowNode = queryName === 'workflowNode'; const currentValue = { label: options.method?.toUpperCase(), value: options.method }; return ( -
+
{this.props.selectedDataSource?.scope == 'global' &&
}{' '}
-
-
+
+

-
-
+
+
{ variant="outline" className="" id={`query-handler-menu-${dataQuery?.id}`} + data-cy={`delete-query-${dataQuery.name.toLowerCase()}`} />
diff --git a/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx b/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx index efba1741b4..ac854f4716 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx @@ -155,6 +155,7 @@ export const QueryPanel = ({ darkMode }) => { justifyContent: 'space-between', alignItems: 'center', zIndex: 2, + width: '100%', }} >
{ const selectedComponentId = useStore((state) => state.selectedComponents?.[0], shallow); const activeTab = useStore((state) => state.activeRightSideBarTab, shallow); - const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin); - const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned); + const setRightSidebarOpen = useStore((state) => state.setRightSidebarOpen); const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab); + + const handleToggle = () => { + setActiveRightSideBarTab(null); + setRightSidebarOpen(false); + }; if (!selectedComponentId && activeTab !== RIGHT_SIDE_BAR_TAB.PAGES) { // return setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.COMPONENTS); return ( <>
Component properties
-
toggleRightSidebarPin()}> - +
+
@@ -39,6 +43,7 @@ export const ComponentConfigurationTab = ({ darkMode, isModuleEditor }) => { selectedComponentId={selectedComponentId} pages={[]} isModuleEditor={isModuleEditor} + handleRightSidebarToggle={handleToggle} /> ); }; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/ComponentModuleTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/ComponentModuleTab.jsx index d86d7e59d6..dd049bf1c4 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/ComponentModuleTab.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/ComponentModuleTab.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import './styles.scss'; -export const ComponentModuleTab = ({ onChangeTab }) => { +export const ComponentModuleTab = ({ onChangeTab, hasModuleAccess }) => { const [activeTab, setActiveTab] = useState(1); const handleChangeTab = (tab) => { @@ -18,13 +18,15 @@ export const ComponentModuleTab = ({ onChangeTab }) => { > Components - + {hasModuleAccess && ( + + )}
); diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentsManagerTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentsManagerTab.jsx index 7f41ec95d0..0c4d1c92ab 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentsManagerTab.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentsManagerTab.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { isEmpty, debounce } from 'lodash'; import { useTranslation } from 'react-i18next'; import { LEGACY_ITEMS, IGNORED_ITEMS } from './constants'; @@ -12,6 +12,32 @@ import sectionConfig from './sectionConfig'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import { ModuleManager } from '@/modules/Modules/components'; import { ComponentModuleTab } from '@/modules/Appbuilder/components'; +import { useLicenseStore } from '@/_stores/licenseStore'; +import { shallow } from 'zustand/shallow'; + +// Simple error boundary component for module errors +class ModuleErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + console.error('Module error:', error, errorInfo); + this.props.onError(); + } + + render() { + if (this.state.hasError) { + return null; // Let parent handle the fallback + } + return this.props.children; + } +} // TODO: Hardcode all the component-section mapping in a constant file and just loop over it // TODO: styling @@ -28,11 +54,31 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => { const [filteredComponents, setFilteredComponents] = useState(componentList); const [searchQuery, setSearchQuery] = useState(''); const [activeTab, setActiveTab] = useState(1); + const [moduleError, setModuleError] = useState(false); const _shouldFreeze = useStore((state) => state.getShouldFreeze()); const isAutoMobileLayout = useStore((state) => state.currentLayout === 'mobile' && state.getIsAutoMobileLayout()); const shouldFreeze = _shouldFreeze || isAutoMobileLayout; - const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin); - const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned); + + const { hasModuleAccess } = useLicenseStore( + (state) => ({ + hasModuleAccess: state.hasModuleAccess, + }), + shallow + ); + + // Force re-render when hasModuleAccess changes + useEffect(() => { + // If modules access is denied and we're on the modules tab, switch to components + if (!hasModuleAccess && activeTab === 2) { + setActiveTab(1); + } + }, [hasModuleAccess, activeTab]); + + const setRightSidebarOpen = useStore((state) => state.setRightSidebarOpen); + const activeRightSideBarTab = useStore((state) => state.activeRightSideBarTab); + const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab); + const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen); + const handleSearchQueryChange = useCallback( debounce((value) => { setSearchQuery(value); @@ -44,6 +90,11 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => { [activeTab] ); + const handleToggle = () => { + setActiveRightSideBarTab(null); + setRightSidebarOpen(false); + }; + const filterComponents = useCallback((value) => { if (value !== '') { const fuse = new Fuse(componentList, { @@ -143,25 +194,52 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => { } const handleChangeTab = (tab) => { + if (tab === 2 && !hasModuleAccess) { + setActiveTab(1); + return; + } setActiveTab(tab); + if (tab === 1) setModuleError(false); // When changing tabs, we don't need to reset the search // The search query will be applied to the new tab }; + // Handle module errors by redirecting to components tab + useEffect(() => { + if (moduleError && activeTab === 2) { + setActiveTab(1); + } + }, [moduleError, activeTab]); + const renderSection = () => { if (activeTab === 1) { return
{segregateSections()}
; } - return ; + + // If there was an error accessing modules, redirect to components tab + if (moduleError) { + return
{segregateSections()}
; + } + + return ( + setModuleError(true)}> + + + ); }; return (
- {isModuleEditor ? ( -

Components

- ) : ( - - )} +
+ {isModuleEditor ? ( +

Components

+ ) : ( + + )} +
+ +
+
{ +export const DragLayer = ({ index, component, isModuleTab = false, disabled = false }) => { const [isRightSidebarOpen, toggleRightSidebar] = useStore( (state) => [state.isRightSidebarOpen, state.toggleRightSidebar], shallow @@ -51,8 +51,16 @@ export const DragLayer = ({ index, component, isModuleTab = false }) => { return ( <> {isDragging && } -
- {isModuleTab ? : } +
+ {isModuleTab ? ( + + ) : ( + + )}
); diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/_components/ColumnMappingComponent.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/_components/ColumnMappingComponent.jsx index 0401c22d57..df6d51e5e0 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/_components/ColumnMappingComponent.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/_components/ColumnMappingComponent.jsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/Button/Button'; import Checkbox from '@/components/ui/Checkbox/Index'; import cx from 'classnames'; @@ -78,10 +78,8 @@ const ModalFooter = ({ currentStatus, refreshData, handleSubmit, isSaving, allSe * Loader component */ const LoaderComponent = () => ( -
-
- -
+
+
); @@ -388,19 +386,40 @@ const ColumnMappingComponent = ({ const [isSaving, setIsSaving] = useState(false); const [refreshedColumns, setRefreshedColumns] = useState([]); const [showLoader, setShowLoader] = useState(false); + const bodyContainerRef = useRef(null); + const lastBodyHeightRef = useRef(60); useEffect(() => { setShowLoader(isDataLoading); }, [isDataLoading]); + // Track body height when content is loaded + useEffect(() => { + if (!showLoader && bodyContainerRef.current) { + // Use setTimeout to ensure DOM is fully rendered + setTimeout(() => { + if (bodyContainerRef.current) { + const height = bodyContainerRef.current.scrollHeight; + if (height > 0) { + lastBodyHeightRef.current = height; + } + } + }, 0); + } + }, [showLoader, groupedColumns]); + const currentStatus = currentStatusRef.current; + console.log('here--- existingResolvedJsonData--- ', existingResolvedJsonData); + const columnsToUse = useColumnBuilder( component, currentStatus, newResolvedJsonData, existingResolvedJsonData, - refreshedColumns?.length === 0 ? newResolvedJsonData : refreshedColumns, + refreshedColumns?.length === 0 || Object.keys(refreshedColumns).length === 0 + ? newResolvedJsonData + : refreshedColumns, getFormFields, getComponentDefinition ); @@ -459,7 +478,11 @@ const ColumnMappingComponent = ({ const modalBody = ( <> -
+
{showLoader && } {!showLoader && ( diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/_hooks/useFormLogic.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/_hooks/useFormLogic.js index 1d0636d222..0484f64f26 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/_hooks/useFormLogic.js +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/_hooks/useFormLogic.js @@ -26,6 +26,12 @@ export const useFormLogic = (component, paramUpdated) => { // Save data section function const saveDataSection = (fields) => { formState.savedSourceValue.current = formState.source.value; + const newJsonData = formState.JSONData; + + if (newJsonData?.value === undefined) { + newJsonData.value = resolveReferences('canvas', formState.source.value); + } + saveFormDataSectionData( component?.id, { diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/handlers/parameterHandlers.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/handlers/parameterHandlers.js index 47747cff87..a120175f67 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/handlers/parameterHandlers.js +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/handlers/parameterHandlers.js @@ -29,6 +29,7 @@ export const createParamUpdatedInterceptor = ({ const { generateFormFrom, JSONData } = getFormDataSectionData(component?.id); if (value === generateFormFrom?.value) { + setSource((prev) => ({ ...prev, value })); return setJSONData({ value: JSONData.value }); } diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/utils/utils.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/utils/utils.js index 3ac6b40a98..d5bb362dda 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/utils/utils.js +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form/utils/utils.js @@ -256,18 +256,18 @@ export const mergeFormFieldsWithNewData = (existingFields, newFields) => { const existingFieldsMap = {}; existingFields.forEach((field) => { - if (field.name) { - existingFieldsMap[field.name] = field; + if (field.key) { + existingFieldsMap[field.key] = field; } }); return newFields.map((newField) => { - if (newField.isNew || !existingFieldsMap[newField.name]) { + if (newField.isNew || !existingFieldsMap[newField.key]) { return newField; } return { ...newField, - ...omit(existingFieldsMap[newField.name], ['isNew']), + ...omit(existingFieldsMap[newField.key], ['isNew']), }; }); }; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx index 278ec7b094..07ce2b2d32 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx @@ -118,7 +118,13 @@ const NEW_REVAMPED_COMPONENTS = [ 'FilePicker', ]; -export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => { +export const Inspector = ({ + componentDefinitionChanged, + darkMode, + pages, + selectedComponentId, + handleRightSidebarToggle, +}) => { const allComponents = useStore((state) => state.getCurrentPageComponents()); const setComponentProperty = useStore((state) => state.setComponentProperty, shallow); const setComponentName = useStore((state) => state.setComponentName, shallow); @@ -528,19 +534,19 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
-
clearSelectedComponents()}> +
clearSelectedComponents()}>
-
{renderAppNameInput()}
+
{renderAppNameInput()}
{!isModuleContainer && ( <> -
+
)} +
+ +
{renderTabs()}
@@ -819,7 +828,7 @@ const GetAccordion = React.memo( return ; case 'Form': - return
; + return ; case 'DropdownV2': case 'MultiselectV2': diff --git a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/AddNewPageMenu.jsx b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/AddNewPageMenu.jsx index 261ab9d0d1..7e6ee48539 100644 --- a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/AddNewPageMenu.jsx +++ b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/AddNewPageMenu.jsx @@ -20,7 +20,7 @@ export function AddNewPageMenu({ darkMode, isLicensed }) { }; return ( -
+
@@ -343,6 +381,9 @@ export const RenderPageAndPageGroup = ({ darkMode={darkMode} linkRefs={linkRefs} isSidebarPinned={isSidebarPinned} + isExpanded={expandedPageGroupId === page.id} + onToggle={handleAccordionToggle} + onPageClick={closeAllAccordions} /> ); @@ -359,6 +400,7 @@ export const RenderPageAndPageGroup = ({ homePageId={homePageId} linkRefs={linkRefs} isSidebarPinned={isSidebarPinned} + onPageClick={closeAllAccordions} /> ); } diff --git a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PageGroupItem.jsx b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PageGroupItem.jsx index 8913fb860c..b748724a68 100644 --- a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PageGroupItem.jsx +++ b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PageGroupItem.jsx @@ -138,7 +138,7 @@ export const PageGroupItem = memo(({ page, index, collapsed, onCollapse, highlig >
{ handleOpenPopup('group', page); diff --git a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PageMenuItem.jsx b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PageMenuItem.jsx index 8a2fd40605..6d0a971f63 100644 --- a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PageMenuItem.jsx +++ b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PageMenuItem.jsx @@ -72,7 +72,7 @@ export const PageMenuItem = withRouter( const icon = (props) => { const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon; // eslint-disable-next-line import/namespace - const Icon = Icons?.[iconName] ?? Icons?.['IconFileDescription']; + const Icon = Icons?.[iconName] ?? Icons?.['IconFile']; return ( @@ -269,7 +269,6 @@ export const PageMenuItem = withRouter( return ''; } - return (
setIsHovered(true)} @@ -281,7 +280,7 @@ export const PageMenuItem = withRouter( <>
{' '}
- {icon()} - +
{icon()}
+ {page.name} - - {PAGE_TYPES[page?.type]} - + {PAGE_TYPES[page?.type] && ( // If 'page' object has a 'type' property like 'URL' + {PAGE_TYPES[page?.type]} + )} {isHomePage && (
@@ -454,7 +453,7 @@ export const PageMenuItem = withRouter( placement="right" show={!licenseValid} > -
+
Page permission
{!licenseValid && }
@@ -462,7 +461,7 @@ export const PageMenuItem = withRouter( } icon="lock" darkMode={darkMode} - disabled={isHomePage} + disabled={!licenseValid} onClick={(e) => { e.stopPropagation(); e.preventDefault(); diff --git a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PagePermission.jsx index 160a941ebe..054325f9ab 100644 --- a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PagePermission.jsx +++ b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PagePermission.jsx @@ -22,9 +22,9 @@ export default function PagePermission({ darkMode }) { const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); const editingPage = useStore((state) => state.editingPage); const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); - const selectedUserGroups = useStore((state) => state.selectedUserGroups); + const selectedUserGroups = useStore((state) => state.appPermission.selectedUserGroups); const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); - const selectedUsers = useStore((state) => state.selectedUsers); + const selectedUsers = useStore((state) => state.appPermission.selectedUsers); const setSelectedUsers = useStore((state) => state.setSelectedUsers); const pagePermission = useStore((state) => state.pagePermission); const setPagePermission = useStore((state) => state.setPagePermission); @@ -353,7 +353,7 @@ export default function PagePermission({ darkMode }) { const UserGroupSelect = () => { const { moduleId } = useModuleContext(); const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); - const selectedUserGroups = useStore((state) => state.selectedUserGroups); + const selectedUserGroups = useStore((state) => state.appPermission.selectedUserGroups); const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); const [userGroups, setUserGroups] = useState([]); useEffect(() => { @@ -415,7 +415,7 @@ const UserSelect = () => { const { moduleId } = useModuleContext(); const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); const editingPage = useStore((state) => state.editingPage); - const selectedUsers = useStore((state) => state.selectedUsers); + const selectedUsers = useStore((state) => state.appPermission.selectedUsers); const setSelectedUsers = useStore((state) => state.setSelectedUsers); const [users, setUsers] = useState([]); useEffect(() => { diff --git a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PagesSidebarNavigation.jsx b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PagesSidebarNavigation.jsx index fd75cf1687..072ca62774 100644 --- a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PagesSidebarNavigation.jsx +++ b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/PagesSidebarNavigation.jsx @@ -10,12 +10,13 @@ import { APP_HEADER_HEIGHT } from '../../../AppCanvas/appCanvasConstants'; import OverflowTooltip from '@/_components/OverflowTooltip'; import AppLogo from '@/_components/AppLogo'; import { DarkModeToggle, ToolTip } from '@/_components'; -import { RenderPageAndPageGroup } from './PageGroup'; +import { RenderPage, RenderPageAndPageGroup } from './PageGroup'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import toast from 'react-hot-toast'; import { shallow } from 'zustand/shallow'; import { resolveReferences } from '@/_helpers/utils'; +import { Overlay, Popover } from 'react-bootstrap'; // import useSidebarMargin from './useSidebarMargin'; export const PagesSidebarNavigation = ({ @@ -33,7 +34,7 @@ export const PagesSidebarNavigation = ({ const { moduleId } = useModuleContext(); const { definition: { styles = {}, properties = {} } = {} } = useStore((state) => state.pageSettings) || {}; const selectedVersionName = useStore((state) => state.selectedVersion?.name); - const currentMode = useStore((state) => state.currentMode); + const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode); const selectedEnvironmentName = useStore((state) => state.selectedEnvironment?.name); const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId); const license = useStore((state) => state.license); @@ -265,6 +266,10 @@ export const PagesSidebarNavigation = ({ const headerHidden = isLicensed ? hideHeader : false; const isPagesSidebarHidden = resolveReferences(disableMenu?.value); + if (hideHeader && hideLogo && isPagesSidebarHidden) { + return null; + } + return (
+ + setShowPopover(false)} + rootClose + > + + + {filteredPagesOverflow.map((page, index) => { + return ( + + ); + })} + + + + + )} +
+ ); +}; + export default PagesSidebarNavigation; diff --git a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/style.scss b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/style.scss index 218af45cf0..5956685a3c 100644 --- a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/style.scss +++ b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/style.scss @@ -60,6 +60,7 @@ align-items: center; padding: 7px 8px; margin-top: 2px; + width: 100%; justify-content: space-between; background-color: var(--interactive-default); &.highlight { @@ -112,6 +113,8 @@ line-height: 18px; margin-left: 8px; gap: 6px; + flex-shrink: 0; + margin-right: 6px; } button.edit-page-overlay-toggle{ opacity: 0; @@ -134,13 +137,23 @@ // margin-left: 15px; display: flex; align-items: center; + flex-grow: 1; + flex-shrink: 1; + min-width: 0; + .main-page-icon-wrapper { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + } .page-name{ overflow: hidden; color: var(--slate12); font-size: 14px; font-style: normal; font-weight: 400; - max-width: 246px; + flex-shrink: 1; + min-width: 0; } } @@ -410,13 +423,25 @@ } .licensed-page-option { + pointer-events: unset !important; button { - pointer-events: none; + cursor: default; &:hover { background-color: unset !important; } } } + + .enterprise-feature { + gap: 6px; + svg { + fill: #FCA23F !important; + + path { + fill: #FCA23F !important + } + } + } } #add-new-page-popup { @@ -457,6 +482,17 @@ } } + .form-control.is-invalid { + background-image: none; + } + + input[type='text'], textarea { + border-radius: 6px; + &:focus { + border: 2px solid var(--border-accent-strong); + } + } + .page-events { .page-empty-events { display: flex; diff --git a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageSettings.jsx b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageSettings.jsx index d2491c1220..34a1cd35b0 100644 --- a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageSettings.jsx +++ b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageSettings.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import cx from 'classnames'; import useStore from '@/AppBuilder/_stores/store'; import ArrowLeft from '@/_ui/Icon/solidIcons/ArrowLeft'; @@ -19,7 +19,7 @@ import { RIGHT_SIDE_BAR_TAB } from '../rightSidebarConstants'; import { SortableTree } from './PageMenu/Tree/SortableTree'; import SortableList from '@/_components/SortableList'; import { PageMenuItem } from './PageMenu/PageMenuItem'; -import { get } from 'lodash'; +import { camelCase, get, startCase, toLower, upperFirst } from 'lodash'; import { Button } from '@/components/ui/Button/Button'; import { AddNewPageMenu } from './PageMenu/AddNewPageMenu'; import { AddNewPagePopup } from './PageMenu/AddNewPagePopup'; @@ -45,7 +45,12 @@ export const PageSettings = () => { const isVersionReleased = useStore((state) => state.isVersionReleased); const switchPage = useStore((state) => state.switchPage); const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin); - const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned); + const setRightSidebarOpen = useStore((state) => state.setRightSidebarOpen); + + const handleToggle = () => { + setActiveRightSideBarTab(null); + setRightSidebarOpen(false); + }; const treeRef = useRef(null); const license = useStore((state) => state.license); @@ -79,7 +84,6 @@ export const PageSettings = () => {
{style.type === 'colorSwatches' && ( handleStyleChange(name, value, false)} /> @@ -162,10 +166,12 @@ export const PageSettings = () => {
-
Pages and navigation
-
-
toggleRightSidebarPin()}> - +
+ Pages and navigation +
+
+
+
@@ -174,82 +180,6 @@ export const PageSettings = () => {
- {/* - -
-
-
- -
-
-
-
- { - if (forceCodeBox) { - setForceCodeBox(false); - } else { - setForceCodeBox(true); - } - }} - /> -
- - {!forceCodeBox && ( -
- - pageSettingChanged( - { - disableMenu: { - value: `{{${e.target.checked}}}`, - fxActive: forceCodeBox, - }, - }, - 'properties' - ) - } - /> -
- )} -
-
-
- {forceCodeBox && ( - { - pageSettingChanged( - { - disableMenu: { - value: value, - fxActive: forceCodeBox, - }, - }, - 'properties' - ); - }} - /> - )} -
*/} @@ -287,7 +217,7 @@ const RenderStyles = React.memo(({ pagesMeta, renderCustomStyles }) => { return Object.keys(groupedStyles).map((style) => { const items = [ { - title: `${style}`, + title: `${upperFirst(toLower(startCase(style)))}`, children: Object.entries(groupedStyles[style]).map(([key, value]) => { const defaultValue = pagesMeta.definition.styles[key].value; return { @@ -300,13 +230,51 @@ const RenderStyles = React.memo(({ pagesMeta, renderCustomStyles }) => { }); }); -const AppHeaderMenu = ({ darkMode, pageSettings, pageSettingChanged, licenseValid }) => { +export const AppHeaderMenu = ({ darkMode, pageSettings, pageSettingChanged, licenseValid }) => { const { moduleId } = useModuleContext(); const [appName] = useStore((state) => [state.appStore.modules[moduleId].app.appName], shallow); const { definition: { properties = {} } = {} } = pageSettings ?? {}; const { hideHeader, name, hideLogo } = properties ?? {}; + const [_name, _setName] = useState(name?.trim() ? name : appName); + const [error, setError] = useState(null); + + useEffect(() => { + const newNameValue = name?.trim() ? name : appName; + if (_name !== newNameValue) { + _setName(newNameValue); + } + }, [name, appName]); + + const handleNameChange = (e) => { + const newValue = e.target.value; + _setName(newValue); + setError(null); + }; + + const handleNameBlur = (e) => { + const newValue = e.target.value.trim(); + + if (newValue === '') { + setError('Title cannot be empty.'); + _setName(name?.trim() ? name : appName); + return; + } + + if (newValue.length > 32) { + setError('Title cannot exceed 32 characters.'); + _setName(name?.trim() ? name : appName); + return; + } + + if (newValue !== (name?.trim() ? name : appName)) { + pageSettingChanged({ name: newValue }, 'properties'); + setError(null); + } else { + setError(null); + } + }; return ( <> @@ -335,12 +303,16 @@ const AppHeaderMenu = ({ darkMode, pageSettings, pageSettingChanged, licenseVali
@@ -391,6 +366,8 @@ const NavigationMenu = ({ darkMode, pageSettings, pageSettingChanged }) => { return str.toLowerCase() === 'true'; } + const [selectedStyle, setSelectedStyle] = useState(style); + return ( <>
@@ -430,6 +407,7 @@ const NavigationMenu = ({ darkMode, pageSettings, pageSettingChanged }) => { pageSettingChanged({ position: value }, 'properties'); }} defaultValue={position?.toString()} + style={{ width: '168px' }} > {POSTIONS.map((mode) => ( @@ -444,14 +422,14 @@ const NavigationMenu = ({ darkMode, pageSettings, pageSettingChanged }) => { { setIsReleasing(true); - const { id: versionToBeReleased, name, app_id, appId } = editingVersion; - appsService .releaseVersion(app_id || appId, versionToBeReleased) .then(() => { @@ -39,7 +37,7 @@ export const ReleaseVersionButton = function DeployVersionButton({ onVersionRele setShowConfirmation(false); }) .catch((_error) => { - toast.error('Oops, something went wrong'); + toast.error(`${name} could not be released. Please try again!`); setIsReleasing(false); }); }; diff --git a/frontend/src/Editor/QueryManager/Components/DataSourcePicker.jsx b/frontend/src/Editor/QueryManager/Components/DataSourcePicker.jsx index 11a9dd6d52..765152cbcc 100644 --- a/frontend/src/Editor/QueryManager/Components/DataSourcePicker.jsx +++ b/frontend/src/Editor/QueryManager/Components/DataSourcePicker.jsx @@ -14,6 +14,7 @@ import { useQueryPanelActions } from '@/_stores/queryPanelStore'; import { Tooltip } from 'react-tooltip'; import { canCreateDataSource } from '@/_helpers'; import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils'; import '../queryManager.theme.scss'; function DataSourcePicker({ dataSources, sampleDataSource, staticDataSources, darkMode, globalDataSources }) { @@ -50,7 +51,7 @@ function DataSourcePicker({ dataSources, sampleDataSource, staticDataSources, da navigate(`/${workspaceId}/data-sources`); }; - const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true'; + const workflowsEnabled = isWorkflowsFeatureEnabled(); return ( <> diff --git a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx index d1afd378f4..aef29d8f52 100644 --- a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx +++ b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx @@ -14,8 +14,17 @@ import { DataBaseSources, ApiSources, CloudStorageSources } from '@/modules/comm import { canCreateDataSource } from '@/_helpers'; import './../queryManager.theme.scss'; import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; +import { workflowDefaultSources } from '../constants'; -function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, staticDataSources }) { +function DataSourceSelect({ + isDisabled, + selectRef, + closePopup, + workflowDataSources, + onNewNode, + staticDataSources, + sampleDataSources = [], +}) { const dataSources = useDataSources(); const globalDataSources = useGlobalDataSources(); const sampleDataSource = useSampleDataSource(); @@ -32,6 +41,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc closePopup(); }; + function cleanWord(word) { + return word.replace(/default/g, ''); + } + useEffect(() => { const shouldAddSampleDataSource = !!sampleDataSource; const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter( @@ -132,6 +145,37 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc ...userDefinedSourcesOpts, ]; + // Group sample data sources by kind + const groupedSampleDataSources = + sampleDataSources && sampleDataSources.length > 0 + ? Object.entries(groupBy(sampleDataSources, 'kind')).map(([kind, sources]) => ({ + label: ( +
+ + {dataSourcesKinds.find((dsk) => dsk.kind === kind)?.name || kind} +
+ ), + options: sources.map((source) => ({ + label: ( +
+ {decodeEntities(source.name)} + +
+ ), + value: source.id, + isNested: true, + source, + })), + })) + : []; + const dataSourcesAvailable = [ { label: ( @@ -146,7 +190,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc label: (
{' '} - {source?.name ?? source.kind} + {workflowDefaultSources[cleanWord(source.name)]?.name}
), value: source.name, @@ -154,6 +198,22 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc })), }, ...userDefinedSourcesOpts, + // Sample data sources group header + ...(groupedSampleDataSources.length > 0 + ? [ + { + label: ( +
+ + Sample data sources + +
+ ), + isDisabled: true, + }, + ...groupedSampleDataSources, + ] + : []), ]; const dataSourceList = workflowDataSources && workflowDataSources ? dataSourcesAvailable : DataSourceOptions; diff --git a/frontend/src/Editor/QueryManager/constants.js b/frontend/src/Editor/QueryManager/constants.js index f06125ef3c..e8f7739429 100644 --- a/frontend/src/Editor/QueryManager/constants.js +++ b/frontend/src/Editor/QueryManager/constants.js @@ -106,3 +106,10 @@ export const defaultSources = { runpy: { kind: 'runpy', id: 'runpy', name: 'Run Python code' }, workflows: { kind: 'workflows', id: 'null', name: 'Run Workflow' }, }; + +export const workflowDefaultSources = { + ...defaultSources, + 'If condition': { kind: 'if', id: 'if', name: 'If condition' }, + Response: { kind: 'response', id: 'response', name: 'Response' }, + Loop: { kind: 'loop', id: 'loop', name: 'Loop' }, +}; diff --git a/frontend/src/HomePage/AppCard.jsx b/frontend/src/HomePage/AppCard.jsx index 9613467c95..a3f5aa51e5 100644 --- a/frontend/src/HomePage/AppCard.jsx +++ b/frontend/src/HomePage/AppCard.jsx @@ -24,6 +24,7 @@ export default function AppCard({ canUpdateApp, currentFolder, appType, + ...props }) { const canUpdate = canUpdateApp(app); const [hoverRef, isHovered] = useHover(); @@ -180,9 +181,9 @@ export default function AppCard({

- {decodeEntities(app.name)} + {decodeEntities(app?.name)}

); @@ -196,77 +197,88 @@ export default function AppCard({ } return ( -
-
-
-
-
-
-
- {AppIcon && AppIcon} + +
+
+
+
+
+
+
+ {AppIcon && AppIcon} +
-
-
- {(canCreateApp(app) || canDeleteApp(app) || canUpdateApp(app)) && ( - deleteApp(app)} - exportApp={() => exportApp(app)} - isMenuOpen={setMenuOpen} - popoverVisible={popoverVisible} - setMenuOpen={setMenuOpen} - darkMode={darkMode} - currentFolder={currentFolder} - appType={appType} - appCreationMode={app?.creation_mode || app?.creationMode} - /> - )} +
+ {(canCreateApp(app) || canDeleteApp(app) || canUpdateApp(app)) && ( + deleteApp(app)} + exportApp={() => exportApp(app)} + isMenuOpen={setMenuOpen} + popoverVisible={popoverVisible} + setMenuOpen={setMenuOpen} + darkMode={darkMode} + currentFolder={currentFolder} + appType={appType} + appCreationMode={app?.creation_mode || app?.creationMode} + /> + )} +
-
-
- -
-
- {canUpdate && ( -
- - {updated === 'just now' ? `Edited ${updated}` : `Edited ${updated} ago`} - -
- )} -
-
- {canUpdate && ( -
- - -
+
+ {canUpdate && ( +
+ + {updated === 'just now' ? `Edited ${updated}` : `Edited ${updated} ago`} + +
+ )} +
+
+ {canUpdate && ( +
+ + - -  {t('globals.edit', 'Edit')} - - - -
- )} - {LaunchButton} + + + +
+ )} + {appType !== 'module' && LaunchButton} +
-
+ ); } diff --git a/frontend/src/HomePage/AppList.jsx b/frontend/src/HomePage/AppList.jsx index ae7e458ced..e0669e1dee 100644 --- a/frontend/src/HomePage/AppList.jsx +++ b/frontend/src/HomePage/AppList.jsx @@ -26,8 +26,8 @@ const AppList = (props) => { )} {!props.isLoading && props.meta.total_count > 0 && ( -
-
+
+
{props.apps.map((app) => { return (
diff --git a/frontend/src/HomePage/AppMenu.jsx b/frontend/src/HomePage/AppMenu.jsx index 05281de413..6c2a521c57 100644 --- a/frontend/src/HomePage/AppMenu.jsx +++ b/frontend/src/HomePage/AppMenu.jsx @@ -16,7 +16,6 @@ export const AppMenu = function AppMenu({ popoverVisible, setMenuOpen, appType, - appCreationMode, }) { const { t } = useTranslation(); const isModuleApp = appType === 'module'; @@ -56,9 +55,11 @@ export const AppMenu = function AppMenu({
{canUpdateApp && ( appCreationMode !== 'GIT' && openAppActionModal('rename-app')} + text={t( + 'homePage.appCard.renameApp', + appType === 'workflow' ? 'Rename workflow' : appType === 'module' ? 'Rename module' : 'Rename app' + )} + onClick={() => openAppActionModal('rename-app')} /> )} {canUpdateApp && ( @@ -67,7 +68,7 @@ export const AppMenu = function AppMenu({ onClick={() => openAppActionModal('change-icon')} /> )} - {canCreateApp && ( + {canCreateApp && appType !== 'module' && ( <> )} - {canUpdateApp && canCreateApp && appType !== 'workflow' && !isModuleApp && ( + {canUpdateApp && canCreateApp && appType !== 'workflow' && ( <> openAppActionModal('clone-app')} /> - + )} {canDeleteApp && ( @@ -96,7 +99,9 @@ export const AppMenu = function AppMenu({ text={ appType === 'workflow' ? t('homePage.appCard.deleteWorkflow', 'Delete workflow') - : t('homePage.appCard.deleteApp', 'Delete app') + : appType === 'front-end' + ? t('homePage.appCard.deleteApp', 'Delete app') + : 'Delete module' } customClass="field__danger" onClick={deleteApp} @@ -108,7 +113,7 @@ export const AppMenu = function AppMenu({
} > -
+
- {appType !== 'workflow' && ( -
- + + -
- )} +   + {appType !== 'workflow' + ? t('blankPage.importApplication', 'Import an app') + : t('blankPage.importWorkflow', 'Import a workflow')} + + + +
diff --git a/frontend/src/HomePage/ExportAppModal.jsx b/frontend/src/HomePage/ExportAppModal.jsx index 1ccb7734fc..7d7564bc59 100644 --- a/frontend/src/HomePage/ExportAppModal.jsx +++ b/frontend/src/HomePage/ExportAppModal.jsx @@ -87,6 +87,21 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam // eslint-disable-next-line react-hooks/exhaustive-deps }, [versionId]); + async function autoExportModule() { + try { + exportApp(app, null, versionId, allTables); + } catch (error) { + closeModal(); + } + } + + // Auto-export for modules + useEffect(() => { + if (app.type === 'module' && show && allTables && currentVersion && !loading) { + autoExportModule(); + } + }, [app, show, allTables, currentVersion, loading]); + const exportApp = (app, versionId, exportTjDb, exportTables) => { const appOpts = { app: [ @@ -104,7 +119,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam }; appsService - .exportResource(requestBody) + .exportResource(requestBody, app.type) .then((data) => { const appName = (app.appName || app.name).replace(/\s+/g, '-').toLowerCase(); const fileName = `${appName}-export-${new Date().getTime()}`; @@ -118,16 +133,22 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam document.body.appendChild(link); link.click(); document.body.removeChild(link); + toast.success(`${app?.type === 'module' ? 'Module' : 'App'} has been exported successfully!`); closeModal(); }) .catch((error) => { - toast.error(`Could not export app: ${error.data.message}`, { + toast.error(`Could not export ${app.type === 'module' ? 'module' : 'app'}: ${error.data.message}`, { position: 'top-center', }); closeModal(); }); }; + // Don't render modal for modules - they auto-export + if (app.type === 'module') { + return null; + } + return ( closeModal(false)} diff --git a/frontend/src/HomePage/Folders.jsx b/frontend/src/HomePage/Folders.jsx index 14597d16bc..c2e473eb87 100644 --- a/frontend/src/HomePage/Folders.jsx +++ b/frontend/src/HomePage/Folders.jsx @@ -12,9 +12,11 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import { SearchBox } from '@/_components/SearchBox'; import _ from 'lodash'; import { validateName, handleHttpErrorMessages, getWorkspaceId } from '@/_helpers/utils'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton'; -export const Folders = function Folders({ +import { Button } from '@/components/ui/Button/Button'; + +export const Folders = function Folders ({ folders, foldersLoading, currentFolder, @@ -42,6 +44,7 @@ export const Folders = function Folders({ const [filteredData, setFilteredData] = useState(folders); const [errorText, setErrorText] = useState(''); const navigate = useNavigate(); + const location = useLocation(); const { t } = useTranslation(); const { updateSidebarNAV } = useContext(BreadCrumbContext); @@ -56,15 +59,14 @@ export const Folders = function Folders({ }, [folders]); useEffect(() => { - if (_.isEmpty(currentFolder)) { - updateSidebarNAV(`All ${appType === 'workflow' ? 'workflows' : appType === 'module' ? 'modules' : 'apps'}`); - setActiveFolder({}); - } else { - updateSidebarNAV(currentFolder.name); - setActiveFolder(currentFolder); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentFolder]); + const noFolder = !currentFolder || _.isEmpty(currentFolder); + const label = noFolder + ? `All ${appType === 'workflow' ? 'workflows' : appType === 'module' ? 'modules' : 'apps'}` + : currentFolder.name; + + updateSidebarNAV(label); + setActiveFolder(currentFolder || {}); + }, [appType, currentFolder, location.pathname]); const handleSearch = (e) => { const value = e?.target?.value; @@ -72,7 +74,7 @@ export const Folders = function Folders({ setFilteredData(filtered); }; - function saveFolder() { + function saveFolder () { const newName = newFolderName?.trim(); if (!newName) { setErrorText("Folder name can't be empty"); @@ -97,45 +99,46 @@ export const Folders = function Folders({ } } - function handleFolderChange(folder) { + const getDefaultLabel = () => { + return `All ${appType === 'workflow' ? 'workflows' : appType === 'module' ? 'modules' : 'apps'}`; + }; + + function handleFolderChange (folder) { if (_.isEmpty(folder)) { setActiveFolder({}); } else { setActiveFolder(folder); } folderChanged(folder); - updateSidebarNAV( - folder?.name ?? `All ${appType === 'front-end' ? 'apps' : appType === 'module' ? 'modules' : 'workflows'}` - ); + updateSidebarNAV(updateSidebarNAV(folder?.name ?? getDefaultLabel())); //update the url query parameter with folder name updateFolderQuery(folder?.name); } - function updateFolderQuery(name) { + function updateFolderQuery (name) { const search = `${name ? `?folder=${name}` : ''}`; navigate( { - pathname: `/${getWorkspaceId()}${ - appType === 'workflow' ? '/workflows' : appType === 'module' ? '/modules' : '' - }`, + pathname: `/${getWorkspaceId()}${appType === 'workflow' ? '/workflows' : appType === 'module' ? '/modules' : '' + }`, search, }, { replace: true } ); } - function deleteFolder(folder) { + function deleteFolder (folder) { setShowDeleteConfirmation(true); setDeletingFolder(folder); } - function updateFolder(folder) { + function updateFolder (folder) { setNewFolderName(folder.name); setShowUpdateForm(true); setUpdatingFolder(folder); } - function executeDeletion() { + function executeDeletion () { setDeletionStatus(true); folderService .deleteFolder(deletingFolder.id) @@ -153,12 +156,12 @@ export const Folders = function Folders({ }); } - function cancelDeleteDialog() { + function cancelDeleteDialog () { setShowDeleteConfirmation(false); setDeletingFolder(null); } - function executeEditFolder() { + function executeEditFolder () { const folderName = newFolderName?.trim(); if (folderName === updatingFolder?.name) { setUpdationStatus(false); @@ -209,7 +212,7 @@ export const Folders = function Folders({ showUpdateForm ? setShowUpdateForm(false) : setShowForm(false); }; - function handleClose() { + function handleClose () { setShowInput(false); setFilteredData(folders); } @@ -244,24 +247,36 @@ export const Folders = function Folders({
{canCreateFolder && ( <> -
{ setNewFolderName(''); setShowForm(true); }} data-cy="create-new-folder-button" > - -
-
+ +
+ + )}
@@ -285,8 +300,7 @@ export const Folders = function Folders({ className={cx( `list-group-item border-0 list-group-item-action d-flex align-items-center all-apps-link tj-text-xsm`, { - 'bg-light-indigo': _.isEmpty(activeFolder) && !darkMode, - 'bg-dark-indigo': _.isEmpty(activeFolder) && darkMode, + 'tw-bg-interactive-default': _.isEmpty(activeFolder), } )} style={{ height: '32px' }} @@ -296,9 +310,9 @@ export const Folders = function Folders({ {appType === 'module' ? 'All modules' : t( - `${appType === 'workflow' ? 'workflowsDashboard' : 'homePage'}.foldersSection.allApplications`, - 'All apps' - )} + `${appType === 'workflow' ? 'workflowsDashboard' : 'homePage'}.foldersSection.allApplications`, + 'All apps' + )}
)} @@ -312,8 +326,7 @@ export const Folders = function Folders({ className={cx( `folder-list-group-item rounded-2 list-group-item h-4 mb-1 list-group-item-action no-border d-flex align-items-center`, { - 'bg-light-indigo': activeFolder.id === folder.id && !darkMode, - 'bg-dark-indigo': activeFolder.id === folder.id && darkMode, + 'tw-bg-interactive-default': activeFolder.id === folder.id, } )} onClick={() => { diff --git a/frontend/src/HomePage/Header.jsx b/frontend/src/HomePage/Header.jsx index fa9867f14a..d37e19bbbc 100644 --- a/frontend/src/HomePage/Header.jsx +++ b/frontend/src/HomePage/Header.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { SearchBox } from '@/_components/SearchBox'; +import { SearchBox } from '@/_components/PageSearchBox'; import { useTranslation } from 'react-i18next'; export default function HomeHeader({ onSearchSubmit, darkMode, appType }) { @@ -8,21 +8,21 @@ export default function HomeHeader({ onSearchSubmit, darkMode, appType }) { const placeholderText = page === 'apps' - ? t('globals.searchItem', 'Search apps in this workspace') + ? appType == 'module' + ? 'Search modules in this workspace' + : t('globals.searchItem', 'Search apps in this workspace') : t('globals.workflowsSearchItem', 'Search workflows in this workspace'); return ( -
-
- -
+
+
); } diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index b69ce93e0e..7e3b477798 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -10,9 +10,9 @@ import { licenseService, pluginsService, } from '@/_services'; -import { ConfirmDialog, AppModal } from '@/_components'; +import { ConfirmDialog, AppModal, ToolTip } from '@/_components'; import Select from '@/_ui/Select'; -import _, { sample, isEmpty } from 'lodash'; +import _, { sample, isEmpty, capitalize, has } from 'lodash'; import { Folders } from './Folders'; import { BlankPage } from './BlankPage'; import { toast } from 'react-hot-toast'; @@ -43,12 +43,13 @@ import { ImportAppMenu, AppActionModal, OrganizationList, - UserGroupMigrationBanner, ConsultationBanner, AppTypeTab, } from '@/modules/dashboard/components'; import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt'; import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils'; +import EmptyModuleSvg from '../../assets/images/icons/empty-modules.svg'; const { iconList, defaultIcon } = configs; @@ -150,6 +151,10 @@ class HomePageComponent extends React.Component { this.fetchWorkflowsWorkspaceLimit(); this.fetchOrgGit(); this.setQueryParameter(); + + // Check module access permission + this.props.checkModuleAccess(); + const hasClosedBanner = localStorage.getItem('hasClosedGroupMigrationBanner'); //Only show the banner once @@ -252,22 +257,28 @@ class HomePageComponent extends React.Component { }; getAppType = () => { - return this.props.appType === 'module' ? 'Module' : this.props.appType === 'workflow' ? 'Workflow' : 'App'; + const { appType } = this.props; + if (appType === 'front-end') return 'App'; + if (appType === 'workflow') return 'Workflow'; + if (appType === 'module') return 'Module'; + return 'app'; }; - createApp = async (appName) => { + createApp = async (appName, type, prompt) => { let _self = this; _self.setState({ creatingApp: true }); - try { const data = await appsService.createApp({ icon: sample(iconList), name: appName, type: this.props.appType, + prompt, }); const workspaceId = getWorkspaceId(); - _self.props.navigate(`/${workspaceId}/apps/${data.id}`, { state: { commitEnabled: this.state.commitEnabled } }); - toast.success(`${this.getAppType()} created successfully!`); + _self.props.navigate(`/${workspaceId}/apps/${data.id}`, { + state: { commitEnabled: this.state.commitEnabled, prompt }, + }); + this.props.appType !== 'front-end' && toast.success(`${capitalize(this.getAppType())} created successfully!`); _self.setState({ creatingApp: false }); return true; } catch (errorResponse) { @@ -284,7 +295,7 @@ class HomePageComponent extends React.Component { let _self = this; _self.setState({ renamingApp: true }); try { - await appsService.saveApp(appId, { name: newAppName }); + await appsService.saveApp(appId, { name: newAppName }, this.props.appType); await this.fetchApps(this.state.currentPage, this.state.currentFolder.id); toast.success(`${this.getAppType()} name has been updated!`); _self.setState({ renamingApp: false }); @@ -306,11 +317,14 @@ class HomePageComponent extends React.Component { cloneApp = async (appName, appId) => { this.setState({ isCloningApp: true }); try { - const data = await appsService.cloneResource({ - app: [{ id: appId, name: appName }], - organization_id: this.state.currentUser?.organization_id, - }); - toast.success('App cloned successfully!'); + const data = await appsService.cloneResource( + { + app: [{ id: appId, name: appName }], + organization_id: this.state.currentUser?.organization_id, + }, + this.props.appType + ); + toast.success(`${this.getAppType()} cloned successfully!`); this.props.navigate(`/${getWorkspaceId()}/apps/${data?.imports?.app[0]?.id}`, { state: { commitEnabled: this.state.commitEnabled }, }); @@ -330,6 +344,66 @@ class HomePageComponent extends React.Component { this.setState({ isExportingApp: true, app: app }); }; + exportAppDirectly = async (app) => { + try { + const fetchVersions = await appsService.getVersions(app.id); + const { versions } = fetchVersions; + + const currentEditingVersion = versions?.filter((version) => version?.isCurrentEditingVersion)[0]; + if (!currentEditingVersion) { + toast.error('Could not find current editing version.', { + position: 'top-center', + }); + return; + } + + // Export all TJDB tables used by default + const fetchTables = await appsService.getTables(app.id); + const { tables: allTables } = fetchTables; + + const versionId = currentEditingVersion.id; + const exportTjDb = true; + const exportTables = allTables; + + const appOpts = { + app: [ + { + id: app.id, + search_params: { version_id: versionId }, + }, + ], + }; + + const requestBody = { + ...appOpts, + ...(exportTjDb && { tooljet_database: exportTables }), + organization_id: app.organization_id, + }; + + const data = await appsService.exportResource(requestBody); + + const appName = app.name.replace(/\s+/g, '-').toLowerCase(); + const fileName = `${appName}-export-${new Date().getTime()}`; + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const href = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.download = fileName + '.json'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + toast.success('Workflow exported successfully!', { + position: 'top-center', + }); + } catch (error) { + toast.error(`Could not export workflow: ${error?.data?.message || error.message}`, { + position: 'top-center', + }); + } + }; + readAndImport = (event) => { try { const file = event.target.files[0]; @@ -404,14 +478,24 @@ class HomePageComponent extends React.Component { let installedPluginsInfo = []; try { if (this.state.dependentPlugins.length) { - ({ installedPluginsInfo = [] } = await pluginsService.installDependentPlugins( + ({ installedPluginsInfo =[] } = await pluginsService.installDependentPlugins( this.state.dependentPlugins, true )); } - const data = await appsService.importResource(requestBody); - toast.success('App imported successfully.'); + if (importJSON.app[0].definition.appV2.type !== this.props.appType) { + toast.error( + `${this.props.appType === 'module' ? 'App' : 'Module'} could not be imported in ${this.props.appType === 'module' ? 'modules' : 'apps' + } section. Switch to ${this.props.appType === 'module' ? 'apps' : 'modules'} section and try again.`, + { style: { maxWidth: '425px' } } + ); + this.setState({ isImportingApp: false }); + return; + } + + const data = await appsService.importResource(requestBody, this.props.appType); + toast.success(`${this.props.appType === 'module' ? 'Module' : 'App'} imported successfully.`); this.setState({ isImportingApp: false }); if (!isEmpty(data.imports.app)) { @@ -433,7 +517,7 @@ class HomePageComponent extends React.Component { this.setState({ isImportingApp: false }); if (error.statusCode === 409) return false; - toast.error(error?.error || error?.message || 'App import failed'); + toast.error(error?.error || error?.message || `${capitalize(this.getAppType())} import failed`); } }; @@ -449,7 +533,7 @@ class HomePageComponent extends React.Component { this.state.shouldAutoImportPlugin ); this.setState({ deploying: false }); - toast.success('App created successfully!', { position: 'top-center' }); + toast.success(`${this.getAppType()} created successfully!`, { position: 'top-center' }); this.props.navigate(`/${getWorkspaceId()}/apps/${data.app[0].id}`, { state: { commitEnabled: this.state.commitEnabled }, }); @@ -465,7 +549,7 @@ class HomePageComponent extends React.Component { }; canViewWorkflow = () => { - return this.canUserPerform(this.state.currentUser, 'view'); + return this.canUserPerform(this.state.currentUser, 'view') && isWorkflowsFeatureEnabled(); }; canUserPerform(user, action, app) { @@ -569,8 +653,7 @@ class HomePageComponent extends React.Component { executeAppDeletion = () => { this.setState({ isDeletingApp: true }); appsService - .deleteApp(this.state.appToBeDeleted.id) - // eslint-disable-next-line no-unused-vars + .deleteApp(this.state.appToBeDeleted.id, this.props.appType) .then((data) => { toast.success(`${this.getAppType()} deleted successfully.`); this.fetchApps( @@ -591,7 +674,6 @@ class HomePageComponent extends React.Component { }) .catch(({ error }) => { toast.error('Could not delete the app.'); - console.log(error); }) .finally(() => { this.cancelDeleteAppDialog(); @@ -935,6 +1017,53 @@ class HomePageComponent extends React.Component { importingGitAppOperations: validationMessage, }); }; + + // Helper functions for workflow limit checks + hasWorkflowLimitReached = () => { + const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state; + + const instanceLimitReached = + workflowInstanceLevelLimit.total === 0 || workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total; + const workspaceLimitReached = + workflowWorkspaceLevelLimit.total === 0 || + workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total; + + return instanceLimitReached || workspaceLimitReached; + }; + + hasWorkflowLimitWarning = () => { + const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state; + return this.hasInstanceLimitWarning() || this.hasWorkspaceLimitWarning(); + }; + + hasInstanceLimitWarning = () => { + const { workflowInstanceLevelLimit } = this.state; + const percentage = workflowInstanceLevelLimit.percentage; + + return ( + workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total || + (percentage >= 90 && percentage < 100) || + workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 + ); + }; + + hasWorkspaceLimitWarning = () => { + const { workflowWorkspaceLevelLimit } = this.state; + const percentage = workflowWorkspaceLevelLimit.percentage; + + return ( + workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total || + (percentage >= 90 && percentage < 100) || + workflowWorkspaceLevelLimit.current === workflowWorkspaceLevelLimit.total - 1 + ); + }; + + getWorkflowLimit = () => { + return this.hasInstanceLimitWarning() + ? this.state.workflowInstanceLevelLimit + : this.state.workflowWorkspaceLevelLimit; + }; + render() { const { apps, @@ -985,8 +1114,18 @@ class HomePageComponent extends React.Component { } = this.state; const invalidLicense = featureAccess?.licenseStatus?.isExpired || !featureAccess?.licenseStatus?.isLicenseValid; - // const invalidLicense = false; + const deleteModuleText = + 'This action will permanently delete the module from all connected applications. This cannot be reversed. Confirm deletion?'; + const getDisabledState = () => { + if (this.props.appType === 'module') { + return invalidLicense; + } else if (this.props.appType === 'front-end') { + return appsLimit?.percentage >= 100; + } else { + return this.hasWorkflowLimitReached(); + } + }; const modalConfigs = { create: { modalType: 'create', @@ -1003,11 +1142,12 @@ class HomePageComponent extends React.Component { closeModal: () => this.setState({ showCloneAppModal: false }), processApp: this.cloneApp, show: () => this.setState({ showCloneAppModal: true }), - title: 'Clone app', - actionButton: 'Clone app', + title: `Clone ${this.getAppType().toLocaleLowerCase()}`, + actionButton: `Clone ${this.getAppType().toLocaleLowerCase()}`, actionLoadingButton: 'Cloning', selectedAppId: appOperations?.selectedApp?.id, selectedAppName: appOperations?.selectedApp?.name, + appType: this.props.appType, }, import: { modalType: 'import', @@ -1084,9 +1224,8 @@ class HomePageComponent extends React.Component {
User groups @@ -1159,7 +1298,11 @@ class HomePageComponent extends React.Component {
- {this.canCreateApp() && (
- - {this.props.appType !== 'workflow' && this.props.appType !== 'module' && ( - = 100 || (this.props.appType === 'module' && invalidLicense) - } - split - className="d-inline" - data-cy="import-dropdown-menu" - /> - )} +
)} - + {this.props.appType === 'module' ? ( +
+

+
+ ) : ( + + )} {this.props.appType === 'front-end' && ( )} @@ -1488,8 +1631,8 @@ class HomePageComponent extends React.Component { classes="mb-3 small" limits={ workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total || - 100 > workflowInstanceLevelLimit.percentage >= 90 || - workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 + 100 > workflowInstanceLevelLimit.percentage >= 90 || + workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 ? workflowInstanceLevelLimit : workflowWorkspaceLevelLimit } @@ -1508,19 +1651,17 @@ class HomePageComponent extends React.Component {
-
+
{featuresLoaded && !isLoading ? ( - + <> + + ) : ( !appSearchKey && )} @@ -1532,15 +1673,12 @@ class HomePageComponent extends React.Component { {(meta?.total_count > 0 || appSearchKey) && ( <> {!(isLoading && !appSearchKey) && ( - <> - -
- + )}
{currentFolder?.count ?? meta?.total_count} APPS @@ -1588,23 +1726,36 @@ class HomePageComponent extends React.Component { appType={this.props.appType} workflowsLimit={ workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total || - 100 > workflowInstanceLevelLimit.percentage >= 90 || - workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 + 100 > workflowInstanceLevelLimit.percentage >= 90 || + workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 ? workflowInstanceLevelLimit : workflowWorkspaceLevelLimit } /> ) : ( -

- You have not created any modules.  - + +

Create a module to reuse on the applications.

+ - Create a module  - - to start using it within your apps. -

+ + + + +
))} {!isLoading && apps?.length === 0 && appSearchKey && (
@@ -1621,7 +1772,7 @@ class HomePageComponent extends React.Component { canUpdateApp={this.canUpdateApp} deleteApp={this.deleteApp} cloneApp={this.cloneApp} - exportApp={this.exportApp} + exportApp={this.props.appType === 'workflow' ? this.exportAppDirectly : this.exportApp} meta={meta} currentFolder={currentFolder} isLoading={isLoading || !featuresLoaded} @@ -1668,15 +1819,25 @@ class HomePageComponent extends React.Component { } const withStore = (Component) => (props) => { - const { featureAccess, featuresLoaded } = useLicenseStore( + const { featureAccess, featuresLoaded, hasModuleAccess } = useLicenseStore( (state) => ({ featureAccess: state.featureAccess, featuresLoaded: state.featuresLoaded, + hasModuleAccess: state.hasModuleAccess, }), shallow ); + const { checkModuleAccess } = useLicenseStore((state) => state.actions, shallow); - return ; + return ( + + ); }; export const HomePage = withTranslation()(withStore(withRouter(HomePageComponent))); diff --git a/frontend/src/HomePage/styles/homepage.scss b/frontend/src/HomePage/styles/homepage.scss new file mode 100644 index 0000000000..5f94c9668f --- /dev/null +++ b/frontend/src/HomePage/styles/homepage.scss @@ -0,0 +1,295 @@ +.home-page-sidebar { + height: calc(100vh - 48px) !important; //64 is navbar height + + .folder-list-user { + height: calc(100vh - 116px) !important; //64 is navbar height + 52 px footer + } +} + +.app-list { + margin: 24px 0; +} + +.home-search-holder { + height: 48px; + width: 100%; + margin-top: 32px; +} +.homepage-app-card-list-item-wrap { + column-gap: 24px; + row-gap: 24px; + flex-wrap: wrap; + display: flex; +} + +.homepage-app-card-list-item { + max-width: 272px; + flex-basis: 33%; + padding: 0 !important; +} + +.homepage-dropdown-style { + min-width: 11rem; + display: block; + align-items: center; + margin: 0; + line-height: 1.4285714; + width: 100%; + padding: 0.5rem 0.75rem !important; + font-weight: 400; + white-space: nowrap; + border: 0; + cursor: pointer; + font-size: 12px; +} + +.homepage-dropdown-style:hover { + background: rgba(101, 109, 119, 0.06); +} + +.menu-icon--trigger { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + background-color: var(--background-surface-layer-01); + box-shadow: none; + transition: all 0.15s ease-in-out; + will-change: background-color, box-shadow; + + &:hover { + background-color: var(--background-surface-layer-02); + box-shadow: var(--elevation-000-box-shadow); + } +} + +.home-app-card-header { + margin-bottom: 32px; +} + +.homepage-app-card { + height: 160px; + padding: 16px; + + .app-icon-main { + background: var(--indigo3) !important; + border-radius: 6px !important; + display: flex; + justify-content: center; + align-items: center; + width: 48px; + height: 48px; + will-change: height, width; + transition: all 0.15s ease-in-out; + } + + .appcard-buttons-wrap { + visibility: hidden; + opacity: 0; + height: 0; + } + + .home-app-card-header { + .menu-ico { + visibility: hidden !important; + } + } + + &:hover { + .home-app-card-header { + margin-bottom: 12px; + + .menu-ico { + visibility: visible !important; + } + } + + .app-creation-time-container { + margin-bottom: 0px; + } + + .app-card-name { + margin-bottom: 0px; + } + + .app-creation-time { + // display: none; + } + + .appcard-buttons-wrap { + display: flex; + visibility: visible; + opacity: 1; + padding: 0px; + gap: 12px; + width: 240px; + height: 28px; + flex-direction: row; + transition: all 0.15s ease-in-out; + will-change: opacity, visibility; + + div { + a { + text-decoration: none; + } + } + } + + .app-icon-main { + width: 36px; + height: 36px; + } + } +} + +.home-page-content-container { + max-width: 880px; + + @media only screen and (max-width: 768px) { + margin-bottom: 0rem !important; + + .liner { + width: unset !important; + } + + .app-list { + overflow-y: auto; + height: calc(100vh - 26rem); + + .skeleton-container { + display: flex; + flex-direction: column; + + .col { + display: flex; + justify-content: center; + margin-bottom: 1rem; + } + + .card-skeleton-container { + width: 304px; + } + } + } + + .menu-ico { + display: none !important; + } + } +} + + +.home-page-footer { + height: 52px; + background-color: var(--page-weak) !important; + border-top: 1px solid var(--border-weak) !important; + width: calc(100% - 336px) !important; + + @media only screen and (max-width: 768px) { + position: unset; + width: 100%; + + .col-4, + .col-5 { + display: none; + } + + .pagination-container { + display: flex !important; + align-items: center; + justify-content: center; + } + } +} + +@media only screen and (min-width: 1728px) { + .homepage-app-card-list-item { + // max-width: 304px; + max-width: calc(33.3% - 16px); + + .edit-button, + .launch-button { + width: 129px !important; + } + } + + .home-page-content-container { + max-width: 976px; + } + + .liner { + width: 976px; + } +} + +@media only screen and (min-width: 1584px) and (max-width: 1727px) { + .homepage-app-card-list-item { + max-width: calc(33.3% - 16px); + } + + .edit-button, + .launch-button { + width: 113px !important; + } +} + +@media only screen and (min-width: 1312px) and (max-width: 1583px) { + .homepage-app-card-list-item { + // max-width: 264px; + max-width: calc(33.3% - 16px); + + .edit-button, + .launch-button { + width: 109px !important; + } + } +} + +@media only screen and (min-width: 993px) and (max-width: 1311px) { + .home-page-content-container { + max-width: 568px; + } + + .homepage-app-card-list-item-wrap { + row-gap: 20px; + } + + .homepage-app-card-list-item { + // max-width: 269px; + max-width: calc(50% - 12px); + flex-basis: 50%; + flex-grow: 1; + flex-shrink: 0; + + .edit-button, + .launch-button { + width: 111.5px !important; + } + } + + .liner { + width: 568px; + } +} + +@media only screen and (max-width: 992px) { + .homepage-app-card-list-item-wrap { + display: flex; + justify-content: center; + width: 100%; + gap: 24px; + } + + .homepage-app-card-list-item { + // max-width: 304px !important; + max-width: calc(50% - 12px); + flex-basis: 100%; + + .edit-button, + .launch-button { + width: 129px !important; + } + } +} diff --git a/frontend/src/MarketplacePage/MarketplaceCard.jsx b/frontend/src/MarketplacePage/MarketplaceCard.jsx index 1a2c07cf61..daf6e470f6 100644 --- a/frontend/src/MarketplacePage/MarketplaceCard.jsx +++ b/frontend/src/MarketplacePage/MarketplaceCard.jsx @@ -52,7 +52,7 @@ export const MarketplaceCard = ({ id, name, repo, description, version, isInstal return (
-
+
diff --git a/frontend/src/Routes/AppsRoute.jsx b/frontend/src/Routes/AppsRoute.jsx index b84a2b04ff..9022a77c87 100644 --- a/frontend/src/Routes/AppsRoute.jsx +++ b/frontend/src/Routes/AppsRoute.jsx @@ -77,7 +77,7 @@ export const AppsRoute = ({ children, componentType }) => { const handleBrowserNavigation = (e) => { const { id, handle } = e.state; - switchPage(id, handle, [], true); + switchPage(id, handle, []); }; return {clonedElement}; diff --git a/frontend/src/SettingsPage/SettingsPage.jsx b/frontend/src/SettingsPage/SettingsPage.jsx index 53dfcad2d8..e7768dcf5f 100644 --- a/frontend/src/SettingsPage/SettingsPage.jsx +++ b/frontend/src/SettingsPage/SettingsPage.jsx @@ -169,7 +169,7 @@ function SettingsPage(props) {
-
+

{t('header.profileSettingPage.profile', 'Profile')} @@ -244,8 +244,7 @@ function SettingsPage(props) {

-
-
+

{t('header.profileSettingPage.changePassword', 'Change password')} diff --git a/frontend/src/TooljetDatabase/Forms/TableSchema.jsx b/frontend/src/TooljetDatabase/Forms/TableSchema.jsx index 9bee86278a..e6a2359739 100644 --- a/frontend/src/TooljetDatabase/Forms/TableSchema.jsx +++ b/frontend/src/TooljetDatabase/Forms/TableSchema.jsx @@ -368,6 +368,12 @@ function TableSchema({ isDisabled={ isEditMode && columnDetails[index]?.constraints_type?.is_primary_key === true ? true : false } + classNames={{ + control: (state) => cx({ + '!tw-border-border-default': true, + }), + + }} />

diff --git a/frontend/src/TooljetDatabase/Forms/styles.scss b/frontend/src/TooljetDatabase/Forms/styles.scss index 7bc6fe366c..233850ee4e 100644 --- a/frontend/src/TooljetDatabase/Forms/styles.scss +++ b/frontend/src/TooljetDatabase/Forms/styles.scss @@ -548,7 +548,7 @@ } .empty-foreignkey-container { - border: 1px dashed #d7dbdf; + border: 1px dashed var(--border-default); height: 40px; width: 270px !important; border-radius: 100px !important; diff --git a/frontend/src/TooljetDatabase/Table/Header.jsx b/frontend/src/TooljetDatabase/Table/Header.jsx index 9774822a6f..53e43309fe 100644 --- a/frontend/src/TooljetDatabase/Table/Header.jsx +++ b/frontend/src/TooljetDatabase/Table/Header.jsx @@ -148,8 +148,8 @@ const Header = ({ return ( <>
-
-
+
+
<> diff --git a/frontend/src/TooljetDatabase/Table/styles.scss b/frontend/src/TooljetDatabase/Table/styles.scss index 5f32c6e68d..120148b5d8 100644 --- a/frontend/src/TooljetDatabase/Table/styles.scss +++ b/frontend/src/TooljetDatabase/Table/styles.scss @@ -97,8 +97,8 @@ z-index: 1; position: sticky; left: 66px; - border-right: 2px solid var(--light-slate-08, #C1C8CD); - background-color: white; + border-right: 2px solid var(--border-weak); + background-color: var(--surfaces-surface-01); } th { @@ -145,14 +145,14 @@ th:nth-child(2) { z-index: 2; left: 66px; - border-right: 2px solid var(--light-slate-08, #C1C8CD); + border-right: 2px solid var(--border-weak); } .dark-background { td:nth-child(1), td:nth-child(2) { - background-color: #2B394A; + background-color: var(--surfaces-surface-01); } } @@ -283,26 +283,6 @@ background-color: #2B2F30 !important; } -.empty-table-description { - font-size: 14px !important; - line-height: 20px !important; - margin-top: 5px !important; -} - -.empty-table-container { - display: flex; - align-items: center; - justify-content: center; - height: calc(100% - 95px); -} - -.tjdb-create-new-table { - width: 180px !important; - margin: 0px auto !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; -} .keyPress-actions { diff --git a/frontend/src/TooljetDatabase/TableList/index.jsx b/frontend/src/TooljetDatabase/TableList/index.jsx index 61afc607c3..6686ffb989 100644 --- a/frontend/src/TooljetDatabase/TableList/index.jsx +++ b/frontend/src/TooljetDatabase/TableList/index.jsx @@ -8,6 +8,7 @@ import { ListItem } from '../TableListItem'; import { BreadCrumbContext } from '../../App/App'; import Search from '../Search'; import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { Button } from '@/components/ui/Button/Button'; const List = () => { const { @@ -83,15 +84,23 @@ const List = () => { <> All tables ({filteredTables.length}) -
{ setShowInput(true); }} data-cy="create-new-folder-button" > - -
+ + ) : ( + + diff --git a/frontend/src/WorkflowEditor/LogsPanel/icons/triangle-right.svg b/frontend/src/WorkflowEditor/LogsPanel/icons/triangle-right.svg new file mode 100644 index 0000000000..9f8648e692 --- /dev/null +++ b/frontend/src/WorkflowEditor/LogsPanel/icons/triangle-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/_components/CopyToClipboard/CopyToClipboard.jsx b/frontend/src/_components/CopyToClipboard/CopyToClipboard.jsx index c700ec5d29..0aa8f21cea 100644 --- a/frontend/src/_components/CopyToClipboard/CopyToClipboard.jsx +++ b/frontend/src/_components/CopyToClipboard/CopyToClipboard.jsx @@ -3,7 +3,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; import { toast } from 'react-hot-toast'; import { ToolTip } from '@/_components/ToolTip'; -export const CopyToClipboardComponent = ({ data, callback, useCopyIcon }) => { +export const CopyToClipboardComponent = ({ children, data, callback, useCopyIcon }) => { const [copied, setCopied] = React.useState(false); const dataToCopy = callback(data); const message = 'Copied to clipboard'; @@ -30,12 +30,14 @@ export const CopyToClipboardComponent = ({ data, callback, useCopyIcon }) => { toast.success(message, { position: 'top-center' }); }} > - - {useCopyIcon ? : } - + {children ?? ( + + {useCopyIcon ? : } + + )} ); diff --git a/frontend/src/_components/DarkModeToggle.jsx b/frontend/src/_components/DarkModeToggle.jsx index b5e314f651..ad309322b5 100644 --- a/frontend/src/_components/DarkModeToggle.jsx +++ b/frontend/src/_components/DarkModeToggle.jsx @@ -55,7 +55,8 @@ export const DarkModeToggle = function DarkModeToggle({ springConfig: { mass: 4, tension: 250, friction: 35 }, }; - const { r, transform, cx, cy, opacity } = properties[darkMode ? 'moon' : 'sun']; + const { r, transform, cx, cy, opacity } = + properties[darkMode || (appMode === 'dark' && toggleForCanvas) ? 'moon' : 'sun']; const svgContainerProps = useSpring({ transform, diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index bdc7f71054..dcb0078909 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -253,6 +253,8 @@ const DynamicForm = ({ }) => { const source = schema?.source?.kind; const darkMode = localStorage.getItem('darkMode') === 'true'; + const workspaceConstant = options?.[key]?.workspace_constant; + const isWorkspaceConstant = !!workspaceConstant; if (!options) return; @@ -264,7 +266,7 @@ const DynamicForm = ({ (options?.[key]?.encrypted !== undefined ? options?.[key].encrypted : encrypted) || type === 'password'; return { type, - placeholder: useEncrypted ? '**************' : description, + placeholder: workspaceConstant ? workspaceConstant : useEncrypted ? '**************' : description, className: `form-control${handleToggle(controller)} ${useEncrypted && 'dynamic-form-encrypted-field'}`, style: { marginBottom: '0px !important' }, value: options?.[key]?.value || '', @@ -276,6 +278,7 @@ const DynamicForm = ({ workspaceVariables, workspaceConstants: currentOrgEnvironmentConstants, encrypted: useEncrypted, + isWorkspaceConstant: isWorkspaceConstant, }; } case 'toggle': @@ -509,10 +512,16 @@ const DynamicForm = ({ return; } const isEditing = computedProps[field]['disabled']; + const workspaceConstant = options?.[field]?.workspace_constant; + const isWorkspaceConstant = !!workspaceConstant; + if (isEditing) { - optionchanged(field, ''); + if (isWorkspaceConstant) { + optionchanged(field, workspaceConstant); + } else { + optionchanged(field, ''); + } } else { - //Send old field value if editing mode disabled for encrypted fields const newOptions = { ...options }; const oldFieldValue = selectedDataSource?.['options']?.[field]; if (oldFieldValue) { diff --git a/frontend/src/_components/DynamicFormV2.jsx b/frontend/src/_components/DynamicFormV2.jsx index 6554f43da2..3ea431b67e 100644 --- a/frontend/src/_components/DynamicFormV2.jsx +++ b/frontend/src/_components/DynamicFormV2.jsx @@ -8,7 +8,6 @@ import Headers from '@/_ui/HttpHeaders'; import Toggle from '@/_ui/Toggle'; import InputV3 from '@/_ui/Input-V3'; import { filter, find, isEmpty } from 'lodash'; -import { ButtonSolid } from './AppButton'; import { useGlobalDataSourcesStatus } from '@/_stores/dataSourcesStore'; import { canDeleteDataSource, canUpdateDataSource } from '@/_helpers'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; @@ -206,39 +205,63 @@ const DynamicFormV2 = ({ } const processFields = (fieldsObject) => { - Object.keys(fieldsObject).forEach((key) => { - const field = fieldsObject[key]; - const { widget, encrypted, key: propertyKey } = field; + const processNestedField = (field, propertyKey) => { + const { widget, encrypted } = field; - if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) { - encryptedFieldsProps[propertyKey] = { - disabled: !!selectedDataSource?.id, - }; - } else if (!isDataSourceEditing) { - if (widget === 'password' || encrypted) { - encryptedFieldsProps[propertyKey] = { - disabled: true, - }; - } - } else { - if ((widget === 'password' || encrypted) && !(propertyKey in computedProps)) { + const isEncryptedField = + widget === 'password-v3' || + widget === 'password-v3-textarea' || + widget === 'password' || + encrypted || + encryptedProperties.includes(propertyKey); + + if (isEncryptedField) { + if (computedProps[propertyKey] !== undefined && computedProps[propertyKey].disabled === false) { + encryptedFieldsProps[propertyKey] = { disabled: false }; + } else if (!isDataSourceEditing) { + encryptedFieldsProps[propertyKey] = { disabled: true }; + } else if (!(propertyKey in computedProps)) { encryptedFieldsProps[propertyKey] = { disabled: !!selectedDataSource?.id, }; } } + }; - // To check for nested dropdown-component-flip - if (widget === 'dropdown-component-flip') { - const selectedOption = options?.[field.key]?.value; + Object.keys(fieldsObject).forEach((key) => { + const field = fieldsObject[key]; - if (field.commonFields) { - processFields(field.commonFields); + if (field.key) { + processNestedField(field, field.key); + } + + // Check for nested structures and recursively process them + if (typeof field === 'object') { + if (field.widget === 'dropdown-component-flip') { + const selectedOption = options?.[field.key]?.value; + + if (field.commonFields) { + Object.keys(field.commonFields).forEach((commonKey) => { + const commonField = field.commonFields[commonKey]; + processNestedField(commonField, commonField.key); + }); + } + + if (selectedOption && fieldsObject[selectedOption]) { + processFields(fieldsObject[selectedOption]); + } } - if (selectedOption && fieldsObject[selectedOption]) { - processFields(fieldsObject[selectedOption]); - } + // For other nested objects, recursively process them + Object.keys(field).forEach((subKey) => { + if (typeof field[subKey] === 'object' && field[subKey] !== null) { + if (field[subKey].widget || field[subKey].key) { + processNestedField(field[subKey], field[subKey].key); + } else { + processFields({ [subKey]: field[subKey] }); + } + } + }); } }); }; @@ -264,6 +287,11 @@ const DynamicFormV2 = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedDataSource?.id, options, isDataSourceEditing]); + React.useEffect(() => { + const requiredFields = processAllOfConditions(schema, options); + setConditionallyRequiredProperties(requiredFields); + }, [options, processAllOfConditions, schema, selectedDataSource.id]); + const getElement = (type) => { switch (type) { case 'password': @@ -295,6 +323,8 @@ const DynamicFormV2 = ({ const currentValue = options?.[key]?.value; const skipValidation = (!hasUserInteracted && !showValidationErrors) || (!interactedFields.has(key) && !showValidationErrors); + const workspaceConstant = options?.[key]?.workspace_constant; + const isEditing = computedProps[key] && computedProps[key].disabled === false; const handleOptionChange = (key, value, flag = true) => { if (!hasUserInteracted) { @@ -309,10 +339,10 @@ const DynamicFormV2 = ({ case 'text': case 'textarea': { return { - key, + propertyKey: key, widget, label, - placeholder: isEncrypted ? '**************' : description, + placeholder: workspaceConstant ? workspaceConstant : isEncrypted ? '**************' : description, className: cx('form-control', { 'dynamic-form-encrypted-field': isEncrypted, }), @@ -321,20 +351,20 @@ const DynamicFormV2 = ({ value: currentValue || '', onChange: (e) => optionchanged(key, e.target.value, true), isGDS: true, - workspaceVariables: [], - workspaceConstants: [], encrypted: isEncrypted, onBlur, + workspaceVariables, + workspaceConstants: currentOrgEnvironmentConstants, }; } case 'password-v3': case 'password-v3-textarea': case 'text-v3': { return { - key, + propertyKey: key, widget, label, - placeholder: isEncrypted ? '**************' : description, + placeholder: workspaceConstant ? workspaceConstant : isEncrypted ? '**************' : description, className: cx('form-control', { 'dynamic-form-encrypted-field': isEncrypted, }), @@ -343,8 +373,6 @@ const DynamicFormV2 = ({ value: currentValue || '', onChange: (e) => handleOptionChange(key, e.target.value, true), isGDS: true, - workspaceVariables: [], - workspaceConstants: [], encrypted: isEncrypted, onBlur, isRequired: isRequired, @@ -356,6 +384,10 @@ const DynamicFormV2 = ({ ? { valid: true, message: '' } : { valid: null, message: '' }, // handle optional && encrypted fields isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(), + workspaceVariables, + workspaceConstants: currentOrgEnvironmentConstants, + isEditing: isEditing, + labelDisabled: false, }; } case 'react-component-headers': { @@ -411,11 +443,18 @@ const DynamicFormV2 = ({ if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) { return; } + const isEditing = computedProps[field]['disabled']; + const workspaceConstant = options?.[field]?.workspace_constant; + const isWorkspaceConstant = !!workspaceConstant; + if (isEditing) { - optionchanged(field, ''); + if (isWorkspaceConstant) { + optionchanged(field, workspaceConstant); + } else { + optionchanged(field, ''); + } } else { - //Send old field value if editing mode disabled for encrypted fields const newOptions = { ...options }; const oldFieldValue = selectedDataSource?.['options']?.[field]; if (oldFieldValue) { @@ -425,6 +464,7 @@ const DynamicFormV2 = ({ optionsChanged({ ...newOptions }); } } + setComputedProps({ ...computedProps, [field]: { @@ -511,6 +551,7 @@ const DynamicFormV2 = ({ dataCy={uiProperties[key].key.replace(/_/g, '-')} //to be removed after whole ui is same isHorizontalLayout={isHorizontalLayout} + handleEncryptedFieldsToggle={handleEncryptedFieldsToggle} />
diff --git a/frontend/src/_components/LegalReasonsErrorModal.jsx b/frontend/src/_components/LegalReasonsErrorModal.jsx index 95dc0aead8..c42eef2502 100644 --- a/frontend/src/_components/LegalReasonsErrorModal.jsx +++ b/frontend/src/_components/LegalReasonsErrorModal.jsx @@ -14,9 +14,9 @@ const LegalReasonsErrorModal = ({ body, showFooter = true, toggleModal, + actionButtonAdmin, }) => { const [isOpen, setShowModal] = useState(propShowModal); - const currentUser = authenticationService.currentSessionValue; const handleClose = () => { setShowModal(false); toggleModal && toggleModal(); @@ -63,7 +63,7 @@ const LegalReasonsErrorModal = ({ - {currentUser?.super_admin && ( + {actionButtonAdmin && (