Merge branch 'appdefinition-architecture-revamp' of https://github.com/ToolJet/ToolJet into feat/react-moveable-integration

This commit is contained in:
Johnson Cherian 2023-10-16 09:49:39 +05:30
commit 7308b860dc
82 changed files with 357441 additions and 875 deletions

View file

@ -1,4 +1,4 @@
name: Cypress E2E Test
name: Cypress App-Builder
on:
pull_request_target:
@ -117,94 +117,80 @@ jobs:
name: screenshots
path: cypress-tests/cypress/screenshots
Cypress-Platform:
Cypress-App-builder-Subpath:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-workspace' }}
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-app-builder-subpath' }}
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.3.0
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
- name: Run PosgtreSQL Database Docker Container
run: |
sudo docker network create tooljet
sudo docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_PORT=5432 -d postgres:13
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Install and build dependencies
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
npm cache clean --force
npm install
npm install --prefix server
npm install --prefix frontend
npm run build:plugins
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
- name: Build docker image
run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress
- name: Set up environment variables
run: |
echo "TOOLJET_HOST=http://localhost:8082" >> .env
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=localhost" >> .env
echo "PG_HOST=postgres" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet" >> .env
echo "TOOLJET_DB=tooljet_db" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=localhost" >> .env
echo "TOOLJET_DB_HOST=postgres" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=localhost:3001" >> .env
echo "PGRST_HOST=postgrest" >> .env
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SUB_PATH=/apps/tooljet/" >> .env
echo "NODE_ENV=production" >> .env
echo "SERVE_CLIENT=true" >> .env
- name: Set up database
run: |
npm run --prefix server db:create
npm run --prefix server db:reset
npm run --prefix server db:seed
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
- name: sleep 5
run: sleep 5
- name: Run docker-compose file
run: docker-compose up -d
- name: Run PostgREST Docker Container
run: |
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
-e PGRST_DB_URI="postgres://postgres:postgres@postgres:5432/tooljet" -e PGRST_DB_ANON_ROLE="postgres" -e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
postgrest/postgrest:v10.1.1.20221215
- name: Checking containers
run: docker ps -a
- name: Run plugins compilation in watch mode
run: cd plugins && npm start &
- name: Run the server
run: cd server && npm run start:dev &
- name: Run the client
run: cd frontend && npm start &
- name: docker logs
run: sudo docker logs Tooljet-app
- name: Wait for the server to be ready
run: |
timeout 1500 bash -c '
until curl --silent --fail http://localhost:8082; do
until curl --silent --fail http://localhost:80/apps/tooljet/; do
sleep 5
done'
- name: docker logs
run: sudo docker logs postgrest
- name: Create Cypress environment file
id: create-json
uses: jsdaniell/create-json@1.1.2
@ -213,57 +199,12 @@ jobs:
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Platform
- name: App Builder subpath
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:8082"
config-file: cypress-workspace.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v3
if: always()
with:
name: screenshots
path: cypress-tests/cypress/screenshots
Cypress-Marketplace:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-marketplace' }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.3.0
- name: Checking the PR URL
run: |
timeout 1500 bash -c '
until curl --silent --fail https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com; do
sleep 100
done'
- name: Create Cypress environment file
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Marketplace
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
config-file: cypress-marketplace.config.js
config: "baseUrl=http://localhost:80/apps/tooljet/"
config-file: cypress-app-builder.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v3

View file

@ -0,0 +1,201 @@
name: Cypress Marketplace
on:
pull_request_target:
types: [labeled, unlabeled, closed]
workflow_dispatch:
env:
PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:
Cypress-Marketplace:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-marketplace' }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
- name: Build docker image
run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress
- name: Set up environment variables
run: |
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=postgres" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet_db" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=postgres" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=postgrest" >> .env
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
- name: Run docker-compose file
run: docker-compose up -d
- name: Checking containers
run: docker ps -a
- name: docker logs
run: sudo docker logs Tooljet-app
- name: Wait for the server to be ready
run: |
timeout 1500 bash -c '
until curl --silent --fail http://localhost:80; do
sleep 5
done'
- name: Create Cypress environment file
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Marketplace
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:80"
config-file: cypress-marketplace.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v3
if: always()
with:
name: screenshots
path: cypress-tests/cypress/screenshots
Cypress-Marketplace-Subpath:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-marketplace-subpath' }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
- name: Build docker image
run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress
- name: Set up environment variables
run: |
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=postgres" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet_db" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=postgres" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=postgrest" >> .env
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SUB_PATH=/apps/tooljet/" >> .env
echo "NODE_ENV=production" >> .env
echo "SERVE_CLIENT=true" >> .env
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
- name: Run docker-compose file
run: docker-compose up -d
- name: Checking containers
run: docker ps -a
- name: docker logs
run: sudo docker logs Tooljet-app
- name: Wait for the server to be ready
run: |
timeout 1500 bash -c '
until curl --silent --fail http://localhost:80/apps/tooljet/; do
sleep 5
done'
- name: Create Cypress environment file
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Marketplace subpath
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:80/apps/tooljet/"
config-file: cypress-marketplace.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v3
if: always()
with:
name: screenshots
path: cypress-tests/cypress/screenshots

218
.github/workflows/cypress-platform.yml vendored Normal file
View file

@ -0,0 +1,218 @@
name: Cypress Platform
on:
pull_request_target:
types: [labeled, unlabeled, closed]
workflow_dispatch:
env:
PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:
Cypress-Platform:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-workspace' }}
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.3.0
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
- name: Run PosgtreSQL Database Docker Container
run: |
sudo docker network create tooljet
sudo docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_PORT=5432 -d postgres:13
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Install and build dependencies
run: |
npm cache clean --force
npm install
npm install --prefix server
npm install --prefix frontend
npm run build:plugins
- name: Set up environment variables
run: |
echo "TOOLJET_HOST=http://localhost:8082" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=localhost" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=localhost" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=localhost:3001" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
- name: Set up database
run: |
npm run --prefix server db:create
npm run --prefix server db:reset
npm run --prefix server db:seed
- name: sleep 5
run: sleep 5
- name: Run PostgREST Docker Container
run: |
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
-e PGRST_DB_URI="postgres://postgres:postgres@postgres:5432/tooljet" -e PGRST_DB_ANON_ROLE="postgres" -e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
postgrest/postgrest:v10.1.1.20221215
- name: Run plugins compilation in watch mode
run: cd plugins && npm start &
- name: Run the server
run: cd server && npm run start:dev &
- name: Run the client
run: cd frontend && npm start &
- name: Wait for the server to be ready
run: |
timeout 1500 bash -c '
until curl --silent --fail http://localhost:8082; do
sleep 5
done'
- name: docker logs
run: sudo docker logs postgrest
- name: Create Cypress environment file
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Platform
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:8082"
config-file: cypress-workspace.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v3
if: always()
with:
name: screenshots
path: cypress-tests/cypress/screenshots
Cypress-Platform-subpath:
runs-on: ubuntu-22.04
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-workspace-subpath' }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
- name: Build docker image
run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress
- name: Set up environment variables
run: |
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
echo "PG_DB=tooljet_development" >> .env
echo "PG_USER=postgres" >> .env
echo "PG_HOST=postgres" >> .env
echo "PG_PASS=postgres" >> .env
echo "PG_PORT=5432" >> .env
echo "ENABLE_TOOLJET_DB=true" >> .env
echo "TOOLJET_DB=tooljet_db" >> .env
echo "TOOLJET_DB_USER=postgres" >> .env
echo "TOOLJET_DB_HOST=postgres" >> .env
echo "TOOLJET_DB_PASS=postgres" >> .env
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
echo "PGRST_HOST=postgrest" >> .env
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
echo "SUB_PATH=/apps/tooljet/" >> .env
echo "NODE_ENV=production" >> .env
echo "SERVE_CLIENT=true" >> .env
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
- name: Run docker-compose file
run: docker-compose up -d
- name: Checking containers
run: docker ps -a
- name: docker logs
run: sudo docker logs Tooljet-app
- name: Wait for the server to be ready
run: |
timeout 1500 bash -c '
until curl --silent --fail http://localhost:80/apps/tooljet/; do
sleep 5
done'
- name: Create Cypress environment file
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "cypress.env.json"
json: ${{ secrets.CYPRESS_SECRETS }}
dir: "./cypress-tests"
- name: Platform-subpath
uses: cypress-io/github-action@v5
with:
working-directory: ./cypress-tests
config: "baseUrl=http://localhost:80/apps/tooljet/"
config-file: cypress-workspace.config.js
- name: Capture Screenshots
uses: actions/upload-artifact@v3
if: always()
with:
name: screenshots
path: cypress-tests/cypress/screenshots

View file

@ -1,351 +0,0 @@
name: Render cypress app deploy
on:
pull_request_target:
types: [labeled, unlabeled, closed]
env:
PR_NUMBER: ${{ github.event.number }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
permissions:
pull-requests: write
issues: write
jobs:
create-review-cypress-app:
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'create-review-cypress-app' }}
runs-on: ubuntu-latest
steps:
- name: Create deployment
id: create-deployment
run: |
export RESPONSE=$(curl --request POST \
--url https://api.render.com/v1/services \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'Authorization: Bearer ${{ secrets.RENDER_API_KEY }}' \
--data '
{
"autoDeploy": "yes",
"branch": "${{ env.BRANCH_NAME }}",
"name": "ToolJet PR CYPRESS #${{ env.PR_NUMBER }}",
"notifyOnFail": "default",
"ownerId": "tea-caeo4bj19n072h3dddc0",
"repo": "${{ github.event.pull_request.head.repo.git_url }}",
"slug": "tooljet-pr-cypress-${{ env.PR_NUMBER }}",
"suspended": "not_suspended",
"suspenders": [],
"type": "web_service",
"envVars": [
{
"key": "PG_HOST",
"value": "${{ secrets.RENDER_PG_HOST }}"
},
{
"key": "PG_PORT",
"value": "5432"
},
{
"key": "PG_USER",
"value": "${{ secrets.RENDER_PG_USER }}"
},
{
"key": "PG_PASS",
"value": "${{ secrets.RENDER_PG_PASS }}"
},
{
"key": "PG_DB",
"value": "${{ env.PR_NUMBER }}_cypress"
},
{
"key": "ENABLE_TOOLJET_DB",
"value": "true"
},
{
"key": "TOOLJET_DB",
"value": "${{ env.PR_NUMBER }}_cypress"
},
{
"key": "TOOLJET_DB_HOST",
"value": "${{ secrets.RENDER_PG_HOST }}"
},
{
"key": "TOOLJET_DB_USER",
"value": "${{ secrets.RENDER_PG_USER }}"
},
{
"key": "TOOLJET_DB_PASS",
"value": "${{ secrets.RENDER_PG_PASS }}"
},
{
"key": "TOOLJET_DB_PORT",
"value": "5432"
},
{
"key": "PGRST_DB_URI",
"value": "postgres://${{ secrets.RENDER_PG_USER }}:${{ secrets.RENDER_PG_PASS }}@${{ secrets.RENDER_PG_HOST }}/${{ env.PR_NUMBER }}_cypress"
},
{
"key": "PGRST_HOST",
"value": "127.0.0.1:3000"
},
{
"key": "PGRST_JWT_SECRET",
"value": "r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj"
},
{
"key": "PGRST_LOG_LEVEL",
"value": "info"
},
{
"key": "PORT",
"value": "80"
},
{
"key": "TOOLJET_HOST",
"value": "https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
},
{
"key": "DISABLE_TOOLJET_TELEMETRY",
"value": "true"
},
{
"key": "SMTP_ADDRESS",
"value": "smtp.mailtrap.io"
},
{
"key": "SMTP_DOMAIN",
"value": "smtp.mailtrap.io"
},
{
"key": "SMTP_PORT",
"value": "2525"
},
{
"key": "SMTP_USERNAME",
"value": "${{ secrets.RENDER_SMTP_USERNAME }}"
},
{
"key": "SMTP_PASSWORD",
"value": "${{ secrets.RENDER_SMTP_PASSWORD }}"
},
{
"key": "ENABLE_MARKETPLACE_FEATURE",
"value": "true"
},
{
"key": "SSO_GIT_OAUTH2_CLIENT_ID",
"value": "dummy"
},
{
"key": "SSO_GIT_OAUTH2_CLIENT_SECRET",
"value": "dummy"
},
{
"key": "SSO_GIT_OAUTH2_HOST",
"value": "dummy"
},
{
"key": "SSO_GOOGLE_OAUTH2_CLIENT_ID",
"value": "dummy"
}
],
"serviceDetails": {
"disk": null,
"env": "docker",
"envSpecificDetails": {
"dockerCommand": "",
"dockerContext": "./",
"dockerfilePath": "./docker/preview.Dockerfile"
},
"healthCheckPath": "/api/health",
"numInstances": 1,
"openPorts": [{
"port": 80,
"protocol": "TCP"
}],
"plan": "pro",
"pullRequestPreviewsEnabled": "no",
"region": "oregon",
"url": "https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
}
}')
echo "response: $RESPONSE"
export SERVICE_ID=$(echo $RESPONSE | jq -r '.service.id')
echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
- name: Comment deployment URL
uses: actions/github-script@v5
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Deployment: https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com \n Dashboard: https://dashboard.render.com/web/${{ env.SERVICE_ID }}'
})
- uses: actions/github-script@v6
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'create-review-cypress-app'
})
} catch (e) {
console.log(e)
}
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['active-review-cypress-app']
})
destroy-review-cypress-app:
if: ${{ (github.event.action == 'labeled' && github.event.label.name == 'destroy-review-cypress-app') || github.event.action == 'closed' }}
runs-on: ubuntu-latest
steps:
- name: Delete service
run: |
export SERVICE_ID=$(curl --request GET \
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20CYPRESS%20%23${{ env.PR_NUMBER }}&limit=1' \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
jq -r '.[0].service.id')
curl --request DELETE \
--url https://api.render.com/v1/services/$SERVICE_ID \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}'
- uses: actions/github-script@v6
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'destroy-review-cypress-app'
})
} catch (e) {
console.log(e)
}
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'suspend-review-cypress-app'
})
} catch (e) {
console.log(e)
}
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'active-review-cypress-app'
})
} catch (e) {
console.log(e)
}
- name: Install PostgreSQL client
run: |
sudo apt update
sudo apt install postgresql-client -y
- name: Wait after installing PostgreSQL
run: sleep 25
- name: Drop PostgreSQL PR database
env:
PGHOST: ${{ secrets.RENDER_DS_PG_HOST }}
PGPORT: 5432
PGUSER: ${{ secrets.RENDER_DS_PG_USER }}
PGDATABASE: ${{ env.PR_NUMBER }}_cypress
run: |
PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -c "drop database \"$PGDATABASE\" ;"
suspend-review-cypress-app:
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'suspend-review-cypress-app' }}
runs-on: ubuntu-latest
steps:
- name: Suspend service
run: |
export SERVICE_ID=$(curl --request GET \
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20CYPRESS%20%23${{ env.PR_NUMBER }}&limit=1' \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
jq -r '.[0].service.id')
curl --request POST \
--url https://api.render.com/v1/services/$SERVICE_ID/suspend \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}'
- uses: actions/github-script@v6
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'active-review-cypress-app'
})
} catch (e) {
console.log(e)
}
resume-review-cypress-app:
if: ${{ github.event.action == 'unlabeled' && github.event.label.name == 'suspend-review-cypress-app' }}
runs-on: ubuntu-latest
steps:
- name: Resume service
run: |
export SERVICE_ID=$(curl --request GET \
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20CYPRESS%20%23${{ env.PR_NUMBER }}&limit=1' \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
jq -r '.[0].service.id')
curl --request POST \
--url https://api.render.com/v1/services/$SERVICE_ID/resume \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}'
- uses: actions/github-script@v6
with:
script: |
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['active-review-cypress-app']
})
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'suspend-review-cypress-app'
})
} catch (e) {
console.log(e)
}

View file

@ -1,45 +0,0 @@
name: Label for stale render cypress app deploy
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
permissions:
issues: write
jobs:
label-stale-deploys:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: akshaysasidrn/stale-label-fetch@v1.1
id: stale-label
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
stale-label: 'active-review-cypress-app'
stale-time: '86400'
type: 'pull_request'
- name: Get stale numbers
run: echo "Matched PR numbers - ${{ steps.stale-label.outputs.stale-numbers }}"
- name: Add suspend label
uses: actions/github-script@v6
env:
STALE_NUMBERS: ${{ steps.stale-label.outputs.stale-numbers }}
with:
github-token: ${{ secrets.TJ_BOT_PAT }}
script: |
if (!process.env.STALE_NUMBERS) return
const prNumbers = process.env.STALE_NUMBERS.split(",")
console.log(`Adding suspend labels for: ${prNumbers}`)
for (const prNumber of prNumbers) {
github.rest.issues.addLabels({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['suspend-review-cypress-app']
})
}

View file

@ -1 +1 @@
2.19.2
2.20.1

View file

@ -0,0 +1,79 @@
---
id: saml
title: SAML
---
ToolJet supports SAML authentication for your workspace. The supported SAML providers are: Okta, Active Directory Federation Services, Azure AD, Auth0 and other SAML SSO providers.
### Configuring SAML
To enable SAML authentication, you need to configure the following workspace settings:
1. Go to **Workspace Settings** > **SSO** > **SAML**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/workspaceset.png" alt="SSO :SAMP" />
</div>
2. By default, SAML is disabled. Toggle it on to enable SAML authentication.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/enable.png" alt="SSO :SAMP" />
</div>
3. Enter the following configuration details:
- **SAML Provider Name**: Enter the name of your SAML provider. This name will be displayed on the login page.
- **Identity provider metadata**: Upload the data from the metadata file provided by your SAML provider. This file contains the SAML configuration details.
- **Group Attribute**: Enter the name of the attribute that contains the group information of the user. This attribute is used to map the user to the appropriate group.
- **Redirect URL**: Copy the redirect URL provided and paste it in the SAML provider's configuration page.
:::tip Downloading the metadata from your identity provider
Generally, the metadata is available in the form of an XML file which can be downloaded from your identity provider's dashboard.
Copy the metadata from the XML file and paste it into the ToolJet's SAML SSO configuration settings. Please ensure that the metadata is pasted in the correct format, as it contains essential configuration details from the identity provider necessary for authentication.
Additionally, you can often find this data by navigating to https://&ltyour-identity-provider&gt/federationmetadata/2007-06/federationmetadata.xml
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/config.png" alt="SSO :SAMP" />
</div>
4. Once configured, click **Save Changes**.
### Logging in with SAML
1. Go to the **[General Settings](/docs/user-authentication/general-settings)** and copy the **Login URL** provided. Furthermore, you have the flexibility to choose whether to turn on 'Enable Signups,' allowing users to signup without an invite. Through SSO authentication, we check if the user already exists; if so, they can sign in seamlessly. Otherwise, an error will be displayed. Conversely, with this option disabled, only invited users can log in, provided SSO authentication is successful.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/url.png" alt="SSO :SAML"/>
</div>
2. The **Login URL** obtained can be used to access the workspace. Please note that ToolJet supports SAML login at the workspace level, ensuring users are logged in specifically to the selected workspace.
As a result, users can now log in to your workspace using the provided Login URL. The login page will prominently feature the name of the SAML provider configured in your workspace settings.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/login.png" alt="SSO :SAMP" />
</div>
3. Click on **Sign in with `SAML Name`** button and you will be redirected to the SAML provider's login page.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/auth.png" alt="SSO :SAMP" />
</div>
4. Enter your credentials and click **Login**. If the user is signing in for the first time, they will be redirected to the ToolJet's onboarding page.

View file

@ -271,6 +271,7 @@ const sidebars = {
],
},
'user-authentication/sso/ldap',
'user-authentication/sso/saml',
],
},
],

BIN
docs/static/img/sso/saml/auth.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
docs/static/img/sso/saml/config.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

BIN
docs/static/img/sso/saml/enable.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
docs/static/img/sso/saml/login.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
docs/static/img/sso/saml/url.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -0,0 +1,79 @@
---
id: saml
title: SAML
---
ToolJet supports SAML authentication for your workspace. The supported SAML providers are: Okta, Active Directory Federation Services, Azure AD, Auth0 and other SAML SSO providers.
### Configuring SAML
To enable SAML authentication, you need to configure the following workspace settings:
1. Go to **Workspace Settings** > **SSO** > **SAML**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/workspaceset.png" alt="SSO :SAMP" />
</div>
2. By default, SAML is disabled. Toggle it on to enable SAML authentication.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/enable.png" alt="SSO :SAMP" />
</div>
3. Enter the following configuration details:
- **SAML Provider Name**: Enter the name of your SAML provider. This name will be displayed on the login page.
- **Identity provider metadata**: Upload the data from the metadata file provided by your SAML provider. This file contains the SAML configuration details.
- **Group Attribute**: Enter the name of the attribute that contains the group information of the user. This attribute is used to map the user to the appropriate group.
- **Redirect URL**: Copy the redirect URL provided and paste it in the SAML provider's configuration page.
:::tip Downloading the metadata from your identity provider
Generally, the metadata is available in the form of an XML file which can be downloaded from your identity provider's dashboard.
Copy the metadata from the XML file and paste it into the ToolJet's SAML SSO configuration settings. Please ensure that the metadata is pasted in the correct format, as it contains essential configuration details from the identity provider necessary for authentication.
Additionally, you can often find this data by navigating to https://&ltyour-identity-provider&gt/federationmetadata/2007-06/federationmetadata.xml
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/config.png" alt="SSO :SAMP" />
</div>
4. Once configured, click **Save Changes**.
### Logging in with SAML
1. Go to the **[General Settings](/docs/user-authentication/general-settings)** and copy the **Login URL** provided. Furthermore, you have the flexibility to choose whether to turn on 'Enable Signups,' allowing users to signup without an invite. Through SSO authentication, we check if the user already exists; if so, they can sign in seamlessly. Otherwise, an error will be displayed. Conversely, with this option disabled, only invited users can log in, provided SSO authentication is successful.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/url.png" alt="SSO :SAML"/>
</div>
2. The **Login URL** obtained can be used to access the workspace. Please note that ToolJet supports SAML login at the workspace level, ensuring users are logged in specifically to the selected workspace.
As a result, users can now log in to your workspace using the provided Login URL. The login page will prominently feature the name of the SAML provider configured in your workspace settings.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/login.png" alt="SSO :SAMP" />
</div>
3. Click on **Sign in with `SAML Name`** button and you will be redirected to the SAML provider's login page.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/sso/saml/auth.png" alt="SSO :SAMP" />
</div>
4. Enter your credentials and click **Login**. If the user is signing in for the first time, they will be redirected to the ToolJet's onboarding page.

View file

@ -260,7 +260,8 @@
"user-authentication/sso/openid/google-openid"
]
},
"user-authentication/sso/ldap"
"user-authentication/sso/ldap",
"user-authentication/sso/saml"
]
}
]
@ -421,4 +422,4 @@
]
}
]
}
}

View file

@ -1 +1 @@
2.19.2
2.20.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

View file

@ -1,14 +1,13 @@
import React, { useEffect } from 'react';
import { withTranslation } from 'react-i18next';
import { appService, organizationService, authenticationService } from '@/_services';
import { Editor } from '../Editor/Editor';
import { Editor, EditorFunc } from '@/Editor';
import { RealtimeEditor } from '@/Editor/RealtimeEditor';
import config from 'config';
import { safelyParseJSON, stripTrailingSlash, redirectToDashboard, getSubpath, getWorkspaceId } from '@/_helpers/utils';
import { toast } from 'react-hot-toast';
import { useParams } from 'react-router-dom';
import { useAppDataActions } from '@/_stores/appDataStore';
import Spinner from '@/_ui/Spinner';
import _ from 'lodash';
const AppLoaderComponent = (props) => {
@ -24,7 +23,7 @@ const AppLoaderComponent = (props) => {
const loadAppDetails = () => {
appService
.getApp(appId, 'edit')
.fetchApp(appId, 'edit')
.then((data) => {
setShouldLoadApp(true);
updateState({
@ -78,12 +77,12 @@ const AppLoaderComponent = (props) => {
}
};
if (!shouldLoadApp) return <Spinner />;
if (!shouldLoadApp) return <></>;
return config.ENABLE_MULTIPLAYER_EDITING ? (
<RealtimeEditor {...props} shouldLoadApp={shouldLoadApp} />
) : (
<Editor {...props} shouldLoadApp={shouldLoadApp} />
<EditorFunc {...props} shouldLoadApp={shouldLoadApp} />
);
};

View file

@ -44,7 +44,7 @@ export const AppVersionsManager = function ({ appId, setAppDefinitionFromVersion
const selectVersion = (id) => {
appVersionService
.getOne(appId, id)
.getAppVersionData(appId, id)
.then((data) => {
const isCurrentVersionReleased = data.currentVersionId ? true : false;
setAppDefinitionFromVersion(data, isCurrentVersionReleased);
@ -83,7 +83,7 @@ export const AppVersionsManager = function ({ appId, setAppDefinitionFromVersion
resetDeleteModal();
});
};
//this
const options = appVersions.map((appVersion) => ({
value: appVersion.id,
isReleasedVersion: appVersion.id === releasedVersionId,

View file

@ -93,7 +93,7 @@ export function CodeHinter({
};
const currentState = useCurrentState();
const [realState, setRealState] = useState(currentState);
const [currentValue, setCurrentValue] = useState(initialValue);
const [currentValue, setCurrentValue] = useState('');
const [prevCurrentValue, setPrevCurrentValue] = useState(null);
const [resolvedValue, setResolvedValue] = useState(null);
@ -120,6 +120,17 @@ export function CodeHinter({
const { variablesExposedForPreview } = useContext(EditorContext);
const prevCountRef = useRef(false);
useEffect(() => {
setCurrentValue(initialValue);
return () => {
setPrevCurrentValue(null);
setResolvedValue(null);
setResolvingError(null);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (_currentState) {
setRealState(_currentState);
@ -127,7 +138,7 @@ export function CodeHinter({
setRealState(currentState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState.components, _currentState]);
}, [JSON.stringify({ currentState, _currentState })]);
useEffect(() => {
const handleClickOutside = (event) => {
@ -149,7 +160,7 @@ export function CodeHinter({
}, [wrapperRef, isFocused, isPreviewFocused, currentValue, prevCountRef, isOpen]);
useEffect(() => {
if (JSON.stringify(currentValue) !== JSON.stringify(prevCurrentValue)) {
if (enablePreview && isFocused && JSON.stringify(currentValue) !== JSON.stringify(prevCurrentValue)) {
const customResolvables = getCustomResolvables();
const [preview, error] = resolveReferences(currentValue, realState, null, customResolvables, true, true);
setPrevCurrentValue(currentValue);
@ -162,13 +173,8 @@ export function CodeHinter({
setResolvedValue(preview);
}
}
return () => {
setPrevCurrentValue(null);
setResolvedValue(null);
setResolvingError(null);
};
}, [JSON.stringify({ currentValue, realState })]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ currentValue, realState, isFocused })]);
function valueChanged(editor, onChange, ignoreBraces) {
if (editor.getValue()?.trim() !== currentValue) {

View file

@ -2,7 +2,7 @@ import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
import React from 'react';
const ClientServerSwitch = ({ value, onChange, cyLabel, meta, paramName }) => {
const ClientServerSwitch = ({ value, onChange, meta }) => {
const options = meta?.options;
const defaultValue = value ? 'serverSide' : 'clientSide';
const handleChange = (_value) => {

View file

@ -26,11 +26,12 @@ export const Item = React.memo(
isFirstItem = false,
setShowModal = () => {},
cardDataAsObj = {},
setLastSelectedCard,
...props
},
ref
) => {
const { id, component, containerProps, fireEvent, setExposedVariable, darkMode } = kanbanProps;
const { id, component, containerProps, fireEvent, darkMode, setExposedVariable } = kanbanProps;
useEffect(() => {
if (!dragOverlay) {
return;
@ -61,6 +62,7 @@ export const Item = React.memo(
)
return;
setExposedVariable('lastSelectedCard', cardDataAsObj[value]);
setLastSelectedCard(cardDataAsObj[value]);
setShowModal(true);
fireEvent('onCardSelected');
}}

View file

@ -37,11 +37,10 @@ const dropAnimation = {
const TRASH_ID = 'void';
export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
const { properties, fireEvent, setExposedVariable, setExposedVariables, exposedVariables, styles } = kanbanProps;
const { lastSelectedCard = {} } = exposedVariables;
const { properties, fireEvent, setExposedVariable, setExposedVariables, styles } = kanbanProps;
const { columnData, cardData, cardWidth, cardHeight, showDeleteButton, enableAddCard } = properties;
const { accentColor } = styles;
const [lastSelectedCard, setLastSelectedCard] = useState({});
// eslint-disable-next-line react-hooks/exhaustive-deps
const columnDataAsObj = useMemo(() => convertArrayToObj(columnData), [JSON.stringify(columnData)]);
@ -85,7 +84,6 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
useEffect(() => {
droppableItemsColumnId.current = containers.find((container) => items[container]?.length > 0);
}, [items, containers]);
useEffect(() => {
setExposedVariable('updateCardData', async function (cardId, value) {
if (cardDataAsObj[cardId] === undefined) return toast.error('Card not found');
@ -105,9 +103,10 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
updatedCardData: getData(cardDataAsObj),
});
fireEvent('onUpdate');
} else {
setExposedVariable('updatedCardData', getData(cardDataAsObj));
fireEvent('onUpdate');
}
setExposedVariable('updatedCardData', getData(cardDataAsObj));
fireEvent('onUpdate');
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastSelectedCard, JSON.stringify(cardDataAsObj)]);
@ -149,7 +148,6 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
...items,
[columnId]: [...items[columnId], cardDetails.id],
}));
setExposedVariables({ lastAddedCard: { ...cardDetails }, updatedCardData: getData(cardDataAsObj) });
fireEvent('onCardAdded');
});
@ -371,6 +369,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
isFirstItem={index === 0 && droppableItemsColumnId.current === columnId}
setShowModal={setShowModal}
cardDataAsObj={cardDataAsObj}
setLastSelectedCard={setLastSelectedCard}
/>
);
})}
@ -427,6 +426,7 @@ function SortableItem({
isFirstItem,
setShowModal,
cardDataAsObj,
setLastSelectedCard,
}) {
const { setNodeRef, setActivatorNodeRef, listeners, isDragging, isSorting, transform, transition } = useSortable({
id,
@ -451,6 +451,7 @@ function SortableItem({
isFirstItem={isFirstItem}
setShowModal={setShowModal}
cardDataAsObj={cardDataAsObj}
setLastSelectedCard={setLastSelectedCard}
/>
);
}

View file

@ -6,6 +6,7 @@ import { useCurrentState } from '@/_stores/currentStateStore';
export const BoardContext = React.createContext({});
// This one is deprecated and not deleted to support backward compatibility
export const KanbanBoard = ({
id,
height,

View file

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
export const Pagination = function Pagination({
onPageIndexChanged,
@ -16,6 +17,7 @@ export const Pagination = function Pagination({
// eslint-disable-next-line no-unused-vars
darkMode,
tableWidth,
loadingState,
}) {
const [pageCount, setPageCount] = useState(autoPageCount);
@ -50,6 +52,16 @@ export const Pagination = function Pagination({
gotoPage(pageIndex - 1);
}
if (loadingState) {
return (
<div className="w-100">
<SkeletonTheme baseColor="var(--slate3)" width="100%">
<Skeleton count={1} width={'100%'} height={28} className="mb-1" />
</SkeletonTheme>
</div>
);
}
return (
<div className="pagination-container d-flex h-100 align-items-center custom-gap-4" data-cy="pagination-section">
<div className="d-flex">

View file

@ -288,7 +288,7 @@ export function Table({
function getExportFileBlob({ columns, fileType, fileName }) {
let headers = columns.map((column) => {
return { exportValue: String(column.exportValue), key: column.key ? String(column.key) : column.key };
return { exportValue: String(column?.exportValue), key: column.key ? String(column.key) : column?.key };
});
let data = globalFilteredRows.map((row) => {
return headers.reduce((accumulator, header) => {
@ -412,7 +412,7 @@ export function Table({
columnData = useMemo(
() =>
columnData.filter((column) => {
if (resolveReferences(column.columnVisibility, currentState)) {
if (resolveReferences(column?.columnVisibility, currentState)) {
return column;
}
}),
@ -457,7 +457,7 @@ export function Table({
return wrapOption?.textWrap;
};
const optionsData = columnData.map((column) => column.columnOptions?.selectOptions);
const optionsData = columnData.map((column) => column?.columnOptions?.selectOptions);
const columns = useMemo(
() => {
return [...leftActionsCellData, ...columnData, ...rightActionsCellData];
@ -917,7 +917,7 @@ export function Table({
</div>
{allColumns.map(
(column) =>
typeof column.Header === 'string' && (
typeof column?.Header === 'string' && (
<div key={column.id}>
<div>
<label className="dropdown-item d-flex cursor-pointer">
@ -1547,15 +1547,7 @@ export function Table({
))}
</div>
<div className={`col d-flex justify-content-center h-100 ${loadingState && 'w-100'}`}>
{loadingState && (
<div className="w-100">
<SkeletonTheme baseColor="var(--slate3)" width="100%">
<Skeleton count={1} width={'100%'} height={28} className="mb-1" />
</SkeletonTheme>
</div>
)}
{enablePagination && !loadingState && (
{enablePagination && (
<Pagination
lastActivePageIndex={pageIndex}
serverSide={serverSidePagination}
@ -1570,6 +1562,7 @@ export function Table({
enablePrevButton={enablePrevButton}
darkMode={darkMode}
tableWidth={width}
loadingState={loadingState}
/>
)}
</div>

View file

@ -29,8 +29,10 @@ export default function generateColumnsData({
tableColumnEvents,
}) {
return columnProperties.map((column) => {
const columnSize = columnSizes[column.id] || columnSizes[column.name];
const columnType = column.columnType;
if (!column) return;
const columnSize = columnSizes[column?.id] || columnSizes[column?.name];
const columnType = column?.columnType;
let sortType = 'alphanumeric';
const columnOptions = {};

View file

@ -843,7 +843,7 @@ export const Container = ({
our&nbsp;
<a
className="color-indigo9 "
href="https://docs.tooljet.com/docs#the-very-quick-quickstart"
href="https://docs.tooljet.com/docs/#quickstart-guide"
target="_blank"
rel="noreferrer"
>

View file

@ -5,6 +5,7 @@ import {
appVersionService,
orgEnvironmentVariableService,
appEnvironmentService,
orgEnvironmentConstantService,
} from '@/_services';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
@ -123,7 +124,7 @@ const EditorComponent = (props) => {
const [currentPageId, setCurrentPageId] = useState(null);
const [zoomLevel, setZoomLevel] = useState(1);
const [isQueryPaneDragging, setIsQueryPaneDragging] = useState(false);
const [isQueryPaneExpanded, setIsQueryPaneExpanded] = useState(false);
const [isQueryPaneExpanded, setIsQueryPaneExpanded] = useState(false); //!check where this is used
const [selectionInProgress, setSelectionInProgress] = useState(false);
const [hoveredComponent, setHoveredComponent] = useState(null);
const [editorMarginLeft, setEditorMarginLeft] = useState(0);
@ -156,15 +157,13 @@ const EditorComponent = (props) => {
useEffect(() => {
updateState({ isLoading: true });
// 1. Get the current session and current user from the authentication service
const currentSession = authenticationService.currentSessionValue;
const currentUser = currentSession?.current_user;
// 2. Subscribe to changes in the current session using RxJS observable pattern
// Subscribe to changes in the current session using RxJS observable pattern
const subscription = authenticationService.currentSession.subscribe((currentSession) => {
// 3. Check if the current user and group permissions are available
if (currentUser && currentSession?.group_permissions) {
// 4. Prepare user details in a format suitable for the application
const userVars = {
email: currentUser.email,
firstName: currentUser.first_name,
@ -227,10 +226,17 @@ const EditorComponent = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]);
useEffect(
() => {
const components = appDefinition?.pages?.[currentPageId]?.components || {};
computeComponentState(components);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentPageId]
);
useEffect(() => {
// This effect runs when lastKeyPressTimestamp changes
// You can place your database update logic here
// Ensure that you only update the database if the timestamp is recent
if (Date.now() - lastKeyPressTimestamp < 500) {
updateEditorState({
isUpdatingEditorStateInProcess: true,
@ -289,6 +295,21 @@ const EditorComponent = (props) => {
});
};
const fetchOrgEnvironmentConstants = () => {
//! for @ee: get the constants from `getConstantsFromEnvironment ` -- '/organization-constants/:environmentId'
orgEnvironmentConstantService.getAll().then(({ constants }) => {
const orgConstants = {};
constants.map((constant) => {
const constantValue = constant.values.find((value) => value.environmentName === 'production')['value'];
orgConstants[constant.name] = constantValue;
});
useCurrentStateStore.getState().actions.setCurrentState({
constants: orgConstants,
});
});
};
const initComponentVersioning = () => {
updateEditorState({
canUndo: false,
@ -327,6 +348,7 @@ const EditorComponent = (props) => {
});
};
//! websocket events do not work
const initEventListeners = () => {
socket?.addEventListener('message', (event) => {
const data = event.data.replace(/^"(.+(?="$))"$/, '$1');
@ -343,9 +365,9 @@ const EditorComponent = (props) => {
window.addEventListener('message', handleMessage);
await fetchApp(props.params.pageHandle, true);
await fetchApps(0);
await fetchOrgEnvironmentVariables();
await fetchOrgEnvironmentConstants();
initComponentVersioning();
initRealtimeSave();
initEventListeners();
@ -469,7 +491,6 @@ const EditorComponent = (props) => {
const handleQueryPaneDragging = (bool) => setIsQueryPaneDragging(bool);
const handleQueryPaneExpanding = (bool) => setIsQueryPaneExpanded(bool);
//! Needs attention
const handleOnComponentOptionChanged = (component, optionName, value) => {
return onComponentOptionChanged(component, optionName, value);
};
@ -631,15 +652,8 @@ const EditorComponent = (props) => {
appDefinitionChanged(newAppDefinition, {
globalSettings: true,
});
// props.ymap?.set('appDef', {
// newDefinition: appDefinition,
// editingVersionId: props.editingVersion?.id,
// });
// autoSave();
};
//!--------
const callBack = async (data, startingPageHandle, versionSwitched = false) => {
setWindowTitle(data.name);
useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version);
@ -698,28 +712,19 @@ const EditorComponent = (props) => {
await handleEvent('onPageLoad', currentPageEvents);
};
//****** */
const fetchApp = async (startingPageHandle, onMount = false) => {
const _appId = props?.params?.id;
if (!onMount) {
await appService.getApp(_appId).then((data) => callBack(data, startingPageHandle));
await appService.fetchApp(_appId).then((data) => callBack(data, startingPageHandle));
} else {
callBack(app, startingPageHandle);
}
};
// !--------
const setAppDefinitionFromVersion = (appData, isCurrentVersionReleased = true) => {
const setAppDefinitionFromVersion = (appData) => {
const version = appData?.editing_version?.id;
if (version?.id !== editingVersion?.id) {
// !Need to fix this
// appDefinitionChanged(defaults(version.definition, defaultDefinition(props.darkMode)), {
// skipAutoSave: true,
// skipYmapUpdate: true,
// versionChanged: true,
// });
if (version?.id === currentVersionId) {
updateEditorState({
canUndo: false,
@ -732,7 +737,6 @@ const EditorComponent = (props) => {
});
callBack(appData, null, true);
initComponentVersioning();
}
};
@ -756,7 +760,6 @@ const EditorComponent = (props) => {
resolve();
});
}
let updatedAppDefinition;
const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition));
@ -830,8 +833,6 @@ const EditorComponent = (props) => {
isUpdatingEditorStateInProcess: updatingEditorStateInProcess,
appDefinition: updatedAppDefinition,
});
computeComponentState(updatedAppDefinition.pages[currentPageId]?.components);
}
if (config.ENABLE_MULTIPLAYER_EDITING && !opts?.skipYmapUpdate && opts?.currentSessionId !== currentSessionId) {
@ -893,9 +894,9 @@ const EditorComponent = (props) => {
isUpdatingEditorStateInProcess: false,
});
} else if (!isEmpty(editingVersion)) {
// param diff ofr table columns needs the complte column data or else the json structure is not correct computeComponentPropertyDiff function handles this
//! The computeComponentPropertyDiff function manages the calculation of differences in table columns by requiring complete column data. Without this complete data, the resulting JSON structure may be incorrect.
const paramDiff = computeComponentPropertyDiff(appDefinitionDiff, appDefinition, appDiffOptions);
const updateDiff = computeAppDiff(paramDiff, currentPageId, appDiffOptions);
const updateDiff = computeAppDiff(paramDiff, currentPageId, appDiffOptions, currentLayout);
updateAppVersion(appId, editingVersion?.id, currentPageId, updateDiff, isUserSwitchedVersion)
.then(() => {
@ -1080,7 +1081,6 @@ const EditorComponent = (props) => {
const removeComponent = (componentId) => {
if (!isVersionReleased) {
let newDefinition = cloneDeep(appDefinition);
// Delete child components when parent is deleted
let childComponents = [];
@ -1198,7 +1198,7 @@ const EditorComponent = (props) => {
icon: '🗑️',
});
}
// appDefinitionChanged(newDefinition);
handleInspectorView();
} else if (isVersionReleased) {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
@ -1233,6 +1233,11 @@ const EditorComponent = (props) => {
return;
}
if (name.length > 32) {
toast.error('Page name cannot be more than 32 characters');
return;
}
const pageHandles = Object.values(appDefinition.pages).map((page) => page.handle);
let newHandle = handle;
@ -1516,8 +1521,6 @@ const EditorComponent = (props) => {
});
};
// !-------
const appVersionPreviewLink = editingVersion
? `/applications/${appId}/versions/${editingVersion.id}/${currentState.page.handle}`
: '';
@ -1558,6 +1561,7 @@ const EditorComponent = (props) => {
);
}
//! Need to move conditionally rendered components to separate components => Widget Manger or Widget Inspector
const shouldrenderWidgetInspector =
currentSidebarTab === 1 &&
selectedComponents?.length === 1 &&
@ -1589,7 +1593,6 @@ const EditorComponent = (props) => {
<EditorHeader
darkMode={props.darkMode}
appDefinition={_.cloneDeep(appDefinition)}
// toggleAppMaintenance={toggleAppMaintenance}
editingVersion={editingVersion}
appVersionPreviewLink={appVersionPreviewLink}
canUndo={canUndo}

View file

@ -29,7 +29,6 @@ export default function EditorHeader({
onVersionRelease,
saveEditingVersion,
onVersionDelete,
isMaintenanceOn,
slug,
darkMode,
}) {
@ -65,7 +64,8 @@ export default function EditorHeader({
updatePresence(initialPresence);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser]);
const handleLogoClick = () => {
const handleLogoClick = (e) => {
e.preventDefault();
// Force a reload for clearing interval triggers
window.location.href = '/';
};

View file

@ -12,11 +12,7 @@ import { componentTypes } from '../WidgetManager/components';
import Select from '@/_ui/Select';
import defaultStyles from '@/_ui/Select/styles';
import { useTranslation } from 'react-i18next';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
import { Tooltip } from 'react-tooltip';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import RunjsParameters from './ActionConfigurationPanels/RunjsParamters';
import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore';
import { isQueryRunnable } from '@/_helpers/utils';
@ -24,7 +20,6 @@ import { shallow } from 'zustand/shallow';
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
import NoListItem from './Components/Table/NoListItem';
import ManageEventButton from './ManageEventButton';
// eslint-disable-next-line import/no-unresolved
export const EventManager = ({
sourceId,
@ -37,7 +32,7 @@ export const EventManager = ({
pages,
hideEmptyEventsAlert,
callerQueryId,
customEventRefs = {},
customEventRefs = undefined,
}) => {
const dataQueries = useDataQueriesStore(({ dataQueries = [] }) => {
if (callerQueryId) {
@ -915,7 +910,7 @@ export const EventManager = ({
{events.map((event, index) => {
const actionMeta = ActionTypes.find((action) => action.id === event.event.actionId);
const rowClassName = `card-body p-0 ${focusedEventIndex === index ? ' bg-azure-lt' : ''}`;
// const rowClassName = `card-body p-0 ${focusedEventIndex === index ? ' bg-azure-lt' : ''}`;
return (
<Draggable key={index} draggableId={`${event.eventId}-${index}`} index={index}>
{renderDraggable((provided, snapshot) => {

View file

@ -14,7 +14,7 @@ import { EventManager } from '@/Editor/Inspector/EventManager';
import { staticDataSources, customToggles, mockDataQueryAsComponent } from '../constants';
import { DataSourceTypes } from '../../DataSourceManager/SourceComponents';
import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions, useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { useSelectedQuery, useSelectedDataSource } from '@/_stores/queryPanelStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
@ -97,14 +97,6 @@ export const QueryManagerBody = ({
validateNewOptions(newOptions);
};
const eventsChanged = (events) => {
optionchanged('events', events);
//added this here since the subscriber added in QueryManager component does not detect this change
useDataQueriesStore
.getState()
.actions.saveData({ ...selectedQuery, options: { ...selectedQuery.options, events: events } });
};
const toggleOption = (option) => {
const currentValue = selectedQuery?.options?.[option] ?? false;
optionchanged(option, !currentValue);

View file

@ -8,13 +8,7 @@ import { ConfirmDialog } from '@/_components/ConfirmDialog';
import { shallow } from 'zustand/shallow';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
export const ReleaseVersionButton = function DeployVersionButton({
appId,
appName,
fetchApp,
onVersionRelease,
saveEditingVersion,
}) {
export const ReleaseVersionButton = function DeployVersionButton({ appId, appName, fetchApp, onVersionRelease }) {
const [isReleasing, setIsReleasing] = useState(false);
const { isVersionReleased, editingVersion } = useAppVersionStore(
(state) => ({

View file

@ -386,7 +386,7 @@ export const SubContainer = ({
enableReleasedVersionPopupState();
return;
}
console.log('---arpit---onDragStop---');
const canvasWidth = getContainerCanvasWidth();
const nodeBounds = direction.node.getBoundingClientRect();

View file

@ -284,7 +284,7 @@ class ViewerComponent extends React.Component {
loadApplicationBySlug = (slug) => {
appService
.getAppBySlug(slug)
.fetchAppBySlug(slug)
.then((data) => {
this.setStateForApp(data, true);
this.setStateForContainer(data);
@ -302,7 +302,7 @@ class ViewerComponent extends React.Component {
loadApplicationByVersion = (appId, versionId) => {
appService
.getAppByVersion(appId, versionId)
.fetchAppByVersion(appId, versionId)
.then((data) => {
this.setStateForApp(data);
this.setStateForContainer(data, versionId);

View file

@ -63,7 +63,7 @@ export default function TemplateLibraryModal(props) {
toast.success('App created.', {
position: 'top-center',
});
navigate(`/${getWorkspaceId()}/apps/${data.id}`);
navigate(`/${getWorkspaceId()}/apps/${data.app[0].id}`);
})
.catch((e) => {
toast.error(e.error, {

View file

@ -939,8 +939,9 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
const options = getQueryVariables(dataQuery.options, getCurrentState());
if (dataQuery.options?.requestConfirmation) {
const runOnPageLoad = dataQuery.options?.runOnPageLoad ?? false;
const queryConfirmationList = runOnPageLoad ? _ref.queryConfirmationList : [];
const queryConfirmationList = useEditorStore.getState().queryConfirmationList
? [...useEditorStore.getState().queryConfirmationList]
: [];
const queryConfirmation = {
queryId,
@ -1175,10 +1176,24 @@ export function renderTooltip({ props, text }) {
);
}
/*
@computeComponentState: (components = {}) => Promise<void>
This change is made to enhance the code readability by optimizing the logic
for computing component state. It replaces the previous try-catch block with
a more efficient approach, precomputing the parent component types and using
conditional checks for better performance and error handling.*/
export function computeComponentState(components = {}) {
let componentState = {};
const currentComponents = getCurrentState().components;
// Precompute parent component types
const parentComponentTypes = {};
Object.keys(components).forEach((key) => {
const { component } = components[key];
parentComponentTypes[key] = component.component;
});
Object.keys(components).forEach((key) => {
if (!components[key]) return;
@ -1189,17 +1204,9 @@ export function computeComponentState(components = {}) {
const existingValues = currentComponents[existingComponentName];
if (component.parent) {
const parentComponent = components[component.parent];
let isListView = false,
isForm = false;
try {
isListView = parentComponent.component.component === 'Listview';
isForm = parentComponent.component.component === 'Form';
} catch {
console.log('error');
}
const parentComponentType = parentComponentTypes[component.parent];
if (!isListView && !isForm) {
if (parentComponentType !== 'Listview' && parentComponentType !== 'Form') {
componentState[component.name] = {
...componentMeta.exposedVariables,
id: key,
@ -1390,11 +1397,27 @@ export const cloneComponents = (
if (selectedComponents.length < 1) return getSelectedText();
const { components: allComponents } = appDefinition.pages[currentPageId];
// if parent is selected, then remove the parent from the selected components
const filteredSelectedComponents = selectedComponents.filter((component) => {
const parentComponentId = component.component?.parent;
if (parentComponentId) {
// Check if the parent component is also selected
const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId);
// If the parent is selected, filter out the child component
if (isParentSelected) {
return false;
}
}
return true;
});
let newDefinition = _.cloneDeep(appDefinition);
let newComponents = [],
newComponentObj = {},
addedComponentId = new Set();
for (let selectedComponent of selectedComponents) {
for (let selectedComponent of filteredSelectedComponents) {
if (addedComponentId.has(selectedComponent.id)) continue;
const component = {
component: allComponents[selectedComponent.id]?.component,
@ -1416,6 +1439,7 @@ export const cloneComponents = (
newComponents,
isCloning,
isCut,
currentPageId,
};
}
@ -1490,7 +1514,7 @@ const updateComponentLayout = (components, parentId, isCut = false) => {
});
});
};
//
const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
const parentId = componentParentId ?? component.component?.parent?.split('-').slice(0, -1).join('-');
@ -1514,7 +1538,7 @@ export const addComponents = (
const finalComponents = {};
const componentMap = {};
let parentComponent = undefined;
const { isCloning, isCut, newComponents: pastedComponents = [] } = newComponentObj;
const { isCloning, isCut, newComponents: pastedComponents = [], currentPageId } = newComponentObj;
if (parentId) {
const id = Object.keys(appDefinition.pages[pageId].components).filter((key) => parentId.startsWith(key));
@ -1528,17 +1552,21 @@ export const addComponents = (
...finalComponents,
});
const isParentAlsoCopied = component.component.parent && componentMap[component.component.parent];
const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, pastedComponents, parentId);
const parentRef = isParentTabOrCalendar
? component.component.parent.split('-').slice(0, -1).join('-')
: component.component.parent;
const isParentAlsoCopied = parentRef && componentMap[parentRef];
componentMap[component.componentId] = newComponentId;
let isChild = parentId ? parentId : component.component.parent;
let isChild = isParentAlsoCopied ? component.component.parent : parentId;
const componentData = JSON.parse(JSON.stringify(component.component));
if (isCloning && parentId && !componentData.parent) {
isChild = component.component.parent;
}
if (!parentComponent && !isParentAlsoCopied && isChild && fromClipboard) {
if (!parentComponent && !isParentAlsoCopied && fromClipboard) {
isChild = undefined;
componentData.parent = undefined;
}
@ -1563,10 +1591,13 @@ export const addComponents = (
},
layouts: component.layouts,
};
finalComponents[newComponentId] = newComponent;
});
!isCloning && updateComponentLayout(pastedComponents, parentId, isCut);
if (currentPageId === pageId) {
updateComponentLayout(pastedComponents, parentId, isCut);
}
updateNewComponents(pageId, appDefinition, finalComponents, appDefinitionChanged, componentMap, isCut);
!isCloning && toast.success('Component pasted succesfully');

View file

@ -14,8 +14,11 @@ export const appService = {
changeIcon,
deleteApp,
getApp,
fetchApp,
getAppBySlug,
fetchAppBySlug,
getAppByVersion,
fetchAppByVersion,
saveApp,
getAppUsers,
createAppUser,
@ -124,6 +127,14 @@ function getApp(id, accessType) {
);
}
// v2 api for fetching app
function fetchApp(id, accessType) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/${id}${accessType ? `?access_type=${accessType}` : ''}`, requestOptions).then(
handleResponse
);
}
function deleteApp(id) {
const requestOptions = { method: 'DELETE', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
@ -134,10 +145,19 @@ function getAppBySlug(slug) {
return fetch(`${config.apiUrl}/apps/slugs/${slug}`, requestOptions).then(handleResponse);
}
function fetchAppBySlug(slug) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/slugs/${slug}`, requestOptions).then(handleResponse);
}
function getAppByVersion(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function fetchAppByVersion(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function saveApp(id, attributes) {
const requestOptions = {

View file

@ -4,6 +4,7 @@ import { authHeader, handleResponse } from '@/_helpers';
export const appVersionService = {
getAll,
getOne,
getAppVersionData,
create,
del,
save,
@ -23,6 +24,10 @@ function getOne(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function getAppVersionData(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
function create(appId, versionName, versionFromId) {
const body = {
@ -78,10 +83,6 @@ function autoSaveApp(appId, versionId, diff, type, pageId, operation, isUserSwit
global_settings: {
update: { ...diff },
},
events: {
update: diff,
create: diff,
},
};
const body = !type
@ -121,7 +122,7 @@ function saveAppVersionEventHandlers(appId, versionId, events, updateType = 'upd
function createAppVersionEventHandler(appId, versionId, event) {
const body = {
event,
...event,
};
const requestOptions = {

View file

@ -46,8 +46,8 @@ const updateType = Object.freeze({
componentDeleted: 'components',
});
export const computeAppDiff = (appDiff, currentPageId, opts) => {
const { updateDiff, type, operation } = updateFor(appDiff, currentPageId, opts);
export const computeAppDiff = (appDiff, currentPageId, opts, currentLayout) => {
const { updateDiff, type, operation } = updateFor(appDiff, currentPageId, opts, currentLayout);
return { updateDiff, type, operation };
};
@ -121,7 +121,7 @@ export const computeComponentPropertyDiff = (appDiff, definition, opts) => {
return _diff;
};
const updateFor = (appDiff, currentPageId, opts) => {
const updateFor = (appDiff, currentPageId, opts, currentLayout) => {
const updateTypeMappings = [
{
updateTypes: ['componentAdded', 'componentDefinitionChanged', 'componentDeleted', 'containerChanges'],
@ -155,7 +155,7 @@ const updateFor = (appDiff, currentPageId, opts) => {
const optionsTypes = _.intersection(options, updateTypes);
if (optionsTypes.length > 0) {
return processingFunction(appDiff, currentPageId, optionsTypes);
return processingFunction(appDiff, currentPageId, optionsTypes, currentLayout);
}
}
@ -197,7 +197,7 @@ const computePageUpdate = (appDiff, currentPageId, opts) => {
return { updateDiff, type, operation };
};
const computeComponentDiff = (appDiff, currentPageId, opts) => {
const computeComponentDiff = (appDiff, currentPageId, opts, currentLayout) => {
let type;
let updateDiff;
let operation = 'update';
@ -220,6 +220,10 @@ const computeComponentDiff = (appDiff, currentPageId, opts) => {
const componentMeta = componentTypes.find((comp) => comp.component === component.component.component);
if (!componentMeta) {
return result;
}
const metaDiff = diff(componentMeta, component.component);
result[id] = _.defaultsDeep(metaDiff, defaultComponent);
@ -228,8 +232,14 @@ const computeComponentDiff = (appDiff, currentPageId, opts) => {
const metaAttributes = _.keys(metaDiff.definition);
metaAttributes.forEach((attribute) => {
if (metaDiff.definition[attribute]?.actions && !_.isEmpty(metaDiff.definition[attribute]?.actions?.value)) {
const actions = _.toArray(metaDiff.definition[attribute]?.actions?.value);
const doesActionsExist =
metaDiff.definition[attribute]?.actions && !_.isEmpty(metaDiff.definition[attribute]?.actions?.value);
const doesColumnsExist =
metaDiff.definition[attribute]?.columns && !_.isEmpty(metaDiff.definition[attribute]?.columns?.value);
if (doesActionsExist || doesColumnsExist) {
const actions = _.toArray(metaDiff.definition[attribute]?.actions?.value) || [];
const columns = _.toArray(metaDiff.definition[attribute]?.columns?.value) || [];
metaDiff.definition = {
...metaDiff.definition,
@ -238,6 +248,9 @@ const computeComponentDiff = (appDiff, currentPageId, opts) => {
actions: {
value: actions,
},
columns: {
value: columns,
},
},
};
}
@ -245,7 +258,7 @@ const computeComponentDiff = (appDiff, currentPageId, opts) => {
});
}
const currentDisplayPreference = _.keys(appDiff.pages[currentPageId].components[id].layouts)[0];
const currentDisplayPreference = currentLayout;
if (currentDisplayPreference === 'mobile') {
result[id].others.showOnMobile = { value: '{{true}}' };

View file

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import useRouter from '@/_hooks/use-router';
import { ToolTip } from '@/_components/ToolTip';

View file

@ -1 +1 @@
2.19.2
2.20.1

View file

@ -28,16 +28,11 @@ import { EntityManager } from 'typeorm';
import { ValidAppInterceptor } from 'src/interceptors/valid.app.interceptor';
import { AppDecorator } from 'src/decorators/app.decorator';
import { PageService } from '@services/page.service';
import { EventsService } from '@services/events_handler.service';
@Controller('apps')
export class AppsController {
constructor(
private appsService: AppsService,
private foldersService: FoldersService,
private pageService: PageService,
private eventsService: EventsService,
private appsAbilityFactory: AppsAbilityFactory
) {}
@ -84,17 +79,11 @@ export class AppsController {
}
const response = decamelizeKeys(app);
const seralizedQueries = [];
const dataQueriesForVersion = app.editingVersion
? await this.appsService.findDataQueriesForVersion(app.editingVersion.id)
: [];
const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(app.editingVersion.id) : [];
const eventsForVersion = app.editingVersion
? await this.eventsService.findEventsForVersion(app.editingVersion.id)
: [];
// serialize queries
for (const query of dataQueriesForVersion) {
const decamelizedQuery = decamelizeKeys(query);
@ -104,8 +93,6 @@ export class AppsController {
response['data_queries'] = seralizedQueries;
response['definition'] = app.editingVersion?.definition;
response['pages'] = pagesForVersion;
response['events'] = eventsForVersion;
//! if editing version exists, camelize the definition
if (app.editingVersion && app.editingVersion.definition) {
@ -136,9 +123,6 @@ export class AppsController {
? await this.appsService.findVersion(app.currentVersionId)
: await this.appsService.findVersion(app.editingVersion?.id);
const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : [];
const eventsForVersion = app.editingVersion ? await this.eventsService.findEventsForVersion(versionToLoad.id) : [];
// serialize
return {
current_version_id: app['currentVersionId'],
@ -148,11 +132,6 @@ export class AppsController {
is_maintenance_on: app.isMaintenanceOn,
name: app.name,
slug: app.slug,
events: eventsForVersion,
pages: pagesForVersion,
homePageId: versionToLoad.homePageId,
globalSettings: versionToLoad.globalSettings,
showViewerNavigation: versionToLoad.showViewerNavigation,
};
}
@ -308,25 +287,7 @@ export class AppsController {
);
}
const pagesForVersion = await this.pageService.findPagesForVersion(versionId);
const eventsForVersion = await this.eventsService.findEventsForVersion(versionId);
const appCurrentEditingVersion = JSON.parse(JSON.stringify(appVersion));
delete appCurrentEditingVersion['app'];
const appData = {
...app,
};
delete appData['editingVersion'];
return {
...appData,
editing_version: camelizeKeys(appCurrentEditingVersion),
pages: pagesForVersion,
events: eventsForVersion,
};
return { ...appVersion, data_queries: appVersion.dataQueries };
}
@UseGuards(JwtAuthGuard)

View file

@ -20,8 +20,8 @@ import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.fact
import { App } from 'src/entities/app.entity';
import { User } from 'src/decorators/user.decorator';
import { VersionEditDto } from '@dto/version-edit.dto';
import { CreatePageDto, UpdatePageDto } from '@dto/pages.dto';
import { CreatePageDto, DeletePageDto } from '@dto/pages.dto';
import { CreateComponentDto, DeleteComponentDto, UpdateComponentDto, LayoutUpdateDto } from '@dto/component.dto';
import { ValidAppInterceptor } from 'src/interceptors/valid.app.interceptor';
import { AppDecorator } from 'src/decorators/app.decorator';
@ -30,6 +30,7 @@ import { ComponentsService } from '@services/components.service';
import { PageService } from '@services/page.service';
import { EventsService } from '@services/events_handler.service';
import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
import { CreateEventHandlerDto, UpdateEventHandlerDto } from '@dto/event-handler.dto';
@Controller({
path: 'apps',
@ -40,6 +41,7 @@ export class AppsControllerV2 {
private appsService: AppsService,
private componentsService: ComponentsService,
private pageService: PageService,
private eventsService: EventsService,
private eventService: EventsService,
private appsAbilityFactory: AppsAbilityFactory
) {}
@ -73,6 +75,9 @@ export class AppsControllerV2 {
: [];
const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(app.editingVersion.id) : [];
const eventsForVersion = app.editingVersion
? await this.eventsService.findEventsForVersion(app.editingVersion.id)
: [];
// serialize queries
for (const query of dataQueriesForVersion) {
@ -84,6 +89,7 @@ export class AppsControllerV2 {
response['data_queries'] = seralizedQueries;
response['definition'] = app.editingVersion?.definition;
response['pages'] = pagesForVersion;
response['events'] = eventsForVersion;
//! if editing version exists, camelize the definition
if (app.editingVersion && app.editingVersion.definition) {
@ -95,6 +101,84 @@ export class AppsControllerV2 {
return response;
}
async appFromSlug(@User() user, @AppDecorator() app: App) {
if (user) {
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('viewApp', app)) {
throw new ForbiddenException(
JSON.stringify({
organizationId: app.organizationId,
})
);
}
}
const versionToLoad = app.currentVersionId
? await this.appsService.findVersion(app.currentVersionId)
: await this.appsService.findVersion(app.editingVersion?.id);
const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : [];
const eventsForVersion = app.editingVersion ? await this.eventsService.findEventsForVersion(versionToLoad.id) : [];
// serialize
return {
current_version_id: app['currentVersionId'],
data_queries: versionToLoad?.dataQueries,
definition: versionToLoad?.definition,
is_public: app.isPublic,
is_maintenance_on: app.isMaintenanceOn,
name: app.name,
slug: app.slug,
events: eventsForVersion,
pages: pagesForVersion,
homePageId: versionToLoad.homePageId,
globalSettings: versionToLoad.globalSettings,
showViewerNavigation: versionToLoad.showViewerNavigation,
};
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Get(':id/versions/:versionId')
async version(@User() user, @Param('id') id, @Param('versionId') versionId) {
const appVersion = await this.appsService.findVersion(versionId);
const app = appVersion.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('fetchVersions', app)) {
throw new ForbiddenException(
JSON.stringify({
organizationId: app.organizationId,
})
);
}
const pagesForVersion = await this.pageService.findPagesForVersion(versionId);
const eventsForVersion = await this.eventsService.findEventsForVersion(versionId);
const appCurrentEditingVersion = JSON.parse(JSON.stringify(appVersion));
delete appCurrentEditingVersion['app'];
const appData = {
...app,
};
delete appData['editingVersion'];
return {
...appData,
editing_version: camelizeKeys(appCurrentEditingVersion),
pages: pagesForVersion,
events: eventsForVersion,
};
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId')
@ -118,6 +202,7 @@ export class AppsControllerV2 {
return await this.appsService.updateAppVersion(version, appVersionUpdateDto);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId/global_settings')
@ -150,7 +235,7 @@ export class AppsControllerV2 {
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() versionEditDto: VersionEditDto
@Body() createComponentDto: CreateComponentDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
@ -164,7 +249,7 @@ export class AppsControllerV2 {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.componentsService.create(versionEditDto.diff, versionEditDto.pageId, versionId);
await this.componentsService.create(createComponentDto.diff, createComponentDto.pageId, versionId);
}
@UseGuards(JwtAuthGuard)
@ -174,7 +259,7 @@ export class AppsControllerV2 {
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() versionEditDto: VersionEditDto
@Body() updateComponentDto: UpdateComponentDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
@ -188,7 +273,7 @@ export class AppsControllerV2 {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.componentsService.update(versionEditDto.diff);
await this.componentsService.update(updateComponentDto.diff, versionId);
}
@UseGuards(JwtAuthGuard)
@ -198,7 +283,7 @@ export class AppsControllerV2 {
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() versionEditDto: VersionEditDto
@Body() deleteComponentDto: DeleteComponentDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
@ -212,7 +297,7 @@ export class AppsControllerV2 {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.componentsService.delete(versionEditDto.diff);
await this.componentsService.delete(deleteComponentDto.diff, versionId);
}
@UseGuards(JwtAuthGuard)
@ -222,7 +307,7 @@ export class AppsControllerV2 {
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() versionEditDto: VersionEditDto
@Body() updateComponentLayout: LayoutUpdateDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
@ -236,7 +321,7 @@ export class AppsControllerV2 {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.componentsService.componentLayoutChange(versionEditDto.diff);
await this.componentsService.componentLayoutChange(updateComponentLayout.diff, versionId);
}
// pages api
@ -282,11 +367,50 @@ export class AppsControllerV2 {
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId/pages')
async updatePages(
async updatePages(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() updatePageDto) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.pageService.updatePage(updatePageDto, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Delete(':id/versions/:versionId/pages')
async deletePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() deletePageDto: DeletePageDto) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.pageService.deletePage(deletePageDto.pageId, versionId);
}
// events api
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Post(':id/versions/:versionId/events')
async createEvent(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() updatePageDto: Partial<UpdatePageDto>
@Body() createEventHandlerDto: CreateEventHandlerDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
@ -300,57 +424,17 @@ export class AppsControllerV2 {
throw new ForbiddenException('You do not have permissions to perform this action');
}
await this.pageService.updatePage({ pageId: updatePageDto.pageId, diff: updatePageDto.diff });
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Delete(':id/versions/:versionId/pages')
async deletePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() body) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
const { pageId } = body;
if (pageId) {
await this.pageService.deletePage(pageId, versionId);
}
}
// events api
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Post(':id/versions/:versionId/events')
async createEvent(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() body) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
if (app.id !== id) {
throw new BadRequestException();
}
const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
const { event } = body;
return this.eventService.createEvent(event, versionId);
return this.eventService.createEvent(createEventHandlerDto, versionId);
}
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Put(':id/versions/:versionId/events')
async updateEvents(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() body) {
async updateEvents(
@User() user,
@Param('id') id,
@Param('versionId') versionId,
@Body() updateEventHandlerDto: UpdateEventHandlerDto
) {
const version = await this.appsService.findVersion(versionId);
const app = version.app;
@ -363,7 +447,9 @@ export class AppsControllerV2 {
throw new ForbiddenException('You do not have permissions to perform this action');
}
return await this.eventService.updateEvent(body?.events, body?.updateType);
const { events, updateType } = updateEventHandlerDto;
return await this.eventService.updateEvent(events, updateType, versionId);
}
@UseGuards(JwtAuthGuard)
@ -382,6 +468,6 @@ export class AppsControllerV2 {
throw new ForbiddenException('You do not have permissions to perform this action');
}
return await this.eventService.deleteEvent(eventId);
return await this.eventService.deleteEvent(eventId, versionId);
}
}

View file

@ -21,9 +21,8 @@ export class LibraryAppsController {
if (!ability.can('createApp', App)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
const newApp = await this.libraryAppCreationService.perform(user, identifier);
return newApp;
const result = await this.libraryAppCreationService.perform(user, identifier);
return result;
}
@Get()

View file

@ -1,4 +1,4 @@
import { IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
import { Transform } from 'class-transformer';
import { sanitizeInput } from '../helpers/utils.helper';
@ -17,7 +17,7 @@ export class AppVersionUpdateDto {
@IsOptional()
showViewerNavigation: boolean;
@IsString()
@IsUUID()
@IsOptional()
homePageId: string;

View file

@ -0,0 +1,102 @@
import { Type } from 'class-transformer';
import { IsArray, IsBoolean, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString, IsUUID } from 'class-validator';
export class ComponentLayoutDto {
@IsNumber()
@IsOptional()
top?: number;
@IsNumber()
@IsOptional()
left?: number;
@IsNumber()
@IsOptional()
width?: number;
@IsNumber()
@IsOptional()
height?: number;
}
export class LayoutData {
@IsObject()
@IsOptional()
desktop?: ComponentLayoutDto;
@IsObject()
@IsOptional()
mobile?: ComponentLayoutDto;
}
class ComponentDto {
@IsString()
name: string;
@IsObject()
properties: Record<string, any>;
@IsObject()
styles: Record<string, any>;
@IsObject()
validation: Record<string, any>;
@IsString()
type: string;
@IsObject()
others: Record<string, any>;
@IsOptional()
@Type(() => ComponentLayoutDto)
layouts: ComponentLayoutDto;
@IsOptional()
parent: string;
}
export class CreateComponentDto {
@IsBoolean()
is_user_switched_version: boolean;
@IsUUID()
pageId: string;
@IsObject()
diff: Record<string, ComponentDto>;
}
export class UpdateComponentDto {
@IsBoolean()
is_user_switched_version: boolean;
@IsUUID()
pageId: string;
@IsObject()
diff: Record<string, ComponentDto>;
}
export class DeleteComponentDto {
@IsBoolean()
is_user_switched_version: boolean;
@IsUUID()
pageId: string;
@IsArray()
diff: string[];
}
export class LayoutUpdateDto {
@IsBoolean()
is_user_switched_version: boolean;
@IsUUID()
pageId: string;
@IsObject()
@IsNotEmpty()
diff: Record<string, { layouts: LayoutData }>;
}

View file

@ -0,0 +1,44 @@
import { IsArray, IsIn, IsNumber, IsObject, IsString, IsUUID, ValidateNested } from 'class-validator';
import { Target } from 'src/entities/event_handler.entity';
export class CreateEventHandlerDto {
@IsObject()
event: any;
@IsString()
eventType: Target;
@IsString()
attachedTo: string;
@IsNumber()
index: number;
}
class UpdateEventDiff {
@IsString()
name: string;
@IsNumber()
index: number;
@IsObject()
@ValidateNested()
event: any;
}
export class UpdateEvent {
@IsUUID()
event_id: string;
@IsObject()
diff: UpdateEventDiff;
}
export class UpdateEventHandlerDto {
@IsArray()
events: UpdateEvent[];
@IsIn(['update', 'reorder'])
updateType: 'update' | 'reorder';
}

View file

@ -1,17 +1,35 @@
import { IsNumber, IsString, IsUUID } from 'class-validator';
import { IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
export class CreatePageDto {
@IsUUID()
@IsNotEmpty()
id: string;
@IsString()
@IsNotEmpty()
@MaxLength(32)
name: string;
@IsString()
@IsNotEmpty()
@MaxLength(50)
handle: string;
@IsNumber()
@IsNotEmpty()
index: number;
@IsOptional()
disabled: boolean;
@IsOptional()
hidden: boolean;
}
export class DeletePageDto {
@IsUUID()
@IsNotEmpty()
pageId: string;
}
export class UpdatePageDto {

View file

@ -80,6 +80,26 @@ export async function dbTransactionWrap(operation: (...args) => any, manager?: E
}
}
export const updateTimestampForAppVersion = async (manager, appVersionId) => {
const appVersion = await manager.findOne('app_versions', appVersionId);
if (appVersion) {
await manager.update('app_versions', appVersionId, { updatedAt: new Date() });
}
};
export async function dbTransactionForAppVersionAssociationsUpdate(
operation: (...args) => any,
appVersionId: string
): Promise<any> {
return await getManager().transaction(async (manager) => {
const result = await operation(manager);
await updateTimestampForAppVersion(manager, appVersionId);
return result;
});
}
export const defaultAppEnvironments = [{ name: 'production', isDefault: true, priority: 3 }];
export async function catchDbException(
operation: () => any,

View file

@ -48,5 +48,6 @@ if (process.env.ENABLE_TOOLJET_DB === 'true') {
CredentialsService,
PostgrestProxyService,
],
exports: [ImportExportResourcesService],
})
export class ImportExportResourcesModule {}

View file

@ -16,9 +16,14 @@ import { PluginsService } from '@services/plugins.service';
import { Plugin } from 'src/entities/plugin.entity';
import { PluginsHelper } from 'src/helpers/plugins.helper';
import { AppEnvironmentService } from '@services/app_environments.service';
import { ImportExportResourcesModule } from '../import_export_resources/import_export_resources.module';
@Module({
imports: [TypeOrmModule.forFeature([App, Credential, File, Plugin, DataSource]), CaslModule],
imports: [
TypeOrmModule.forFeature([App, Credential, File, Plugin, DataSource]),
CaslModule,
ImportExportResourcesModule,
],
providers: [
EncryptionService,
CredentialsService,

View file

@ -114,6 +114,7 @@ export class AppEnvironmentService {
order: {
createdAt: 'DESC',
},
select: ['id', 'name', 'appId'],
});
});
}

View file

@ -4,7 +4,7 @@ import { EntityManager, Repository } from 'typeorm';
import { Component } from 'src/entities/component.entity';
import { Layout } from 'src/entities/layout.entity';
import { Page } from 'src/entities/page.entity';
import { dbTransactionWrap } from 'src/helpers/utils.helper';
import { dbTransactionForAppVersionAssociationsUpdate, dbTransactionWrap } from 'src/helpers/utils.helper';
import { EventsService } from './events_handler.service';
@ -22,12 +22,13 @@ export class ComponentsService {
}
async create(componentDiff: object, pageId: string, appVersionId: string) {
return dbTransactionWrap(async (manager: EntityManager) => {
return dbTransactionForAppVersionAssociationsUpdate(async (manager: EntityManager) => {
const page = await manager.findOne(Page, {
where: { appVersionId, id: pageId },
});
const newComponents = this.transformComponentData(componentDiff);
const componentLayouts = [];
newComponents.forEach((component) => {
@ -58,11 +59,11 @@ export class ComponentsService {
await manager.save(Layout, componentLayouts);
return {};
});
}, appVersionId);
}
async update(componentDiff: object) {
return dbTransactionWrap(async (manager) => {
async update(componentDiff: object, appVersionId: string) {
return dbTransactionForAppVersionAssociationsUpdate(async (manager) => {
for (const componentId in componentDiff) {
const { component } = componentDiff[componentId];
@ -105,11 +106,11 @@ export class ComponentsService {
return;
}
});
}, appVersionId);
}
async delete(componentIds: string[]) {
return dbTransactionWrap(async (manager: EntityManager) => {
async delete(componentIds: string[], appVersionId: string) {
return dbTransactionForAppVersionAssociationsUpdate(async (manager: EntityManager) => {
const components = await manager.findByIds(Component, componentIds);
if (!components.length) {
@ -125,17 +126,15 @@ export class ComponentsService {
});
await manager.delete(Component, componentIds);
});
}, appVersionId);
}
async componentLayoutChange(componenstLayoutDiff: object) {
return dbTransactionWrap(async (manager: EntityManager) => {
async componentLayoutChange(componenstLayoutDiff: object, appVersionId: string) {
return dbTransactionForAppVersionAssociationsUpdate(async (manager: EntityManager) => {
for (const componentId in componenstLayoutDiff) {
const { layouts } = componenstLayoutDiff[componentId];
const doesComponentExist = await manager.findAndCount(Component, { id: componentId });
const componentLayout = await manager.findOne(Layout, { componentId });
if (!componentLayout) {
if (!doesComponentExist[1]) {
return {
error: {
message: `Component with id ${componentId} does not exist`,
@ -143,22 +142,21 @@ export class ComponentsService {
};
}
const { layouts } = componenstLayoutDiff[componentId];
for (const type in layouts) {
const layout = {
type,
...layouts[type],
};
const currentLayout = Object.assign({}, componentLayout);
const componentLayout = await manager.findOne(Layout, { componentId, type });
const newLayout = {
...currentLayout,
...layout,
};
if (componentLayout) {
const layout = {
...layouts[type],
} as Partial<Layout>;
await manager.update(Layout, { id: componentLayout.id }, newLayout);
await manager.update(Layout, { id: componentLayout.id }, layout);
}
}
}
});
}, appVersionId);
}
async getAllComponents(pageId: string) {

View file

@ -1,15 +1,14 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, Repository } from 'typeorm';
import { Component } from 'src/entities/component.entity';
import { EventHandler } from 'src/entities/event_handler.entity';
import { dbTransactionWrap } from 'src/helpers/utils.helper';
import { dbTransactionWrap, dbTransactionForAppVersionAssociationsUpdate } from 'src/helpers/utils.helper';
import { CreateEventHandlerDto, UpdateEvent } from '@dto/event-handler.dto';
@Injectable()
export class EventsService {
constructor(
@InjectRepository(Component)
@InjectRepository(EventHandler)
private eventsRepository: Repository<EventHandler>
) {}
@ -23,11 +22,8 @@ export class EventsService {
}
async findAllEventsWithSourceId(sourceId: string): Promise<EventHandler[]> {
return dbTransactionWrap(async (manager: EntityManager) => {
const allEvents = await manager.find(EventHandler, {
where: { sourceId },
});
return allEvents;
return this.eventsRepository.find({
where: { sourceId },
});
}
@ -41,31 +37,38 @@ export class EventsService {
});
}
async createEvent(eventObj, versionId) {
if (Object.keys(eventObj).length === 0) {
return new BadRequestException('No event found');
async createEvent(eventHandler: CreateEventHandlerDto, versionId) {
if (!eventHandler.attachedTo) {
throw new BadRequestException('No attachedTo found');
}
const newEvent = {
name: eventObj.event.eventId,
sourceId: eventObj.attachedTo,
target: eventObj.eventType,
event: eventObj.event,
appVersionId: versionId,
index: eventObj.index,
};
if (!eventHandler.eventType) {
throw new BadRequestException('No eventType found');
}
if (!eventHandler.event) {
throw new BadRequestException('No event found');
}
return await dbTransactionForAppVersionAssociationsUpdate(async (manager: EntityManager) => {
const newEvent = new EventHandler();
newEvent.name = eventHandler.event.eventId;
newEvent.sourceId = eventHandler.attachedTo;
newEvent.target = eventHandler.eventType;
newEvent.event = eventHandler.event;
newEvent.index = eventHandler.index;
newEvent.appVersionId = versionId;
return await dbTransactionWrap(async (manager: EntityManager) => {
const event = await manager.save(EventHandler, newEvent);
return event;
});
}, versionId);
}
async updateEvent(events: [], updateType: 'update' | 'reorder') {
return await dbTransactionWrap(async (manager: EntityManager) => {
async updateEvent(events: UpdateEvent[], updateType: 'update' | 'reorder', appVersionId: string) {
return await dbTransactionForAppVersionAssociationsUpdate(async (manager: EntityManager) => {
return await Promise.all(
events.map(async (event) => {
const { event_id, diff } = event as any;
const { event_id, diff } = event;
const eventDiff = diff?.event;
const eventToUpdate = await manager.findOne(EventHandler, {
@ -78,12 +81,13 @@ export class EventsService {
const updatedEvent = {
...eventToUpdate,
event: {
...eventToUpdate.event,
...eventDiff,
},
};
if (updateType === 'update') {
updatedEvent.name = eventDiff?.eventId;
updatedEvent.event = eventDiff;
}
if (updateType === 'reorder') {
updatedEvent.index = diff.index;
}
@ -91,33 +95,32 @@ export class EventsService {
return await manager.save(EventHandler, updatedEvent);
})
);
});
}, appVersionId);
}
async updateEventsOrderOnDelete(sourceId: string, deletedIndex: number) {
const allEvents = await this.findAllEventsWithSourceId(sourceId);
const eventsToUpdate = allEvents.filter((event) => event.index > deletedIndex);
return await dbTransactionWrap(async (manager: EntityManager) => {
return await Promise.all(
eventsToUpdate.map(async (event) => {
const updatedEvent = {
...event,
index: event.index - 1,
};
return await manager.save(EventHandler, updatedEvent);
return await manager.update(EventHandler, { id: event.id }, { index: event.index - 1 });
})
);
});
}
async deleteEvent(eventId: string) {
return await dbTransactionWrap(async (manager: EntityManager) => {
async deleteEvent(eventId: string, appVersionId: string) {
return await dbTransactionForAppVersionAssociationsUpdate(async (manager: EntityManager) => {
const event = await manager.findOne(EventHandler, {
where: { id: eventId },
});
const sourceId = event.sourceId;
const deletedIndex = event.index;
if (!event) {
return new BadRequestException('No event found');
}
@ -127,8 +130,8 @@ export class EventsService {
if (!deleteResponse?.affected) {
throw new NotFoundException();
}
await this.updateEventsOrderOnDelete(event.sourceId, event.index);
await this.updateEventsOrderOnDelete(sourceId, deletedIndex);
return deleteResponse;
});
}, appVersionId);
}
}

View file

@ -1,30 +1,63 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { App } from '../entities/app.entity';
import { User } from '../entities/user.entity';
import { AppImportExportService } from './app_import_export.service';
import { readFileSync } from 'fs';
import { Logger } from 'nestjs-pino';
import { ImportExportResourcesService } from './import_export_resources.service';
import { ImportResourcesDto } from '@dto/import-resources.dto';
import { AppImportExportService } from './app_import_export.service';
@Injectable()
export class LibraryAppCreationService {
constructor(private readonly appImportExportService: AppImportExportService, private readonly logger: Logger) {}
constructor(
private readonly importExportResourcesService: ImportExportResourcesService,
private readonly appImportExportService: AppImportExportService,
private readonly logger: Logger
) {}
async perform(currentUser: User, identifier: string): Promise<App> {
const newApp = await this.appImportExportService.import(currentUser, this.findAppDefinition(identifier));
async perform(currentUser: User, identifier: string) {
const templateDefinition = this.findTemplateDefinition(identifier);
const importDto = new ImportResourcesDto();
importDto.organization_id = currentUser.organizationId;
importDto.app = templateDefinition.app || templateDefinition.appV2;
importDto.tooljet_database = templateDefinition.tooljet_database;
return newApp;
if (this.isVersionGreaterThanOrEqual(templateDefinition.tooljet_version, '2.16.0')) {
return await this.importExportResourcesService.import(currentUser, importDto);
} else {
const importedApp = await this.appImportExportService.import(currentUser, templateDefinition);
return {
app: [importedApp],
tooljet_database: [],
};
}
}
findAppDefinition(identifier: string) {
let appDefinition: object;
findTemplateDefinition(identifier: string) {
try {
appDefinition = JSON.parse(readFileSync(`templates/${identifier}/definition.json`, 'utf-8'));
return JSON.parse(readFileSync(`templates/${identifier}/definition.json`, 'utf-8'));
} catch (err) {
this.logger.error(err);
throw new BadRequestException('App definition not found');
}
}
return appDefinition;
isVersionGreaterThanOrEqual(version1: string, version2: string) {
if (!version1) return false;
const v1Parts = version1.split('-')[0].split('.').map(Number);
const v2Parts = version2.split('-')[0].split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = +v1Parts[i] || 0;
const v2Part = +v2Parts[i] || 0;
if (v1Part < v2Part) {
return false;
} else if (v1Part > v2Part) {
return true;
}
}
return true;
}
}

View file

@ -6,7 +6,7 @@ import { Page } from 'src/entities/page.entity';
import { ComponentsService } from './components.service';
import { CreatePageDto, UpdatePageDto } from '@dto/pages.dto';
import { AppsService } from './apps.service';
import { dbTransactionWrap } from 'src/helpers/utils.helper';
import { dbTransactionWrap, dbTransactionForAppVersionAssociationsUpdate } from 'src/helpers/utils.helper';
import { EventsService } from './events_handler.service';
import { Component } from 'src/entities/component.entity';
import { Layout } from 'src/entities/layout.entity';
@ -42,54 +42,58 @@ export class PageService {
}
async createPage(page: CreatePageDto, appVersionId: string): Promise<Page> {
const newPage = new Page();
newPage.id = page.id;
newPage.name = page.name;
newPage.handle = page.handle;
newPage.index = page.index;
newPage.appVersionId = appVersionId;
return dbTransactionForAppVersionAssociationsUpdate(async (manager) => {
const newPage = new Page();
newPage.id = page.id;
newPage.name = page.name;
newPage.handle = page.handle;
newPage.index = page.index;
newPage.appVersionId = appVersionId;
return this.pageRepository.save(newPage);
return await manager.save(Page, newPage);
}, appVersionId);
}
async clonePage(pageId: string, appVersionId: string) {
const pageToClone = await this.pageRepository.findOne(pageId);
return dbTransactionForAppVersionAssociationsUpdate(async (manager) => {
const pageToClone = await manager.findOne(Page, pageId);
if (!pageToClone) {
throw new Error('Page not found');
}
if (!pageToClone) {
throw new Error('Page not found');
}
let pageName = `${pageToClone.name} (copy)`;
let pageHandle = `${pageToClone.handle}-copy`;
let pageName = `${pageToClone.name} (copy)`;
let pageHandle = `${pageToClone.handle}-copy`;
const allPages = await this.pageRepository.find({ appVersionId });
const allPages = await this.pageRepository.find({ appVersionId });
const pageNameORHandleExists = allPages.filter((page) => {
return page.name.includes(pageName) || page.handle.includes(pageHandle);
});
const pageNameORHandleExists = allPages.filter((page) => {
return page.name.includes(pageName) || page.handle.includes(pageHandle);
});
if (pageNameORHandleExists.length > 0) {
pageName = `${pageToClone.name} (copy ${pageNameORHandleExists.length})`;
pageHandle = `${pageToClone.handle}-copy-${pageNameORHandleExists.length}`;
}
if (pageNameORHandleExists.length > 0) {
pageName = `${pageToClone.name} (copy ${pageNameORHandleExists.length})`;
pageHandle = `${pageToClone.handle}-copy-${pageNameORHandleExists.length}`;
}
const newPage = new Page();
newPage.name = pageName;
newPage.handle = pageHandle;
newPage.index = pageToClone.index + 1;
newPage.appVersionId = appVersionId;
const newPage = new Page();
newPage.name = pageName;
newPage.handle = pageHandle;
newPage.index = pageToClone.index + 1;
newPage.appVersionId = appVersionId;
const clonedpage = await this.pageRepository.save(newPage);
const clonedpage = await this.pageRepository.save(newPage);
await this.clonePageEventsAndComponents(pageId, clonedpage.id, appVersionId);
await this.clonePageEventsAndComponents(pageId, clonedpage.id);
const pages = await this.findPagesForVersion(appVersionId);
const events = await this.eventHandlerService.findEventsForVersion(appVersionId);
const pages = await this.findPagesForVersion(appVersionId);
const events = await this.eventHandlerService.findEventsForVersion(appVersionId);
return { pages, events };
return { pages, events };
}, appVersionId);
}
async clonePageEventsAndComponents(pageId: string, clonePageId: string, appVersionId: string) {
async clonePageEventsAndComponents(pageId: string, clonePageId: string) {
return dbTransactionWrap(async (manager: EntityManager) => {
const pageComponents = await manager.find(Component, { pageId });
const pageEvents = await this.eventHandlerService.findAllEventsWithSourceId(pageId);
@ -98,7 +102,22 @@ export class PageService {
// Clone events
await Promise.all(
pageEvents.map(async (event) => {
const clonedEvent = { ...event, id: undefined, sourceId: clonePageId };
const eventDefinition = event.event;
if (eventDefinition?.actionId === 'control-component') {
eventDefinition.componentId = componentsIdMap[eventDefinition.componentId];
}
event.event = eventDefinition;
const clonedEvent = new EventHandler();
clonedEvent.event = event.event;
clonedEvent.index = event.index;
clonedEvent.name = event.name;
clonedEvent.sourceId = clonePageId;
clonedEvent.target = event.target;
clonedEvent.appVersionId = event.appVersionId;
await manager.save(EventHandler, clonedEvent);
})
);
@ -119,11 +138,24 @@ export class PageService {
// Clone component events
const clonedComponentEvents = await this.eventHandlerService.findAllEventsWithSourceId(component.id);
const clonedEvents = clonedComponentEvents.map((event) => ({
...event,
id: undefined,
sourceId: newComponent.id,
}));
const clonedEvents = clonedComponentEvents.map((event) => {
const eventDefinition = event.event;
if (eventDefinition?.actionId === 'control-component') {
eventDefinition.componentId = componentsIdMap[eventDefinition.componentId];
}
event.event = eventDefinition;
const clonedEvent = new EventHandler();
clonedEvent.event = event.event;
clonedEvent.index = event.index;
clonedEvent.name = event.name;
clonedEvent.sourceId = newComponent.id;
clonedEvent.target = event.target;
clonedEvent.appVersionId = event.appVersionId;
return clonedEvent;
});
await manager.save(Layout, clonedLayouts);
await manager.save(EventHandler, clonedEvents);
@ -141,9 +173,9 @@ export class PageService {
});
}
async updatePage(pageUpdates: UpdatePageDto) {
async updatePage(pageUpdates: UpdatePageDto, appVersionId: string) {
if (Object.keys(pageUpdates.diff).length > 1) {
return this.updatePagesOrder(pageUpdates.diff);
return this.updatePagesOrder(pageUpdates.diff, appVersionId);
}
const currentPage = await this.pageRepository.findOne(pageUpdates.pageId);
@ -154,7 +186,7 @@ export class PageService {
return this.pageRepository.update(pageUpdates.pageId, pageUpdates.diff);
}
async updatePagesOrder(pages) {
async updatePagesOrder(pages, appVersionId: string) {
const pagesToPage = Object.keys(pages).map((pageId) => {
return {
id: pageId,
@ -162,18 +194,18 @@ export class PageService {
};
});
return await dbTransactionWrap(async (manager: EntityManager) => {
return await dbTransactionForAppVersionAssociationsUpdate(async (manager: EntityManager) => {
await Promise.all(
pagesToPage.map(async (page) => {
await manager.update(Page, page.id, page);
})
);
});
}, appVersionId);
}
async deletePage(pageId: string, appVersionId: string) {
const { editingVersion } = await this.appService.findAppFromVersion(appVersionId);
return dbTransactionWrap(async (manager: EntityManager) => {
return dbTransactionForAppVersionAssociationsUpdate(async (manager: EntityManager) => {
const pageExists = await manager.findOne(Page, pageId);
if (!pageExists) {
@ -200,7 +232,7 @@ export class PageService {
await manager.update(Page, page.id, page);
})
);
});
}, appVersionId);
}
rearrangePagesOnDelete(pages: Page[], pageDeletedIndex: number) {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"name": "Customer support admin",
"description": "The Customer Support Admin template streamlines support with a Main Dashboard for ticket management and All Contacts for maintaining customer data.",
"widgets": ["Table", "Chart"],
"sources": [
{
"name": "Tooljet Database",
"id": "tooljetdb"
}
],
"id": "customer-support-admin",
"category": "operations"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"name": "Customer ticketing form",
"description": "The Customer Ticketing Form optimizes support ticket management, seamlessly gathering customer info and tracking ticket progress with deep integration into Customer Support Admin.",
"widgets": ["Table", "Chart"],
"sources": [
{
"name": "Tooljet Database",
"id": "tooljetdb"
}
],
"id": "customer-ticketing-form",
"category": "operations"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"name": "Inventory management",
"description": "Easily manage, control, and optimise your inventory with our single-page Inventory Management Template.",
"widgets": ["Table", "Chart"],
"sources": [
{
"name": "Tooljet Database",
"id": "tooljetdb"
}
],
"id": "inventory-management",
"category": "operations"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"name": "Lead management system",
"description": "The Lead Management System template streamlines lead lifecycle with four status templates: Leads (capturing potential leads) and Opportunities (converting leads), along with Customers (effective relationship management) and Lost (learning from lost opportunities).",
"widgets": ["Table", "Chart"],
"sources": [
{
"name": "Tooljet Database",
"id": "tooljetdb"
}
],
"id": "lead-management-system",
"category": "sales"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"name": "Sales analytics dashboard",
"description": "The Sales Analytics Dashboard template offers comprehensive sales monitoring and insights with four key sections: Main Dashboard (critical metrics), Orders (order details), Customers (demographics & history), and Products (product performance).",
"widgets": ["Table", "Chart"],
"sources": [
{
"name": "Tooljet Database",
"id": "tooljetdb"
}
],
"id": "sales-analytics-dashboard",
"category": "sales"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"name": "Supply chain management",
"description": "The Supply Chain Management template optimizes operations with a Main Dashboard for KPIs, Product Inventory for stock management, and All Orders for tracking and updates.",
"widgets": ["Table", "Chart"],
"sources": [
{
"name": "Tooljet Database",
"id": "tooljetdb"
}
],
"id": "supply-chain-management",
"category": "operations"
}

View file

@ -42,12 +42,12 @@ describe('library apps controller', () => {
response = await request(app.getHttpServer())
.post('/api/library_apps')
.send({ identifier: 'github-contributors' })
.send({ identifier: 'supply-chain-management' })
.set('tj-workspace-id', adminUserData.user.defaultOrganizationId)
.set('Cookie', adminUserData['tokenCookie']);
expect(response.statusCode).toBe(201);
expect(response.body.name).toContain('GitHub Contributor Leaderboard');
expect(response.body.app[0].name).toContain('Supply Chain Management');
});
it('should return error if template identifier is not found', async () => {