Merge branch 'appdefinition-architecture-revamp' of https://github.com/ToolJet/ToolJet into feat/react-moveable-integration
|
|
@ -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
|
||||
201
.github/workflows/cypress-marketplace.yml
vendored
Normal 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
|
|
@ -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
|
||||
351
.github/workflows/render-preview-deploy-pro.yml
vendored
|
|
@ -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)
|
||||
}
|
||||
45
.github/workflows/render-suspend-labeler-pro.yml
vendored
|
|
@ -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']
|
||||
})
|
||||
}
|
||||
2
.version
|
|
@ -1 +1 @@
|
|||
2.19.2
|
||||
2.20.1
|
||||
|
|
|
|||
79
docs/docs/user-authentication/sso/saml.md
Normal 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://<your-identity-provider>/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.
|
||||
|
|
@ -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
|
After Width: | Height: | Size: 192 KiB |
BIN
docs/static/img/sso/saml/config.png
vendored
Normal file
|
After Width: | Height: | Size: 431 KiB |
BIN
docs/static/img/sso/saml/enable.png
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/sso/saml/login.png
vendored
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
docs/static/img/sso/saml/url.png
vendored
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
docs/static/img/sso/saml/workspaceset.png
vendored
Normal file
|
After Width: | Height: | Size: 102 KiB |
|
|
@ -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://<your-identity-provider>/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.
|
||||
|
|
@ -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 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
2.19.2
|
||||
2.20.1
|
||||
|
|
|
|||
BIN
frontend/assets/images/templates/customer-support-admin-dark.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
frontend/assets/images/templates/customer-support-admin.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 53 KiB |
BIN
frontend/assets/images/templates/customer-ticketing-form.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
frontend/assets/images/templates/inventory-management-dark.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
frontend/assets/images/templates/inventory-management.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
frontend/assets/images/templates/lead-management-system-dark.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/assets/images/templates/lead-management-system.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 64 KiB |
BIN
frontend/assets/images/templates/sales-analytics-dashboard.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 239 KiB |
BIN
frontend/assets/images/templates/supply-chain-management.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
|
|
@ -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} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
|
|
|||
|
|
@ -843,7 +843,7 @@ export const Container = ({
|
|||
our
|
||||
<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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = '/';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ export const SubContainer = ({
|
|||
enableReleasedVersionPopupState();
|
||||
return;
|
||||
}
|
||||
console.log('---arpit---onDragStop---');
|
||||
|
||||
const canvasWidth = getContainerCanvasWidth();
|
||||
const nodeBounds = direction.node.getBoundingClientRect();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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}}' };
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.19.2
|
||||
2.20.1
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
102
server/src/dto/component.dto.ts
Normal 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 }>;
|
||||
}
|
||||
44
server/src/dto/event-handler.dto.ts
Normal 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';
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -48,5 +48,6 @@ if (process.env.ENABLE_TOOLJET_DB === 'true') {
|
|||
CredentialsService,
|
||||
PostgrestProxyService,
|
||||
],
|
||||
exports: [ImportExportResourcesService],
|
||||
})
|
||||
export class ImportExportResourcesModule {}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export class AppEnvironmentService {
|
|||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
select: ['id', 'name', 'appId'],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
60908
server/templates/customer-support-admin/definition.json
Normal file
13
server/templates/customer-support-admin/manifest.json
Normal 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"
|
||||
}
|
||||
29622
server/templates/customer-ticketing-form/definition.json
Normal file
13
server/templates/customer-ticketing-form/manifest.json
Normal 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"
|
||||
}
|
||||
39620
server/templates/inventory-management/definition.json
Normal file
13
server/templates/inventory-management/manifest.json
Normal 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"
|
||||
}
|
||||
43519
server/templates/lead-management-system/definition.json
Normal file
13
server/templates/lead-management-system/manifest.json
Normal 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"
|
||||
}
|
||||
70760
server/templates/sales-analytics-dashboard/definition.json
Normal file
13
server/templates/sales-analytics-dashboard/manifest.json
Normal 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"
|
||||
}
|
||||
111567
server/templates/supply-chain-management/definition.json
Normal file
13
server/templates/supply-chain-management/manifest.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||