diff --git a/.all-contributorsrc b/.all-contributorsrc index cdcc6091b5..b87bbae192 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -216,7 +216,79 @@ "contributions": [ "code" ] + }, + { + "login": "nidhi-nair", + "name": "Nidhi", + "avatar_url": "https://avatars2.githubusercontent.com/u/5298848?v=4", + "profile": "https://github.com/nidhi-nair", + "contributions": [ + "code" + ] + }, + { + "login": "jsartisan", + "name": "Pawan Kumar", + "avatar_url": "https://avatars1.githubusercontent.com/u/6636360?v=4", + "profile": "https://github.com/jsartisan", + "contributions": [ + "code" + ] + }, + { + "login": "sumitsum", + "name": "Sumit Kumar", + "avatar_url": "https://avatars0.githubusercontent.com/u/1757421?v=4", + "profile": "https://github.com/sumitsum", + "contributions": [ + "code" + ] } + { + "login": "rishabhsaxena", + "name": "Rishabh Saxena ", + "avatar_url": "https://avatars0.githubusercontent.com/u/1944800?v=4", + "profile": "https://github.com/rishabhsaxena", + "contributions": [ + "code" + ] + }, + { + "login": "ofpiyush", + "name": "Piyush Mishra", + "avatar_url": "https://avatars0.githubusercontent.com/u/292174?v=4", + "profile": "https://github.com/ofpiyush", + "contributions": [ + "code" + ] + }, + { + "login": "akash-codemonk", + "name": "akash-codemonk", + "avatar_url": "https://avatars2.githubusercontent.com/u/67054171?v=4", + "profile": "https://github.com/akash-codemonk", + "contributions": [ + "code" + ] + }, + { + "login": "vicky-primathon", + "name": "vicky-primathon", + "avatar_url": "https://avatars2.githubusercontent.com/u/67091118?v=4", + "profile": "https://github.com/vicky-primathon", + "contributions": [ + "code" + ] + }, + { + "login": "devrk96", + "name": "devrk96", + "avatar_url": "https://avatars0.githubusercontent.com/u/68607686?v=4", + "profile": "https://github.com/devrk96", + "contributions": [ + "code" + ] + }, ], "contributorsPerLine": 7, "projectName": "appsmith", diff --git a/.github/config.json b/.github/config.json index a049a7317e..65df53be3a 100644 --- a/.github/config.json +++ b/.github/config.json @@ -215,11 +215,21 @@ "color": "e8b851", "description": "Needs attention from maintainers to triage" }, + "New Widget": { + "name": "New Widget", + "color": "693BA1", + "description": "Issues related to new widgets" + }, "Omnibar": { "name": "Omnibar", "color": "10b5ce", "description": "" }, + "Onboarding": { + "name": "Onboarding", + "color": "079829", + "description": "" + }, "Pages": { "name": "Pages", "color": "dee258", @@ -305,10 +315,15 @@ "color": "d130d1", "description": "" }, + "UI Building Pod": { + "name": "UI Building Pod", + "color": "e2ffb2", + "description": "Issues picked up by the UI building pod" + }, "UI Building": { "name": "UI Building", - "color": "e2ffb2", - "description": "" + "color": "006b75", + "description": "Issues related to the building UI on the canvas" }, "UI Improvement": { "name": "UI Improvement", @@ -400,10 +415,15 @@ "color": "e053d6", "description": "API configuration section" }, + "Actions Pod": { + "name": "Actions Pod", + "color": "61ed84", + "description": "Issues picked up by the actions pod" + }, "Actions": { "name": "Actions", - "color": "61ed84", - "description": "Issues related to API / Query execution" + "color": "c5def5", + "description": "Issues related to API / Query / JS execution" }, "Datasources": { "name": "Datasources", @@ -436,7 +456,7 @@ "prereleaseName": "alpha", "issue": { "labels": { - "Actions": { + "Actions Pod": { "requires": 1, "conditions": [ { @@ -463,6 +483,11 @@ "type": "hasLabel", "label": "Auto Complete", "value": true + }, + { + "type": "hasLabel", + "label": "Actions", + "value": true } ] }, @@ -559,6 +584,11 @@ "type": "hasLabel", "value": true }, + { + "label": "New Widget", + "type": "hasLabel", + "value": true + }, { "label": "Widgets", "type": "hasLabel", @@ -566,7 +596,7 @@ } ] }, - "UI Building": { + "UI Building Pod": { "requires": 1, "conditions": [ { @@ -583,6 +613,11 @@ "type": "hasLabel", "label": "Property Pane", "value": true + }, + { + "type": "hasLabel", + "label": "UI Building", + "value": true } ] }, @@ -638,6 +673,16 @@ "type": "hasLabel", "label": "Telemetry", "value": true + }, + { + "type": "hasLabel", + "label": "Editor", + "value": true + }, + { + "type": "hasLabel", + "label": "Onboarding", + "value": true } ] } diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index af402acc1d..f216705ee0 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -1,12 +1,16 @@ name: Appsmith Client Workflow on: + # This line enables manual triggering of this workflow. + workflow_dispatch: + push: branches: [release, master] # Only trigger if files have changed in this specific path paths: - 'app/client/**' - '!app/client/cypress/manual_TestSuite/**' + pull_request_target: branches: [release, master] paths: @@ -29,14 +33,17 @@ jobs: steps: # Checkout the code - name: Checkout the merged commit from PR and base branch - if: ${{ github.event_name == 'pull_request_target' }} + if: github.event_name == 'pull_request_target' uses: actions/checkout@v2 with: + fetch-depth: 0 ref: refs/pull/${{ github.event.pull_request.number }}/merge - name: Checkout the head commit of the branch - if: ${{ github.event_name == 'push' }} + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Figure out the PR number run: echo ${{ github.event.pull_request.number }} @@ -75,6 +82,13 @@ jobs: if [[ "${{github.ref}}" == "refs/heads/release" ]]; then echo "::set-output name=REACT_APP_ENVIRONMENT::STAGING" fi + # Since this is an unreleased build, we set the version to incremented version number with + # a `-SNAPSHOT` suffix. + latest_released_version="$(git tag --list 'v*' --sort=-version:refname | head -1)" + echo "latest_released_version = $latest_released_version" + next_version="$(echo "$latest_released_version" | awk -F. -v OFS=. '{ $NF++; print }')" + echo "next_version = $next_version" + echo ::set-output name=version::$next_version-SNAPSHOT - name: Run the jest tests run: REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} yarn run test:unit @@ -82,7 +96,13 @@ jobs: # We burn React environment & the Segment analytics key into the build itself. # This is to ensure that we don't need to configure it in each installation - name: Create the bundle - run: REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} REACT_APP_FUSIONCHARTS_LICENSE_KEY=${{ secrets.APPSMITH_FUSIONCHARTS_LICENSE_KEY }} REACT_APP_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} yarn build + run: | + REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} \ + REACT_APP_FUSIONCHARTS_LICENSE_KEY=${{ secrets.APPSMITH_FUSIONCHARTS_LICENSE_KEY }} \ + REACT_APP_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} \ + REACT_APP_VERSION_ID=${{ steps.vars.outputs.version }} \ + REACT_APP_VERSION_RELEASE_DATE=$(date +%Y-%m-%d) \ + yarn build # Upload the build artifact so that it can be used by the test & deploy job in the workflow - name: Upload react build bundle @@ -121,13 +141,13 @@ jobs: steps: # Checkout the code - name: Checkout the merged commit from PR and base branch - if: ${{ github.event_name == 'pull_request_target' }} + if: github.event_name == 'pull_request_target' uses: actions/checkout@v2 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge - name: Checkout the head commit of the branch - if: ${{ github.event_name == 'push' }} + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' uses: actions/checkout@v2 - name: Use Node.js 10.16.3 @@ -150,7 +170,6 @@ jobs: ${{ runner.OS }}-node- ${{ runner.OS }}- - # Install all the dependencies - name: Install dependencies run: yarn install @@ -168,12 +187,15 @@ jobs: echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker run -d --net=host --name appsmith-internal-server -p 8080:8080 \ - --env APPSMITH_MONGODB_URI=mongodb://localhost:27017/appsmith \ - --env APPSMITH_REDIS_URL=redis://localhost:6379 \ - --env APPSMITH_ENCRYPTION_PASSWORD=password \ - --env APPSMITH_ENCRYPTION_SALT=salt \ - --env APPSMITH_IS_SELF_HOSTED=false \ - appsmith/appsmith-server:release + --env APPSMITH_MONGODB_URI=mongodb://localhost:27017/appsmith \ + --env APPSMITH_REDIS_URL=redis://localhost:6379 \ + --env APPSMITH_ENCRYPTION_PASSWORD=password \ + --env APPSMITH_ENCRYPTION_SALT=salt \ + --env APPSMITH_IS_SELF_HOSTED=false \ + --env APPSMITH_CLOUD_SERVICES_BASE_URL= \ + --env APPSMITH_CLOUD_SERVICES_USERNAME= \ + --env APPSMITH_CLOUD_SERVICES_PASSWORD= \ + ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:release - name: Pull master server docker container and start it locally if: github.ref == 'refs/heads/master' @@ -182,12 +204,15 @@ jobs: echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker run -d --net=host --name appsmith-internal-server -p 8080:8080 \ - --env APPSMITH_MONGODB_URI=mongodb://localhost:27017/appsmith \ - --env APPSMITH_REDIS_URL=redis://localhost:6379 \ - --env APPSMITH_ENCRYPTION_PASSWORD=password \ - --env APPSMITH_ENCRYPTION_SALT=salt \ - --env APPSMITH_IS_SELF_HOSTED=false \ - appsmith/appsmith-server:nightly + --env APPSMITH_MONGODB_URI=mongodb://localhost:27017/appsmith \ + --env APPSMITH_REDIS_URL=redis://localhost:6379 \ + --env APPSMITH_ENCRYPTION_PASSWORD=password \ + --env APPSMITH_ENCRYPTION_SALT=salt \ + --env APPSMITH_IS_SELF_HOSTED=false \ + --env APPSMITH_CLOUD_SERVICES_BASE_URL= \ + --env APPSMITH_CLOUD_SERVICES_USERNAME= \ + --env APPSMITH_CLOUD_SERVICES_PASSWORD= \ + ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:nightly - name: Installing Yarn serve run: | @@ -259,13 +284,13 @@ jobs: steps: # Checkout the code - name: Checkout the merged commit from PR and base branch - if: ${{ github.event_name == 'pull_request_target' }} + if: github.event_name == 'pull_request_target' uses: actions/checkout@v2 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge - name: Checkout the head commit of the branch - if: ${{ github.event_name == 'push' }} + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' uses: actions/checkout@v2 - name: Download the react build artifact @@ -282,17 +307,17 @@ jobs: # Build release Docker image and push to Docker Hub - name: Push release image to Docker Hub - if: success() && github.ref == 'refs/heads/release' && github.event_name == 'push' + if: success() && github.ref == 'refs/heads/release' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') run: | - docker build -t appsmith/appsmith-editor:${{steps.branch_name.outputs.tag}} . + docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${{steps.branch_name.outputs.tag}} . echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - docker push appsmith/appsmith-editor + docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor # Build master Docker image and push to Docker Hub - name: Push production image to Docker Hub with commit tag - if: success() && github.ref == 'refs/heads/master' && github.event_name == 'push' + if: success() && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') run: | - docker build -t appsmith/appsmith-editor:${GITHUB_SHA} . - docker build -t appsmith/appsmith-editor:nightly . + docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${GITHUB_SHA} . + docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:nightly . echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - docker push appsmith/appsmith-editor + docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index edb7477e04..523c864831 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -1,14 +1,53 @@ name: Appsmith Github Release Workflow +# This workflow builds Docker images for server and client, and then pushes them to Docher Hub. +# The docker-tag with which this push happens in the release tag (e.g., v1.2.3 etc.). +# In addition to the above tag, unless the git-tag matches `*beta*`, we also push to the `latest` docker-tag. +# This workflow does NOT run tests. +# This workflow is automatically triggered when a relese is created on GitHub. + on: - push: - # Only trigger if a tag has been created and pushed to this branch - tags: - - v* + # Ref: . + release: + types: + # Unlike the `released` event, the `published` event triggers for pre-releases as well. + - released jobs: - build-client: + prelude: runs-on: ubuntu-latest + + outputs: + tag: ${{ steps.get_version.outputs.tag }} + version: ${{ steps.get_version.outputs.version }} + is_beta: ${{ steps.get_version.outputs.is_beta }} + + steps: + - name: Environment details + run: | + echo "PWD: $PWD" + echo "GITHUB_REF: $GITHUB_REF" + echo "GITHUB_SHA: $GITHUB_SHA" + echo "GITHUB_EVENT_NAME: $GITHUB_EVENT_NAME" + + - name: Get the version + id: get_version + run: | + tag="${GITHUB_REF#refs/tags/}" + echo "::set-output name=version::${tag#v}" + echo "::set-output name=tag::$tag" + if [[ $tag == *"beta"* ]]; then + echo "::set-output name=is_beta::true" + else + echo "::set-output name=is_beta::false" + fi + + build-client: + needs: + - prelude + + runs-on: ubuntu-latest + defaults: run: working-directory: app/client @@ -39,41 +78,40 @@ jobs: - name: Install dependencies run: yarn install - - name: Set the build environment based on the branch - id: vars - run: | - REACT_APP_ENVIRONMENT="PRODUCTION" - echo ::set-output name=REACT_APP_ENVIRONMENT::${REACT_APP_ENVIRONMENT} - - name: Create the bundle - run: REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} REACT_APP_FUSIONCHARTS_LICENSE_KEY=${{ secrets.APPSMITH_FUSIONCHARTS_LICENSE_KEY }} REACT_APP_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} yarn build - - - name: Get the version - id: get_version - run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + env: + REACT_APP_ENVIRONMENT: 'PRODUCTION' + REACT_APP_FUSIONCHARTS_LICENSE_KEY: '${{ secrets.APPSMITH_FUSIONCHARTS_LICENSE_KEY }}' + REACT_APP_SEGMENT_CE_KEY: '${{ secrets.APPSMITH_SEGMENT_CE_KEY }}' + REACT_APP_VERSION_ID: '${{ needs.prelude.outputs.version }}' + run: 'REACT_APP_VERSION_RELEASE_DATE="$(date +%Y-%m-%d)" yarn build' # Build Docker image and push to Docker Hub - name: Push production image to Docker Hub with commit tag run: | - docker build -t appsmith/appsmith-editor:${{steps.get_version.outputs.tag}} . + docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${{needs.prelude.outputs.tag}} . # Only build & tag with latest if the tag doesn't contain beta - if [[ ! ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then - docker build -t appsmith/appsmith-editor:latest . + if [[ ! ${{needs.prelude.outputs.tag}} == *"beta"* ]]; then + docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:latest . fi echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - docker push appsmith/appsmith-editor + docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor build-server: + needs: + - prelude + runs-on: ubuntu-latest + defaults: run: working-directory: app/server steps: - # Checkout the code - - uses: actions/checkout@v2 + - name: Checkout the code + uses: actions/checkout@v2 # Setup Java - name: Set up JDK 1.11 @@ -94,55 +132,22 @@ jobs: # Build the code - name: Build without running any tests - run: mvn -B package -DskipTests - - - name: Get the version - id: get_version - run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + run: | + mvn --batch-mode versions:set \ + -DnewVersion=${{ needs.prelude.outputs.version }} \ + -DgenerateBackupPoms=false \ + -DprocessAllModules=true + mvn --batch-mode package -DskipTests # Build Docker image and push to Docker Hub - name: Push image to Docker Hub run: | - docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t appsmith/appsmith-server:${{steps.get_version.outputs.tag}} . + docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:${{needs.prelude.outputs.tag}} . # Only build & tag with latest if the tag doesn't contain beta - if [[ ! ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then - docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t appsmith/appsmith-server:latest . + if [[ ! ${{needs.prelude.outputs.tag}} == *"beta"* ]]; then + docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:latest . fi echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - docker push appsmith/appsmith-server - - create-release: - needs: - - build-server - - build-client - runs-on: ubuntu-latest - - steps: - # Creating the release on Github - - name: Get the version - id: get_version - run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - # If the tag has the string "beta", then mark the Github release as a pre-release - - name: Get the version - id: get_prerelease - run: | - STATUS=false - if [[ ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then - STATUS=true - fi - - echo ::set-output name=status::${STATUS} - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: false - prerelease: ${{steps.get_prerelease.outputs.status}} + docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 50f2274515..a34cf4c033 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -2,15 +2,20 @@ name: Appsmith Server Workflow on: + # This line enables manual triggering of this workflow. + workflow_dispatch: + push: branches: [ release, master ] # Only trigger if files have changed in this specific path paths: - 'app/server/**' + pull_request: branches: [ release, master ] paths: - 'app/server/**' + # Change the working directory for all the jobs in this workflow defaults: run: @@ -33,6 +38,8 @@ jobs: steps: # Checkout the code - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Setup Java - name: Set up JDK 1.11 @@ -51,6 +58,22 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 + # Here, the GITHUB_REF is of type /refs/head/. We extract branch_name from this by removing the + # first 11 characters. This can be used to build images for several branches + # Since this is an unreleased build, we get the latest released version number, increment the minor number in it, + # append a `-SNAPSHOT` at it's end to prepare the snapshot version number. This is used as the project's version. + - name: Get the version to tag the Docker image + id: vars + run: | + # Since this is an unreleased build, we set the version to incremented version number with a + # `-SNAPSHOT` suffix. + latest_released_version="$(git tag --list 'v*' --sort=-version:refname | head -1)" + echo "latest_released_version = $latest_released_version" + next_version="$(echo "$latest_released_version" | awk -F. -v OFS=. '{ $NF++; print }')" + echo "next_version = $next_version" + echo ::set-output name=version::$next_version-SNAPSHOT + echo ::set-output name=tag::$(echo ${GITHUB_REF:11}) + # Build and test the code - name: Build and test env: @@ -59,37 +82,36 @@ jobs: APPSMITH_ENCRYPTION_PASSWORD: "password" APPSMITH_ENCRYPTION_SALT: "salt" APPSMITH_IS_SELF_HOSTED: false - run: mvn -B package - - # Here, the GITHUB_REF is of type /refs/head/. We extract branch_name from this by removing the - # first 11 characters. This can be used to build images for several branches - - name: Get the version to tag the Docker image - id: vars - run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11}) + run: | + mvn --batch-mode versions:set \ + -DnewVersion=${{ steps.vars.outputs.version }} \ + -DgenerateBackupPoms=false \ + -DprocessAllModules=true + mvn --batch-mode package # Build release Docker image and push to Docker Hub - name: Push release image to Docker Hub if: success() && github.ref == 'refs/heads/release' run: | - docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t appsmith/appsmith-server:${{steps.vars.outputs.tag}} . + docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:${{steps.vars.outputs.tag}} . echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - # This command pushes all the tags on the machine to Docker hub. This has been written for ease of reading. Be very careful + # This command pushes all the tags on the machine to Docker hub. This has been written for ease of reading. Be very careful # with this command - docker push appsmith/appsmith-server + docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server # Build master Docker image and push to Docker Hub - name: Push master image to Docker Hub with commit tag if: success() && github.ref == 'refs/heads/master' run: | - docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t appsmith/appsmith-server:${GITHUB_SHA} . - docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t appsmith/appsmith-server:nightly . + docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:${GITHUB_SHA} . + docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:nightly . echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - # This command pushes all the tags on the machine to Docker hub. This has been written for ease of reading. Be very careful + # This command pushes all the tags on the machine to Docker hub. This has been written for ease of reading. Be very careful # with this command - docker push appsmith/appsmith-server - - # These are dummy jobs in the CI build to satisfy required status checks for merging PRs. This is a hack because Github doesn't support conditional - # required checks in monorepos. These jobs are a clone of similarly named jobs in client.yml. + docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server + + # These are dummy jobs in the CI build to satisfy required status checks for merging PRs. This is a hack because Github doesn't support conditional + # required checks in monorepos. These jobs are a clone of similarly named jobs in client.yml. # # Check support request at: https://github.community/t/feature-request-conditional-required-checks/16761 ui-test: @@ -98,25 +120,25 @@ jobs: fail-fast: false matrix: job: [0, 1, 2, 3, 4, 5, 6] - + steps: # Checkout the code - uses: actions/checkout@v2 - + - name: Do nothing as this is a dummy step shell: bash run: | exit 0 - + package: runs-on: ubuntu-latest steps: # Checkout the code - uses: actions/checkout@v2 - + - name: Do nothing as this is a dummy step shell: bash run: | exit 0 - + diff --git a/.github/workflows/sync-community-repo.yml b/.github/workflows/sync-community-repo.yml index a9a581db97..55dae7573e 100644 --- a/.github/workflows/sync-community-repo.yml +++ b/.github/workflows/sync-community-repo.yml @@ -2,13 +2,14 @@ name: Sync Community workflow on: + workflow_dispatch: schedule: - - cron: "0 * * * *" + - cron: "0 * * * *" jobs: repo-sync: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v2 with: diff --git a/README.md b/README.md index b09ae48ae2..8f0cb4774d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Appsmith is a JavaScript-based visual development platform to build and launch i **UI Components**: Table, Chart, Form, Map, Image, Video, and many more.
**API Support**: REST APIs
-**Database Support**: PostgreSQL, MongoDB, MySQL, Redshift, Elastic Search, DynamoDB, Redis, and MSFT SQL Server
+**Database Support**: PostgreSQL, MongoDB, MySQL, Firestore, Redshift, Elastic Search, DynamoDB, Redis, and MSFT SQL Server
**Hosting**: Cloud-hosted & On-premise Already familiar with Appsmith? [Quickly start building on your own](#%EF%B8%8F-quickstart). @@ -101,37 +101,48 @@ The Appsmith platform is available under the [Apache License 2.0](https://www.ap - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - + + + + + + + + + + +

Arpit Mohan

💻

Nikhil Nandagopal

📖 💻 📆

areyabhishek

🤔 🎨

Trisha Anand

💻 🚇 🤔

Hetu Nandu

💻 ⚠️ 🤔

Abhinav Jha

💻

satbir121

💻 🤔

Arpit Mohan

💻

Nikhil Nandagopal

📖 💻 📆

areyabhishek

🤔 🎨

Trisha Anand

💻 🚇 🤔

Hetu Nandu

💻 ⚠️ 🤔

Abhinav Jha

💻

satbir121

💻 🤔

Shrikant Sharat Kandula

💻 🔌

Aakash Shrivastava

🎨

Debsourabh Ghosh

🎨

NandanAnantharamu

⚠️

prapullac

🐛 ⚠️

Saket Agrawal

🐛 📖

Harish Kotra

🐛

Shrikant Sharat Kandula

💻 🔌

Aakash Shrivastava

🎨

Debsourabh Ghosh

🎨

NandanAnantharamu

⚠️

prapullac

🐛 ⚠️

Saket Agrawal

🐛 📖

Harish Kotra

🐛

Ajay Kumar

🐛 📖

Anshul Bansal

🐛 💻

Navia Garg

🐛

Xniveres

🐛

Daniel Shuy

💻 📖

Prashant Chaubey

💻

Adam

💻

Ajay Kumar

🐛 📖

Anshul Bansal

🐛 💻

Navia Garg

🐛

Xniveres

🐛

Daniel Shuy

💻 📖

Prashant Chaubey

💻

Adam

💻

Sumanth Yedoti

💻

Sumanth Yedoti

💻

Nidhi

💻

Pawan Kumar

💻

Sumit Kumar

💻

Rishabh Saxena

💻

Piyush Mishra

💻

akash-codemonk

💻

vicky-primathon

💻

devrk96

💻
- + + diff --git a/app/client/.eslintrc.json b/app/client/.eslintrc.json index ba5f880ccc..0e6d1239e1 100644 --- a/app/client/.eslintrc.json +++ b/app/client/.eslintrc.json @@ -26,18 +26,21 @@ "import/no-webpack-loader-syntax": 0, "no-undef": 0, "react/prop-types": 0, - "@typescript-eslint/explicit-module-boundary-types": 0 + "@typescript-eslint/explicit-module-boundary-types": 0, + "cypress/no-unnecessary-waiting": 0, + "cypress/no-assigning-return-values": 0 }, "settings": { "react": { "pragma": "React", // Tells eslint-plugin-react to automatically detect the version of React to use - "version": "detect" + "version": "detect" } }, "env": { "browser": true, "node": true, - "cypress/globals": true + "cypress/globals": true, + "worker": true } } diff --git a/app/client/README.md b/app/client/README.md index 0ae785bd51..2a275ea504 100755 --- a/app/client/README.md +++ b/app/client/README.md @@ -7,4 +7,3 @@ This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). For details on setting up your development machine, please refer to the [Setup Guide](https://github.com/appsmithorg/appsmith/blob/release/contributions/ClientSetup.md) - diff --git a/app/client/craco.build.config.js b/app/client/craco.build.config.js index 8b02d51143..038b77f9b6 100644 --- a/app/client/craco.build.config.js +++ b/app/client/craco.build.config.js @@ -18,16 +18,25 @@ plugins.push( ); if (env === "PRODUCTION" || env === "STAGING") { - plugins.push( - new SentryWebpackPlugin({ - include: "build", - ignore: ["node_modules", "webpack.config.js"], - release: process.env.REACT_APP_SENTRY_RELEASE, - deploy: { - env: process.env.REACT_APP_SENTRY_ENVIRONMENT - } - }) - ); + if ( + process.env.SENTRY_AUTH_TOKEN != null && + process.env.SENTRY_AUTH_TOKEN !== "" + ) { + plugins.push( + new SentryWebpackPlugin({ + include: "build", + ignore: ["node_modules", "webpack.config.js"], + release: process.env.REACT_APP_SENTRY_RELEASE, + deploy: { + env: process.env.REACT_APP_SENTRY_ENVIRONMENT + } + }) + ); + } else { + console.log( + "Sentry configuration missing in process environment. Sentry will be disabled.", + ); + } } module.exports = merge(common, { diff --git a/app/client/cypress/fixtures/appsmithlogo.png b/app/client/cypress/fixtures/appsmithlogo.png index 949bfcc3fd..856b062b7e 100644 Binary files a/app/client/cypress/fixtures/appsmithlogo.png and b/app/client/cypress/fixtures/appsmithlogo.png differ diff --git a/app/client/cypress/fixtures/datasources.json b/app/client/cypress/fixtures/datasources.json index 65bcd07d77..c9bb0e0069 100644 --- a/app/client/cypress/fixtures/datasources.json +++ b/app/client/cypress/fixtures/datasources.json @@ -1,9 +1,9 @@ { - "mongo-host": "ds119422.mlab.com", + "mongo-host": "cypress-test.zljea.mongodb.net", "mongo-port": 19422, - "mongo-databaseName": "heroku_bcmprc4k", - "mongo-username": "akash", - "mongo-password": "123wheel", + "mongo-databaseName": "admin", + "mongo-username": "cypress-test", + "mongo-password": "RaopEmky505xYV4p", "mongo-authenticationAuthtype": "SCRAM-SHA-1", "mongo-sslAuthtype": "No SSL", "postgres-host": "postgres-test-db.cz8diybf9wdj.ap-south-1.rds.amazonaws.com", @@ -11,5 +11,7 @@ "postgres-databaseName": "fakeapi", "postgres-username": "postgres", "postgres-password": "Appsmith2019#", - "restapi-url": "https://my-json-server.typicode.com/typicode/demo/posts" + "restapi-url": "https://my-json-server.typicode.com/typicode/demo/posts", + "mongo-defaultDatabaseName": "sample_airbnb", + "connection-type": "Replica set" } diff --git a/app/client/cypress/fixtures/formdsl1.json b/app/client/cypress/fixtures/formdsl1.json index 32bdad64a2..22d7d0acc4 100644 --- a/app/client/cypress/fixtures/formdsl1.json +++ b/app/client/cypress/fixtures/formdsl1.json @@ -91,7 +91,7 @@ }, { "isVisible": true, - "defaultText": "This is the initial content of the editor", + "defaultText": "", "isDisabled": false, "widgetName": "RichTextEditor1", "isDefaultClickDisabled": true, diff --git a/app/client/cypress/fixtures/newFormDsl.json b/app/client/cypress/fixtures/newFormDsl.json index ee06ab89bb..f321674a52 100644 --- a/app/client/cypress/fixtures/newFormDsl.json +++ b/app/client/cypress/fixtures/newFormDsl.json @@ -126,14 +126,12 @@ "label": "", "options": [ { - "id": "1", - "label": "Male", - "value": "M" + "label":"Male", + "value":"M" }, { - "id": "2", - "label": "Female", - "value": "F" + "label":"Female", + "value":"F" } ], "defaultOptionValue": "1", diff --git a/app/client/cypress/fixtures/testFile.mov b/app/client/cypress/fixtures/testFile.mov new file mode 100644 index 0000000000..af5b36c705 Binary files /dev/null and b/app/client/cypress/fixtures/testFile.mov differ diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index f62b32c3a6..6bc87d181f 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -87,90 +87,6 @@ "userName": "Tobias Funke", "productName": "Beef steak", "orderAmount": 19.99 - }, - { - "id": 7434532, - "email": "byron.fields@reqres.in", - "userName": "Byron Fields", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 7434532, - "email": "ryan.holmes@reqres.in", - "userName": "Ryan Holmes", - "productName": "Avocado Panini", - "orderAmount": 7.99 - }, - { - "id": 2381224, - "email": "michael.lawson@reqres.in", - "userName": "Michael Lawson", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 2736212, - "email": "lindsay.ferguson@reqres.in", - "userName": "Lindsay Ferguson", - "productName": "Tuna Salad", - "orderAmount": 9.99 - }, - { - "id": 6788734, - "email": "tobias.funke@reqres.in", - "userName": "Tobias Funke", - "productName": "Beef steak", - "orderAmount": 19.99 - }, - { - "id": 7434532, - "email": "byron.fields@reqres.in", - "userName": "Byron Fields", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 7434532, - "email": "ryan.holmes@reqres.in", - "userName": "Ryan Holmes", - "productName": "Avocado Panini", - "orderAmount": 7.99 - }, - { - "id": 2381224, - "email": "michael.lawson@reqres.in", - "userName": "Michael Lawson", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 2736212, - "email": "lindsay.ferguson@reqres.in", - "userName": "Lindsay Ferguson", - "productName": "Tuna Salad", - "orderAmount": 9.99 - }, - { - "id": 6788734, - "email": "tobias.funke@reqres.in", - "userName": "Tobias Funke", - "productName": "Beef steak", - "orderAmount": 19.99 - }, - { - "id": 7434532, - "email": "byron.fields@reqres.in", - "userName": "Byron Fields", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 7434532, - "email": "ryan.holmes@reqres.in", - "userName": "Ryan Holmes", - "productName": "Avocado Panini", - "orderAmount": 7.99 } ], "addInputWidgetBinding": "{{Table1.selectedRow.id", diff --git a/app/client/cypress/init-pg-dump-for-test.sql b/app/client/cypress/init-pg-dump-for-test.sql new file mode 100644 index 0000000000..63c399d0be --- /dev/null +++ b/app/client/cypress/init-pg-dump-for-test.sql @@ -0,0 +1,53 @@ +CREATE TABLE public.configs ( + id integer NOT NULL, + "configName" text NOT NULL, + "configJson" jsonb, + "configVersion" integer, + "updatedAt" timestamp with time zone, + "updatedBy" text +); + +CREATE SEQUENCE public.configs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER TABLE public.configs_id_seq OWNER TO postgres; + +CREATE TABLE public.users ( + id integer NOT NULL, + name character varying, + "createdAt" timestamp with time zone, + "updatedAt" timestamp with time zone, + status character varying, + gender character varying, + avatar character varying, + email character varying, + address text, + role text, + dob date, + "phoneNo" text +); + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.users_id_seq OWNER TO postgres; + +insert into public.configs (id, "configName", "configJson", "configVersion", "updatedAt", "updatedBy") +values (3, 'New Config', '{"key": "val1"}', 1, '2020-08-26 11:14:28.458209+00', ''), +(5, 'New Config', '{"key": "val2"}', 1, '2020-08-26 11:14:28.458209+00', ''); + +insert into public.users (id, name, "createdAt", "updatedAt", status, gender, avatar, email, address, role, dob, "phoneNo") values +(7, 'Test user 7', '2019-08-07 21:36:27+00', '2019-10-21 03:23:42+00', 'APPROVED', 'Male', 'https://robohash.org/quiofficiadicta.jpg?size=100x100&set=set1' ,'xkainz6@ihg.com', '19624 Scofield Way', 'Admin','1993-08-14', ''), +(8, 'Test user 8', '2019-08-07 21:36:27+00', '2019-10-21 03:23:42+00', 'APPROVED', 'Male', 'https://robohash.org/quiofficiadicta.jpg?size=100x100&set=set1' ,'xkainz6@ihg.com', '19624 Scofield Way', 'Admin','1993-08-14', ''), +(9, 'Test user 9', '2019-08-07 21:36:27+00', '2019-10-21 03:23:42+00', 'APPROVED', 'Male', 'https://robohash.org/quiofficiadicta.jpg?size=100x100&set=set1' ,'xkainz6@ihg.com', '19624 Scofield Way', 'Admin','1993-08-14', '') diff --git a/app/client/cypress/integration/Smoke_TestSuite/ActionExecution/Action_PageOnLoad_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ActionExecution/Action_PageOnLoad_spec.js index 6cec9bc128..d660efca3d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ActionExecution/Action_PageOnLoad_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ActionExecution/Action_PageOnLoad_spec.js @@ -1,4 +1,5 @@ const dsl = require("../../../fixtures/tableWidgetDsl.json"); +const commonlocators = require("../../../locators/commonlocators.json"); describe("API Panel Test Functionality", function() { before(() => { @@ -29,21 +30,26 @@ describe("API Panel Test Functionality", function() { ); }); - it("Will not crash the app for failure", function() { - cy.SearchEntityandOpen("PageLoadApi"); - cy.get("li:contains('Settings')").click({ force: true }); - cy.get("[data-cy='actionConfiguration.timeoutInMillisecond']") - .find(".bp3-input") - .type("{backspace}{backspace}{backspace}"); - + it("Shows which action failed on action fail.", function() { cy.NavigateToAPI_Panel(); - cy.CreateAPI("NormalApi"); - cy.enterDatasourceAndPath("https://reqres.in/api/", "users"); + cy.CreateAPI("PageLoadApi2"); + cy.enterDatasourceAndPath("https://abc.com", "users"); cy.WaitAutoSave(); + cy.get("li:contains('Settings')").click({ force: true }); + cy.get("[data-cy=executeOnLoad]") + .find(".bp3-switch") + .click(); + + cy.wait("@setExecuteOnLoad"); + + cy.SearchEntityandOpen("Table1"); + cy.testJsontext("tabledata", "{{PageLoadApi2.data.data"); + + cy.wait("@updateLayout"); cy.reload(); - cy.wait("@postExecute"); - cy.RunAPI(); - cy.ResponseStatusCheck("200 OK"); + cy.get(commonlocators.toastMsg).contains( + `The action "PageLoadApi2" has failed.`, + ); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js index f9de412f2e..971e16e463 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js @@ -13,11 +13,11 @@ describe("Test curl import flow", function() { ); cy.get("textarea").type("curl -X GET https://mock-api.appsmith.com/users"); cy.importCurl(); - cy.get("@curlImport").then(response => { + cy.get("@curlImport").then((response) => { cy.expect(response.response.body.responseMeta.success).to.eq(true); cy.get(apiwidget.ApiName) .invoke("text") - .then(text => { + .then((text) => { const someText = text; expect(someText).to.equal(response.response.body.data.name); }); @@ -28,10 +28,10 @@ describe("Test curl import flow", function() { cy.get(ApiEditor.formActionButtons).should("be.visible"); cy.get(ApiEditor.ApiDeleteBtn).click(); cy.wait("@deleteAction"); - cy.get("@deleteAction").then(response => { + cy.get("@deleteAction").then((response) => { cy.expect(response.response.body.responseMeta.success).to.eq(true); }); cy.get(ApiEditor.ApiHomePage).should("be.visible"); - cy.get(ApiEditor.formActionButtons).should("not.be.visible"); + cy.get(ApiEditor.formActionButtons).should("not.exist"); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js index 072e8d0019..2c444fe58e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js @@ -4,18 +4,18 @@ const pages = require("../../../locators/Pages.json"); const apiPage = require("../../../locators/ApiEditor.json"); const publishPage = require("../../../locators/publishWidgetspage.json"); -describe("Test Create Api and Bind to Table widget", function() { +describe("Test Create Api and Bind to Table widget", function () { before(() => { cy.addDsl(dsl); }); - it("Test_Add Paginate with Table Page No and Execute the Api", function() { + it("Test_Add Paginate with Table Page No and Execute the Api", function () { /**Create an Api1 of Paginate with Table Page No */ cy.createAndFillApi(this.data.paginationUrl, this.data.paginationParam); cy.RunAPI(); }); - it("Table-Text, Validate Server Side Pagination of Paginate with Table Page No", function() { + it("Table-Text, Validate Server Side Pagination of Paginate with Table Page No", function () { cy.SearchEntityandOpen("Table1"); /**Bind Api1 with Table widget */ cy.testJsontext("tabledata", "{{Api1.data.users}}"); @@ -31,21 +31,33 @@ describe("Test Create Api and Bind to Table widget", function() { /**Validate Table data on current page(page1) */ cy.ValidateTableData("1"); cy.get(commonlocators.tableNextPage).click({ force: true }); + cy.wait(5000); + /* + cy.wait("@postExecute").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); cy.validateToastMessage("done"); /**Validate Table data on next page(page2) */ - cy.ValidateTableData("11"); + //cy.ValidateTableData("11"); }); - it("Table-Text, Validate Publish Mode on Server Side Pagination of Paginate with Table Page No", function() { + it("Table-Text, Validate Publish Mode on Server Side Pagination of Paginate with Table Page No", function () { cy.PublishtheApp(); cy.ValidatePublishTableData("1"); cy.get(commonlocators.tableNextPage).click({ force: true }); + cy.wait("@postExecute").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); cy.validateToastMessage("done"); cy.ValidatePublishTableData("11"); cy.get(publishPage.backToEditor).click({ force: true }); }); - it("Test_Add Paginate with Response URL and Execute the Api", function() { + it("Test_Add Paginate with Response URL and Execute the Api", function () { /** Create Api2 of Paginate with Response URL*/ cy.createAndFillApi(this.data.paginationUrl, "users"); cy.RunAPI(); @@ -67,7 +79,7 @@ describe("Test Create Api and Bind to Table widget", function() { cy.callApi("Api2"); }); - it("Table-Text, Validate Server Side Pagination of Paginate with Response URL", function() { + it("Table-Text, Validate Server Side Pagination of Paginate with Response URL", function () { /**Validate Response data with Table data in Text Widget */ cy.ValidatePaginateResponseUrlData(apiPage.apiPaginationPrevTest); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Binding_Table_Widget_DefaultSearch_Input_widget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Binding_Table_Widget_DefaultSearch_Input_widget_spec.js index 16b98f57c5..62536d7ea3 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Binding_Table_Widget_DefaultSearch_Input_widget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Binding_Table_Widget_DefaultSearch_Input_widget_spec.js @@ -27,7 +27,7 @@ describe("Binding the Table and input Widget", function() { .type("2736212", { force: true }); cy.get(commonlocators.editPropCrossButton).click(); cy.wait("@updateLayout").isSelectRow(0); - cy.readTabledataPublish("0", "0").then(tabData => { + cy.readTabledataPublish("0", "0").then((tabData) => { const tabValue = tabData; expect(tabValue).to.be.equal("2736212"); cy.log("the value is" + tabValue); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/ButtonWidgets_NavigateTo_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/ButtonWidgets_NavigateTo_validation_spec.js index 7a14847995..85bba06ad2 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/ButtonWidgets_NavigateTo_validation_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/ButtonWidgets_NavigateTo_validation_spec.js @@ -29,7 +29,7 @@ describe("Binding the button Widgets and validating NavigateTo Page functionalit cy.PublishtheApp(); cy.get(publish.buttonWidget).click(); cy.wait(500); - cy.get(publish.buttonWidget).should("not.be.visible"); + cy.get(publish.buttonWidget).should("not.exist"); cy.go("back"); cy.get(publish.backToEditor) .first() diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/InputWidgets_NavigateTo_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/InputWidgets_NavigateTo_validation_spec.js index 0b5d29e6d1..d7d81832b6 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/InputWidgets_NavigateTo_validation_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/InputWidgets_NavigateTo_validation_spec.js @@ -37,7 +37,7 @@ describe("Binding the multiple Widgets and validating NavigateTo Page", function it("Validate NavigateTo Page functionality ", function() { cy.SearchEntityandOpen("Table1"); cy.isSelectRow(1); - cy.readTabledataPublish("1", "0").then(tabData => { + cy.readTabledataPublish("1", "0").then((tabData) => { const tabValue = tabData; expect(tabValue).to.be.equal("2736212"); cy.log("the value is" + tabValue); @@ -45,7 +45,7 @@ describe("Binding the multiple Widgets and validating NavigateTo Page", function .first() .invoke("attr", "value") .should("contain", tabValue); - cy.get(widgetsPage.chartWidget).should("not.be.visible"); + cy.get(widgetsPage.chartWidget).should("not.exist"); cy.get(publish.inputGrp) .first() diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/TableWidgets_NavigateTo_Validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/TableWidgets_NavigateTo_Validation_spec.js index 8e1bc81d5c..75500bcb3a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/TableWidgets_NavigateTo_Validation_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/TableWidgets_NavigateTo_Validation_spec.js @@ -17,7 +17,7 @@ describe("Table Widget and Navigate to functionality validation", function() { cy.openPropertyPane("tablewidget"); cy.widgetText("Table1", widgetsPage.tableWidget, commonlocators.tableInner); cy.testJsontext("tabledata", JSON.stringify(testdata.TablePagination)); - cy.get(widgetsPage.tableActionSelect).click(); + cy.get(widgetsPage.tableOnRowSelect).click(); cy.get(commonlocators.chooseAction) .children() .contains("Navigate To") @@ -35,21 +35,12 @@ describe("Table Widget and Navigate to functionality validation", function() { it("Validate NavigateTo Page functionality ", function() { cy.SearchEntityandOpen("Table1"); + //Below test to be enabled once the bug related to change of page in table in fixed + //cy.get('.t--table-widget-next-page') + // .click(); + cy.PublishtheApp(); + cy.get(widgetsPage.chartWidget).should("not.exist"); cy.isSelectRow(1); - cy.readTabledataPublish("1", "2").then(tabData => { - const tabValue = tabData; - expect(tabValue).to.be.equal("Lindsay Ferguson"); - cy.log("the value is" + tabValue); - cy.wait(500); - cy.get(widgetsPage.chartWidget).should("not.be.visible"); - //Below test to be enabled once the bug related to change of page in table in fixed - //cy.get('.t--table-widget-next-page') - // .click(); - cy.PublishtheApp(); - cy.get(publish.searchInput) - .first() - .type(tabData); - cy.get(widgetsPage.chartWidget).should("be.visible"); - }); + cy.get(widgetsPage.chartWidget).should("be.visible"); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js index 8f038e5490..aaa02271b0 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js @@ -45,12 +45,12 @@ describe("Chart Widget Functionality", function() { cy.testJsontext("chartdata", JSON.stringify(this.data.chartInput)); cy.get(viewWidgetsPage.chartWidget) .should("be.visible") - .and(chart => { + .and((chart) => { expect(chart.height()).to.be.greaterThan(200); }); cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1"); const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; - [0, 1, 2, 3, 4, 5, 6].forEach(k => { + [0, 1, 2, 3, 4, 5, 6].forEach((k) => { cy.get(viewWidgetsPage.rectangleChart) .eq(k) .trigger("mousemove", { force: true }); @@ -72,7 +72,7 @@ describe("Chart Widget Functionality", function() { it("Chart Widget Functionality To Unchecked Visible Widget", function() { cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.chartWidget).should("not.be.visible"); + cy.get(publish.chartWidget).should("not.exist"); cy.get(publish.backToEditor).click(); }); it("Chart Widget Functionality To Check Visible Widget", function() { @@ -84,7 +84,7 @@ describe("Chart Widget Functionality", function() { it("Chart Widget Functionality To Uncheck Horizontal Scroll Visible", function() { cy.togglebarDisable(commonlocators.horizontalScroll); cy.PublishtheApp(); - cy.get(publish.horizontalTab).should("not.visible"); + cy.get(publish.horizontalTab).should("not.exist"); cy.get(publish.backToEditor).click(); }); it("Chart Widget Functionality To Check Horizontal Scroll Visible", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js index 8f81e886d6..20c355abe5 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js @@ -65,7 +65,7 @@ describe("Image Widget Functionality", function() { cy.openPropertyPane("imagewidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.imageWidget).should("not.be.visible"); + cy.get(publish.imageWidget).should("not.exist"); cy.get(publish.backToEditor).click(); }); it("Image Widget Functionality To Check Visible Widget", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Map_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Map_spec.js index 7dd657abf2..0037464370 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Map_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Map_spec.js @@ -86,7 +86,7 @@ if (Cypress.env("APPSMITH_GOOGLE_MAPS_API_KEY")) { * Disable the Search Location checkbox and Validate the same in editor mode */ cy.UncheckWidgetProperties(commonlocators.enableSearchLocCheckbox); - cy.get(viewWidgetsPage.mapSearch).should("not.be.visible"); + cy.get(viewWidgetsPage.mapSearch).should("not.exist"); /** * Disable the Pick Location checkbox and Validate the same in editor mode */ @@ -105,7 +105,7 @@ if (Cypress.env("APPSMITH_GOOGLE_MAPS_API_KEY")) { /** * Publish mode Validation */ - cy.get(publishPage.mapSearch).should("not.be.visible"); + cy.get(publishPage.mapSearch).should("not.exist"); cy.get(publishPage.pickMyLocation).should("not.exist"); cy.get(publishPage.backToEditor).click(); }); @@ -141,7 +141,7 @@ if (Cypress.env("APPSMITH_GOOGLE_MAPS_API_KEY")) { //Uncheck the disabled checkbox and validate cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publishPage.mapWidget).should("not.be.visible"); + cy.get(publishPage.mapWidget).should("not.exist"); }); }); } diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js index 0250f4f034..86f7509713 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js @@ -37,7 +37,7 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { }); cy.DeleteWidgetFromSideBar(); cy.wait(500); - cy.get(apiwidget.propertyList).should("not.be.visible"); + cy.get(apiwidget.propertyList).should("not.exist"); /* To be enabled once widget delete click works cy.get('.t--delete-widget') diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js index 0586b1ec96..109a4b46ae 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js @@ -75,7 +75,7 @@ describe("Button Widget Functionality", function() { //Uncheck the disabled checkbox and validate cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publishPage.buttonWidget).should("not.be.visible"); + cy.get(publishPage.buttonWidget).should("not.exist"); }); it("Button-Check Visible field Validation", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js index 3588508fea..8376d983e2 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js @@ -62,14 +62,14 @@ describe("Checkbox Widget Functionality", function() { cy.openPropertyPane("checkboxwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.checkboxWidget + " " + "input").should("not.be.visible"); + cy.get(publish.checkboxWidget + " " + "input").should("not.exist"); cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Check Visible Widget", function() { cy.openPropertyPane("checkboxwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.checkboxWidget + " " + "input").should("be.visible"); + cy.get(publish.checkboxWidget + " " + "input").should("be.checked"); cy.get(publish.backToEditor).click(); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js index 20af27aa1d..0fc6821471 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js @@ -146,7 +146,7 @@ describe("DatePicker Widget Functionality", function() { // Check the visible checkbox cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publishPage.datepickerWidget).should("not.be.visible"); + cy.get(publishPage.datepickerWidget).should("not.exist"); }); it("DatePicker-uncheck Visible field validation", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js index e6386ee261..ec6d5727eb 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js @@ -62,7 +62,7 @@ describe("Dropdown Widget Functionality", function() { cy.openPropertyPane("dropdownwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.dropdownWidget + " " + "input").should("not.be.visible"); + cy.get(publish.dropdownWidget + " " + "input").should("not.exist"); cy.get(publish.backToEditor).click(); }); it("Dropdown Functionality To Check Visible Widget", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js index 09963ee502..08a8e52607 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js @@ -17,7 +17,7 @@ describe("FilePicker Widget Functionality", function() { it("It checks the loading state of filepicker on call the action", function() { cy.openPropertyPane("filepickerwidget"); - const fixturePath = "example.json"; + const fixturePath = "testFile.mov"; cy.getAlert(commonlocators.filePickerOnFilesSelected); cy.get(commonlocators.filePickerButton).click(); cy.get(commonlocators.filePickerInput) @@ -25,6 +25,13 @@ describe("FilePicker Widget Functionality", function() { .attachFile(fixturePath); cy.get(commonlocators.filePickerUploadButton).click(); cy.get(".bp3-spinner").should("have.length", 1); + cy.wait("@updateLayout").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.wait(500); + cy.get("button").contains("1 files selected"); }); afterEach(() => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js index 92d6ef913e..d06f0fe713 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js @@ -47,7 +47,7 @@ describe("Form Widget Functionality", function() { cy.openPropertyPane("formwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.formWidget).should("not.be.visible"); + cy.get(publish.formWidget).should("not.exist"); cy.get(publish.backToEditor).click(); }); it("Form Widget Functionality To Check Visible Widget", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js index 9870301748..46790422bd 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js @@ -101,7 +101,7 @@ describe("Input Widget Functionality", function() { cy.openPropertyPane("inputwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.inputWidget + " " + "input").should("not.be.visible"); + cy.get(publish.inputWidget + " " + "input").should("not.exist"); cy.get(publish.backToEditor).click({ force: true }); }); it("Input Functionality To Check Visible Widget", function() { @@ -123,7 +123,7 @@ describe("Input Widget Functionality", function() { .click() .clear() .type("1.255"); - cy.get(".bp3-popover-content").should($x => { + cy.get(".bp3-popover-content").should(($x) => { expect($x).contain("Invalid input"); }); cy.get(widgetsPage.innertext) diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js index aa6edace77..28aa7ed7bb 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js @@ -58,14 +58,14 @@ describe("Radio Widget Functionality", function() { cy.openPropertyPane("radiogroupwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.radioWidget + " " + "input").should("not.be.visible"); + cy.get(publish.radioWidget + " " + "input").should("not.exist"); cy.get(publish.backToEditor).click(); }); it("Radio Functionality To Check Visible Widget", function() { cy.openPropertyPane("radiogroupwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.radioWidget + " " + "input").should("be.visible"); + cy.get(publish.radioWidget + " " + "input").should("be.checked"); }); it("Radio Functionality To Button Text", function() { cy.get(publish.radioWidget + " " + "label") diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js index 46aa8617b8..694d23eb2d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js @@ -31,6 +31,15 @@ describe("RichTextEditor Widget Functionality", function() { "This is a Heading", ); + // validate after reload + cy.reload(true); + cy.wait(2000); + cy.validateHTMLText( + formWidgetsPage.richTextEditorWidget, + "h1", + "This is a Heading", + ); + cy.PublishtheApp(); cy.validateHTMLText( publishPage.richTextEditorWidget, @@ -73,7 +82,7 @@ describe("RichTextEditor Widget Functionality", function() { // Uncheck the visible checkbox cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publishPage.richTextEditorWidget).should("not.be.visible"); + cy.get(publishPage.richTextEditorWidget).should("not.exist"); }); it("RichTextEditor-uncheck Visible field validation", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js index e358cada5e..f37e38e974 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js @@ -39,7 +39,7 @@ describe("Tab widget test", function() { .click({ force: true }); cy.get(Layoutpage.tabWidget) .contains("Day") - .should("not.to.be.visible"); + .should("not.exist"); /** * @param{toggleButton Css} Assert to be checked */ @@ -63,7 +63,7 @@ describe("Tab widget test", function() { cy.openPropertyPane("tabswidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); - cy.get(publish.tabWidget).should("not.be.visible"); + cy.get(publish.tabWidget).should("not.exist"); cy.get(publish.backToEditor).click(); }); it("Tab Widget Functionality To Check Visible Widget", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/Onboarding/Onboarding_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Onboarding/Onboarding_spec.js index 0def9905fc..8c602e8e93 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Onboarding/Onboarding_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Onboarding/Onboarding_spec.js @@ -56,7 +56,7 @@ describe("Onboarding", function() { cy.openPropertyPane("tablewidget"); cy.closePropertyPane(); - cy.get(".t--application-feedback-btn").should("not.be.visible"); + cy.get(".t--application-feedback-btn").should("not.exist"); }); // Similar to PublishtheApp command with little changes diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateAppWithSameName_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateAppWithSameName_spec.js index 7d0404578c..c6f78dbbad 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateAppWithSameName_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateAppWithSameName_spec.js @@ -6,7 +6,7 @@ describe("Create org and a new app / delete and recreate app", function() { it("create app within an org and delete and re-create another app with same name", function() { cy.NavigateToHome(); - cy.generateUUID().then(uid => { + cy.generateUUID().then((uid) => { orgid = uid; appid = uid; localStorage.setItem("OrgName", orgid); diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateDuplicateAppWithinOrg_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateDuplicateAppWithinOrg_spec.js index 9b906e31a2..3ae3b48dd8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateDuplicateAppWithinOrg_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateDuplicateAppWithinOrg_spec.js @@ -6,7 +6,7 @@ describe("Create new org and an app within the same", function() { it("create multiple apps and validate", function() { cy.NavigateToHome(); - cy.generateUUID().then(uid => { + cy.generateUUID().then((uid) => { orgid = uid; appid = uid; localStorage.setItem("OrgName", orgid); diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js index 06956c5dd4..62a13a40a9 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js @@ -2,13 +2,13 @@ const homePage = require("../../../locators/HomePage.json"); -describe("Create new org and share with a user", function () { +describe("Create new org and share with a user", function() { let orgid; let appid; - it("create org and then share with a user from UI", function () { + it("create org and then share with a user from UI", function() { cy.NavigateToHome(); - cy.generateUUID().then(uid => { + cy.generateUUID().then((uid) => { orgid = uid; appid = uid; localStorage.setItem("OrgName", orgid); @@ -25,7 +25,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as invited user and then validate viewer privilage", function () { + it("login as invited user and then validate viewer privilage", function() { cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); cy.get(homePage.searchInput).type(appid); cy.wait(2000); @@ -39,7 +39,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as Org owner and update the invited user role to developer", function () { + it("login as Org owner and update the invited user role to developer", function() { cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); cy.visit("/applications"); cy.wait("@applications").should( @@ -58,7 +58,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as invited user and then validate developer privilage", function () { + it("login as invited user and then validate developer privilage", function() { cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); cy.get(homePage.searchInput).type(appid); cy.wait(2000); @@ -76,7 +76,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as Org owner and update the invited user role to administrator", function () { + it("login as Org owner and update the invited user role to administrator", function() { cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); cy.visit("/applications"); cy.wait("@applications").should( @@ -95,7 +95,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as invited user and then validate administrator privilage", function () { + it("login as invited user and then validate administrator privilage", function() { cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); cy.get(homePage.searchInput).type(appid); cy.wait(2000); @@ -108,7 +108,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as Org owner and delete App ", function () { + it("login as Org owner and delete App ", function() { cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); cy.visit("/applications"); cy.wait("@applications").should( @@ -119,7 +119,7 @@ describe("Create new org and share with a user", function () { cy.get(homePage.searchInput).type(appid); cy.wait(2000); cy.navigateToOrgSettings(orgid); - cy.get(homePage.emailList).then(function ($list) { + cy.get(homePage.emailList).then(function($list) { expect($list).to.have.length(3); expect($list.eq(0)).to.contain(Cypress.env("USERNAME")); expect($list.eq(1)).to.contain(Cypress.env("TESTUSERNAME1")); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js index 12c2f9169c..b9c299b86d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js @@ -35,9 +35,11 @@ describe("Create a query with a mongo datasource, run, save and then delete the cy.get(".CodeMirror textarea") .first() .focus() - .type(`{"find": "planets"}`, { parseSpecialCharSequences: false }); + .type(`{"find": "listingsAndReviews","limit": 10}`, { + parseSpecialCharSequences: false, + }); - cy.EvaluateCurrentValue(`{"find": "planets"}`); + cy.EvaluateCurrentValue(`{"find": "listingsAndReviews","limit": 10}`); cy.runAndDeleteQuery(); cy.get("@createDatasource").then((httpResponse) => { diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json index 7360ed384c..441a498e0e 100644 --- a/app/client/cypress/locators/DatasourcesEditor.json +++ b/app/client/cypress/locators/DatasourcesEditor.json @@ -15,5 +15,7 @@ "sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]", "PostgresEntity": ".t--entity-name:contains(PostgreSQL)", "createQuerty": ".t--create-query", - "editDatasource": ".t--edit-datasource" + "editDatasource": ".t--edit-datasource", + "defaultDatabaseName": "input[name='datasourceConfiguration.connection.defaultDatabaseName']", + "selConnectionType": "[data-cy='datasourceConfiguration.connection.type']" } diff --git a/app/client/cypress/locators/Widgets.json b/app/client/cypress/locators/Widgets.json index cc7e648811..d3521c7412 100644 --- a/app/client/cypress/locators/Widgets.json +++ b/app/client/cypress/locators/Widgets.json @@ -46,5 +46,6 @@ "widgetBtn": ".t--widget-buttonwidget button", "actionSelect": ".t--open-dropdown-Select-Action", "tableActionSelect": ".t--property-control-onsearchtextchanged .t--open-dropdown-Select-Action", - "chartWidget": ".t--widget-chartwidget" + "chartWidget": ".t--widget-chartwidget", + "tableOnRowSelect": ".t--property-control-onrowselected .t--open-dropdown-Select-Action" } diff --git a/app/client/cypress/manual_TestSuite/API_Datasource_Spec.js b/app/client/cypress/manual_TestSuite/API_Datasource_Spec.js new file mode 100644 index 0000000000..4b7aa39c71 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/API_Datasource_Spec.js @@ -0,0 +1,60 @@ +const commonlocators = require("../../../locators/commonlocators.json"); + +describe("API associated with Datasource", function() { + it("Edit name of the Datasource from Pane and refeclected in the Page ", function() + { + // Click on the API datasource + // Click on Action icon (Three Dots) + // Click on "Edit Name" + // Rename the Datasource + // Click on the datasource + // Ensure the name is updated on the Page + } + ) + it("Edit name of the Datasource from Page and refeclected in the Pane", function() + { + // Click on the API datasource + // Navigate to respective + // Click on "Edit " option next to the Name of the datasource + // Rename the Datasource + // Ensure the name is updated in the Pane + } + ) + it("Edit the API Datasource", function() + { + // Click on the API datasource + // Ensure navigation to respective page + // Click on "EDIT" + // Make some changes + // Click on Test + // Click on Save + // Ensure it is refelected in the API + } + ) + it("Error on trying to Deleting an API Datasource when associated with API ", function() + { + // Click on API associated Datasource + // Navigate to respective page + // Click on "Delete" + // Ensure an error message is displayed to user + } + ) + it("Adding the API to an exsisting Datasource", function() + { + // Click on exsisting Datasource + // Navigate to Datasource list page + // Click on "+ New API" + // Ensure new API is added in the RHS Pane + // Click on "Run" + } + ) + it("Refresh an Datasource ", function() + { + // Navigate to the Datasource + // Click on Action icon (Three Dots) + // Click on "Refresh" + // Ensure loading icon + } + ) +} +) diff --git a/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js b/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js index bc2aa2edfa..f9ddcc04c8 100644 --- a/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js +++ b/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js @@ -1,19 +1,15 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { - it("Duplicating an application", function() - { - // Navigate to home Page - // Click on any application action icon (Three dots) - // Click on "Duplicate" option - // Ensure the application gets copied - // Click on "Appsmith" to navigate to homepage - // Click on action icon - // Click on Delete option - // Click on "Are You Sure?" option - // Ensure the App gets deleted - } - ) -} -) \ No newline at end of file +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Click on "Appsmith" to navigate to homepage + // Click on action icon + // Click on Delete option + // Click on "Are You Sure?" option + // Ensure the App gets deleted + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Duplicate_App.js b/app/client/cypress/manual_TestSuite/Duplicate_App.js index 46134860f6..8b70a6bbb3 100644 --- a/app/client/cypress/manual_TestSuite/Duplicate_App.js +++ b/app/client/cypress/manual_TestSuite/Duplicate_App.js @@ -1,15 +1,11 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { - it("Duplicating an application", function() - { - // Navigate to home Page - // Click on any application action icon (Three dots) - // Click on "Duplicate" option - // Ensure the application gets copied - // Ensure the name is appended with the word "Copy" - } - ) -} -) \ No newline at end of file +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Ensure the name is appended with the word "Copy" + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js b/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js index c724d651c8..1b6eb8240a 100644 --- a/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js +++ b/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js @@ -1,28 +1,22 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { - it("Duplicating an application", function() - { - // Navigate to home Page - // Click on any application action icon (Three dots) - // Click on "Duplicate" option - // Ensure the application gets copied - // Ensure the name is appended with the word "Copy" - } - ) - it("Deleting the duplicated Application ", function() - { - // Navigate to home Page - // Click on any application action icon (Three dots) - // Click on "Duplicate" option - // Ensure the application gets copied - // Click on "Appsmith" to navigate to homepage - // Click on action icon - // Click on Delete option - // Click on "Are You Sure?" option - // Ensure the App gets deleted - } - ) -} -) \ No newline at end of file +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Ensure the name is appended with the word "Copy" + }); + it("Deleting the duplicated Application ", function() { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Click on "Appsmith" to navigate to homepage + // Click on action icon + // Click on Delete option + // Click on "Are You Sure?" option + // Ensure the App gets deleted + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Organisation_Name.js b/app/client/cypress/manual_TestSuite/Organisation_Name.js index f4314ed760..4babed2641 100644 --- a/app/client/cypress/manual_TestSuite/Organisation_Name.js +++ b/app/client/cypress/manual_TestSuite/Organisation_Name.js @@ -1,15 +1,11 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Checking for error message on Organisation Name ", function() { - it("Ensure of Inactive Submit button ", function() - { - // Navigate to home Page - // Click on Create Organisation - // Type "Space" as first character - // Ensure "Submit" button does not get Active - // Now click on "X" (Close icon) ensure the pop up closes - } - ) -} -) \ No newline at end of file +describe("Checking for error message on Organisation Name ", function() { + it("Ensure of Inactive Submit button ", function() { + // Navigate to home Page + // Click on Create Organisation + // Type "Space" as first character + // Ensure "Submit" button does not get Active + // Now click on "X" (Close icon) ensure the pop up closes + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js b/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js index 88ca89850f..035ad05bc2 100644 --- a/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js +++ b/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js @@ -1,6 +1,5 @@ const homePage = require("../../../locators/HomePage.json"); - describe("Checking for error message on Organisation Name ", function() { it("Ensure of Inactive Submit button ", function() { @@ -18,7 +17,7 @@ describe("Checking for error message on Organisation Name ", function() { // Add some widgets // Navigate back to the application // Delete the Application - // Click on "Create New" option under samee organisation + // Click on "Create New" option under same organisation // Enter the name "XYZ" // Ensure the application can be created with the same name } @@ -32,5 +31,17 @@ describe("Checking for error message on Organisation Name ", function() { // Now click outside and ensure the pop up closes } ) + it("Reuse the name of the deleted application name on the other organisation", function() + { + // Navigate to home Page + // Create an Application by name "XYZ" + // Add some widgets + // Navigate back to the application + // Delete the Application + // Click on "Create New" option under different organisation + // Enter the name "XYZ" + // Ensure the application can be created with the same name + } + ) } ) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js b/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js index 186ba3b586..879abc8b3b 100644 --- a/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js +++ b/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js @@ -1,18 +1,14 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Reuse the name of the deleted application name inside the same organisation", function() { - it("Reuse the name of the deleted application name ", function() - { - // Navigate to home Page - // Create an Application by name "XYZ" - // Add some widgets - // Navigate back to the application - // Delete the Application - // Click on "Create New" option under samee organisation - // Enter the name "XYZ" - // Ensure the application can be created with the same name - } - ) -} -) \ No newline at end of file +describe("Reuse the name of the deleted application name inside the same organisation", function() { + it("Reuse the name of the deleted application name ", function() { + // Navigate to home Page + // Create an Application by name "XYZ" + // Add some widgets + // Navigate back to the application + // Delete the Application + // Click on "Create New" option under samee organisation + // Enter the name "XYZ" + // Ensure the application can be created with the same name + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js b/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js index 195fc06229..39093565c6 100644 --- a/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js +++ b/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js @@ -1,15 +1,11 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Adding Special Character ", function() { - it("Adding Special Character ", function() - { - // Navigate to home Page - // Click on Create Organisation - // Add special as first character - // Ensure "Submit" get Active - // Now click outside and ensure the pop up closes - } - ) -} -) \ No newline at end of file +describe("Adding Special Character ", function() { + it("Adding Special Character ", function() { + // Navigate to home Page + // Click on Create Organisation + // Add special as first character + // Ensure "Submit" get Active + // Now click outside and ensure the pop up closes + }); +}); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index ae358d6ec5..fad8acc5a5 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -20,7 +20,7 @@ const explorer = require("../locators/explorerlocators.json"); let pageidcopy = " "; -Cypress.Commands.add("createOrg", orgName => { +Cypress.Commands.add("createOrg", (orgName) => { cy.get(homePage.createOrg) .should("be.visible") .first() @@ -45,7 +45,7 @@ Cypress.Commands.add( }, ); -Cypress.Commands.add("navigateToOrgSettings", orgName => { +Cypress.Commands.add("navigateToOrgSettings", (orgName) => { cy.get(homePage.orgList.concat(orgName).concat(")")) .scrollIntoView() .should("be.visible"); @@ -207,7 +207,7 @@ Cypress.Commands.add("updateUserRoleForOrg", (orgName, email, role) => { ); }); -Cypress.Commands.add("launchApp", appName => { +Cypress.Commands.add("launchApp", (appName) => { cy.get(homePage.appView) .should("be.visible") .first() @@ -239,7 +239,7 @@ Cypress.Commands.add("CreateAppForOrg", (orgName, appname) => { ); }); -Cypress.Commands.add("CreateApp", appname => { +Cypress.Commands.add("CreateApp", (appname) => { cy.get(homePage.createNew) .first() .click({ force: true }); @@ -259,7 +259,7 @@ Cypress.Commands.add("CreateApp", appname => { ); }); -Cypress.Commands.add("DeleteApp", appName => { +Cypress.Commands.add("DeleteApp", (appName) => { cy.get(commonlocators.homeIcon).click({ force: true }); cy.wait("@applications").should( "have.nested.property", @@ -312,13 +312,13 @@ Cypress.Commands.add("LoginFromAPI", (uname, pword) => { username: uname, password: pword, }, - }).then(response => { + }).then((response) => { expect(response.status).equal(302); cy.log(response.body); }); }); -Cypress.Commands.add("DeleteApp", appName => { +Cypress.Commands.add("DeleteApp", (appName) => { cy.get(commonlocators.homeIcon).click({ force: true }); cy.get(homePage.searchInput).type(appName); cy.wait(2000); @@ -360,7 +360,7 @@ Cypress.Commands.add("NavigateToHome", () => { ); }); -Cypress.Commands.add("NavigateToWidgets", pageName => { +Cypress.Commands.add("NavigateToWidgets", (pageName) => { cy.get(pages.pagesIcon).click({ force: true }); cy.get(".t--page-sidebar-" + pageName + "") .find(">div") @@ -371,7 +371,7 @@ Cypress.Commands.add("NavigateToWidgets", pageName => { cy.get("#loading").should("not.exist"); }); -Cypress.Commands.add("SearchApp", appname => { +Cypress.Commands.add("SearchApp", (appname) => { cy.get(homePage.searchInput).type(appname); cy.wait(2000); cy.get(homePage.applicationCard) @@ -395,10 +395,10 @@ Cypress.Commands.add("SearchEntity", (apiname1, apiname2) => { ).should("be.visible"); cy.get( commonlocators.entitySearchResult.concat(apiname2).concat("')"), - ).should("not.be.visible"); + ).should("not.exist"); }); -Cypress.Commands.add("GlobalSearchEntity", apiname1 => { +Cypress.Commands.add("GlobalSearchEntity", (apiname1) => { cy.get(commonlocators.entityExplorersearch).should("be.visible"); cy.get(commonlocators.entityExplorersearch) .clear() @@ -409,12 +409,12 @@ Cypress.Commands.add("GlobalSearchEntity", apiname1 => { ).should("be.visible"); }); -Cypress.Commands.add("ResponseStatusCheck", statusCode => { +Cypress.Commands.add("ResponseStatusCheck", (statusCode) => { cy.xpath(apiwidget.responseStatus).should("be.visible"); cy.xpath(apiwidget.responseStatus).contains(statusCode); }); -Cypress.Commands.add("ResponseCheck", textTocheck => { +Cypress.Commands.add("ResponseCheck", (textTocheck) => { //Explicit assert cy.get(apiwidget.responseText).should("be.visible"); }); @@ -433,7 +433,7 @@ Cypress.Commands.add("NavigateToEntityExplorer", () => { cy.get("#loading").should("not.exist"); }); -Cypress.Commands.add("CreateAPI", apiname => { +Cypress.Commands.add("CreateAPI", (apiname) => { cy.get(apiwidget.createapi).click({ force: true }); cy.wait("@createNewApi"); cy.get(apiwidget.resourceUrl).should("be.visible"); @@ -449,7 +449,7 @@ Cypress.Commands.add("CreateAPI", apiname => { cy.wait(2000); }); -Cypress.Commands.add("CreateSubsequentAPI", apiname => { +Cypress.Commands.add("CreateSubsequentAPI", (apiname) => { cy.get(apiwidget.createApiOnSideBar) .first() .click({ force: true }); @@ -462,7 +462,7 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => { cy.WaitAutoSave(); }); -Cypress.Commands.add("EditApiName", apiname => { +Cypress.Commands.add("EditApiName", (apiname) => { cy.get(apiwidget.ApiName).click({ force: true }); cy.get(apiwidget.apiTxt) .clear() @@ -471,7 +471,7 @@ Cypress.Commands.add("EditApiName", apiname => { cy.WaitAutoSave(); }); -Cypress.Commands.add("EditApiNameFromExplorer", apiname => { +Cypress.Commands.add("EditApiNameFromExplorer", (apiname) => { cy.xpath(apiwidget.popover) .last() .click({ force: true }); @@ -530,7 +530,7 @@ Cypress.Commands.add("validateRequest", (baseurl, path, verb) => { .click({ force: true }); }); -Cypress.Commands.add("SelectAction", action => { +Cypress.Commands.add("SelectAction", (action) => { cy.get(ApiEditor.ApiVerb) .first() .click({ force: true }); @@ -565,7 +565,7 @@ Cypress.Commands.add( }, ); -Cypress.Commands.add("SearchEntityandOpen", apiname1 => { +Cypress.Commands.add("SearchEntityandOpen", (apiname1) => { cy.get(commonlocators.entityExplorersearch).should("be.visible"); cy.get(commonlocators.entityExplorersearch) .clear() @@ -595,7 +595,7 @@ Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => { .type(path, { parseSpecialCharSequences: false }); }); -Cypress.Commands.add("changeZoomLevel", zoomValue => { +Cypress.Commands.add("changeZoomLevel", (zoomValue) => { cy.get(commonlocators.changeZoomlevel).click(); cy.get("ul.bp3-menu") .children() @@ -609,7 +609,7 @@ Cypress.Commands.add("changeZoomLevel", zoomValue => { cy.get(commonlocators.selectedZoomlevel) .first() .invoke("text") - .then(text => { + .then((text) => { const someText = text; expect(someText).to.equal(zoomValue); }); @@ -663,14 +663,14 @@ Cypress.Commands.add("switchToAPIInputTab", () => { .click({ force: true }); }); -Cypress.Commands.add("selectPaginationType", option => { +Cypress.Commands.add("selectPaginationType", (option) => { cy.get(apiwidget.paginationOption) .first() .click({ force: true }); cy.xpath(option).click({ force: true }); }); -Cypress.Commands.add("clickTest", testbutton => { +Cypress.Commands.add("clickTest", (testbutton) => { cy.wait(2000); cy.wait("@saveAction"); cy.get(testbutton) @@ -726,7 +726,7 @@ Cypress.Commands.add( }, ); -Cypress.Commands.add("CreationOfUniqueAPIcheck", apiname => { +Cypress.Commands.add("CreationOfUniqueAPIcheck", (apiname) => { cy.get(pages.addEntityAPI).click(); cy.get(apiwidget.createapi).click({ force: true }); cy.wait("@createNewApi"); @@ -738,13 +738,13 @@ Cypress.Commands.add("CreationOfUniqueAPIcheck", apiname => { .type(apiname, { force: true }) .should("have.value", apiname) .focus(); - cy.get(".bp3-popover-content").should($x => { + cy.get(".bp3-popover-content").should(($x) => { console.log($x); expect($x).contain(apiname.concat(" is already being used.")); }); }); -Cypress.Commands.add("MoveAPIToHome", apiname => { +Cypress.Commands.add("MoveAPIToHome", (apiname) => { cy.xpath(apiwidget.popover) .last() .click({ force: true }); @@ -757,7 +757,7 @@ Cypress.Commands.add("MoveAPIToHome", apiname => { ); }); -Cypress.Commands.add("MoveAPIToPage", pageName => { +Cypress.Commands.add("MoveAPIToPage", (pageName) => { cy.xpath(apiwidget.popover) .last() .click({ force: true }); @@ -772,7 +772,7 @@ Cypress.Commands.add("MoveAPIToPage", pageName => { ); }); -Cypress.Commands.add("copyEntityToPage", pageName => { +Cypress.Commands.add("copyEntityToPage", (pageName) => { cy.xpath(apiwidget.popover) .last() .click({ force: true }); @@ -825,7 +825,7 @@ Cypress.Commands.add("deleteEntity", () => { cy.get(apiwidget.delete).click({ force: true }); }); -Cypress.Commands.add("DeleteAPI", apiname => { +Cypress.Commands.add("DeleteAPI", (apiname) => { cy.get(apiwidget.deleteAPI) .first() .click({ force: true }); @@ -896,14 +896,14 @@ Cypress.Commands.add("createModal", (modalType, ModalName) => { cy.get(".bp3-overlay-backdrop").click({ force: true }); }); -Cypress.Commands.add("CheckWidgetProperties", checkboxCss => { +Cypress.Commands.add("CheckWidgetProperties", (checkboxCss) => { cy.get(checkboxCss).check({ force: true, }); cy.assertPageSave(); }); -Cypress.Commands.add("UncheckWidgetProperties", checkboxCss => { +Cypress.Commands.add("UncheckWidgetProperties", (checkboxCss) => { cy.get(checkboxCss).uncheck({ force: true, }); @@ -925,7 +925,7 @@ Cypress.Commands.add( Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { cy.get(commonlocators.editWidgetName) .click({ force: true }) - .type(text) + .type(text, { delay: 300 }) .type("{enter}"); cy.get(inputcss) .first() @@ -933,13 +933,13 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { cy.get(innercss).should("have.text", text); }); -Cypress.Commands.add("EvaluateDataType", dataType => { +Cypress.Commands.add("EvaluateDataType", (dataType) => { cy.get(commonlocators.evaluatedType) .should("be.visible") .contains(dataType); }); -Cypress.Commands.add("EvaluateCurrentValue", currentValue => { +Cypress.Commands.add("EvaluateCurrentValue", (currentValue) => { cy.wait(2000); cy.get(commonlocators.evaluatedCurrentValue) .should("be.visible") @@ -954,8 +954,8 @@ Cypress.Commands.add("PublishtheApp", () => { cy.assertPageSave(); // Stubbing window.open to open in the same tab - cy.window().then(window => { - cy.stub(window, "open").callsFake(url => { + cy.window().then((window) => { + cy.stub(window, "open").callsFake((url) => { window.location.href = Cypress.config().baseUrl + url.substring(1); window.location.target = "_self"; }); @@ -976,12 +976,12 @@ Cypress.Commands.add("getCodeMirror", () => { .type("{ctrl}{shift}{downarrow}"); }); -Cypress.Commands.add("testCodeMirror", value => { +Cypress.Commands.add("testCodeMirror", (value) => { cy.get(".CodeMirror textarea") .first() .focus() .type("{ctrl}{shift}{downarrow}") - .then($cm => { + .then(($cm) => { if ($cm.val() !== "") { cy.get(".CodeMirror textarea") .first() @@ -1009,7 +1009,7 @@ Cypress.Commands.add("testJsontext", (endp, value) => { .focus({ force: true }) .type("{uparrow}", { force: true }) .type("{ctrl}{shift}{downarrow}", { force: true }); - cy.focused().then($cm => { + cy.focused().then(($cm) => { if ($cm.contents != "") { cy.log("The field is empty"); cy.get(".t--property-control-" + endp + " .CodeMirror textarea") @@ -1028,14 +1028,14 @@ Cypress.Commands.add("testJsontext", (endp, value) => { cy.wait(200); }); -Cypress.Commands.add("selectShowMsg", value => { +Cypress.Commands.add("selectShowMsg", (value) => { cy.get(commonlocators.chooseAction) .children() .contains("Show Message") .click(); }); -Cypress.Commands.add("addSuccessMessage", value => { +Cypress.Commands.add("addSuccessMessage", (value) => { cy.get(commonlocators.chooseMsgType) .last() .click(); @@ -1052,12 +1052,12 @@ Cypress.Commands.add("SetDateToToday", () => { cy.assertPageSave(); }); -Cypress.Commands.add("enterActionValue", value => { +Cypress.Commands.add("enterActionValue", (value) => { cy.get(".CodeMirror textarea") .last() .focus() .type("{ctrl}{shift}{downarrow}") - .then($cm => { + .then(($cm) => { if ($cm.val() !== "") { cy.get(".CodeMirror textarea") .last() @@ -1079,7 +1079,7 @@ Cypress.Commands.add("enterActionValue", value => { }); }); -Cypress.Commands.add("enterNavigatePageName", value => { +Cypress.Commands.add("enterNavigatePageName", (value) => { cy.get("ul.tree") .children() .first() @@ -1088,7 +1088,7 @@ Cypress.Commands.add("enterNavigatePageName", value => { .first() .focus() .type("{ctrl}{shift}{downarrow}") - .then($cm => { + .then(($cm) => { if ($cm.val() !== "") { cy.get(".CodeMirror textarea") .first() @@ -1125,7 +1125,7 @@ Cypress.Commands.add("DeleteModal", () => { .click({ force: true }); }); -Cypress.Commands.add("Createpage", Pagename => { +Cypress.Commands.add("Createpage", (Pagename) => { cy.get(pages.pagesIcon).click({ force: true }); cy.get(pages.AddPage) .first() @@ -1148,7 +1148,7 @@ Cypress.Commands.add("Createpage", Pagename => { cy.wait(2000); }); -Cypress.Commands.add("Deletepage", Pagename => { +Cypress.Commands.add("Deletepage", (Pagename) => { cy.get(pages.pagesIcon).click({ force: true }); cy.get(".t--page-sidebar-" + Pagename + ""); cy.get( @@ -1167,11 +1167,11 @@ Cypress.Commands.add("generateUUID", () => { return id.split("-")[0]; }); -Cypress.Commands.add("addDsl", dsl => { +Cypress.Commands.add("addDsl", (dsl) => { let currentURL; let pageid; let layoutId; - cy.url().then(url => { + cy.url().then((url) => { currentURL = url; const myRegexp = /pages(.*)/; const match = myRegexp.exec(currentURL); @@ -1180,7 +1180,7 @@ Cypress.Commands.add("addDsl", dsl => { cy.log(pageid + "page id"); //Fetch the layout id cy.server(); - cy.request("GET", "api/v1/pages/" + pageid).then(response => { + cy.request("GET", "api/v1/pages/" + pageid).then((response) => { const len = JSON.stringify(response.body); cy.log(len); layoutId = JSON.parse(len).data.layouts[0].id; @@ -1189,7 +1189,7 @@ Cypress.Commands.add("addDsl", dsl => { "PUT", "api/v1/layouts/" + layoutId + "/pages/" + pageid, dsl, - ).then(response => { + ).then((response) => { expect(response.status).equal(200); cy.reload(); }); @@ -1200,7 +1200,7 @@ Cypress.Commands.add("addDsl", dsl => { Cypress.Commands.add("DeleteAppByApi", () => { let currentURL; let appId; - cy.url().then(url => { + cy.url().then((url) => { currentURL = url; const myRegexp = /applications(.*)/; const match = myRegexp.exec(currentURL); @@ -1212,7 +1212,7 @@ Cypress.Commands.add("DeleteAppByApi", () => { } }); }); -Cypress.Commands.add("togglebar", value => { +Cypress.Commands.add("togglebar", (value) => { cy.get(value) .check({ force: true }) .should("be.checked"); @@ -1229,7 +1229,7 @@ Cypress.Commands.add("optionValue", (value, value2) => { .clear() .type(value2); }); -Cypress.Commands.add("dropdownDynamic", text => { +Cypress.Commands.add("dropdownDynamic", (text) => { cy.wait(2000); cy.get("ul[class='bp3-menu']") .first() @@ -1238,24 +1238,6 @@ Cypress.Commands.add("dropdownDynamic", text => { .should("have.text", text); }); -Cypress.Commands.add("getAlert", alertcss => { - cy.get(commonlocators.dropdownSelectButton).click({ force: true }); - cy.get(widgetsPage.menubar) - .contains("Show Alert") - .click({ force: true }) - .should("have.text", "Show Alert"); - - cy.get(alertcss) - .click({ force: true }) - .type("{command}{A}{del}") - .type("hello") - .should("not.to.be.empty"); - cy.get(".t--open-dropdown-Select-type").click({ force: true }); - cy.get(".bp3-popover-content .bp3-menu li") - .contains("Success") - .click({ force: true }); -}); - Cypress.Commands.add("tabVerify", (index, text) => { cy.get(".t--property-control-tabs input") .eq(index) @@ -1268,18 +1250,18 @@ Cypress.Commands.add("tabVerify", (index, text) => { .should("be.visible"); }); -Cypress.Commands.add("togglebar", value => { +Cypress.Commands.add("togglebar", (value) => { cy.get(value) .check({ force: true }) .should("be.checked"); }); -Cypress.Commands.add("togglebarDisable", value => { +Cypress.Commands.add("togglebarDisable", (value) => { cy.get(value) .uncheck({ force: true }) .should("not.checked"); }); -Cypress.Commands.add("getAlert", alertcss => { +Cypress.Commands.add("getAlert", (alertcss) => { cy.get(commonlocators.dropdownSelectButton).click({ force: true }); cy.get(widgetsPage.menubar) .contains("Show Message") @@ -1354,10 +1336,11 @@ Cypress.Commands.add("testCreateApiButton", () => { Cypress.Commands.add("testSaveDeleteDatasource", () => { cy.get(".t--test-datasource").click(); - cy.wait("@testDatasource") + cy.wait("@testDatasource"); + /* .should("have.nested.property", "response.body.data.success", true) .debug(); - + */ cy.get(".t--save-datasource").click(); cy.wait("@saveDatasource").should( "have.nested.property", @@ -1397,11 +1380,14 @@ Cypress.Commands.add("NavigateToQueryEditor", () => { Cypress.Commands.add("testDatasource", () => { cy.get(".t--test-datasource").click(); - cy.wait("@testDatasource").should( + cy.wait("@testDatasource"); + /* + .should( "have.nested.property", "response.body.data.success", true, ); + */ }); Cypress.Commands.add("saveDatasource", () => { @@ -1421,7 +1407,12 @@ Cypress.Commands.add("testSaveDatasource", () => { Cypress.Commands.add("fillMongoDatasourceForm", () => { cy.get(datasourceEditor["host"]).type(datasourceFormData["mongo-host"]); - cy.get(datasourceEditor["port"]).type(datasourceFormData["mongo-port"]); + //cy.get(datasourceEditor["port"]).type(datasourceFormData["mongo-port"]); + cy.get(datasourceEditor["selConnectionType"]).click(); + cy.contains(datasourceFormData["connection-type"]).click(); + cy.get(datasourceEditor["defaultDatabaseName"]).type( + datasourceFormData["mongo-defaultDatabaseName"], + ); cy.get(datasourceEditor.sectionAuthentication).click(); cy.get(datasourceEditor["databaseName"]) @@ -1473,7 +1464,7 @@ Cypress.Commands.add("createPostgresDatasource", () => { cy.testSaveDatasource(); }); -Cypress.Commands.add("deleteDatasource", datasourceName => { +Cypress.Commands.add("deleteDatasource", (datasourceName) => { cy.NavigateToQueryEditor(); cy.contains(".t--datasource-name", datasourceName) @@ -1553,7 +1544,7 @@ Cypress.Commands.add("dragAndDropToCanvas", (widgetType, { x, y }) => { .trigger("mouseup", { force: true }); }); -Cypress.Commands.add("executeDbQuery", queryName => { +Cypress.Commands.add("executeDbQuery", (queryName) => { cy.get(widgetsPage.buttonOnClick) .get(commonlocators.dropdownSelectButton) .click({ force: true }) @@ -1567,7 +1558,7 @@ Cypress.Commands.add("executeDbQuery", queryName => { .click({ force: true }); }); -Cypress.Commands.add("openPropertyPane", widgetType => { +Cypress.Commands.add("openPropertyPane", (widgetType) => { const selector = `.t--draggable-${widgetType}`; cy.get(selector) .first() @@ -1585,13 +1576,13 @@ Cypress.Commands.add("closePropertyPane", () => { Cypress.Commands.add("createAndFillApi", (url, parameters) => { cy.NavigateToApiEditor(); cy.testCreateApiButton(); - cy.get("@createNewApi").then(response => { + cy.get("@createNewApi").then((response) => { cy.get(ApiEditor.ApiNameField).should("be.visible"); cy.expect(response.response.body.responseMeta.success).to.eq(true); cy.get(ApiEditor.ApiNameField) .click() .invoke("text") - .then(text => { + .then((text) => { const someText = text; expect(someText).to.equal(response.response.body.data.name); }); @@ -1612,7 +1603,7 @@ Cypress.Commands.add("createAndFillApi", (url, parameters) => { cy.get(ApiEditor.ApiRunBtn).should("not.be.disabled"); }); -Cypress.Commands.add("isSelectRow", index => { +Cypress.Commands.add("isSelectRow", (index) => { cy.get( '.tbody .td[data-rowindex="' + index + '"][data-colindex="' + 0 + '"]', ).click({ force: true }); @@ -1640,13 +1631,13 @@ Cypress.Commands.add("setDate", (date, dateFormate) => { cy.get(sel).click(); }); -Cypress.Commands.add("pageNo", index => { +Cypress.Commands.add("pageNo", (index) => { cy.get(".page-item") .first() .click({ force: true }); }); -Cypress.Commands.add("pageNoValidate", index => { +Cypress.Commands.add("pageNoValidate", (index) => { const data = '.e-numericcontainer a[index="' + index + '"]'; const pageVal = cy.get(data); return pageVal; @@ -1661,7 +1652,7 @@ Cypress.Commands.add("validateEnableWidget", (widgetCss, disableCss) => { }); Cypress.Commands.add("validateHTMLText", (widgetCss, htmlTag, value) => { - cy.get(widgetCss + " iframe").then($iframe => { + cy.get(widgetCss + " iframe").then(($iframe) => { const $body = $iframe.contents().find("body"); cy.wrap($body) .find(htmlTag) @@ -1670,6 +1661,7 @@ Cypress.Commands.add("validateHTMLText", (widgetCss, htmlTag, value) => { }); Cypress.Commands.add("startServerAndRoutes", () => { + //To update route with intercept after working on alias wrt wait and alias cy.server(); cy.route("GET", "/api/v1/applications/new").as("applications"); cy.route("GET", "/api/v1/users/profile").as("getUser"); @@ -1747,7 +1739,7 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("DELETE", "/api/v1/organizations/*/logo").as("deleteLogo"); }); -Cypress.Commands.add("alertValidate", text => { +Cypress.Commands.add("alertValidate", (text) => { cy.get(commonlocators.success) .should("be.visible") .and("have.text", text); @@ -1777,7 +1769,7 @@ Cypress.Commands.add("scrollTabledataPublish", (rowNum, colNum) => { return tabVal; }); -Cypress.Commands.add("assertEvaluatedValuePopup", expectedType => { +Cypress.Commands.add("assertEvaluatedValuePopup", (expectedType) => { cy.get(dynamicInputLocators.evaluatedValue) .should("be.visible") .children("p") @@ -1787,7 +1779,7 @@ Cypress.Commands.add("assertEvaluatedValuePopup", expectedType => { .should("have.text", expectedType); }); -Cypress.Commands.add("validateToastMessage", value => { +Cypress.Commands.add("validateToastMessage", (value) => { cy.get(commonlocators.toastMsg).should("have.text", value); }); @@ -1802,23 +1794,23 @@ Cypress.Commands.add("NavigateToPaginationTab", () => { .type("{enter}"); }); -Cypress.Commands.add("ValidateTableData", value => { +Cypress.Commands.add("ValidateTableData", (value) => { // cy.isSelectRow(0); - cy.readTabledata("0", "0").then(tabData => { + cy.readTabledata("0", "0").then((tabData) => { const tableData = tabData; expect(tableData).to.equal(value.toString()); }); }); -Cypress.Commands.add("ValidatePublishTableData", value => { +Cypress.Commands.add("ValidatePublishTableData", (value) => { cy.isSelectRow(0); - cy.readTabledataPublish("0", "0").then(tabData => { + cy.readTabledataPublish("0", "0").then((tabData) => { const tableData = tabData; expect(tableData).to.equal(value); }); }); -Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { +Cypress.Commands.add("ValidatePaginateResponseUrlData", (runTestCss) => { cy.SearchEntityandOpen("Api2"); cy.NavigateToPaginationTab(); cy.RunAPI(); @@ -1832,7 +1824,7 @@ Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { .contains("name") .siblings("span") .invoke("text") - .then(tabData => { + .then((tabData) => { const respBody = tabData.match(/"(.*)"/)[0]; localStorage.setItem("respBody", respBody); cy.log(respBody); @@ -1840,7 +1832,7 @@ Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { // cy.openPropertyPane("tablewidget"); // cy.testJsontext("tabledata", "{{Api2.data.results}}"); cy.isSelectRow(0); - cy.readTabledata("0", "1").then(tabData => { + cy.readTabledata("0", "1").then((tabData) => { const tableData = tabData; expect(`\"${tableData}\"`).to.equal(respBody); }); @@ -1849,13 +1841,13 @@ Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { Cypress.Commands.add("ValidatePaginationInputData", () => { cy.isSelectRow(0); - cy.readTabledataPublish("0", "1").then(tabData => { + cy.readTabledataPublish("0", "1").then((tabData) => { const tableData = tabData; expect(`\"${tableData}\"`).to.equal(localStorage.getItem("respBody")); }); }); -Cypress.Commands.add("callApi", apiname => { +Cypress.Commands.add("callApi", (apiname) => { cy.get(commonlocators.callApi) .first() .click(); diff --git a/app/client/package.json b/app/client/package.json index 0b99883e20..a0d273bc70 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -8,12 +8,13 @@ }, "cracoConfig": "craco.dev.config.js", "dependencies": { - "@blueprintjs/core": "^3.18.1", + "@blueprintjs/core": "^3.36.0", "@blueprintjs/datetime": "^3.14.0", "@blueprintjs/icons": "^3.10.0", "@blueprintjs/select": "^3.10.0", "@blueprintjs/timezone": "^3.6.0", "@craco/craco": "^5.7.0", + "@github/g-emoji-element": "^1.1.5", "@manaflair/redux-batch": "^1.0.0", "@optimizely/optimizely-sdk": "^4.0.0", "@sentry/react": "^5.24.2", @@ -44,12 +45,13 @@ "@uppy/webcam": "^1.3.1", "@welldone-software/why-did-you-render": "^4.2.5", "algoliasearch": "^4.2.0", - "axios": "^0.18.0", + "axios": "^0.21.1", "chance": "^1.1.3", "codemirror": "^5.55.0", "copy-to-clipboard": "^3.3.1", "craco-alias": "^2.1.1", "cypress-log-to-output": "^1.1.2", + "deep-diff": "^1.0.2", "downloadjs": "^1.4.7", "eslint": "^7.11.0", "fast-deep-equal": "^3.1.1", @@ -166,6 +168,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", "@types/codemirror": "^0.0.96", + "@types/deep-diff": "^1.0.0", "@types/downloadjs": "^1.4.2", "@types/jest": "^24.0.22", "@types/react-beautiful-dnd": "^11.0.4", @@ -181,7 +184,7 @@ "babel-loader": "^8.1.0", "babel-plugin-styled-components": "^1.10.7", "craco-babel-loader": "^0.1.4", - "cypress": "5.3.0", + "cypress": "6.2.1", "cypress-file-upload": "^4.1.1", "cypress-multi-reporters": "^1.2.4", "cypress-xpath": "^1.4.0", diff --git a/app/client/public/index.html b/app/client/public/index.html index dd8b335d3d..22c5699535 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -160,7 +160,6 @@ }; - diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index 5f50311cc4..11f299e8a0 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -7,9 +7,7 @@ import { import { Action } from "entities/Action"; import { batchAction } from "actions/batchActions"; -export const createActionRequest = ( - payload: Partial & { eventData: any }, -) => { +export const createActionRequest = (payload: Partial) => { return { type: ReduxActionTypes.CREATE_ACTION_INIT, payload, diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index a33b627cd5..b5ff5a68da 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -1,5 +1,6 @@ import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; -import { CreateDatasourceConfig, Datasource } from "api/DatasourcesApi"; +import { CreateDatasourceConfig } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; export const createDatasourceFromForm = (payload: CreateDatasourceConfig) => { return { diff --git a/app/client/src/actions/queryPaneActions.ts b/app/client/src/actions/queryPaneActions.ts index eb70aa04ff..57394784f1 100644 --- a/app/client/src/actions/queryPaneActions.ts +++ b/app/client/src/actions/queryPaneActions.ts @@ -1,7 +1,7 @@ import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; -import { RestAction } from "entities/Action"; +import { Action } from "entities/Action"; -export const createQueryRequest = (payload: Partial) => { +export const createQueryRequest = (payload: Partial) => { return { type: ReduxActionTypes.CREATE_QUERY_INIT, payload, diff --git a/app/client/src/actions/releasesActions.ts b/app/client/src/actions/releasesActions.ts new file mode 100644 index 0000000000..34ab848418 --- /dev/null +++ b/app/client/src/actions/releasesActions.ts @@ -0,0 +1,5 @@ +import { ReduxActionTypes } from "constants/ReduxActionConstants"; + +export const resetReleasesCount = () => ({ + type: ReduxActionTypes.RESET_UNREAD_RELEASES_COUNT, +}); diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index e75b83e9c0..50674ebaf5 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -23,10 +23,12 @@ export const executeAction = ( export const executeActionError = ( executeErrorPayload: ExecuteErrorPayload, -): ReduxAction => ({ - type: ReduxActionErrorTypes.EXECUTE_ACTION_ERROR, - payload: executeErrorPayload, -}); +): ReduxAction => { + return { + type: ReduxActionErrorTypes.EXECUTE_ACTION_ERROR, + payload: executeErrorPayload, + }; +}; export const executePageLoadActions = ( payload: PageAction[][], diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index 6dff6c434b..bc7a280b87 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -5,7 +5,7 @@ import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS, } from "constants/ApiConstants"; import axios, { AxiosPromise, CancelTokenSource } from "axios"; -import { Action, RestAction } from "entities/Action"; +import { Action } from "entities/Action"; export interface CreateActionRequest extends APIRequest { datasourceId: string; @@ -78,7 +78,7 @@ export interface ActionApiResponse { } export interface ActionResponse { - body: Record; + body: unknown; headers: Record; request?: ActionApiResponseReq; statusCode: string; @@ -88,12 +88,12 @@ export interface ActionResponse { } export interface MoveActionRequest { - action: RestAction; + action: Action; destinationPageId: string; } export interface CopyActionRequest { - action: RestAction; + action: Action; pageId: string; } @@ -109,7 +109,7 @@ class ActionAPI extends API { static apiUpdateCancelTokenSource: CancelTokenSource; static queryUpdateCancelTokenSource: CancelTokenSource; - static fetchAPI(id: string): AxiosPromise> { + static fetchAPI(id: string): AxiosPromise> { return API.get(`${ActionAPI.url}/${id}`); } @@ -121,30 +121,33 @@ class ActionAPI extends API { static fetchActions( applicationId: string, - ): AxiosPromise> { + ): AxiosPromise> { return API.get(ActionAPI.url, { applicationId }); } static fetchActionsForViewMode( applicationId: string, - ): AxiosPromise> { + ): AxiosPromise> { return API.get(`${ActionAPI.url}/view`, { applicationId }); } static fetchActionsByPageId( pageId: string, - ): AxiosPromise> { + ): AxiosPromise> { return API.get(ActionAPI.url, { pageId }); } static updateAPI( - apiConfig: Partial, + apiConfig: Partial, ): AxiosPromise { if (ActionAPI.apiUpdateCancelTokenSource) { ActionAPI.apiUpdateCancelTokenSource.cancel(); } ActionAPI.apiUpdateCancelTokenSource = axios.CancelToken.source(); - return API.put(`${ActionAPI.url}/${apiConfig.id}`, apiConfig, undefined, { + const action = Object.assign({}, apiConfig); + // While this line is not required, name can not be changed from this endpoint + delete action.name; + return API.put(`${ActionAPI.url}/${action.id}`, action, undefined, { cancelToken: ActionAPI.apiUpdateCancelTokenSource.token, }); } diff --git a/app/client/src/api/Api.tsx b/app/client/src/api/Api.tsx index 5d5e4c0dea..cfc9661605 100644 --- a/app/client/src/api/Api.tsx +++ b/app/client/src/api/Api.tsx @@ -9,7 +9,7 @@ import { ActionApiResponse } from "./ActionAPI"; import { AUTH_LOGIN_URL } from "constants/routes"; import history from "utils/history"; import { convertObjectToQueryParams } from "utils/AppsmithUtils"; -import { SERVER_API_TIMEOUT_ERROR } from "../constants/messages"; +import { ERROR_500, SERVER_API_TIMEOUT_ERROR } from "../constants/messages"; //TODO(abhinav): Refactor this to make more composable. export const apiRequestConfig = { @@ -71,15 +71,16 @@ axiosInstance.interceptors.response.use( code: ERROR_CODES.REQUEST_TIMEOUT, }); } - if (error.response.status === API_STATUS_CODES.SERVER_ERROR) { - return Promise.reject({ - ...error, - crash: true, - code: ERROR_CODES.REQUEST_TIMEOUT, - message: SERVER_API_TIMEOUT_ERROR, - }); - } + if (error.response) { + if (error.response.status === API_STATUS_CODES.SERVER_ERROR) { + return Promise.reject({ + ...error, + code: ERROR_CODES.SERVER_ERROR, + message: ERROR_500, + }); + } + // The request was made and the server responded with a status code // that falls out of the range of 2xx // console.log(error.response.data); @@ -91,7 +92,7 @@ axiosInstance.interceptors.response.use( // Redirect to login and set a redirect url. history.replace({ pathname: AUTH_LOGIN_URL, - search: `redirectTo=${currentUrl}`, + search: `redirectUrl=${currentUrl}`, }); return Promise.reject({ code: ERROR_CODES.REQUEST_NOT_AUTHORISED, diff --git a/app/client/src/api/ApplicationApi.tsx b/app/client/src/api/ApplicationApi.tsx index f2531e8190..55844389ab 100644 --- a/app/client/src/api/ApplicationApi.tsx +++ b/app/client/src/api/ApplicationApi.tsx @@ -106,6 +106,8 @@ export interface FetchUsersApplicationsOrgsResponse extends ApiResponse { data: { organizationApplications: Array; user: string; + newReleasesCount: string; + releaseItems: Array>; }; } diff --git a/app/client/src/api/DatasourcesApi.ts b/app/client/src/api/DatasourcesApi.ts index d94f280de7..21ccd4afb9 100644 --- a/app/client/src/api/DatasourcesApi.ts +++ b/app/client/src/api/DatasourcesApi.ts @@ -1,59 +1,9 @@ +import { DEFAULT_TEST_DATA_SOURCE_TIMEOUT_MS } from "constants/ApiConstants"; import API from "./Api"; import { GenericApiResponse } from "./ApiResponses"; import { AxiosPromise } from "axios"; -import { DEFAULT_TEST_DATA_SOURCE_TIMEOUT_MS } from "constants/ApiConstants"; -import { Property } from "entities/Action"; - -interface DatasourceAuthentication { - authType?: string; - username?: string; - password?: string; -} - -export interface QueryTemplate { - title: string; - body: string; -} - -export interface DatasourceColumns { - name: string; - type: string; -} - -export interface DatasourceKeys { - name: string; - type: string; -} - -export interface DatasourceTable { - type: string; - name: string; - columns: DatasourceColumns[]; - keys: DatasourceKeys[]; - templates: QueryTemplate[]; -} - -export interface DatasourceStructure { - tables?: DatasourceTable[]; -} - -export interface Datasource { - id: string; - name: string; - pluginId: string; - organizationId?: string; - datasourceConfiguration: { - url: string; - authentication?: DatasourceAuthentication; - properties?: Record; - headers?: Property[]; - databaseName?: string; - }; - invalids?: string[]; - isValid?: boolean; - structure?: DatasourceStructure; -} +import { DatasourceAuthentication, Datasource } from "entities/Datasource"; export interface CreateDatasourceConfig { name: string; pluginId: string; @@ -66,6 +16,15 @@ export interface CreateDatasourceConfig { appName?: string; } +export interface EmbeddedRestDatasourceRequest { + datasourceConfiguration: { url: string }; + invalids: Array; + isValid: boolean; + name: string; + organizationId: string; + pluginId: string; +} + class DatasourcesApi extends API { static url = "v1/datasources"; diff --git a/app/client/src/api/ReleasesAPI.tsx b/app/client/src/api/ReleasesAPI.tsx new file mode 100644 index 0000000000..943d433767 --- /dev/null +++ b/app/client/src/api/ReleasesAPI.tsx @@ -0,0 +1,13 @@ +import { AxiosPromise } from "axios"; +import Api from "./Api"; +import { ApiResponse } from "./ApiResponses"; + +class ReleasesAPI extends Api { + static markAsReadURL = `v1/users/setReleaseNotesViewed`; + + static markAsRead(): AxiosPromise { + return Api.put(ReleasesAPI.markAsReadURL); + } +} + +export default ReleasesAPI; diff --git a/app/client/src/assets/icons/ads/app-icons/airplane.svg b/app/client/src/assets/icons/ads/app-icons/airplane.svg index fec26c75e5..f326a7d8df 100644 --- a/app/client/src/assets/icons/ads/app-icons/airplane.svg +++ b/app/client/src/assets/icons/ads/app-icons/airplane.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/alien.svg b/app/client/src/assets/icons/ads/app-icons/alien.svg index 4d694875e8..b3cb31a2b9 100644 --- a/app/client/src/assets/icons/ads/app-icons/alien.svg +++ b/app/client/src/assets/icons/ads/app-icons/alien.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/bar-graph.svg b/app/client/src/assets/icons/ads/app-icons/bar-graph.svg index bb13e5d1e1..cc0d2c07c1 100644 --- a/app/client/src/assets/icons/ads/app-icons/bar-graph.svg +++ b/app/client/src/assets/icons/ads/app-icons/bar-graph.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/basketball.svg b/app/client/src/assets/icons/ads/app-icons/basketball.svg index 1d9bdc7c3b..a9abe25022 100644 --- a/app/client/src/assets/icons/ads/app-icons/basketball.svg +++ b/app/client/src/assets/icons/ads/app-icons/basketball.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/bicycle.svg b/app/client/src/assets/icons/ads/app-icons/bicycle.svg index 068bbf8e40..83676fc004 100644 --- a/app/client/src/assets/icons/ads/app-icons/bicycle.svg +++ b/app/client/src/assets/icons/ads/app-icons/bicycle.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/bird.svg b/app/client/src/assets/icons/ads/app-icons/bird.svg index 41cf391efc..73b6ce5c85 100644 --- a/app/client/src/assets/icons/ads/app-icons/bird.svg +++ b/app/client/src/assets/icons/ads/app-icons/bird.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/bitcoin.svg b/app/client/src/assets/icons/ads/app-icons/bitcoin.svg index 0b5fda23af..ea39a19f3f 100644 --- a/app/client/src/assets/icons/ads/app-icons/bitcoin.svg +++ b/app/client/src/assets/icons/ads/app-icons/bitcoin.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/burger.svg b/app/client/src/assets/icons/ads/app-icons/burger.svg index 7b6ce4ec80..6a6be1ce9f 100644 --- a/app/client/src/assets/icons/ads/app-icons/burger.svg +++ b/app/client/src/assets/icons/ads/app-icons/burger.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/bus.svg b/app/client/src/assets/icons/ads/app-icons/bus.svg index 8cf680168e..185ea20016 100644 --- a/app/client/src/assets/icons/ads/app-icons/bus.svg +++ b/app/client/src/assets/icons/ads/app-icons/bus.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/call.svg b/app/client/src/assets/icons/ads/app-icons/call.svg index 1fca983245..db6a08dc29 100644 --- a/app/client/src/assets/icons/ads/app-icons/call.svg +++ b/app/client/src/assets/icons/ads/app-icons/call.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/car.svg b/app/client/src/assets/icons/ads/app-icons/car.svg index 26c7e15e55..b21baff53f 100644 --- a/app/client/src/assets/icons/ads/app-icons/car.svg +++ b/app/client/src/assets/icons/ads/app-icons/car.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/card.svg b/app/client/src/assets/icons/ads/app-icons/card.svg index 8bbeadf6cb..a68fe32705 100644 --- a/app/client/src/assets/icons/ads/app-icons/card.svg +++ b/app/client/src/assets/icons/ads/app-icons/card.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/cat.svg b/app/client/src/assets/icons/ads/app-icons/cat.svg index 94fcb591c8..ea0da21257 100644 --- a/app/client/src/assets/icons/ads/app-icons/cat.svg +++ b/app/client/src/assets/icons/ads/app-icons/cat.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/chat.svg b/app/client/src/assets/icons/ads/app-icons/chat.svg index 4e8b9218f9..7878e30543 100644 --- a/app/client/src/assets/icons/ads/app-icons/chat.svg +++ b/app/client/src/assets/icons/ads/app-icons/chat.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/chinese-remnibi.svg b/app/client/src/assets/icons/ads/app-icons/chinese-remnibi.svg index c44009709c..4e59f5470b 100644 --- a/app/client/src/assets/icons/ads/app-icons/chinese-remnibi.svg +++ b/app/client/src/assets/icons/ads/app-icons/chinese-remnibi.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/cloud.svg b/app/client/src/assets/icons/ads/app-icons/cloud.svg index d73e0c36af..e007fedfa8 100644 --- a/app/client/src/assets/icons/ads/app-icons/cloud.svg +++ b/app/client/src/assets/icons/ads/app-icons/cloud.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/coding.svg b/app/client/src/assets/icons/ads/app-icons/coding.svg index 599b1963e0..66428360ef 100644 --- a/app/client/src/assets/icons/ads/app-icons/coding.svg +++ b/app/client/src/assets/icons/ads/app-icons/coding.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/couples.svg b/app/client/src/assets/icons/ads/app-icons/couples.svg index 05f29680e0..0a3ded4ecf 100644 --- a/app/client/src/assets/icons/ads/app-icons/couples.svg +++ b/app/client/src/assets/icons/ads/app-icons/couples.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/cricket.svg b/app/client/src/assets/icons/ads/app-icons/cricket.svg index a710ba3b86..2bafba2f2d 100644 --- a/app/client/src/assets/icons/ads/app-icons/cricket.svg +++ b/app/client/src/assets/icons/ads/app-icons/cricket.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/diamond.svg b/app/client/src/assets/icons/ads/app-icons/diamond.svg index 478ac6de5b..a8f9b4ccd1 100644 --- a/app/client/src/assets/icons/ads/app-icons/diamond.svg +++ b/app/client/src/assets/icons/ads/app-icons/diamond.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/dog.svg b/app/client/src/assets/icons/ads/app-icons/dog.svg index e9331e2401..2a51dd8b23 100644 --- a/app/client/src/assets/icons/ads/app-icons/dog.svg +++ b/app/client/src/assets/icons/ads/app-icons/dog.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/dollar.svg b/app/client/src/assets/icons/ads/app-icons/dollar.svg index d7ee880fc5..de659efb62 100644 --- a/app/client/src/assets/icons/ads/app-icons/dollar.svg +++ b/app/client/src/assets/icons/ads/app-icons/dollar.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/earth.svg b/app/client/src/assets/icons/ads/app-icons/earth.svg index 1500003082..27cf1978f0 100644 --- a/app/client/src/assets/icons/ads/app-icons/earth.svg +++ b/app/client/src/assets/icons/ads/app-icons/earth.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/email.svg b/app/client/src/assets/icons/ads/app-icons/email.svg index 502a94157a..2e2d1ca9b6 100644 --- a/app/client/src/assets/icons/ads/app-icons/email.svg +++ b/app/client/src/assets/icons/ads/app-icons/email.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/euros.svg b/app/client/src/assets/icons/ads/app-icons/euros.svg index 80194307f7..56045acc1c 100644 --- a/app/client/src/assets/icons/ads/app-icons/euros.svg +++ b/app/client/src/assets/icons/ads/app-icons/euros.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/family.svg b/app/client/src/assets/icons/ads/app-icons/family.svg index 7daf4aa511..5f0b05e8c3 100644 --- a/app/client/src/assets/icons/ads/app-icons/family.svg +++ b/app/client/src/assets/icons/ads/app-icons/family.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/flag.svg b/app/client/src/assets/icons/ads/app-icons/flag.svg index 9636741042..29b8d23c37 100644 --- a/app/client/src/assets/icons/ads/app-icons/flag.svg +++ b/app/client/src/assets/icons/ads/app-icons/flag.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/football.svg b/app/client/src/assets/icons/ads/app-icons/football.svg index 68261826f9..cb8cf2058e 100644 --- a/app/client/src/assets/icons/ads/app-icons/football.svg +++ b/app/client/src/assets/icons/ads/app-icons/football.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/hat.svg b/app/client/src/assets/icons/ads/app-icons/hat.svg index 1f1810c60a..078af711ae 100644 --- a/app/client/src/assets/icons/ads/app-icons/hat.svg +++ b/app/client/src/assets/icons/ads/app-icons/hat.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/headphones.svg b/app/client/src/assets/icons/ads/app-icons/headphones.svg index ce376c7869..47b4439192 100644 --- a/app/client/src/assets/icons/ads/app-icons/headphones.svg +++ b/app/client/src/assets/icons/ads/app-icons/headphones.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/hospital.svg b/app/client/src/assets/icons/ads/app-icons/hospital.svg index b63bb8ef58..93169f404d 100644 --- a/app/client/src/assets/icons/ads/app-icons/hospital.svg +++ b/app/client/src/assets/icons/ads/app-icons/hospital.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/joystick.svg b/app/client/src/assets/icons/ads/app-icons/joystick.svg index ea52d91c7a..de02d59764 100644 --- a/app/client/src/assets/icons/ads/app-icons/joystick.svg +++ b/app/client/src/assets/icons/ads/app-icons/joystick.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/laptop.svg b/app/client/src/assets/icons/ads/app-icons/laptop.svg index 9877af6ff1..78b260a162 100644 --- a/app/client/src/assets/icons/ads/app-icons/laptop.svg +++ b/app/client/src/assets/icons/ads/app-icons/laptop.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/lightning.svg b/app/client/src/assets/icons/ads/app-icons/lightning.svg index 6fc909af9c..f253e4ab4c 100644 --- a/app/client/src/assets/icons/ads/app-icons/lightning.svg +++ b/app/client/src/assets/icons/ads/app-icons/lightning.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/line-chart.svg b/app/client/src/assets/icons/ads/app-icons/line-chart.svg index 6e3ed5f721..1fe99fbde1 100644 --- a/app/client/src/assets/icons/ads/app-icons/line-chart.svg +++ b/app/client/src/assets/icons/ads/app-icons/line-chart.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/location.svg b/app/client/src/assets/icons/ads/app-icons/location.svg index 77683a951a..0e38f83404 100644 --- a/app/client/src/assets/icons/ads/app-icons/location.svg +++ b/app/client/src/assets/icons/ads/app-icons/location.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/lotus.svg b/app/client/src/assets/icons/ads/app-icons/lotus.svg index 1f67e4dea2..935b75c915 100644 --- a/app/client/src/assets/icons/ads/app-icons/lotus.svg +++ b/app/client/src/assets/icons/ads/app-icons/lotus.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/love.svg b/app/client/src/assets/icons/ads/app-icons/love.svg index 3dd1b53756..e49f14bf4b 100644 --- a/app/client/src/assets/icons/ads/app-icons/love.svg +++ b/app/client/src/assets/icons/ads/app-icons/love.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/medal.svg b/app/client/src/assets/icons/ads/app-icons/medal.svg index 0e4723b7ae..4ea85cda8e 100644 --- a/app/client/src/assets/icons/ads/app-icons/medal.svg +++ b/app/client/src/assets/icons/ads/app-icons/medal.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/medical.svg b/app/client/src/assets/icons/ads/app-icons/medical.svg index 29c265ff5d..f100d4694d 100644 --- a/app/client/src/assets/icons/ads/app-icons/medical.svg +++ b/app/client/src/assets/icons/ads/app-icons/medical.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/money.svg b/app/client/src/assets/icons/ads/app-icons/money.svg index 03c37e2bcb..1cc599753f 100644 --- a/app/client/src/assets/icons/ads/app-icons/money.svg +++ b/app/client/src/assets/icons/ads/app-icons/money.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/moon.svg b/app/client/src/assets/icons/ads/app-icons/moon.svg index efde97011d..f25cb1129a 100644 --- a/app/client/src/assets/icons/ads/app-icons/moon.svg +++ b/app/client/src/assets/icons/ads/app-icons/moon.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/mug.svg b/app/client/src/assets/icons/ads/app-icons/mug.svg index ebec567f8a..6f03079cb1 100644 --- a/app/client/src/assets/icons/ads/app-icons/mug.svg +++ b/app/client/src/assets/icons/ads/app-icons/mug.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/music.svg b/app/client/src/assets/icons/ads/app-icons/music.svg index c90999c2d9..777d84bef3 100644 --- a/app/client/src/assets/icons/ads/app-icons/music.svg +++ b/app/client/src/assets/icons/ads/app-icons/music.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/pants.svg b/app/client/src/assets/icons/ads/app-icons/pants.svg index 22f51260ca..fe49a5e926 100644 --- a/app/client/src/assets/icons/ads/app-icons/pants.svg +++ b/app/client/src/assets/icons/ads/app-icons/pants.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/pie-chart.svg b/app/client/src/assets/icons/ads/app-icons/pie-chart.svg index a748d7884f..ed3accc76f 100644 --- a/app/client/src/assets/icons/ads/app-icons/pie-chart.svg +++ b/app/client/src/assets/icons/ads/app-icons/pie-chart.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/pizza.svg b/app/client/src/assets/icons/ads/app-icons/pizza.svg index 267f53f196..91816e598f 100644 --- a/app/client/src/assets/icons/ads/app-icons/pizza.svg +++ b/app/client/src/assets/icons/ads/app-icons/pizza.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/plant.svg b/app/client/src/assets/icons/ads/app-icons/plant.svg index 500d8aa93f..4d2a2c01f4 100644 --- a/app/client/src/assets/icons/ads/app-icons/plant.svg +++ b/app/client/src/assets/icons/ads/app-icons/plant.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/rainy-weather.svg b/app/client/src/assets/icons/ads/app-icons/rainy-weather.svg index 8afcfffad3..5102d69c42 100644 --- a/app/client/src/assets/icons/ads/app-icons/rainy-weather.svg +++ b/app/client/src/assets/icons/ads/app-icons/rainy-weather.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/repeat.svg b/app/client/src/assets/icons/ads/app-icons/repeat.svg index 3458657d81..c517ed66f3 100644 --- a/app/client/src/assets/icons/ads/app-icons/repeat.svg +++ b/app/client/src/assets/icons/ads/app-icons/repeat.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/restaurant.svg b/app/client/src/assets/icons/ads/app-icons/restaurant.svg index d526017d6a..137210a609 100644 --- a/app/client/src/assets/icons/ads/app-icons/restaurant.svg +++ b/app/client/src/assets/icons/ads/app-icons/restaurant.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/rocket.svg b/app/client/src/assets/icons/ads/app-icons/rocket.svg index b21023ba01..92eb43ac7f 100644 --- a/app/client/src/assets/icons/ads/app-icons/rocket.svg +++ b/app/client/src/assets/icons/ads/app-icons/rocket.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/rose.svg b/app/client/src/assets/icons/ads/app-icons/rose.svg index 559aaa9936..2abbc19053 100644 --- a/app/client/src/assets/icons/ads/app-icons/rose.svg +++ b/app/client/src/assets/icons/ads/app-icons/rose.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/rupee.svg b/app/client/src/assets/icons/ads/app-icons/rupee.svg index 4c9e2a0529..2967a77a9d 100644 --- a/app/client/src/assets/icons/ads/app-icons/rupee.svg +++ b/app/client/src/assets/icons/ads/app-icons/rupee.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/saturn.svg b/app/client/src/assets/icons/ads/app-icons/saturn.svg index ff28ec7b8b..72763e1864 100644 --- a/app/client/src/assets/icons/ads/app-icons/saturn.svg +++ b/app/client/src/assets/icons/ads/app-icons/saturn.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/server.svg b/app/client/src/assets/icons/ads/app-icons/server.svg index 2dba69a4fc..dc0e7d9d8a 100644 --- a/app/client/src/assets/icons/ads/app-icons/server.svg +++ b/app/client/src/assets/icons/ads/app-icons/server.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/shake-hands.svg b/app/client/src/assets/icons/ads/app-icons/shake-hands.svg index 6ab04552cf..97a325292d 100644 --- a/app/client/src/assets/icons/ads/app-icons/shake-hands.svg +++ b/app/client/src/assets/icons/ads/app-icons/shake-hands.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/shirt.svg b/app/client/src/assets/icons/ads/app-icons/shirt.svg index 92e7e8cb01..c0559f1b7a 100644 --- a/app/client/src/assets/icons/ads/app-icons/shirt.svg +++ b/app/client/src/assets/icons/ads/app-icons/shirt.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/shop.svg b/app/client/src/assets/icons/ads/app-icons/shop.svg index 37b1b07ce2..555ec181c2 100644 --- a/app/client/src/assets/icons/ads/app-icons/shop.svg +++ b/app/client/src/assets/icons/ads/app-icons/shop.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/single-person.svg b/app/client/src/assets/icons/ads/app-icons/single-person.svg index 7511e197fa..cf66b53e16 100644 --- a/app/client/src/assets/icons/ads/app-icons/single-person.svg +++ b/app/client/src/assets/icons/ads/app-icons/single-person.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/smartphone.svg b/app/client/src/assets/icons/ads/app-icons/smartphone.svg index d16b3595f9..a1f4885589 100644 --- a/app/client/src/assets/icons/ads/app-icons/smartphone.svg +++ b/app/client/src/assets/icons/ads/app-icons/smartphone.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/snowy-weather.svg b/app/client/src/assets/icons/ads/app-icons/snowy-weather.svg index 0c4c3876ac..7cbaf6e113 100644 --- a/app/client/src/assets/icons/ads/app-icons/snowy-weather.svg +++ b/app/client/src/assets/icons/ads/app-icons/snowy-weather.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/stars.svg b/app/client/src/assets/icons/ads/app-icons/stars.svg index 49f523ea5e..748760d53e 100644 --- a/app/client/src/assets/icons/ads/app-icons/stars.svg +++ b/app/client/src/assets/icons/ads/app-icons/stars.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/steam-bowl.svg b/app/client/src/assets/icons/ads/app-icons/steam-bowl.svg index 5cd6e6acca..11651581cd 100644 --- a/app/client/src/assets/icons/ads/app-icons/steam-bowl.svg +++ b/app/client/src/assets/icons/ads/app-icons/steam-bowl.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/sunflower.svg b/app/client/src/assets/icons/ads/app-icons/sunflower.svg index 370f3d99f7..9c2aaf3157 100644 --- a/app/client/src/assets/icons/ads/app-icons/sunflower.svg +++ b/app/client/src/assets/icons/ads/app-icons/sunflower.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/system.svg b/app/client/src/assets/icons/ads/app-icons/system.svg index 4c6fd9037b..649057b7ca 100644 --- a/app/client/src/assets/icons/ads/app-icons/system.svg +++ b/app/client/src/assets/icons/ads/app-icons/system.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/team.svg b/app/client/src/assets/icons/ads/app-icons/team.svg index e0f6459a67..a5cdf7afe7 100644 --- a/app/client/src/assets/icons/ads/app-icons/team.svg +++ b/app/client/src/assets/icons/ads/app-icons/team.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/tree.svg b/app/client/src/assets/icons/ads/app-icons/tree.svg index 362535398e..4f95e0e6f1 100644 --- a/app/client/src/assets/icons/ads/app-icons/tree.svg +++ b/app/client/src/assets/icons/ads/app-icons/tree.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/uk-pounds.svg b/app/client/src/assets/icons/ads/app-icons/uk-pounds.svg index 1776161216..146967dee8 100644 --- a/app/client/src/assets/icons/ads/app-icons/uk-pounds.svg +++ b/app/client/src/assets/icons/ads/app-icons/uk-pounds.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/website.svg b/app/client/src/assets/icons/ads/app-icons/website.svg index 8d1778a39d..b65159e209 100644 --- a/app/client/src/assets/icons/ads/app-icons/website.svg +++ b/app/client/src/assets/icons/ads/app-icons/website.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/app-icons/yen.svg b/app/client/src/assets/icons/ads/app-icons/yen.svg index 3bd059fbbd..34ee2cb633 100644 --- a/app/client/src/assets/icons/ads/app-icons/yen.svg +++ b/app/client/src/assets/icons/ads/app-icons/yen.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/view-less.svg b/app/client/src/assets/icons/ads/view-less.svg new file mode 100644 index 0000000000..033fd57406 --- /dev/null +++ b/app/client/src/assets/icons/ads/view-less.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/client/src/assets/icons/help/updates.svg b/app/client/src/assets/icons/help/updates.svg new file mode 100644 index 0000000000..b87e18c3c1 --- /dev/null +++ b/app/client/src/assets/icons/help/updates.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/components/ads/DialogComponent.tsx b/app/client/src/components/ads/DialogComponent.tsx new file mode 100644 index 0000000000..fd49a8d6c3 --- /dev/null +++ b/app/client/src/components/ads/DialogComponent.tsx @@ -0,0 +1,137 @@ +import React, { ReactNode, useState, useEffect } from "react"; +import styled from "styled-components"; +import { Dialog, Classes } from "@blueprintjs/core"; + +const StyledDialog = styled(Dialog)<{ + setMaxWidth?: boolean; + width?: string; + maxHeight?: string; + showHeaderUnderline?: boolean; +}>` + && { + border-radius: ${(props) => props.theme.radii[0]}px; + padding-bottom: ${(props) => props.theme.spaces[2]}px; + background: ${(props) => props.theme.colors.modal.bg}; + ${(props) => (props.maxHeight ? `max-height: ${props.maxHeight};` : "")} + width: ${(props) => props.width || "640px"}; + ${(props) => props.setMaxWidth && `width: 100vh;`} + + & .${Classes.DIALOG_HEADER} { + position: relative; + padding: ${(props) => props.theme.spaces[4]}px; + background: ${(props) => props.theme.colors.modal.bg}; + box-shadow: none; + .${Classes.ICON} { + color: ${(props) => props.theme.colors.modal.iconColor}; + } + + .${Classes.BUTTON}.${Classes.MINIMAL}:hover { + background-color: ${(props) => props.theme.colors.modal.bg}; + } + } + + .${Classes.HEADING} { + color: ${(props) => props.theme.colors.modal.headerText}; + display: flex; + justify-content: center; + margin-top: ${(props) => props.theme.spaces[9]}px; + font-weight: ${(props) => props.theme.typography.h1.fontWeight}; + font-size: ${(props) => props.theme.typography.h1.fontSize}px; + line-height: ${(props) => props.theme.typography.h1.lineHeight}px; + letter-spacing: ${(props) => props.theme.typography.h1.letterSpacing}; + } + + ${(props) => + props.showHeaderUnderline + ? ` + & .${Classes.DIALOG_HEADER}:after { + content: ""; + width: calc(100% - 40px); + height: 1px; + position: absolute; + background: white; + left: 50%; + bottom: 0; + transform: translateX(-50%); + background-color: ${props.theme.colors.modal.separator}; + } + + .${Classes.HEADING} { + margin-bottom: ${props.theme.spaces[7]}px; + } + ` + : ""} + + & .${Classes.DIALOG_BODY} { + padding: ${(props) => props.theme.spaces[9]}px; + margin: 0; + overflow: auto; + } + + & .${Classes.DIALOG_FOOTER_ACTIONS} { + display: block; + } + } +`; + +const TriggerWrapper = styled.div``; + +type DialogComponentProps = { + isOpen?: boolean; + canOutsideClickClose?: boolean; + title?: string; + trigger: ReactNode; + setMaxWidth?: boolean; + children: ReactNode; + width?: string; + maxHeight?: string; + onOpening?: () => void; + triggerZIndex?: number; + showHeaderUnderline?: boolean; + getHeader?: () => ReactNode; + canEscapeKeyClose?: boolean; +}; + +export const DialogComponent = (props: DialogComponentProps) => { + const [isOpen, setIsOpen] = useState(!!props.isOpen); + + const onClose = () => { + setIsOpen(false); + }; + + useEffect(() => { + setIsOpen(!!props.isOpen); + }, [props.isOpen]); + + const getHeader = props.getHeader; + + return ( + + { + setIsOpen(true); + }} + style={{ zIndex: props.triggerZIndex }} + > + {props.trigger} + + + {getHeader && getHeader()} +
{props.children}
+
+
+ ); +}; + +export default DialogComponent; diff --git a/app/client/src/components/ads/Dropdown.tsx b/app/client/src/components/ads/Dropdown.tsx index b25638b1b3..dba7b6ea31 100644 --- a/app/client/src/components/ads/Dropdown.tsx +++ b/app/client/src/components/ads/Dropdown.tsx @@ -3,6 +3,7 @@ import Icon, { IconName, IconSize } from "./Icon"; import { CommonComponentProps, Classes } from "./common"; import styled from "styled-components"; import Text, { TextType } from "./Text"; +import { Popover, Position } from "@blueprintjs/core"; type DropdownOption = { label?: string; @@ -56,15 +57,12 @@ const Selected = styled.div<{ isOpen: boolean; disabled?: boolean }>` } `; -const DropdownWrapper = styled.div` - position: absolute; - top: 38px; - left: 0px; +const DropdownWrapper = styled.div<{ width?: number }>` + width: ${(props) => props.width || 260}px; z-index: 1; margin-top: ${(props) => props.theme.spaces[2] - 1}px; background: ${(props) => props.theme.colors.dropdown.menuBg}; box-shadow: 0px 12px 28px ${(props) => props.theme.colors.dropdown.menuShadow}; - width: 100%; `; const OptionWrapper = styled.div<{ selected: boolean }>` @@ -128,6 +126,7 @@ export default function Dropdown(props: DropdownProps) { const { onSelect } = { ...props }; const [isOpen, setIsOpen] = useState(false); const [selected, setSelected] = useState(props.selected); + const [containerWidth, setContainerWidth] = useState(0); useEffect(() => { setSelected(props.selected); @@ -143,23 +142,30 @@ export default function Dropdown(props: DropdownProps) { [onSelect], ); - return ( - setIsOpen(false)} - data-cy={props.cypressSelector} - > - setIsOpen(!isOpen)} - > - {selected.value} - - + const measuredRef = useCallback((node) => { + if (node !== null) { + setContainerWidth(node.getBoundingClientRect().width); + } + }, []); - {isOpen && !props.disabled ? ( - + return ( + + setIsOpen(state)} + boundary="viewport" + > + setIsOpen(!isOpen)} + > + {selected.value} + + + {props.options.map((option: DropdownOption, index: number) => { return ( - ) : null} + ); } diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx index 31f2ca1e1c..6afa528566 100644 --- a/app/client/src/components/ads/EditableText.tsx +++ b/app/client/src/components/ads/EditableText.tsx @@ -6,7 +6,7 @@ import { import styled from "styled-components"; import Text, { TextType } from "./Text"; import Spinner from "./Spinner"; -import { Classes, CommonComponentProps } from "./common"; +import { CommonComponentProps } from "./common"; import { noop } from "lodash"; import Icon, { IconSize } from "./Icon"; import { getThemeDetails } from "selectors/themeSelectors"; diff --git a/app/client/src/components/ads/EditableTextWrapper.tsx b/app/client/src/components/ads/EditableTextWrapper.tsx index 0361812a76..41d9a93931 100644 --- a/app/client/src/components/ads/EditableTextWrapper.tsx +++ b/app/client/src/components/ads/EditableTextWrapper.tsx @@ -96,7 +96,7 @@ export default function EditableTextWrapper(props: EditableTextWrapperProps) { props.onBlur(value); }} className={props.className} - onTextChanged={(value: string) => setIsEditing(true)} + onTextChanged={() => setIsEditing(true)} isInvalid={(value: string) => { setIsEditing(true); if (props.isInvalid) { diff --git a/app/client/src/components/ads/FilePicker.tsx b/app/client/src/components/ads/FilePicker.tsx index 8bd777ed3d..52eebae6b1 100644 --- a/app/client/src/components/ads/FilePicker.tsx +++ b/app/client/src/components/ads/FilePicker.tsx @@ -335,7 +335,7 @@ const FilePickerComponent = (props: FilePickerProps) => { icon="delete" size={Size.medium} category={Category.tertiary} - onClick={(el) => removeFile()} + onClick={() => removeFile()} /> diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index 3a973712ec..34e880b2c0 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -16,6 +16,7 @@ import { ReactComponent as WorkspaceIcon } from "assets/icons/ads/workspace.svg" import { ReactComponent as CreateNewIcon } from "assets/icons/ads/create-new.svg"; import { ReactComponent as InviteUserIcon } from "assets/icons/ads/invite-users.svg"; import { ReactComponent as ViewAllIcon } from "assets/icons/ads/view-all.svg"; +import { ReactComponent as ViewLessIcon } from "assets/icons/ads/view-less.svg"; import { ReactComponent as ContextMenuIcon } from "assets/icons/ads/context-menu.svg"; import { ReactComponent as DuplicateIcon } from "assets/icons/ads/duplicate.svg"; import { ReactComponent as LogoutIcon } from "assets/icons/ads/logout.svg"; @@ -87,6 +88,7 @@ export const IconCollection = [ "plus", "invite-user", "view-all", + "view-less", "warning", "downArrow", "context-menu", @@ -107,7 +109,7 @@ const IconWrapper = styled.span` width: ${(props) => sizeHandler(props.size)}px; height: ${(props) => sizeHandler(props.size)}px; path { - fill: ${(props) => props.theme.colors.icon.normal}; + fill: ${(props) => props.fillColor || props.theme.colors.icon.normal}; } } ${(props) => (props.invisible ? `visibility: hidden;` : null)}; @@ -133,6 +135,7 @@ export type IconProps = { invisible?: boolean; className?: string; onClick?: () => void; + fillColor?: string; }; const Icon = forwardRef( @@ -187,6 +190,9 @@ const Icon = forwardRef( case "view-all": returnIcon = ; break; + case "view-less": + returnIcon = ; + break; case "context-menu": returnIcon = ; break; diff --git a/app/client/src/components/ads/LighteningMenu.tsx b/app/client/src/components/ads/LighteningMenu.tsx index c5cf4a5774..5c9ff4bd59 100644 --- a/app/client/src/components/ads/LighteningMenu.tsx +++ b/app/client/src/components/ads/LighteningMenu.tsx @@ -1,4 +1,4 @@ // TODO -export default function TreeView(props: any) { +export default function TreeView() { return null; } diff --git a/app/client/src/components/ads/Tag.tsx b/app/client/src/components/ads/Tag.tsx index 40c6f8a5c7..ebb94d2e38 100644 --- a/app/client/src/components/ads/Tag.tsx +++ b/app/client/src/components/ads/Tag.tsx @@ -6,6 +6,7 @@ type TagProps = CommonComponentProps & { variant?: "success" | "info" | "warning" | "danger"; //default info }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars export default function Tag(props: TagProps) { return null; } diff --git a/app/client/src/components/ads/TagInputComponent.tsx b/app/client/src/components/ads/TagInputComponent.tsx index b2de647581..5a8bda155d 100644 --- a/app/client/src/components/ads/TagInputComponent.tsx +++ b/app/client/src/components/ads/TagInputComponent.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react"; import styled from "styled-components"; import { Classes, TagInput } from "@blueprintjs/core"; import { Intent } from "constants/DefaultTheme"; -import { WrappedFieldMetaProps } from "redux-form"; import { INVITE_USERS_VALIDATION_EMAIL_LIST } from "constants/messages"; import { isEmail } from "utils/formhelpers"; const TagInputWrapper = styled.div<{ intent?: Intent }>` diff --git a/app/client/src/components/ads/Tree.tsx b/app/client/src/components/ads/Tree.tsx index 3aa88e5264..d47d1d2697 100644 --- a/app/client/src/components/ads/Tree.tsx +++ b/app/client/src/components/ads/Tree.tsx @@ -1,4 +1,5 @@ //TODO +// eslint-disable-next-line @typescript-eslint/no-unused-vars export default function Tree(props: any) { return null; } diff --git a/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx b/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx index bb072dce66..9711c52d06 100644 --- a/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx @@ -40,7 +40,15 @@ const ContainerComponent = (props: ContainerComponentProps) => { const containerRef: RefObject = useRef(null); useEffect(() => { if (!props.shouldScrollContents) { - containerRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + const supportsNativeSmoothScroll = + "scrollBehavior" in document.documentElement.style; + if (supportsNativeSmoothScroll) { + containerRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + } else { + if (containerRef.current) { + containerRef.current.scrollTop = 0; + } + } } }, [props.shouldScrollContents]); return ( diff --git a/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx b/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx index 1a4da5d7c7..ccaae70649 100644 --- a/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx @@ -83,7 +83,7 @@ export class DroppableComponent extends React.Component< return ( - {({ innerRef, droppableProps, placeholder }) => ( + {({ innerRef, droppableProps }) => ( } {...droppableProps} diff --git a/app/client/src/components/designSystems/appsmith/ImageComponent.tsx b/app/client/src/components/designSystems/appsmith/ImageComponent.tsx index e82a6303e1..dd66dc6154 100644 --- a/app/client/src/components/designSystems/appsmith/ImageComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ImageComponent.tsx @@ -120,7 +120,7 @@ class ImageComponent extends React.Component< } }} > - {({ zoomIn, zoomOut, setScale, ...rest }: any) => ( + {({ zoomIn, zoomOut }: any) => ( { props.selectedMarker.lat === marker.lat && props.selectedMarker.long === marker.long } - onClick={(e) => { + onClick={() => { setMapCenter({ ...marker, lng: marker.long, diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx index f4a3200b8f..9de8406e6d 100644 --- a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -12,7 +12,6 @@ import { ReactTableFilter, } from "widgets/TableWidget"; import { EventType } from "constants/ActionConstants"; -import produce from "immer"; export interface ColumnMenuOptionProps { content: string | JSX.Element; diff --git a/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx b/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx index 1f8e8f4cd8..20a9819579 100644 --- a/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState, useRef } from "react"; import { debounce } from "lodash"; import styled from "styled-components"; +import { useScript, ScriptStatus } from "utils/hooks/useScript"; const StyledRTEditor = styled.div` && { width: 100%; @@ -22,6 +23,12 @@ export interface RichtextEditorComponentProps { export const RichtextEditorComponent = ( props: RichtextEditorComponentProps, ) => { + const status = useScript( + "https://cdnjs.cloudflare.com/ajax/libs/tinymce/5.4.0/tinymce.min.js", + ); + + const [isEditorInitialised, setIsEditorInitialised] = useState(false); + const [editorInstance, setEditorInstance] = useState(null as any); /* Using editorContent as a variable to save editor content locally to verify against new content*/ const editorContent = useRef(""); @@ -32,7 +39,7 @@ export const RichtextEditorComponent = ( props.isDisabled === true ? "readonly" : "design", ); } - }, [props.isDisabled]); + }, [props.isDisabled, editorInstance, isEditorInitialised]); useEffect(() => { if ( @@ -40,17 +47,17 @@ export const RichtextEditorComponent = ( (editorContent.current.length === 0 || editorContent.current !== props.defaultValue) ) { - setTimeout(() => { - const content = props.defaultValue - ? props.defaultValue.replace(/\n/g, "
") - : props.defaultValue; - editorInstance.setContent(content, { - format: "html", - }); - }, 200); + const content = props.defaultValue + ? props.defaultValue.replace(/\n/g, "
") + : props.defaultValue; + + editorInstance.setContent(content, { + format: "html", + }); } - }, [props.defaultValue]); + }, [props.defaultValue, editorInstance, isEditorInitialised]); useEffect(() => { + if (status !== ScriptStatus.READY) return; const onChange = debounce((content: string) => { editorContent.current = content; props.onValueChange(content); @@ -65,13 +72,10 @@ export const RichtextEditorComponent = ( resize: false, setup: (editor: any) => { editor.mode.set(props.isDisabled === true ? "readonly" : "design"); - // Without timeout default value is not set on browser refresh. - setTimeout(() => { - const content = props.defaultValue - ? props.defaultValue.replace(/\n/g, "
") - : props.defaultValue; - editor.setContent(content, { format: "html" }); - }, 300); + const content = props.defaultValue + ? props.defaultValue.replace(/\n/g, "
") + : props.defaultValue; + editor.setContent(content, { format: "html" }); editor .on("Change", () => { onChange(editor.getContent()); @@ -86,6 +90,9 @@ export const RichtextEditorComponent = ( onChange(editor.getContent()); }); setEditorInstance(editor); + editor.on("init", () => { + setIsEditorInitialised(true); + }); }, plugins: [ "advlist autolink lists link image charmap print preview anchor", @@ -100,7 +107,10 @@ export const RichtextEditorComponent = ( (window as any).tinyMCE.EditorManager.remove(selector); editorInstance !== null && editorInstance.remove(); }; - }, []); + }, [status]); + + if (status !== ScriptStatus.READY) return null; + return ( diff --git a/app/client/src/components/designSystems/appsmith/Table.tsx b/app/client/src/components/designSystems/appsmith/Table.tsx index 21187fb300..46a7bd65cf 100644 --- a/app/client/src/components/designSystems/appsmith/Table.tsx +++ b/app/client/src/components/designSystems/appsmith/Table.tsx @@ -74,7 +74,7 @@ export const Table = (props: TableProps) => { columnActions: props.columnActions, compactMode: props.compactMode, }); - // eslint-disable-next-line react-hooks/exhaustive-deps + const columns = React.useMemo(() => props.columns, [columnString]); const pageCount = Math.ceil(props.data.length / props.pageSize); diff --git a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx index 46453803df..9676f1a010 100644 --- a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx +++ b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx @@ -608,7 +608,7 @@ const RenameColumn = (props: { defaultValue={columnName} onChange={onColumnNameChange} onKeyPress={(e) => onKeyPress(e.key)} - onBlur={(e) => handleColumnNameUpdate()} + onBlur={handleColumnNameUpdate} /> ); }; diff --git a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx index e5a3364a9d..f8139b3cfc 100644 --- a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx @@ -111,6 +111,7 @@ const StyledText = styled.div` `; const TabsComponent = (props: TabsComponentProps) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { onTabChange, ...remainingProps } = props; const tabContainerRef: RefObject = useRef( null, diff --git a/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx b/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx index afc9dc5608..8f8b766fc5 100644 --- a/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx @@ -11,6 +11,7 @@ import { import { ComponentProps } from "./BaseComponent"; import ErrorTooltip from "components/editorComponents/ErrorTooltip"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars export const TextInput = styled(({ hasError, ...rest }) => ( ))<{ hasError: boolean }>` @@ -79,6 +80,7 @@ export interface TextInputProps extends IInputGroupProps { refHandler?: any; noValidate?: boolean; readonly?: boolean; + autoFocus?: boolean; } interface TextInputState { diff --git a/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx b/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx index 270d9947d5..ca7fd9216b 100644 --- a/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx +++ b/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx @@ -287,6 +287,7 @@ const HelpFooter = styled.div` padding: 5px 10px; height: 30px; color: rgba(255, 255, 255, 0.7); + font-size: 6pt; `; const HelpBody = styled.div` diff --git a/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx b/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx index 13d49a37a9..d455d75cd3 100644 --- a/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx +++ b/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx @@ -16,7 +16,6 @@ import { AppState } from "reducers"; import { getCurrentUser } from "selectors/usersSelectors"; import { User } from "constants/userConstants"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import { Icon } from "@blueprintjs/core"; const { algolia, cloudHosting, intercomAppID } = getAppsmithConfigs(); const HelpButton = styled.button<{ @@ -49,7 +48,7 @@ const HelpButton = styled.button<{ `; const MODAL_WIDTH = 240; -const MODAL_HEIGHT = 198; +const MODAL_HEIGHT = 206; const MODAL_BOTTOM_DISTANCE = 45; const MODAL_RIGHT_DISTANCE = 30; diff --git a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx index 0759a877c5..bfe3dd995c 100644 --- a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx @@ -2,6 +2,7 @@ import React, { ReactNode, RefObject, useRef, useEffect } from "react"; import { Overlay, Classes } from "@blueprintjs/core"; import styled from "styled-components"; import { getCanvasClassName } from "utils/generators"; + const Container = styled.div<{ width: number; height: number; diff --git a/app/client/src/components/editorComponents/ActionNameEditor.tsx b/app/client/src/components/editorComponents/ActionNameEditor.tsx index 96cbca1a20..e5480d9d9b 100644 --- a/app/client/src/components/editorComponents/ActionNameEditor.tsx +++ b/app/client/src/components/editorComponents/ActionNameEditor.tsx @@ -8,7 +8,7 @@ import EditableText, { } from "components/editorComponents/EditableText"; import { removeSpecialChars, isNameValid } from "utils/helpers"; import { AppState } from "reducers"; -import { RestAction } from "entities/Action"; +import { Action } from "entities/Action"; import { getDataTree } from "selectors/dataTreeSelectors"; import { getExistingPageNames } from "sagas/selectors"; @@ -49,11 +49,11 @@ export const ActionNameEditor = () => { return isInOnboarding && currentStep < OnboardingStep.ADD_WIDGET; }); - const actions: RestAction[] = useSelector((state: AppState) => + const actions: Action[] = useSelector((state: AppState) => state.entities.actions.map((action) => action.config), ); - const currentActionConfig: RestAction | undefined = actions.find( + const currentActionConfig: Action | undefined = actions.find( (action) => action.id === params.apiId || action.id === params.queryId, ); diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 7242de3ceb..ddfe36a462 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -1,7 +1,6 @@ import React, { useState } from "react"; import { connect } from "react-redux"; import { withRouter, RouteComponentProps } from "react-router"; -import FormRow from "./FormRow"; import { BaseText } from "components/designSystems/blueprint/TextComponent"; import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView"; import styled from "styled-components"; diff --git a/app/client/src/components/editorComponents/DropTargetComponent.tsx b/app/client/src/components/editorComponents/DropTargetComponent.tsx index 8fb3d91473..d63b769641 100644 --- a/app/client/src/components/editorComponents/DropTargetComponent.tsx +++ b/app/client/src/components/editorComponents/DropTargetComponent.tsx @@ -40,13 +40,6 @@ type DropTargetComponentProps = WidgetProps & { minHeight: number; }; -type DropTargetBounds = { - x: number; - y: number; - width: number; - height: number; -}; - const StyledDropTarget = styled.div` transition: height 100ms ease-in; width: 100%; diff --git a/app/client/src/components/editorComponents/LightningMenu/helpers.tsx b/app/client/src/components/editorComponents/LightningMenu/helpers.tsx index 0f28d86643..c5873269b4 100644 --- a/app/client/src/components/editorComponents/LightningMenu/helpers.tsx +++ b/app/client/src/components/editorComponents/LightningMenu/helpers.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { RestAction } from "entities/Action"; +import { Action } from "entities/Action"; import { Directions } from "utils/helpers"; import { WidgetProps } from "widgets/BaseWidget"; import CustomizedDropdown, { @@ -24,7 +24,7 @@ import { ReduxAction } from "constants/ReduxActionConstants"; export const getApiOptions = ( skin: Skin, - apis: RestAction[], + apis: Action[], pageId: string, dispatch: (action: ReduxAction) => void, updateDynamicInputValue: (value: string, cursor?: number) => void, @@ -73,7 +73,7 @@ export const getApiOptions = ( export const getQueryOptions = ( skin: Skin, - queries: RestAction[], + queries: Action[], pageId: string, dispatch: (action: ReduxAction) => void, updateDynamicInputValue: (value: string, cursor?: number) => void, @@ -151,8 +151,8 @@ export const getWidgetOptions = ( }); export const getLightningMenuOptions = ( - apis: RestAction[], - queries: RestAction[], + apis: Action[], + queries: Action[], widgets: WidgetProps[], pageId: string, dispatch: (action: ReduxAction) => void, diff --git a/app/client/src/components/editorComponents/LightningMenu/hooks.ts b/app/client/src/components/editorComponents/LightningMenu/hooks.ts index a20b55b370..66af064609 100644 --- a/app/client/src/components/editorComponents/LightningMenu/hooks.ts +++ b/app/client/src/components/editorComponents/LightningMenu/hooks.ts @@ -1,7 +1,7 @@ import { useSelector } from "react-redux"; import { AppState } from "reducers"; import { WidgetProps } from "widgets/BaseWidget"; -import { RestAction } from "entities/Action"; +import { Action } from "entities/Action"; export const useWidgets = () => { return useSelector((state: AppState) => { @@ -21,11 +21,11 @@ export const useActions = () => { (action) => action.config.pageId === currentPageId, ); }); - const apis: RestAction[] = actions + const apis: Action[] = actions .filter((action) => action.config.pluginType === "API") .map((action) => action.config); - const queries: RestAction[] = actions + const queries: Action[] = actions .filter((action) => action.config.pluginType === "DB") .map((action) => action.config); diff --git a/app/client/src/components/editorComponents/LightningMenu/index.tsx b/app/client/src/components/editorComponents/LightningMenu/index.tsx index 8da9f90699..aa7ffe5245 100644 --- a/app/client/src/components/editorComponents/LightningMenu/index.tsx +++ b/app/client/src/components/editorComponents/LightningMenu/index.tsx @@ -4,7 +4,7 @@ import CustomizedDropdown, { } from "pages/common/CustomizedDropdown"; import { Directions } from "utils/helpers"; -import { RestAction } from "entities/Action"; +import { Action } from "entities/Action"; import { WidgetProps } from "widgets/BaseWidget"; import { getLightningMenuOptions } from "./helpers"; import { LightningMenuTrigger } from "./LightningMenuTrigger"; @@ -15,8 +15,8 @@ import { useDispatch } from "react-redux"; const lightningMenuOptions = ( skin: Skin, - apis: RestAction[], - queries: RestAction[], + apis: Action[], + queries: Action[], widgets: WidgetProps[], pageId: string, dispatch: (action: unknown) => void, diff --git a/app/client/src/components/editorComponents/Onboarding/CompletionDialog.tsx b/app/client/src/components/editorComponents/Onboarding/CompletionDialog.tsx index 41ad231ace..c0423d558e 100644 --- a/app/client/src/components/editorComponents/Onboarding/CompletionDialog.tsx +++ b/app/client/src/components/editorComponents/Onboarding/CompletionDialog.tsx @@ -104,7 +104,7 @@ const CompletionDialog = () => { if (params.onboardingComplete && inOnboarding) { setTimeout(() => { setIsOpen(true); - }, 3000); + }, 1000); } }; diff --git a/app/client/src/components/editorComponents/Onboarding/Tooltip.tsx b/app/client/src/components/editorComponents/Onboarding/Tooltip.tsx index 19ddb89629..e5fe35aa81 100644 --- a/app/client/src/components/editorComponents/Onboarding/Tooltip.tsx +++ b/app/client/src/components/editorComponents/Onboarding/Tooltip.tsx @@ -17,6 +17,7 @@ import { Colors } from "constants/Colors"; import { OnboardingStep, OnboardingTooltip, + OnboardingConfig, } from "constants/OnboardingConstants"; import { BaseModifier } from "popper.js"; import AnalyticsUtil from "utils/AnalyticsUtil"; @@ -195,6 +196,10 @@ type ToolTipContentProps = { }; const ToolTipContent = (props: ToolTipContentProps) => { + const showingTooltip = useSelector( + (state) => state.ui.onBoarding.showingTooltip, + ); + const dispatch = useDispatch(); const { title, @@ -211,7 +216,10 @@ const ToolTipContent = (props: ToolTipContentProps) => { }; const skipOnboarding = () => { - AnalyticsUtil.logEvent("SKIP_ONBOARDING"); + const onboardingStep = OnboardingConfig[showingTooltip].name; + + // Logging at which step was the skip onboarding clicked + AnalyticsUtil.logEvent("SKIP_ONBOARDING", { step: onboardingStep }); dispatch(endOnboarding()); }; diff --git a/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx b/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx index 4fca930104..56b78c2195 100644 --- a/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx +++ b/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx @@ -189,40 +189,6 @@ const enumTypeGetter = ( return defaultValue; }; -const objectTypeSetter = ( - obj: Object, - currentValue: string, - argNum: number, -): string => { - const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)]; - let args: string[] = []; - if (matches.length) { - args = argsStringToArray(matches[0][2]); - args[argNum] = JSON.stringify(obj); - } - const result = currentValue.replace( - ACTION_TRIGGER_REGEX, - `{{$1(${args.join(",")})}}`, - ); - return result; -}; - -const objectTypeGetter = ( - value: string, - argNum: number, - defaultValue = undefined, -): Object | undefined => { - const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; - if (matches.length) { - const args = argsStringToArray(matches[0][2]); - const arg = args[argNum]; - if (arg) { - return JSON.parse(arg.trim()); - } - } - return defaultValue; -}; - type ActionCreatorProps = { value: string; isValid: boolean; @@ -240,6 +206,7 @@ const ActionType = { showAlert: "showAlert", storeValue: "storeValue", download: "download", + copyToClipboard: "copyToClipboard", }; type ActionType = typeof ActionType[keyof typeof ActionType]; @@ -348,6 +315,7 @@ const FieldType = { DOWNLOAD_DATA_FIELD: "DOWNLOAD_DATA_FIELD", DOWNLOAD_FILE_NAME_FIELD: "DOWNLOAD_FILE_NAME_FIELD", DOWNLOAD_FILE_TYPE_FIELD: "DOWNLOAD_FILE_TYPE_FIELD", + COPY_TEXT_FIELD: "COPY_TEXT_FIELD", }; type FieldType = typeof FieldType[keyof typeof FieldType]; @@ -378,11 +346,7 @@ const fieldConfigs: FieldConfigs = { } return mainFuncSelectedValue; }, - setter: ( - option: TreeDropdownOption, - currentValue: string, - defaultValue?: string, - ) => { + setter: (option: TreeDropdownOption) => { const type: ActionType = option.type || option.value; let value = option.value; switch (type) { @@ -513,6 +477,15 @@ const fieldConfigs: FieldConfigs = { enumTypeSetter(option.value, currentValue, 2), view: ViewTypes.SELECTOR_VIEW, }, + [FieldType.COPY_TEXT_FIELD]: { + getter: (value: any) => { + return textGetter(value, 0); + }, + setter: (option: any, currentValue: string) => { + return textSetter(option, currentValue, 0); + }, + view: ViewTypes.TEXT_VIEW, + }, }; const baseOptions: any = [ @@ -553,6 +526,10 @@ const baseOptions: any = [ label: "Download", value: ActionType.download, }, + { + label: "Copy to Clipboard", + value: ActionType.copyToClipboard, + }, ]; function getOptionsWithChildren( options: TreeDropdownOption[], @@ -696,6 +673,11 @@ function getFieldFromValue( }, ); } + if (value.indexOf("copyToClipboard") !== -1) { + fields.push({ + field: FieldType.COPY_TEXT_FIELD, + }); + } return fields; } @@ -833,6 +815,7 @@ function renderField(props: { case FieldType.QUERY_PARAMS_FIELD: case FieldType.DOWNLOAD_DATA_FIELD: case FieldType.DOWNLOAD_FILE_NAME_FIELD: + case FieldType.COPY_TEXT_FIELD: let fieldLabel = ""; if (fieldType === FieldType.ALERT_TEXT_FIELD) { fieldLabel = "Message"; @@ -848,6 +831,8 @@ function renderField(props: { fieldLabel = "Data to download"; } else if (fieldType === FieldType.DOWNLOAD_FILE_NAME_FIELD) { fieldLabel = "File name with extension"; + } else if (fieldType === FieldType.COPY_TEXT_FIELD) { + fieldLabel = "Text to be copied to clipboard"; } viewElement = (view as (props: TextViewProps) => JSX.Element)({ label: fieldLabel, diff --git a/app/client/src/components/editorComponents/form/FormDialogComponent.tsx b/app/client/src/components/editorComponents/form/FormDialogComponent.tsx index b23de00b59..8b0ea25a8c 100644 --- a/app/client/src/components/editorComponents/form/FormDialogComponent.tsx +++ b/app/client/src/components/editorComponents/form/FormDialogComponent.tsx @@ -1,47 +1,6 @@ -import React, { ReactNode, useState } from "react"; -import styled from "styled-components"; -import { Dialog, Classes } from "@blueprintjs/core"; +import React, { ReactNode, useState, useCallback } from "react"; import { isPermitted } from "pages/Applications/permissionHelpers"; - -const StyledDialog = styled(Dialog)<{ setMaxWidth?: boolean }>` - && { - border-radius: 0px; - padding-bottom: 5px; - background: ${(props) => props.theme.colors.modal.bg}; - width: 640px; - - & .${Classes.DIALOG_HEADER} { - padding: ${(props) => props.theme.spaces[4]}px; - background: ${(props) => props.theme.colors.modal.bg}; - box-shadow: none; - .${Classes.ICON} { - color: ${(props) => props.theme.colors.modal.iconColor}; - } - .${Classes.HEADING} { - color: ${(props) => props.theme.colors.modal.headerText}; - display: flex; - justify-content: center; - margin-top: 20px; - font-size: 20px; - line-height: 24px; - font-weight: 500; - } - - .${Classes.BUTTON}.${Classes.MINIMAL}:hover { - background-color: ${(props) => props.theme.colors.modal.bg}; - } - } - & .${Classes.DIALOG_BODY} { - margin: ${(props) => props.theme.spaces[9]}px; - } - & .${Classes.DIALOG_FOOTER_ACTIONS} { - display: block; - } - ${(props) => props.setMaxWidth && `width: 100vh;`} - } -`; - -const TriggerWrapper = styled.div``; +import Dialog from "components/ads/DialogComponent"; type FormDialogComponentProps = { isOpen?: boolean; @@ -59,9 +18,14 @@ type FormDialogComponentProps = { export const FormDialogComponent = (props: FormDialogComponentProps) => { const [isOpen, setIsOpen] = useState(!!props.isOpen); - const onClose = () => { + const onClose = useCallback(() => { setIsOpen(false); - }; + }, []); + + // track if the dialog is open to close it when clicking cancel within the form + const onOpening = useCallback(() => { + setIsOpen(true); + }, []); const Form = props.Form; @@ -74,30 +38,20 @@ export const FormDialogComponent = (props: FormDialogComponentProps) => { return ( - { - setIsOpen(true); - }} - > - {props.trigger} - - - -
-
-
-
+ +
); }; diff --git a/app/client/src/components/editorComponents/form/FormTextField.tsx b/app/client/src/components/editorComponents/form/FormTextField.tsx index 5d7b7a9ee1..cfb7786c6b 100644 --- a/app/client/src/components/editorComponents/form/FormTextField.tsx +++ b/app/client/src/components/editorComponents/form/FormTextField.tsx @@ -35,10 +35,11 @@ type FormTextFieldProps = { autoFocus?: boolean; }; +// trigger tests const FormTextField = (props: FormTextFieldProps) => { return ( - + ); }; diff --git a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx index c6f208b48a..1171db6b20 100644 --- a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx +++ b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx @@ -12,9 +12,12 @@ import CodeEditor, { import { API_EDITOR_FORM_NAME } from "constants/forms"; import { AppState } from "reducers"; import { connect } from "react-redux"; -import { Datasource } from "api/DatasourcesApi"; import _ from "lodash"; -import { DEFAULT_DATASOURCE, EmbeddedDatasource } from "entities/Datasource"; +import { + DEFAULT_DATASOURCE, + EmbeddedRestDatasource, + Datasource, +} from "entities/Datasource"; import CodeMirror from "codemirror"; import { EditorModes, @@ -29,12 +32,12 @@ import { urlGroupsRegexExp } from "constants/ActionConstants"; type ReduxStateProps = { orgId: string; - datasource: Datasource | EmbeddedDatasource; + datasource: Datasource | EmbeddedRestDatasource; datasourceList: Datasource[]; }; type ReduxDispatchProps = { - updateDatasource: (datasource: Datasource | EmbeddedDatasource) => void; + updateDatasource: (datasource: Datasource | EmbeddedRestDatasource) => void; }; type Props = EditorProps & diff --git a/app/client/src/components/propertyControls/ChartDataControl.tsx b/app/client/src/components/propertyControls/ChartDataControl.tsx index 3a23c9fa5d..7e6cb01137 100644 --- a/app/client/src/components/propertyControls/ChartDataControl.tsx +++ b/app/client/src/components/propertyControls/ChartDataControl.tsx @@ -282,7 +282,7 @@ class ChartDataControl extends BaseControl { seriesName: string; data: string; }> = this.props.propertyValue; - chartData.push({ seriesName: "", data: '[{ x: "", y: "" }]' }); + chartData.push({ seriesName: "", data: '[{ "x": "label", "y": 50 }]' }); this.updateProperty(this.props.propertyName, chartData); }; diff --git a/app/client/src/components/propertyControls/LocationSearchControl.tsx b/app/client/src/components/propertyControls/LocationSearchControl.tsx index 52f3e27847..89d6178ab4 100644 --- a/app/client/src/components/propertyControls/LocationSearchControl.tsx +++ b/app/client/src/components/propertyControls/LocationSearchControl.tsx @@ -76,6 +76,7 @@ const MapScriptWrapper = (props: MapScriptWrapperProps) => { AddScriptTo.HEAD, ); const [title, setTitle] = useState(""); + return (
{status === ScriptStatus.READY && ( @@ -89,7 +90,7 @@ const MapScriptWrapper = (props: MapScriptWrapperProps) => { { const val = ev.target.value; if (val === "") { diff --git a/app/client/src/components/propertyControls/index.test.ts b/app/client/src/components/propertyControls/index.test.ts deleted file mode 100644 index 7762b4de37..0000000000 --- a/app/client/src/components/propertyControls/index.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -// TODO Abhinav, this is not passing -// import { getPropertyControlTypes } from "./index"; -import _ from "lodash"; - -// const types = getPropertyControlTypes(); - -// it("Checks for uniqueness of control types", () => { -// const result = Object.keys(getPropertyControlTypes()); -// const output = _.uniq(result); -// expect(types.length).toEqual(output.length); -// }); - -it("mock test", () => { - expect(1).toBe(1); -}); diff --git a/app/client/src/components/stories/ColorSelector.stories.tsx b/app/client/src/components/stories/ColorSelector.stories.tsx index 1109660e68..44a49480f5 100644 --- a/app/client/src/components/stories/ColorSelector.stories.tsx +++ b/app/client/src/components/stories/ColorSelector.stories.tsx @@ -5,7 +5,7 @@ import { withKnobs, array, boolean } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; // import { appCardColors } from "constants/AppConstants"; import { StoryWrapper } from "components/ads/common"; -import { theme, light, dark } from "constants/DefaultTheme"; +import { theme } from "constants/DefaultTheme"; export default { title: "ColorSelector", diff --git a/app/client/src/components/stories/Menu.stories.tsx b/app/client/src/components/stories/Menu.stories.tsx index 9ca8736ca1..b7c9fa916b 100644 --- a/app/client/src/components/stories/Menu.stories.tsx +++ b/app/client/src/components/stories/Menu.stories.tsx @@ -22,14 +22,6 @@ export default { decorators: [withKnobs, withDesign], }; -const calls = (value: string, callback: any) => { - setTimeout(() => { - return callback(false, SavingState.SUCCESS); - }, 2000); - - return callback(true); -}; - const errorFunction = (name: string) => { if (name === "") { return "Name cannot be empty"; diff --git a/app/client/src/components/utils/Skeleton.tsx b/app/client/src/components/utils/Skeleton.tsx index c3205d4faf..199f773b28 100644 --- a/app/client/src/components/utils/Skeleton.tsx +++ b/app/client/src/components/utils/Skeleton.tsx @@ -8,10 +8,6 @@ const StyledDiv = styled.div` display: block; `; -type SkeletonProps = { - type?: string; -}; - export const Skeleton = () => { return ; }; diff --git a/app/client/src/configs/types.ts b/app/client/src/configs/types.ts index 2c4adfe9d8..a401061cd6 100644 --- a/app/client/src/configs/types.ts +++ b/app/client/src/configs/types.ts @@ -5,8 +5,6 @@ export type SentryConfig = { environment: string; }; -type Milliseconds = number; - export enum FeatureFlagsEnum {} export type FeatureFlags = Record; diff --git a/app/client/src/constants/ActionConstants.tsx b/app/client/src/constants/ActionConstants.tsx index f34df4e257..56d053506b 100644 --- a/app/client/src/constants/ActionConstants.tsx +++ b/app/client/src/constants/ActionConstants.tsx @@ -66,10 +66,13 @@ export interface ExecuteErrorPayload { actionId: string; error: any; isPageLoad?: boolean; + show?: boolean; } // Group 1 = datasource (https://www.domain.com) // Group 2 = path (/nested/path) // Group 3 = params (?param=123¶m2=12) export const urlGroupsRegexExp = /^(https?:\/{2}\S+?)(\/\S*?)(\?\S*)?$/; + export const EXECUTION_PARAM_KEY = "executionParams"; +export const EXECUTION_PARAM_REFERENCE_REGEX = /this.params/g; diff --git a/app/client/src/constants/ApiEditorConstants.ts b/app/client/src/constants/ApiEditorConstants.ts index 9b382be1bd..a34f59b33a 100644 --- a/app/client/src/constants/ApiEditorConstants.ts +++ b/app/client/src/constants/ApiEditorConstants.ts @@ -1,4 +1,4 @@ -import { RestAction } from "entities/Action"; +import { ApiActionConfig } from "entities/Action"; import { DEFAULT_ACTION_TIMEOUT } from "constants/ApiConstants"; import { zipObject } from "lodash"; @@ -23,19 +23,17 @@ export const HTTP_METHOD_OPTIONS = HTTP_METHODS.map((method) => ({ export const REST_PLUGIN_PACKAGE_NAME = "restapi-plugin"; -export const DEFAULT_API_ACTION: Partial = { - actionConfiguration: { - timeoutInMillisecond: DEFAULT_ACTION_TIMEOUT, - httpMethod: HTTP_METHODS[0], - headers: [ - { key: "", value: "" }, - { key: "", value: "" }, - ], - queryParameters: [ - { key: "", value: "" }, - { key: "", value: "" }, - ], - }, +export const DEFAULT_API_ACTION_CONFIG: ApiActionConfig = { + timeoutInMillisecond: DEFAULT_ACTION_TIMEOUT, + httpMethod: HTTP_METHODS[0], + headers: [ + { key: "", value: "" }, + { key: "", value: "" }, + ], + queryParameters: [ + { key: "", value: "" }, + { key: "", value: "" }, + ], }; export const PLUGIN_TYPE_API = "API"; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index db1d817c54..d8781662c9 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -679,6 +679,9 @@ type ColorType = { }; manageUser: ShadeColor; scrollbar: ShadeColor; + separator: ShadeColor; + title: ShadeColor; + link: string; }; tagInput: { bg: ShadeColor; @@ -730,6 +733,7 @@ type ColorType = { textColor: string; bg: ShadeColor; }; + floatingBtn: any; }; export const dark: ColorType = { @@ -954,6 +958,9 @@ export const dark: ColorType = { }, manageUser: darkShades[6], scrollbar: darkShades[5], + separator: darkShades[4], + title: darkShades[8], + link: "#F86A2B", }, tagInput: { bg: darkShades[0], @@ -1005,6 +1012,11 @@ export const dark: ColorType = { textColor: "#090707", bg: darkShades[8], }, + floatingBtn: { + tagBackground: "#e22c2c", + backgroundColor: darkShades[3], + iconColor: darkShades[6], + }, }; export const light: ColorType = { @@ -1229,6 +1241,9 @@ export const light: ColorType = { }, manageUser: lightShades[6], scrollbar: lightShades[5], + separator: lightShades[4], + title: lightShades[8], + link: "#F86A2B", }, tagInput: { bg: lightShades[2], @@ -1280,6 +1295,11 @@ export const light: ColorType = { textColor: "#F7F7F7", bg: lightShades[10], }, + floatingBtn: { + tagBackground: "#e22c2c", + backgroundColor: lightShades[3], + iconColor: lightShades[7], + }, }; export const theme: Theme = { @@ -1360,6 +1380,18 @@ export const theme: Theme = { letterSpacing: 0.4, fontWeight: 600, }, + floatingBtn: { + fontSize: 14, + lineHeight: 17, + letterSpacing: -0.24, + fontWeight: "normal", + }, + releaseList: { + fontSize: 14, + lineHeight: 23, + letterSpacing: -0.24, + fontWeight: "normal", + }, }, iconSizes: { XXS: 8, diff --git a/app/client/src/constants/FieldExpectedValue.ts b/app/client/src/constants/FieldExpectedValue.ts index 854b13b78b..a41adfe2d1 100644 --- a/app/client/src/constants/FieldExpectedValue.ts +++ b/app/client/src/constants/FieldExpectedValue.ts @@ -99,6 +99,7 @@ const FIELD_VALUES: Record< // onClick: "Function Call", }, MAP_WIDGET: { + mapCenter: "{ lat: number, long: number }", defaultMarkers: "Array<{ lat: number, long: number }>", enableSearch: "boolean", enablePickLocation: "boolean", diff --git a/app/client/src/constants/OnboardingConstants.tsx b/app/client/src/constants/OnboardingConstants.tsx index 13aa546587..d8d11bb6ec 100644 --- a/app/client/src/constants/OnboardingConstants.tsx +++ b/app/client/src/constants/OnboardingConstants.tsx @@ -1,5 +1,4 @@ import { ReduxAction, ReduxActionTypes } from "./ReduxActionConstants"; -import { EventName } from "../utils/AnalyticsUtil"; import { showTooltip } from "actions/onboardingActions"; export enum OnboardingStep { @@ -27,13 +26,14 @@ export type OnboardingTooltip = { }; export type OnboardingStepConfig = { + name: string; setup: () => { type: string; payload?: any }[]; tooltip: OnboardingTooltip; - eventName?: EventName; }; export const OnboardingConfig: Record = { [OnboardingStep.NONE]: { + name: "NONE", setup: () => { return []; }, @@ -43,6 +43,7 @@ export const OnboardingConfig: Record = { }, }, [OnboardingStep.WELCOME]: { + name: "WELCOME", setup: () => { // To setup the state if any // Return action that needs to be dispatched @@ -56,9 +57,9 @@ export const OnboardingConfig: Record = { title: "", description: "", }, - eventName: "ONBOARDING_WELCOME", }, [OnboardingStep.EXAMPLE_DATABASE]: { + name: "EXAMPLE_DATABASE", setup: () => { return [ { @@ -73,9 +74,9 @@ export const OnboardingConfig: Record = { title: "We’ve connected to an example Postgres database. You can now query it.", }, - eventName: "ONBOARDING_EXAMPLE_DATABASE", }, [OnboardingStep.RUN_QUERY]: { + name: "RUN_QUERY", setup: () => { return []; }, @@ -83,9 +84,9 @@ export const OnboardingConfig: Record = { title: "This is where you query data. Here’s one that fetches a list of users stored in the DB.", }, - eventName: "ONBOARDING_RUN_QUERY", }, [OnboardingStep.RUN_QUERY_SUCCESS]: { + name: "RUN_QUERY_SUCCESS", setup: () => { return [ { @@ -100,9 +101,9 @@ export const OnboardingConfig: Record = { title: "This is the response from your query. Now let’s connect it to a UI widget.", }, - eventName: "ONBOARDING_RUN_QUERY", }, [OnboardingStep.ADD_WIDGET]: { + name: "ADD_WIDGET", setup: () => { return []; }, @@ -111,9 +112,9 @@ export const OnboardingConfig: Record = { "Your first widget 🎉 Copy the snippet below and paste it inside TableData to see the magic", snippet: "{{ExampleQuery.data}}", }, - eventName: "ONBOARDING_ADD_WIDGET", }, [OnboardingStep.SUCCESSFUL_BINDING]: { + name: "SUCCESSFUL_BINDING", setup: () => { return []; }, @@ -123,9 +124,9 @@ export const OnboardingConfig: Record = { "You can access widgets and actions as JS variables anywhere inside {{ }}", onClickOutside: showTooltip(OnboardingStep.DEPLOY), }, - eventName: "ONBOARDING_SUCCESSFUL_BINDING", }, [OnboardingStep.DEPLOY]: { + name: "DEPLOY", setup: () => { return [ { @@ -137,10 +138,10 @@ export const OnboardingConfig: Record = { title: "You’re almost done! Just Hit Deploy", isFinalStep: true, }, - eventName: "ONBOARDING_DEPLOY", }, // Final step [OnboardingStep.FINISH]: { + name: "FINISH", setup: () => { return []; }, diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index faa39bfbc4..6106ea2652 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -10,6 +10,7 @@ export const ReduxActionTypes: { [key: string]: string } = { FLUSH_ERRORS: "FLUSH_ERRORS", FLUSH_AND_REDIRECT: "FLUSH_AND_REDIRECT", SAFE_CRASH_APPSMITH: "SAFE_CRASH_APPSMITH", + SAFE_CRASH_APPSMITH_REQUEST: "SAFE_CRASH_APPSMITH_REQUEST", UPDATE_CANVAS: "UPDATE_CANVAS", FETCH_CANVAS: "FETCH_CANVAS", CLEAR_CANVAS: "CLEAR_CANVAS", @@ -50,7 +51,6 @@ export const ReduxActionTypes: { [key: string]: string } = { WIDGET_MOVE: "WIDGET_MOVE", WIDGET_RESIZE: "WIDGET_RESIZE", WIDGET_DELETE: "WIDGET_DELETE", - WIDGETS_LOADING: "WIDGETS_LOADING", SHOW_PROPERTY_PANE: "SHOW_PROPERTY_PANE", UPDATE_WIDGET_PROPERTY_REQUEST: "UPDATE_WIDGET_PROPERTY_REQUEST", UPDATE_WIDGET_PROPERTY: "UPDATE_WIDGET_PROPERTY", @@ -307,11 +307,17 @@ export const ReduxActionTypes: { [key: string]: string } = { CUT_SELECTED_WIDGET: "CUT_SELECTED_WIDGET", WIDGET_ADD_CHILDREN: "WIDGET_ADD_CHILDREN", SET_EVALUATED_TREE: "SET_EVALUATED_TREE", + SET_EVALUATION_INVERSE_DEPENDENCY_MAP: + "SET_EVALUATION_INVERSE_DEPENDENCY_MAP", BATCH_UPDATES_SUCCESS: "BATCH_UPDATES_SUCCESS", UPDATE_CANVAS_STRUCTURE: "UPDATE_CANVAS_STRUCTURE", SET_SELECTED_WIDGET_ANCESTORY: "SET_SELECTED_WIDGET_ANCESTORY", START_EVALUATION: "START_EVALUATION", CURRENT_APPLICATION_NAME_UPDATE: "CURRENT_APPLICATION_NAME_UPDATE", + SET_WIDGET_LOADING: "SET_WIDGET_LOADING", + FETCH_RELEASES_SUCCESS: "FETCH_RELEASES_SUCCESS", + RESET_UNREAD_RELEASES_COUNT: "RESET_UNREAD_RELEASES_COUNT", + SET_LOADING_ENTITIES: "SET_LOADING_ENTITIES", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 31eea37b1a..3cc4e8cef1 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -88,3 +88,5 @@ export const MAIN_CONTAINER_WIDGET_ID = "0"; export const MAIN_CONTAINER_WIDGET_NAME = "MainContainer"; export const WIDGET_DELETE_UNDO_TIMEOUT = 7000; + +export const DEFAULT_CENTER = { lat: -34.397, lng: 150.644 }; diff --git a/app/client/src/constants/WidgetValidation.ts b/app/client/src/constants/WidgetValidation.ts index 29ea489338..3193be57fd 100644 --- a/app/client/src/constants/WidgetValidation.ts +++ b/app/client/src/constants/WidgetValidation.ts @@ -22,6 +22,7 @@ export const VALIDATION_TYPES = { SELECTED_TAB: "SELECTED_TAB", DEFAULT_OPTION_VALUE: "DEFAULT_OPTION_VALUE", DEFAULT_SELECTED_ROW: "DEFAULT_SELECTED_ROW", + LAT_LONG: "LAT_LONG", }; export type ValidationResponse = { diff --git a/app/client/src/entities/Action/index.ts b/app/client/src/entities/Action/index.ts index b9332849cc..636dff8006 100644 --- a/app/client/src/entities/Action/index.ts +++ b/app/client/src/entities/Action/index.ts @@ -1,5 +1,6 @@ -import { Datasource } from "api/DatasourcesApi"; +import { EmbeddedRestDatasource } from "entities/Datasource"; import { DynamicPath } from "../../utils/DynamicBindingUtils"; +import _ from "lodash"; export enum PluginType { API = "API", @@ -13,7 +14,7 @@ export enum PaginationType { } export interface ActionConfig { - timeoutInMillisecond: number; + timeoutInMillisecond?: number; paginationType?: PaginationType; } @@ -49,47 +50,62 @@ export interface ApiActionConfig extends ActionConfig { } export interface QueryActionConfig extends ActionConfig { - body: string; + body?: string; } -export interface Action { +export const isStoredDatasource = (val: any): val is StoredDatasource => { + if (!_.isObject(val)) return false; + if (!("id" in val)) return false; + return true; +}; +export interface StoredDatasource { + id: string; +} + +interface BaseAction { id: string; name: string; - datasource: Partial; organizationId: string; pageId: string; collectionId?: string; - actionConfiguration: Partial; pluginId: string; - pluginType: PluginType; executeOnLoad: boolean; dynamicBindingPathList: DynamicPath[]; isValid: boolean; invalids: string[]; jsonPathKeys: string[]; cacheResponse: string; - templateId?: string; - providerId?: string; - provider?: ActionProvider; - documentation?: { text: string }; confirmBeforeExecute?: boolean; -} - -export interface RestAction extends Action { - actionConfiguration: Partial; eventData?: any; } -export interface RapidApiAction extends Action { - actionConfiguration: Partial; +interface BaseApiAction extends BaseAction { + pluginType: PluginType.API; + actionConfiguration: ApiActionConfig; +} + +export interface EmbeddedApiAction extends BaseApiAction { + datasource: EmbeddedRestDatasource; +} + +export interface StoredDatasourceApiAction extends BaseApiAction { + datasource: StoredDatasource; +} + +export type ApiAction = EmbeddedApiAction | StoredDatasourceApiAction; + +export type RapidApiAction = ApiAction & { templateId: string; proverId: string; provider: ActionProvider; pluginId: string; documentation: { text: string }; +}; + +export interface QueryAction extends BaseAction { + pluginType: PluginType.DB; + actionConfiguration: QueryActionConfig; + datasource: StoredDatasource; } -export interface QueryAction extends Action { - actionConfiguration: Partial; - eventData?: any; -} +export type Action = ApiAction | QueryAction; diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index 1acca1846d..54b51b45e8 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -126,6 +126,9 @@ export class DataTreeFactory { const derivedPropertyMap = WidgetFactory.getWidgetDerivedPropertiesMap( widget.type, ); + const defaultProps = WidgetFactory.getWidgetDefaultPropertiesMap( + widget.type, + ); const derivedProps: any = {}; const dynamicBindingPathList = getEntityDynamicBindingPathList(widget); dynamicBindingPathList.forEach((dynamicPath) => { @@ -137,6 +140,7 @@ export class DataTreeFactory { } }); Object.keys(derivedPropertyMap).forEach((propertyName) => { + // TODO regex is too greedy derivedProps[propertyName] = derivedPropertyMap[propertyName].replace( /this./g, `${widget.widgetName}.`, @@ -145,11 +149,18 @@ export class DataTreeFactory { key: propertyName, }); }); + const unInitializedDefaultProps: Record = {}; + Object.values(defaultProps).forEach((propertyName) => { + if (!(propertyName in widget)) { + unInitializedDefaultProps[propertyName] = undefined; + } + }); dataTree[widget.widgetName] = { ...widget, ...defaultMetaProps, ...widgetMetaProps, ...derivedProps, + ...unInitializedDefaultProps, dynamicBindingPathList, ENTITY_TYPE: ENTITY_TYPE.WIDGET, }; diff --git a/app/client/src/entities/Datasource/index.ts b/app/client/src/entities/Datasource/index.ts index 04d71509d6..1339ab8cde 100644 --- a/app/client/src/entities/Datasource/index.ts +++ b/app/client/src/entities/Datasource/index.ts @@ -1,11 +1,79 @@ -import { Datasource } from "api/DatasourcesApi"; +import { Property } from "entities/Action"; +import _ from "lodash"; +export interface DatasourceAuthentication { + authType?: string; + username?: string; + password?: string; +} -export type EmbeddedDatasource = Omit; +export interface DatasourceColumns { + name: string; + type: string; +} + +export interface DatasourceKeys { + name: string; + type: string; +} + +export interface DatasourceStructure { + tables?: DatasourceTable[]; +} + +export interface QueryTemplate { + title: string; + body: string; +} +export interface DatasourceTable { + type: string; + name: string; + columns: DatasourceColumns[]; + keys: DatasourceKeys[]; + templates: QueryTemplate[]; +} + +// todo: check which fields are truly optional and move the common ones into base +interface BaseDatasource { + pluginId: string; + name: string; + organizationId: string; + isValid: boolean; +} + +export const isEmbeddedRestDatasource = ( + val: any, +): val is EmbeddedRestDatasource => { + if (!_.isObject(val)) return false; + if (!("datasourceConfiguration" in val)) return false; + val = val; + // Object should exist and have value + if (!val.datasourceConfiguration) return false; + //url might exist as a key but not have value, so we won't check value + if (!("url" in val.datasourceConfiguration)) return false; + return true; +}; + +export interface EmbeddedRestDatasource extends BaseDatasource { + datasourceConfiguration: { url: string }; + invalids: Array; +} +export interface Datasource extends BaseDatasource { + id: string; + datasourceConfiguration: { + url: string; + authentication?: DatasourceAuthentication; + properties?: Record; + headers?: Property[]; + databaseName?: string; + }; + invalids?: string[]; + structure?: DatasourceStructure; +} export const DEFAULT_DATASOURCE = ( pluginId: string, organizationId: string, -): EmbeddedDatasource => ({ +): EmbeddedRestDatasource => ({ name: "DEFAULT_REST_DATASOURCE", datasourceConfiguration: { url: "", diff --git a/app/client/src/icons/HelpIcons.tsx b/app/client/src/icons/HelpIcons.tsx index 3a2175c99b..5f2669d7f3 100644 --- a/app/client/src/icons/HelpIcons.tsx +++ b/app/client/src/icons/HelpIcons.tsx @@ -5,6 +5,7 @@ import { ReactComponent as DocumentIcon } from "assets/icons/help/document.svg"; import { ReactComponent as HelpIcon } from "assets/icons/help/help.svg"; import { ReactComponent as GithubIcon } from "assets/icons/help/github-icon.svg"; import { ReactComponent as DiscordIcon } from "assets/icons/help/discord.svg"; +import { ReactComponent as UpdatesIcon } from "assets/icons/help/updates.svg"; import { Icon } from "@blueprintjs/core"; /* eslint-disable react/display-name */ @@ -47,6 +48,11 @@ export const HelpIcons: { ), + UPDATES: (props: IconProps) => ( + + + + ), }; export type HelpIconName = keyof typeof HelpIcons; diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index e6a48d11ae..1ca3aea44c 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -262,7 +262,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "BUTTON_WIDGET", - position: { left: 10, top: 4 }, + position: { left: 9, top: 4 }, size: { rows: 1, cols: 3 }, props: { text: "Cancel", @@ -271,8 +271,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "BUTTON_WIDGET", - position: { left: 13, top: 4 }, - size: { rows: 1, cols: 3 }, + position: { left: 12, top: 4 }, + size: { rows: 1, cols: 4 }, props: { text: "Confirm", buttonStyle: "PRIMARY_BUTTON", diff --git a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx index c911eab61f..f55cca55ba 100644 --- a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx +++ b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useRef, useEffect, useState } from "react"; +import React, { useRef, useEffect, useState } from "react"; import { Link, NavLink, useLocation } from "react-router-dom"; import { Helmet } from "react-helmet"; import styled from "styled-components"; @@ -11,7 +11,6 @@ import { PERMISSION_TYPE, } from "pages/Applications/permissionHelpers"; import { - Page, ApplicationPayload, PageListPayload, } from "constants/ReduxActionConstants"; diff --git a/app/client/src/pages/Applications/ApplicationCard.tsx b/app/client/src/pages/Applications/ApplicationCard.tsx index 53f2e8112a..5b6c4ef96e 100644 --- a/app/client/src/pages/Applications/ApplicationCard.tsx +++ b/app/client/src/pages/Applications/ApplicationCard.tsx @@ -1,4 +1,4 @@ -import React, { createRef, useEffect, useState, useRef } from "react"; +import React, { useEffect, useState, useRef } from "react"; import styled from "styled-components"; import { getApplicationViewerPageURL, diff --git a/app/client/src/pages/Applications/ProductUpdatesModal/ReleaseComponent.tsx b/app/client/src/pages/Applications/ProductUpdatesModal/ReleaseComponent.tsx new file mode 100644 index 0000000000..0eca7c9d62 --- /dev/null +++ b/app/client/src/pages/Applications/ProductUpdatesModal/ReleaseComponent.tsx @@ -0,0 +1,161 @@ +import React, { useState, useRef, useEffect, useCallback } from "react"; +import styled from "styled-components"; +import moment from "moment"; +import "@github/g-emoji-element"; +import Icon, { IconSize } from "components/ads/Icon"; + +const StyledContainer = styled.div` + color: ${(props) => props.theme.colors.text.normal}; + margin-bottom: ${(props) => props.theme.spaces[7]}px; +`; + +const StyledTitle = styled.div` + font-weight: ${(props) => props.theme.typography.h2.fontWeight}; + font-size: ${(props) => props.theme.typography.h2.fontSize}px; + line-height: ${(props) => props.theme.typography.h2.lineHeight}px; + letter-spacing: ${(props) => props.theme.typography.h2.letterSpacing}px; + color: ${(props) => props.theme.colors.modal.title}; +`; + +export const StyledSeparator = styled.div` + width: 100%; + background-color: ${(props) => props.theme.colors.modal.separator}; + opacity: 0.6; + height: 1px; +`; + +const StyledDate = styled.div` + font-weight: ${(props) => props.theme.typography.releaseList.fontWeight}; + font-size: ${(props) => props.theme.typography.releaseList.fontSize}px; + line-height: ${(props) => props.theme.typography.releaseList.lineHeight}px; + letter-spacing: ${(props) => + props.theme.typography.releaseList.letterSpacing}px; + color: ${(props) => props.theme.colors.text.normal}; + margin-top: ${(props) => props.theme.spaces[3]}px; +`; + +const StyledContent = styled.div<{ maxHeight: number }>` + li, + p { + font-weight: ${(props) => props.theme.typography.releaseList.fontWeight}; + font-size: ${(props) => props.theme.typography.releaseList.fontSize}px; + line-height: ${(props) => props.theme.typography.releaseList.lineHeight}px; + letter-spacing: ${(props) => + props.theme.typography.releaseList.letterSpacing}px; + color: ${(props) => props.theme.colors.text.normal}; + } + a { + color: ${(props) => props.theme.colors.modal.link}; + } + h1, + h2, + h3, + h4 { + color: ${(props) => props.theme.colors.modal.title}; + } + + transition: max-height 0.15s ease-out; + overflow: hidden; + max-height: ${(props) => props.maxHeight}px; +`; + +export type Release = { + descriptionHtml: string; + name: string; + publishedAt?: string; +}; + +type ReleaseProps = { + release: Release; +}; + +enum ReleaseComponentViewState { + "collapsed", + "expanded", +} + +const StyledReadMore = styled.div` + font-weight: ${(props) => props.theme.typography.btnMedium.fontWeight}; + font-size: ${(props) => props.theme.typography.btnMedium.fontSize}px; + line-height: ${(props) => props.theme.typography.btnMedium.lineHeight}px; + letter-spacing: ${(props) => + props.theme.typography.btnMedium.letterSpacing}px; + text-transform: uppercase; + padding: ${(props) => props.theme.spaces[8]}px 0; + display: flex; + cursor: pointer; +`; + +const ReadMore = ({ + currentState, + onClick, +}: { + currentState: ReleaseComponentViewState; + onClick: () => void; +}) => ( + +
+ {currentState === ReleaseComponentViewState.collapsed + ? "read more" + : "read less"} +
+ +
+); + +const ReleaseComponent = ({ release }: ReleaseProps) => { + const { name, publishedAt, descriptionHtml } = release; + const [isCollapsed, setCollapsed] = useState(true); + const [shouldShowReadMore, setShouldShowReadMore] = useState(false); + const contentRef = useRef(null); + + useEffect(() => { + if (contentRef.current) { + if (contentRef.current.scrollHeight >= 500) { + setShouldShowReadMore(true); + } + } + }); + + const getReadMoreState = useCallback((): ReleaseComponentViewState => { + if (isCollapsed) return ReleaseComponentViewState.collapsed; + return ReleaseComponentViewState.expanded; + }, [isCollapsed]); + + const toggleCollapsedState = useCallback(() => { + setCollapsed(!isCollapsed); + }, [isCollapsed]); + + const getHeight = useCallback(() => { + if (!contentRef.current) return 500; + return isCollapsed ? 500 : contentRef.current.scrollHeight; + }, [isCollapsed]); + + return descriptionHtml ? ( + + {name} + {moment(publishedAt).format("Do MMMM, YYYY")} + + {shouldShowReadMore && ( + + )} + + + ) : null; +}; + +export default ReleaseComponent; diff --git a/app/client/src/pages/Applications/ProductUpdatesModal/UpdatesButton.tsx b/app/client/src/pages/Applications/ProductUpdatesModal/UpdatesButton.tsx new file mode 100644 index 0000000000..88a91cb3d6 --- /dev/null +++ b/app/client/src/pages/Applications/ProductUpdatesModal/UpdatesButton.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import styled from "styled-components"; +import { HelpIcons } from "icons/HelpIcons"; +import { Colors } from "constants/Colors"; +import { withTheme } from "styled-components"; + +const StyledUpdatesButton = styled.div` + position: absolute; + left: 30px; + bottom: 25px; + width: 190px; + height: 38px; + display: flex; + align-items: center; + box-shadow: 0px 12px 34px rgba(0, 0, 0, 0.75); + padding: 0 ${(props) => props.theme.spaces[5]}px; + justify-content: space-between; + cursor: pointer; + background-color: ${(props) => + props.theme.colors.floatingBtn.backgroundColor}; +`; + +const StyledTag = styled.div` + font-weight: ${(props) => props.theme.typography.p2.fontWeight}; + font-size: ${(props) => props.theme.typography.p2.fontSize}px; + line-height: ${(props) => props.theme.typography.p2.lineHeight}px; + letter-spacing: ${(props) => props.theme.typography.p2.letterSpacing}}; + padding: ${(props) => props.theme.spaces[1]}px; + background: ${(props) => props.theme.colors.floatingBtn.tagBackground}; + border-radius: 100px; + text-align: center; + color: ${Colors.WHITE}; +`; + +const UpdatesButtonTextContainer = styled.div` + font-weight: ${(props) => props.theme.typography.floatingBtn.fontWeight}; + font-size: ${(props) => props.theme.typography.floatingBtn.fontSize}px; + line-height: ${(props) => props.theme.typography.floatingBtn.lineHeight}px; + letter-spacing: ${(props) => + props.theme.typography.floatingBtn.letterSpacing}px; + display: flex; + align-items: center; + margin-left: ${(props) => props.theme.spaces[3]}px; + color: ${(props) => props.theme.colors.text.normal}; +`; + +const UpdatesIcon = withTheme(({ theme }) => ( + +)); + +const UpdatesButton = ({ newReleasesCount }: { newReleasesCount: string }) => ( + +
+ + What's New? +
+ {newReleasesCount && {newReleasesCount}} +
+); + +export default UpdatesButton; diff --git a/app/client/src/pages/Applications/ProductUpdatesModal/index.tsx b/app/client/src/pages/Applications/ProductUpdatesModal/index.tsx new file mode 100644 index 0000000000..8b3dd0d170 --- /dev/null +++ b/app/client/src/pages/Applications/ProductUpdatesModal/index.tsx @@ -0,0 +1,118 @@ +import React, { useState, useCallback, useContext } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import styled from "styled-components"; +import "@github/g-emoji-element"; +import Dialog from "components/ads/DialogComponent"; +import UpdatesButton from "./UpdatesButton"; +import { AppState } from "reducers"; +import { LayersContext } from "constants/Layers"; +import ReleasesAPI from "api/ReleasesAPI"; +import { resetReleasesCount } from "actions/releasesActions"; +import { HelpIcons } from "icons/HelpIcons"; +import ReleaseComponent, { Release, StyledSeparator } from "./ReleaseComponent"; +import { withTheme } from "styled-components"; +import { Color } from "constants/Colors"; + +const CloseIcon = HelpIcons.CLOSE_ICON; + +const HeaderContents = styled.div` + padding: ${(props) => props.theme.spaces[9]}px; + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: ${(props) => props.theme.spaces[7]}px; +`; + +const Heading = styled.div` + color: ${(props) => props.theme.colors.modal.headerText}; + display: flex; + justify-content: center; + font-weight: ${(props) => props.theme.typography.h1.fontWeight}; + font-size: ${(props) => props.theme.typography.h1.fontSize}px; + line-height: ${(props) => props.theme.typography.h1.lineHeight}px; + letter-spacing: ${(props) => props.theme.typography.h1.letterSpacing}; +`; + +const ViewInGithubLink = styled.a` + cursor: pointer; + text-decoration: none; + :hover { + text-decoration: none; + color: ${(props) => props.theme.colors.text.normal}; + } + font-weight: ${(props) => props.theme.typography.releaseList.fontWeight}; + font-size: ${(props) => props.theme.typography.releaseList.fontSize}px; + line-height: ${(props) => props.theme.typography.releaseList.lineHeight}px; + letter-spacing: ${(props) => + props.theme.typography.releaseList.letterSpacing}px; + color: ${(props) => props.theme.colors.text.normal}; + margin-right: ${(props) => props.theme.spaces[4]}px; +`; + +const HeaderRight = styled.div` + display: flex; +`; + +const Header = withTheme( + ({ onClose, theme }: { onClose: () => void; theme: any }) => ( + <> + + Product Updates + + + View on Github + +
+ +
+
+
+
+ +
+ + ), +); + +const ProductUpdatesModal = () => { + const { releaseItems, newReleasesCount } = useSelector( + (state: AppState) => state.ui.releases, + ); + const dispatch = useDispatch(); + const onOpening = useCallback(async () => { + setIsOpen(true); + dispatch(resetReleasesCount()); + await ReleasesAPI.markAsRead(); + }, []); + + const Layers = useContext(LayersContext); + const [isOpen, setIsOpen] = useState(false); + + return Array.isArray(releaseItems) && releaseItems.length > 0 ? ( + } + width={"580px"} + maxHeight={"80vh"} + triggerZIndex={Layers.max} + showHeaderUnderline + onOpening={onOpening} + isOpen={isOpen} + getHeader={() =>
setIsOpen(false)} />} + canOutsideClickClose + canEscapeKeyClose + > + {releaseItems.map((release: Release, index: number) => ( + + ))} +
+ ) : null; +}; + +export default ProductUpdatesModal; diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx index 38f0684c30..a258ab7a1a 100644 --- a/app/client/src/pages/Applications/index.tsx +++ b/app/client/src/pages/Applications/index.tsx @@ -65,6 +65,7 @@ import Spinner from "components/ads/Spinner"; import ProfileImage from "pages/common/ProfileImage"; import { getThemeDetails } from "selectors/themeSelectors"; import { AppIconCollection } from "components/ads/AppIcon"; +import ProductUpdatesModal from "pages/Applications/ProductUpdatesModal"; const OrgDropDown = styled.div` display: flex; @@ -654,15 +655,13 @@ const ApplicationsSection = (props: any) => { !isFetchingApplications && ( - {userRoles - .slice(0, 5) - .map((el: UserRoles, index: number) => ( - - ))} + {userRoles.slice(0, 5).map((el: UserRoles) => ( + + ))} {userRoles.length > 5 ? ( + ; +type Props = APIFormProps & InjectedFormProps; export const NameWrapper = styled.div` width: 49%; @@ -309,7 +309,7 @@ export default connect((state: AppState) => { actionConfigurationHeaders, }; })( - reduxForm({ + reduxForm({ form: API_EDITOR_FORM_NAME, })(ApiEditorForm), ); diff --git a/app/client/src/pages/Editor/APIEditor/Pagination.tsx b/app/client/src/pages/Editor/APIEditor/Pagination.tsx index fddc11e90d..ab0a1b948e 100644 --- a/app/client/src/pages/Editor/APIEditor/Pagination.tsx +++ b/app/client/src/pages/Editor/APIEditor/Pagination.tsx @@ -39,11 +39,14 @@ const StyledDynamicTextField = styled(DynamicTextField)` const TestButton = styled(BaseButton)` &&& { - max-width: 72px; margin: 0 5px; min-height: 32px; padding-right: 4px; } + + &&&& { + width: auto; + } `; export default function Pagination(props: PaginationProps) { diff --git a/app/client/src/pages/Editor/APIEditor/ProviderTemplates.tsx b/app/client/src/pages/Editor/APIEditor/ProviderTemplates.tsx index 3ac0c48af3..18afe59021 100644 --- a/app/client/src/pages/Editor/APIEditor/ProviderTemplates.tsx +++ b/app/client/src/pages/Editor/APIEditor/ProviderTemplates.tsx @@ -251,10 +251,6 @@ class ProviderTemplates extends React.Component { this.setState({ addedTemplates }); }; - handleSearchChange = (e: React.ChangeEvent<{ value: string }>) => { - const value = e.target.value; - }; - handleIsOpen = (templateId: string) => { const { toggeledTemplates } = this.state; diff --git a/app/client/src/pages/Editor/APIEditor/RapidApiEditorForm.tsx b/app/client/src/pages/Editor/APIEditor/RapidApiEditorForm.tsx index b4a5f8e61d..0b38690c8b 100644 --- a/app/client/src/pages/Editor/APIEditor/RapidApiEditorForm.tsx +++ b/app/client/src/pages/Editor/APIEditor/RapidApiEditorForm.tsx @@ -15,7 +15,7 @@ import CredentialsTooltip from "components/editorComponents/form/CredentialsTool import { FormIcons } from "icons/FormIcons"; import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView"; import Pagination from "./Pagination"; -import { PaginationType, RestAction } from "entities/Action"; +import { PaginationType, Action } from "entities/Action"; import ActionNameEditor from "components/editorComponents/ActionNameEditor"; import { NameWrapper } from "./Form"; const Form = styled.form` @@ -113,7 +113,7 @@ interface APIFormProps { dispatch: any; } -type Props = APIFormProps & InjectedFormProps; +type Props = APIFormProps & InjectedFormProps; const RapidApiEditorForm: React.FC = (props: Props) => { const { @@ -307,7 +307,7 @@ export default connect((state) => { providerCredentialSteps, }; })( - reduxForm({ + reduxForm({ form: API_EDITOR_FORM_NAME, destroyOnUnmount: false, })(RapidApiEditorForm), diff --git a/app/client/src/pages/Editor/APIEditor/Search.tsx b/app/client/src/pages/Editor/APIEditor/Search.tsx index ba89c8926f..af4c5a9d28 100644 --- a/app/client/src/pages/Editor/APIEditor/Search.tsx +++ b/app/client/src/pages/Editor/APIEditor/Search.tsx @@ -19,20 +19,10 @@ const SearchBar = styled(BaseTextInput)` `; class Search extends React.Component { - handleSearchChange = (e: any) => { - console.log("test"); - }; - render() { return ( - +

Providers

); diff --git a/app/client/src/pages/Editor/APIEditor/index.tsx b/app/client/src/pages/Editor/APIEditor/index.tsx index 33a7e87377..6fe50d8315 100644 --- a/app/client/src/pages/Editor/APIEditor/index.tsx +++ b/app/client/src/pages/Editor/APIEditor/index.tsx @@ -22,7 +22,7 @@ import { getIsEditorInitialized, } from "selectors/editorSelectors"; import { Plugin } from "api/PluginApi"; -import { RapidApiAction, RestAction, PaginationType } from "entities/Action"; +import { RapidApiAction, Action, PaginationType } from "entities/Action"; import { getApiName } from "selectors/formSelectors"; import Spinner from "components/editorComponents/Spinner"; import styled from "styled-components"; @@ -49,7 +49,7 @@ interface ReduxStateProps { pages: any; plugins: Plugin[]; pluginId: any; - apiAction: RestAction | ActionData | RapidApiAction | undefined; + apiAction: Action | ActionData | RapidApiAction | undefined; paginationType: PaginationType; isEditorInitialized: boolean; } diff --git a/app/client/src/pages/Editor/DataSourceEditor/Connected.tsx b/app/client/src/pages/Editor/DataSourceEditor/Connected.tsx index 88fb19f625..4ac8182acf 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/Connected.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/Connected.tsx @@ -17,8 +17,8 @@ import { } from "constants/routes"; import { createNewApiName, createNewQueryName } from "utils/AppsmithUtils"; import { getCurrentPageId } from "selectors/editorSelectors"; -import { DEFAULT_API_ACTION } from "constants/ApiEditorConstants"; -import { ApiActionConfig, PluginType } from "entities/Action"; +import { DEFAULT_API_ACTION_CONFIG } from "constants/ApiEditorConstants"; +import { ApiActionConfig, PluginType, QueryAction } from "entities/Action"; import { renderDatasourceSection } from "./DatasourceSection"; import { Toaster } from "components/ads/Toast"; import { Variant } from "components/ads/common"; @@ -96,18 +96,20 @@ const Connected = () => { actionType: "Query", from: "datasource-pane", }, - }; - - // If in onboarding and tooltip is being shown - if (isInOnboarding && showingTooltip === OnboardingStep.EXAMPLE_DATABASE) { - payload = { - ...payload, - name: "ExampleQuery", - actionConfiguration: { - body: "select * from public.users limit 10", - }, - }; - } + } as Partial; // TODO: refactor later. Handle case for undefined datasource before we reach here. + if (datasource) + if ( + isInOnboarding && + showingTooltip === OnboardingStep.EXAMPLE_DATABASE + ) { + // If in onboarding and tooltip is being shown + payload = Object.assign({}, payload, { + name: "ExampleQuery", + actionConfiguration: { + body: "select * from public.users limit 10", + }, + }); + } dispatch(createActionRequest(payload)); history.push( @@ -122,11 +124,9 @@ const Connected = () => { const createApiAction = useCallback(() => { const newApiName = createNewApiName(actions, currentPageId || ""); const headers = datasource?.datasourceConfiguration?.headers ?? []; - const defaultAction: Partial | undefined = { - ...DEFAULT_API_ACTION.actionConfiguration, - headers: headers.length - ? headers - : DEFAULT_API_ACTION.actionConfiguration?.headers, + const defaultApiActionConfig: ApiActionConfig = { + ...DEFAULT_API_ACTION_CONFIG, + headers: headers.length ? headers : DEFAULT_API_ACTION_CONFIG.headers, }; if (!datasource?.datasourceConfiguration?.url) { @@ -142,17 +142,15 @@ const Connected = () => { createActionRequest({ name: newApiName, pageId: currentPageId, - pluginId: datasource?.pluginId, + pluginId: datasource.pluginId, datasource: { - id: datasource?.id, + id: datasource.id, }, eventData: { actionType: "API", from: "datasource-pane", }, - actionConfiguration: { - ...defaultAction, - }, + actionConfiguration: defaultApiActionConfig, }), ); history.push( @@ -196,7 +194,7 @@ const Connected = () => {
- {!isNil(currentFormConfig) + {!isNil(currentFormConfig) && !isNil(datasource) ? renderDatasourceSection(currentFormConfig[0], datasource) : undefined}
diff --git a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx index 453db88140..d9e4b783ea 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx @@ -16,7 +16,7 @@ import Connected from "./Connected"; import { HelpBaseURL, HelpMap } from "constants/HelpConstants"; import Button from "components/editorComponents/Button"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { reduxForm, InjectedFormProps } from "redux-form"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import { APPSMITH_IP_ADDRESSES } from "constants/DatasourceEditorConstants"; diff --git a/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx b/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx index 5dfcfd8cf5..e8f7c6db6e 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx @@ -1,4 +1,4 @@ -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import React from "react"; import { map, get } from "lodash"; import { Colors } from "constants/Colors"; @@ -29,16 +29,17 @@ const FieldWrapper = styled.div` export const renderDatasourceSection = ( config: any, - datasource: Datasource | undefined, + datasource: Datasource, ): any => { return ( - <> + {map(config.children, (section) => { if ("children" in section) { return renderDatasourceSection(section, datasource); } else { try { const { label, configProperty, controlType } = section; + const reactKey = datasource.id + "_" + label; let value = get(datasource, configProperty); if (controlType === "KEYVALUE_ARRAY") { @@ -55,7 +56,7 @@ export const renderDatasourceSection = ( if (controlType === "FIXED_KEY_INPUT") { return ( - + {configProperty.key}: {" "} {configProperty.value} @@ -64,7 +65,7 @@ export const renderDatasourceSection = ( if (controlType === "KEY_VAL_INPUT") { return ( - + {label} {value.map((val: { key: string; value: string }) => { return ( @@ -85,13 +86,13 @@ export const renderDatasourceSection = ( } return ( - + {label}: {value} ); } catch (e) {} } })} - + ); }; diff --git a/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx b/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx index 369818ef1d..9726dd8071 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx @@ -8,7 +8,7 @@ import EditableText, { import { AppState } from "reducers"; import { getDatasource } from "selectors/entitiesSelector"; import { useSelector, useDispatch } from "react-redux"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { getDataSources } from "selectors/editorSelectors"; import { getDataTree } from "selectors/dataTreeSelectors"; import { isNameValid } from "utils/helpers"; diff --git a/app/client/src/pages/Editor/DataSourceEditor/index.tsx b/app/client/src/pages/Editor/DataSourceEditor/index.tsx index bbba7137dc..6f47d53974 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/index.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/index.tsx @@ -19,7 +19,7 @@ import { import { DATASOURCE_DB_FORM } from "constants/forms"; import DatasourceHome from "./DatasourceHome"; import DataSourceEditorForm from "./DBForm"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { RouteComponentProps } from "react-router"; interface ReduxStateProps { diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx index 1af159f244..3919f8eb82 100644 --- a/app/client/src/pages/Editor/EditorHeader.tsx +++ b/app/client/src/pages/Editor/EditorHeader.tsx @@ -23,6 +23,7 @@ import { getCurrentPageId, getIsPageSaving, getIsPublishingApplication, + getPageSavingError, } from "selectors/editorSelectors"; import { getCurrentOrgId } from "selectors/organizationSelectors"; import { connect, useDispatch, useSelector } from "react-redux"; @@ -70,24 +71,6 @@ const AppsmithLogoImg = styled.img` max-width: 110px; `; -const ApplicationName = styled.span` - font-weight: 500; - font-size: 14px; - line-height: 14px; - color: #fff; - margin-bottom: 6px; -`; - -const PageName = styled.span` - display: flex; - flex: 1; - font-size: 12px; - line-height: 12px; - letter-spacing: 0.04em; - color: #ffffff; - opacity: 0.5; -`; - const SaveStatusContainer = styled.div` margin: 0 10px; border: 1px solid rgb(95, 105, 116); @@ -152,7 +135,6 @@ export const EditorHeader = (props: EditorHeaderProps) => { isPublishing, orgId, applicationId, - pageName, publishApplication, } = props; @@ -337,6 +319,7 @@ export const EditorHeader = (props: EditorHeaderProps) => { const mapStateToProps = (state: AppState) => ({ pageName: state.ui.editor.currentPageName, isSaving: getIsPageSaving(state), + pageSaveError: getPageSavingError(state), orgId: getCurrentOrgId(state), applicationId: getCurrentApplicationId(state), currentApplication: state.ui.applications.currentApplication, diff --git a/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx b/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx index 6ae3ce5b13..51ed09b906 100644 --- a/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx @@ -5,8 +5,6 @@ import history from "utils/history"; import { saveActionName } from "actions/actionActions"; import EntityProperties from "../Entity/EntityProperties"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; -import { ExplorerURLParams } from "../helpers"; -import { useParams } from "react-router"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; @@ -26,7 +24,6 @@ type ExplorerActionEntityProps = { }; export const ExplorerActionEntity = memo((props: ExplorerActionEntityProps) => { - const { pageId } = useParams(); const switchToAction = useCallback(() => { PerformanceTracker.startTracking(PerformanceTransactionName.OPEN_ACTION, { url: props.url, diff --git a/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx b/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx index c5c325cdcc..022066adea 100644 --- a/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx +++ b/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx @@ -12,7 +12,7 @@ import { import { Page } from "constants/ReduxActionConstants"; import { ExplorerURLParams } from "../helpers"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { Plugin } from "api/PluginApi"; import PluginGroup from "../PluginGroup/PluginGroup"; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx index b3c1f431f6..79d08966e6 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from "react"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { Plugin } from "api/PluginApi"; import DataSourceContextMenu from "./DataSourceContextMenu"; import { getPluginIcon } from "../ExplorerIcons"; @@ -21,6 +21,7 @@ import { useDispatch, useSelector } from "react-redux"; import { AppState } from "reducers"; import { DatasourceStructureContainer } from "./DatasourceStructureContainer"; import { getAction } from "selectors/entitiesSelector"; +import { isStoredDatasource } from "entities/Action"; type ExplorerDatasourceEntityProps = { plugin: Plugin; @@ -78,6 +79,13 @@ export const ExplorerDatasourceEntity = ( [datasourceStructure, props.datasource.id, dispatch], ); + let isDefaultExpanded = false; + if (expandDatasourceId === props.datasource.id) { + isDefaultExpanded = true; + } else if (queryAction && isStoredDatasource(queryAction.datasource)) { + isDefaultExpanded = queryAction.datasource.id === props.datasource.id; + } + return ( ` padding-left: ${(props) => diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx index bcec1093ab..896b884d36 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx @@ -8,7 +8,7 @@ import { EntityTogglesWrapper } from "../ExplorerStyledComponents"; import styled from "styled-components"; import QueryTemplates from "./QueryTemplates"; import DatasourceField from "./DatasourceField"; -import { DatasourceTable } from "api/DatasourcesApi"; +import { DatasourceTable } from "entities/Datasource"; const Wrapper = styled(EntityTogglesWrapper)` &&&& { diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx index a81a73d1d0..fdd2a02113 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx @@ -1,7 +1,7 @@ import { DatasourceStructure as DatasourceStructureType, DatasourceTable, -} from "api/DatasourcesApi"; +} from "entities/Datasource"; import React, { memo, ReactNode } from "react"; import EntityPlaceholder from "../Entity/Placeholder"; import { useEntityUpdateState } from "../hooks"; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx index 872b6c9fa4..a0571999ab 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx @@ -9,7 +9,7 @@ import { getCurrentPageId } from "selectors/editorSelectors"; import { QueryAction } from "entities/Action"; import { Classes } from "@blueprintjs/core"; import history from "utils/history"; -import { Datasource, QueryTemplate } from "api/DatasourcesApi"; +import { Datasource, QueryTemplate } from "entities/Datasource"; import { useParams } from "react-router"; import { ExplorerURLParams } from "../helpers"; import { QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID } from "constants/routes"; @@ -48,7 +48,9 @@ export const QueryTemplates = (props: QueryTemplatesProps) => { (template: QueryTemplate) => { const newQueryName = createNewQueryName(actions, currentPageId || ""); const queryactionConfiguration: Partial = { - actionConfiguration: { body: template.body }, + actionConfiguration: { + body: template.body, + }, }; dispatch( diff --git a/app/client/src/pages/Editor/Explorer/ExplorerTitle.tsx b/app/client/src/pages/Editor/Explorer/ExplorerTitle.tsx index 21081515d9..5fba596e49 100644 --- a/app/client/src/pages/Editor/Explorer/ExplorerTitle.tsx +++ b/app/client/src/pages/Editor/Explorer/ExplorerTitle.tsx @@ -18,22 +18,11 @@ const ActionIconGroup = styled.div` justify-content: space-between; align-items: center; `; - -export const ExplorerTitle = (props: { - isCollapsed: boolean; - onCollapseToggle: () => void; -}) => { +export const ExplorerTitle = () => { return (

EXPLORER

- - {/* - */} - +
); }; diff --git a/app/client/src/pages/Editor/Explorer/Onboarding/DBQueryGroup.tsx b/app/client/src/pages/Editor/Explorer/Onboarding/DBQueryGroup.tsx index 6d126853e6..8665c28312 100644 --- a/app/client/src/pages/Editor/Explorer/Onboarding/DBQueryGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Onboarding/DBQueryGroup.tsx @@ -1,6 +1,5 @@ import Boxed from "components/editorComponents/Onboarding/Boxed"; import OnboardingIndicator from "components/editorComponents/Onboarding/Indicator"; -import { Colors } from "constants/Colors"; import { OnboardingStep } from "constants/OnboardingConstants"; import { PluginType } from "entities/Action"; import React from "react"; @@ -8,15 +7,16 @@ import { useSelector } from "react-redux"; import { AppState } from "reducers"; import { getPlugins } from "selectors/entitiesSelector"; import styled from "styled-components"; +import AnalyticsUtil from "utils/AnalyticsUtil"; import { getPluginGroups, ACTION_PLUGIN_MAP } from "../Actions/helpers"; import { useActions, useFilteredDatasources } from "../hooks"; const AddWidget = styled.button` margin-bottom: 25px; - padding: 6px 38px; - background-color: ${Colors.MINE_SHAFT}; - border: 1px solid #f3672a; - color: #f3672a; + padding: 7px 38px; + background-color: #f3672a; + color: white; + border: none; font-weight: bold; cursor: pointer; `; @@ -53,7 +53,10 @@ const DBQueryGroup = (props: any) => { > { + AnalyticsUtil.logEvent("ONBOARDING_ADD_WIDGET_CLICK"); + props.showWidgetsSidebar(); + }} > Add Widget diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx index 069b91615d..97c331980e 100644 --- a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx @@ -15,7 +15,7 @@ import { getPluginGroups } from "../Actions/helpers"; import ExplorerWidgetGroup from "../Widgets/WidgetGroup"; import { resolveAsSpaceChar } from "utils/helpers"; import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructure"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { Plugin } from "api/PluginApi"; type ExplorerPageEntityProps = { diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx index 795308ef56..a3c62766aa 100644 --- a/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx @@ -11,7 +11,7 @@ import { Page } from "constants/ReduxActionConstants"; import ExplorerPageEntity from "./PageEntity"; import { AppState } from "reducers"; import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructure"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { Plugin } from "api/PluginApi"; type ExplorerPageGroupProps = { diff --git a/app/client/src/pages/Editor/Explorer/PluginGroup/PluginGroup.tsx b/app/client/src/pages/Editor/Explorer/PluginGroup/PluginGroup.tsx index 11360a1a18..b1c62ba960 100644 --- a/app/client/src/pages/Editor/Explorer/PluginGroup/PluginGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/PluginGroup/PluginGroup.tsx @@ -1,4 +1,4 @@ -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { Page } from "constants/ReduxActionConstants"; import { keyBy } from "lodash"; import React, { memo, useCallback, useMemo } from "react"; @@ -12,8 +12,6 @@ import ExplorerDatasourceEntity from "../Datasources/DatasourceEntity"; import Entity from "../Entity"; import EntityPlaceholder from "../Entity/Placeholder"; import { ExplorerURLParams } from "../helpers"; -import OnboardingTooltip from "components/editorComponents/Onboarding/Tooltip"; -import { OnboardingStep } from "constants/OnboardingConstants"; type ExplorerPluginGroupProps = { step: number; diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx index ea83374c87..fe50cabc6c 100644 --- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback, ReactNode, memo } from "react"; +import React, { useMemo, useCallback, memo } from "react"; import Entity, { EntityClassNames } from "../Entity"; import { WidgetProps } from "widgets/BaseWidget"; import { WidgetTypes, WidgetType } from "constants/WidgetConstants"; diff --git a/app/client/src/pages/Editor/Explorer/hooks.ts b/app/client/src/pages/Editor/Explorer/hooks.ts index bf5d91780f..c0ae4634db 100644 --- a/app/client/src/pages/Editor/Explorer/hooks.ts +++ b/app/client/src/pages/Editor/Explorer/hooks.ts @@ -8,7 +8,8 @@ import { import { useSelector } from "react-redux"; import { AppState } from "reducers"; import { compact, groupBy } from "lodash"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; +import { isStoredDatasource } from "entities/Action"; import { debounce } from "lodash"; import { WidgetProps } from "widgets/BaseWidget"; import log from "loglevel"; @@ -46,9 +47,17 @@ export const useFilteredDatasources = (searchKeyword?: string) => { const datasources = useMemo(() => { const datasourcesPageMap: Record = {}; for (const [key, value] of Object.entries(actions)) { - const datasourceIds = value.map((action) => action.config.datasource?.id); + const datasourceIds = new Set(); + value.forEach((action) => { + if ( + isStoredDatasource(action.config.datasource) && + action.config.datasource.id + ) { + datasourceIds.add(action.config.datasource.id); + } + }); const activeDatasources = reducerDatasources.filter((datasource) => - datasourceIds.includes(datasource.id), + datasourceIds.has(datasource.id), ); datasourcesPageMap[key] = activeDatasources; } diff --git a/app/client/src/pages/Editor/QueryEditor/DatasourceCard.tsx b/app/client/src/pages/Editor/QueryEditor/DatasourceCard.tsx index f008c1c40a..11f0c6b644 100644 --- a/app/client/src/pages/Editor/QueryEditor/DatasourceCard.tsx +++ b/app/client/src/pages/Editor/QueryEditor/DatasourceCard.tsx @@ -1,10 +1,12 @@ -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; +import { isStoredDatasource } from "entities/Action"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import React from "react"; import { isNil } from "lodash"; import { useDispatch, useSelector } from "react-redux"; import { Colors } from "constants/Colors"; import { useParams } from "react-router"; + import { getPluginImages, getQueryActionsForCurrentPage, @@ -89,7 +91,9 @@ const DatasourceCard = (props: DatasourceCardProps) => { ); const queryActions = useSelector(getQueryActionsForCurrentPage); const queriesWithThisDatasource = queryActions.filter( - (action) => action.config.datasource.id === datasource.id, + (action) => + isStoredDatasource(action.config.datasource) && + action.config.datasource.id === datasource.id, ).length; const currentFormConfig: Array = diff --git a/app/client/src/pages/Editor/QueryEditor/Form.tsx b/app/client/src/pages/Editor/QueryEditor/Form.tsx index 6d681aeee4..93afb4f7e1 100644 --- a/app/client/src/pages/Editor/QueryEditor/Form.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Form.tsx @@ -16,14 +16,14 @@ import Button from "components/editorComponents/Button"; import FormRow from "components/editorComponents/FormRow"; import DropdownField from "components/editorComponents/form/fields/DropdownField"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView"; import { QUERY_EDITOR_FORM_NAME } from "constants/forms"; import { Colors } from "constants/Colors"; import JSONViewer from "./JSONViewer"; import FormControl from "../FormControl"; import Table from "./Table"; -import { RestAction } from "entities/Action"; +import { Action } from "entities/Action"; import { connect, useDispatch } from "react-redux"; import { AppState } from "reducers"; import ActionNameEditor from "components/editorComponents/ActionNameEditor"; @@ -291,8 +291,7 @@ type ReduxProps = { export type StateAndRouteProps = QueryFormProps & ReduxProps; -type Props = StateAndRouteProps & - InjectedFormProps; +type Props = StateAndRouteProps & InjectedFormProps; const QueryEditorForm: React.FC = (props: Props) => { const { @@ -639,7 +638,7 @@ const mapStateToProps = (state: AppState) => { }; export default connect(mapStateToProps)( - reduxForm({ + reduxForm({ form: QUERY_EDITOR_FORM_NAME, enableReinitialize: true, })(QueryEditorForm), diff --git a/app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx b/app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx index 60414c2039..12da0f9d60 100644 --- a/app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx +++ b/app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx @@ -6,7 +6,7 @@ import { AppState } from "reducers"; import { createNewQueryName } from "utils/AppsmithUtils"; import { getPluginImages } from "selectors/entitiesSelector"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { createActionRequest } from "actions/actionActions"; import { Page } from "constants/ReduxActionConstants"; import { diff --git a/app/client/src/pages/Editor/QueryEditor/index.tsx b/app/client/src/pages/Editor/QueryEditor/index.tsx index 92ade9f32b..9d05287713 100644 --- a/app/client/src/pages/Editor/QueryEditor/index.tsx +++ b/app/client/src/pages/Editor/QueryEditor/index.tsx @@ -11,7 +11,7 @@ import { AppState } from "reducers"; import { getIsEditorInitialized } from "selectors/editorSelectors"; import { QUERY_EDITOR_FORM_NAME } from "constants/forms"; import { Plugin } from "api/PluginApi"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { getPluginIdsOfPackageNames, getPlugins, @@ -180,9 +180,15 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { const { editorConfigs, loadingFormConfigs } = plugins; const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as QueryAction; - const queryAction = getAction(state, props.match.params.queryId); + const queryAction = getAction( + state, + props.match.params.queryId, + ) as QueryAction; + let pluginId; + if (queryAction) { + pluginId = queryAction.pluginId; + } let editorConfig: any; - const pluginId = queryAction?.datasource?.pluginId; if (editorConfigs && pluginId) { editorConfig = editorConfigs[pluginId]; diff --git a/app/client/src/pages/Editor/Welcome.tsx b/app/client/src/pages/Editor/Welcome.tsx index 44afb0ef94..75d773ca36 100644 --- a/app/client/src/pages/Editor/Welcome.tsx +++ b/app/client/src/pages/Editor/Welcome.tsx @@ -132,7 +132,9 @@ const Welcome = () => { Not your first time with Appsmith?{" "} { - AnalyticsUtil.logEvent("SKIP_ONBOARDING"); + AnalyticsUtil.logEvent("SKIP_ONBOARDING", { + step: "WELCOME", + }); dispatch(endOnboarding()); }} > diff --git a/app/client/src/pages/Editor/WidgetCard.tsx b/app/client/src/pages/Editor/WidgetCard.tsx index ea2fedbca0..5cc01b3f57 100644 --- a/app/client/src/pages/Editor/WidgetCard.tsx +++ b/app/client/src/pages/Editor/WidgetCard.tsx @@ -67,7 +67,6 @@ export const IconLabel = styled.h5` } `; -/* eslint-disable @typescript-eslint/no-unused-vars */ const WidgetCard = (props: CardProps) => { const { setIsDragging } = useWidgetDragResize(); const { selectWidget } = useWidgetSelection(); diff --git a/app/client/src/pages/UserAuth/Login.tsx b/app/client/src/pages/UserAuth/Login.tsx index 3bb36f06a6..8850421fce 100644 --- a/app/client/src/pages/UserAuth/Login.tsx +++ b/app/client/src/pages/UserAuth/Login.tsx @@ -92,9 +92,9 @@ export const Login = (props: LoginFormProps) => { let loginURL = "/api/v1/" + LOGIN_SUBMIT_PATH; let signupURL = SIGN_UP_URL; - if (queryParams.has("redirectTo")) { - loginURL += `?redirectUrl=${queryParams.get("redirectTo")}`; - signupURL += `?redirectTo=${queryParams.get("redirectTo")}`; + if (queryParams.has("redirectUrl")) { + loginURL += `?redirectUrl=${queryParams.get("redirectUrl")}`; + signupURL += `?redirectUrl=${queryParams.get("redirectUrl")}`; } let forgotPasswordURL = `${FORGOT_PASSWORD_URL}`; diff --git a/app/client/src/pages/UserAuth/SignUp.tsx b/app/client/src/pages/UserAuth/SignUp.tsx index 55fde3883f..364fd9a986 100644 --- a/app/client/src/pages/UserAuth/SignUp.tsx +++ b/app/client/src/pages/UserAuth/SignUp.tsx @@ -110,8 +110,8 @@ export const SignUp = (props: SignUpFormProps) => { let signupURL = "/api/v1/" + SIGNUP_SUBMIT_PATH; if (queryParams.has("appId")) { signupURL += `?appId=${queryParams.get("appId")}`; - } else if (queryParams.has("redirectTo")) { - signupURL += `?redirectUrl=${queryParams.get("redirectTo")}`; + } else if (queryParams.has("redirectUrl")) { + signupURL += `?redirectUrl=${queryParams.get("redirectUrl")}`; } return ( diff --git a/app/client/src/pages/UserAuth/ThirdPartyAuth.tsx b/app/client/src/pages/UserAuth/ThirdPartyAuth.tsx index 527882541a..30cbfb01db 100644 --- a/app/client/src/pages/UserAuth/ThirdPartyAuth.tsx +++ b/app/client/src/pages/UserAuth/ThirdPartyAuth.tsx @@ -77,8 +77,8 @@ const SocialLoginButton = (props: { const location = useLocation(); const queryParams = new URLSearchParams(location.search); let url = props.url; - if (queryParams.has("redirectTo")) { - url += `?redirectUrl=${queryParams.get("redirectTo")}`; + if (queryParams.has("redirectUrl")) { + url += `?redirectUrl=${queryParams.get("redirectUrl")}`; } return ( { } render() { - const { component: Component, currentTheme, ...rest } = this.props; + const { currentTheme, ...rest } = this.props; if ( window.location.pathname === "/applications" || window.location.pathname.indexOf("/settings/") !== -1 @@ -38,7 +38,7 @@ class AppRouteWithoutProps extends React.Component { } else { document.body.style.backgroundColor = currentTheme.colors.appBackground; } - return ; + return ; } } const mapStateToProps = (state: AppState) => ({ diff --git a/app/client/src/pages/common/CustomizedDropdown/OrgDropdownData.tsx b/app/client/src/pages/common/CustomizedDropdown/OrgDropdownData.tsx index b4a09130f5..c1e9691e7a 100644 --- a/app/client/src/pages/common/CustomizedDropdown/OrgDropdownData.tsx +++ b/app/client/src/pages/common/CustomizedDropdown/OrgDropdownData.tsx @@ -1,43 +1,11 @@ import React from "react"; import Badge from "./Badge"; import { Directions } from "utils/helpers"; -import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { getOnSelectAction, DropdownOnSelectActions } from "./dropdownHelpers"; import { CustomizedDropdownProps } from "./index"; -import { Org } from "constants/orgConstants"; import { User } from "constants/userConstants"; import _ from "lodash"; -const switchdropdown = ( - orgs: Org[], - currentOrg: Org, -): CustomizedDropdownProps => ({ - sections: [ - { - isSticky: true, - }, - { - options: orgs - .filter((org) => org.id !== currentOrg.id) - .map((org) => ({ - content: org.name, - onSelect: () => - getOnSelectAction(DropdownOnSelectActions.DISPATCH, { - type: ReduxActionTypes.SWITCH_ORGANIZATION_INIT, - payload: { - orgId: org.id, - }, - }), - })), - }, - ], - trigger: { - text: "Switch Organization", - }, - openDirection: Directions.RIGHT, - openOnHover: true, -}); - export const options = ( user: User, orgName: string, diff --git a/app/client/src/pages/common/CustomizedDropdown/index.tsx b/app/client/src/pages/common/CustomizedDropdown/index.tsx index addfff0ae6..e72990bcb4 100644 --- a/app/client/src/pages/common/CustomizedDropdown/index.tsx +++ b/app/client/src/pages/common/CustomizedDropdown/index.tsx @@ -61,8 +61,8 @@ export const getIcon = (icon?: string | MaybeElement, intent?: Intent) => { height: 16, }); } - const iconNames = Object.values({ ...IconNames }); - if (iconNames.indexOf(icon as IconName) > -1) { + const iconNames: string[] = Object.values({ ...IconNames }); + if (iconNames.indexOf(icon) > -1) { return ( { const location = useLocation(); const queryParams = new URLSearchParams(location.search); let loginUrl = AUTH_LOGIN_URL; - if (queryParams.has("redirectTo")) { - loginUrl += `?redirectTo=${queryParams.get("redirectTo")}`; + if (queryParams.has("redirectUrl")) { + loginUrl += `?redirectUrl=${queryParams.get("redirectUrl")}`; } return ( diff --git a/app/client/src/pages/common/PageHeader.tsx b/app/client/src/pages/common/PageHeader.tsx index 038ee645a1..becb17993f 100644 --- a/app/client/src/pages/common/PageHeader.tsx +++ b/app/client/src/pages/common/PageHeader.tsx @@ -45,8 +45,8 @@ export const PageHeader = (props: PageHeaderProps) => { const location = useLocation(); const queryParams = new URLSearchParams(location.search); let loginUrl = AUTH_LOGIN_URL; - if (queryParams.has("redirectTo")) { - loginUrl += `?redirectTo=${queryParams.get("redirectTo")}`; + if (queryParams.has("redirectUrl")) { + loginUrl += `?redirectUrl=${queryParams.get("redirectUrl")}`; } return ( diff --git a/app/client/src/pages/common/PageNotFound.tsx b/app/client/src/pages/common/PageNotFound.tsx index 7ca5b5d6fa..9c48135b3a 100644 --- a/app/client/src/pages/common/PageNotFound.tsx +++ b/app/client/src/pages/common/PageNotFound.tsx @@ -1,10 +1,10 @@ import React from "react"; import { connect } from "react-redux"; import styled from "styled-components"; -import Button from "components/editorComponents/Button"; -import PageUnavailableImage from "assets/images/404-image.png"; import { APPLICATIONS_URL } from "constants/routes"; +import Button from "components/editorComponents/Button"; import { flushErrorsAndRedirect } from "actions/errorActions"; +import PageUnavailableImage from "assets/images/404-image.png"; const Wrapper = styled.div` text-align: center; @@ -24,6 +24,7 @@ const Wrapper = styled.div` interface Props { flushErrorsAndRedirect?: any; } + const PageNotFound: React.FC = (props: Props) => { const { flushErrorsAndRedirect } = props; diff --git a/app/client/src/pages/organization/CreateOrganizationForm.tsx b/app/client/src/pages/organization/CreateOrganizationForm.tsx index a705e5cd2c..52c34d71dc 100644 --- a/app/client/src/pages/organization/CreateOrganizationForm.tsx +++ b/app/client/src/pages/organization/CreateOrganizationForm.tsx @@ -25,7 +25,12 @@ export const CreateApplicationForm = ( {error && !pristine && } - + ; + config: { id: string }; data?: ActionResponse; } @@ -29,7 +28,7 @@ const initialState: ActionDataState = []; const actionsReducer = createReducer(initialState, { [ReduxActionTypes.FETCH_ACTIONS_SUCCESS]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ): ActionDataState => { return action.payload.map((action) => { const foundAction = state.find((currentAction) => { @@ -44,7 +43,7 @@ const actionsReducer = createReducer(initialState, { }, [ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ): ActionDataState => action.payload.map((a) => ({ isLoading: false, @@ -52,13 +51,13 @@ const actionsReducer = createReducer(initialState, { })), [ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ): ActionDataState => { if (action.payload.length > 0) { const stateActionMap = _.keyBy(state, "config.id"); const result: ActionDataState = []; - action.payload.forEach((actionPayload: RestAction) => { + action.payload.forEach((actionPayload: Action) => { const stateAction = stateActionMap[actionPayload.id]; if (stateAction) { result.push({ @@ -86,13 +85,13 @@ const actionsReducer = createReducer(initialState, { }, [ReduxActionTypes.SUBMIT_CURL_FORM_SUCCESS]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ) => state.concat([{ config: action.payload, isLoading: false }]), [ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: () => initialState, [ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR]: () => initialState, [ReduxActionTypes.CREATE_ACTION_INIT]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ): ActionDataState => state.concat([ { @@ -102,7 +101,7 @@ const actionsReducer = createReducer(initialState, { ]), [ReduxActionTypes.CREATE_ACTION_SUCCESS]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ): ActionDataState => state.map((a) => { if ( @@ -115,7 +114,7 @@ const actionsReducer = createReducer(initialState, { }), [ReduxActionTypes.CREATE_ACTION_ERROR]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ): ActionDataState => state.filter( (a) => @@ -124,7 +123,7 @@ const actionsReducer = createReducer(initialState, { ), [ReduxActionTypes.UPDATE_ACTION_SUCCESS]: ( state: ActionDataState, - action: ReduxAction<{ data: RestAction }>, + action: ReduxAction<{ data: Action }>, ): ActionDataState => state.map((a) => { if (a.config.id === action.payload.data.id) @@ -257,7 +256,7 @@ const actionsReducer = createReducer(initialState, { }), [ReduxActionTypes.MOVE_ACTION_SUCCESS]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ): ActionDataState => state.map((a) => { if (a.config.id === action.payload.id) { @@ -306,7 +305,7 @@ const actionsReducer = createReducer(initialState, { ), [ReduxActionTypes.COPY_ACTION_SUCCESS]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction, ): ActionDataState => state.map((a) => { if ( @@ -351,52 +350,6 @@ const actionsReducer = createReducer(initialState, { }); }); }, - [ReduxActionTypes.FETCH_DATASOURCES_SUCCESS]: ( - state: ActionDataState, - action: ReduxAction, - ) => { - const datasources = action.payload; - - return state.map((action) => { - const datasourceId = action.config.datasource.id; - if (datasourceId) { - const datasource = datasources.find( - (datasource) => datasource.id === datasourceId, - ); - - return { - ...action, - config: { - ...action.config, - datasource: datasource || action.config.datasource, // fallback to original datasource if datasource not available. - }, - }; - } - - return action; - }); - }, - [ReduxActionTypes.UPDATE_DATASOURCE_SUCCESS]: ( - state: ActionDataState, - action: ReduxAction, - ) => { - const datasource = action.payload; - - return state.map((action) => { - const datasourceId = action.config.datasource.id; - if (datasourceId && datasource.id === datasourceId) { - return { - ...action, - config: { - ...action.config, - datasource: datasource, - }, - }; - } - - return action; - }); - }, }); export default actionsReducer; diff --git a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx index 2588ca7dcc..fa01e3eb7f 100644 --- a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx +++ b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx @@ -14,6 +14,7 @@ export type FlattenedWidgetProps = WidgetProps & { }; const canvasWidgetsReducer = createImmerReducer(initialState, { + // TODO Rename to INIT_LAYOUT [ReduxActionTypes.UPDATE_CANVAS]: ( state: CanvasWidgetsReduxState, action: ReduxAction, diff --git a/app/client/src/reducers/entityReducers/datasourceReducer.ts b/app/client/src/reducers/entityReducers/datasourceReducer.ts index 9c42eed5da..d404a91107 100644 --- a/app/client/src/reducers/entityReducers/datasourceReducer.ts +++ b/app/client/src/reducers/entityReducers/datasourceReducer.ts @@ -4,7 +4,7 @@ import { ReduxAction, ReduxActionErrorTypes, } from "constants/ReduxActionConstants"; -import { Datasource, DatasourceStructure } from "api/DatasourcesApi"; +import { Datasource, DatasourceStructure } from "entities/Datasource"; export interface DatasourceDataState { list: Datasource[]; diff --git a/app/client/src/reducers/entityReducers/metaReducer.ts b/app/client/src/reducers/entityReducers/metaReducer.ts index 5cd2459cc8..5deb65a212 100644 --- a/app/client/src/reducers/entityReducers/metaReducer.ts +++ b/app/client/src/reducers/entityReducers/metaReducer.ts @@ -45,16 +45,10 @@ export const metaReducer = createReducer(initialState, { } return state; }, - [ReduxActionTypes.FETCH_PAGE_SUCCESS]: ( - state: MetaState, - action: ReduxAction<{ widgetId: string }>, - ) => { + [ReduxActionTypes.FETCH_PAGE_SUCCESS]: () => { return initialState; }, - [ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS]: ( - state: MetaState, - action: ReduxAction<{ widgetId: string }>, - ) => { + [ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS]: () => { return initialState; }, }); diff --git a/app/client/src/reducers/evaluationReducers/dependencyReducer.ts b/app/client/src/reducers/evaluationReducers/dependencyReducer.ts index 8800ec8dbb..8c2d72242e 100644 --- a/app/client/src/reducers/evaluationReducers/dependencyReducer.ts +++ b/app/client/src/reducers/evaluationReducers/dependencyReducer.ts @@ -1,21 +1,25 @@ import { createReducer } from "utils/AppsmithUtils"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; +import { DependencyMap } from "../../utils/DynamicBindingUtils"; export type EvaluationDependencyState = { - dependencyMap: Record>; - dependencyTree: Array<[string, string]>; + inverseDependencyMap: DependencyMap; }; const initialState: EvaluationDependencyState = { - dependencyMap: {}, - dependencyTree: [], + inverseDependencyMap: {}, }; const evaluationDependencyReducer = createReducer(initialState, { - [ReduxActionTypes.SET_EVALUATION_DEPENDENCIES]: ( + [ReduxActionTypes.SET_EVALUATION_INVERSE_DEPENDENCY_MAP]: ( state: EvaluationDependencyState, - action: ReduxAction, - ) => action.payload, + action: ReduxAction<{ + inverseDependencyMap: DependencyMap; + }>, + ): EvaluationDependencyState => ({ + ...state, + inverseDependencyMap: action.payload.inverseDependencyMap, + }), }); export default evaluationDependencyReducer; diff --git a/app/client/src/reducers/evaluationReducers/index.ts b/app/client/src/reducers/evaluationReducers/index.ts index 926bf4f999..524186dcb0 100644 --- a/app/client/src/reducers/evaluationReducers/index.ts +++ b/app/client/src/reducers/evaluationReducers/index.ts @@ -1,8 +1,10 @@ import { combineReducers } from "redux"; import evaluatedTreeReducer from "./treeReducer"; import evaluationDependencyReducer from "./dependencyReducer"; +import loadingEntitiesReducer from "./loadingEntitiesReducer"; export default combineReducers({ tree: evaluatedTreeReducer, dependencies: evaluationDependencyReducer, + loadingEntities: loadingEntitiesReducer, }); diff --git a/app/client/src/reducers/evaluationReducers/loadingEntitiesReducer.ts b/app/client/src/reducers/evaluationReducers/loadingEntitiesReducer.ts new file mode 100644 index 0000000000..c93f20f589 --- /dev/null +++ b/app/client/src/reducers/evaluationReducers/loadingEntitiesReducer.ts @@ -0,0 +1,16 @@ +import { createReducer } from "utils/AppsmithUtils"; +import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; + +export type LoadingEntitiesState = Set; + +const initialState: LoadingEntitiesState = new Set(); + +const loadingEntitiesReducer = createReducer(initialState, { + [ReduxActionTypes.SET_LOADING_ENTITIES]: ( + state: LoadingEntitiesState, + action: ReduxAction>, + ): LoadingEntitiesState => action.payload, + [ReduxActionTypes.FETCH_PAGE_INIT]: () => initialState, +}); + +export default loadingEntitiesReducer; diff --git a/app/client/src/reducers/evaluationReducers/treeReducer.ts b/app/client/src/reducers/evaluationReducers/treeReducer.ts index 5c95524c7e..79e2ae1f2a 100644 --- a/app/client/src/reducers/evaluationReducers/treeReducer.ts +++ b/app/client/src/reducers/evaluationReducers/treeReducer.ts @@ -11,6 +11,7 @@ const evaluatedTreeReducer = createImmerReducer(initialState, { state: EvaluatedTreeState, action: ReduxAction, ) => action.payload, + [ReduxActionTypes.FETCH_PAGE_INIT]: () => initialState, }); export default evaluatedTreeReducer; diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx index 1ac0dc4516..c1ad5cf494 100644 --- a/app/client/src/reducers/index.tsx +++ b/app/client/src/reducers/index.tsx @@ -39,6 +39,8 @@ import { EvaluatedTreeState } from "./evaluationReducers/treeReducer"; import { EvaluationDependencyState } from "./evaluationReducers/dependencyReducer"; import { PageWidgetsReduxState } from "./uiReducers/pageWidgetsReducer"; import { OnboardingState } from "./uiReducers/onBoardingReducer"; +import { ReleasesState } from "./uiReducers/releasesReducer"; +import { LoadingEntitiesState } from "./evaluationReducers/loadingEntitiesReducer"; const appReducer = combineReducers({ entities: entityReducer, @@ -76,6 +78,7 @@ export interface AppState { datasourceName: DatasourceNameReduxState; theme: ThemeState; onBoarding: OnboardingState; + releases: ReleasesState; }; entities: { canvasWidgets: CanvasWidgetsReduxState; @@ -91,5 +94,6 @@ export interface AppState { evaluations: { tree: EvaluatedTreeState; dependencies: EvaluationDependencyState; + loadingEntities: LoadingEntitiesState; }; } diff --git a/app/client/src/reducers/uiReducers/apiPaneReducer.ts b/app/client/src/reducers/uiReducers/apiPaneReducer.ts index 1ecdab2334..2629a79dad 100644 --- a/app/client/src/reducers/uiReducers/apiPaneReducer.ts +++ b/app/client/src/reducers/uiReducers/apiPaneReducer.ts @@ -4,7 +4,7 @@ import { ReduxActionErrorTypes, ReduxAction, } from "constants/ReduxActionConstants"; -import { RestAction } from "entities/Action"; +import { Action } from "entities/Action"; import { UpdateActionPropertyActionPayload } from "actions/actionActions"; const initialState: ApiPaneReduxState = { @@ -115,7 +115,7 @@ const apiPaneReducer = createReducer(initialState, { }), [ReduxActionTypes.UPDATE_ACTION_SUCCESS]: ( state: ApiPaneReduxState, - action: ReduxAction<{ data: RestAction }>, + action: ReduxAction<{ data: Action }>, ) => ({ ...state, isSaving: { diff --git a/app/client/src/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/reducers/uiReducers/applicationsReducer.tsx index f1dfdb56bf..5012c23cba 100644 --- a/app/client/src/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/reducers/uiReducers/applicationsReducer.tsx @@ -26,7 +26,6 @@ const initialState: ApplicationsReduxState = { const applicationsReducer = createReducer(initialState, { [ReduxActionTypes.DELETE_APPLICATION_INIT]: ( state: ApplicationsReduxState, - action: ReduxAction<{ applicationId: string; orgId: string }>, ) => { return { ...state, deletingApplication: true }; }, @@ -61,7 +60,6 @@ const applicationsReducer = createReducer(initialState, { }, [ReduxActionTypes.DELETE_APPLICATION_ERROR]: ( state: ApplicationsReduxState, - action: ReduxAction<{ orgId: string }>, ) => { return { ...state, deletingApplication: false }; }, @@ -243,22 +241,19 @@ const applicationsReducer = createReducer(initialState, { if (action.payload.name) { isSavingAppName = true; } - const _organizations = state.userOrgs.map( - (org: Organization, index: number) => { - const appIndex = org.applications.findIndex( - (app) => app.id === action.payload.id, - ); - const { id, ...rest } = action.payload; - if (appIndex !== -1) { - org.applications[appIndex] = { - ...org.applications[appIndex], - ...rest, - }; - } + const { id, ...rest } = action.payload; + const _organizations = state.userOrgs.map((org: Organization) => { + const appIndex = org.applications.findIndex((app) => app.id === id); - return org; - }, - ); + if (appIndex !== -1) { + org.applications[appIndex] = { + ...org.applications[appIndex], + ...rest, + }; + } + + return org; + }); return { ...state, diff --git a/app/client/src/reducers/uiReducers/datasourcePaneReducer.ts b/app/client/src/reducers/uiReducers/datasourcePaneReducer.ts index 479b5ad8e5..3662ef3284 100644 --- a/app/client/src/reducers/uiReducers/datasourcePaneReducer.ts +++ b/app/client/src/reducers/uiReducers/datasourcePaneReducer.ts @@ -1,6 +1,6 @@ import { createReducer } from "utils/AppsmithUtils"; import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import _ from "lodash"; const initialState: DatasourcePaneReduxState = { diff --git a/app/client/src/reducers/uiReducers/editorReducer.tsx b/app/client/src/reducers/uiReducers/editorReducer.tsx index 661baf57a1..0c5550b771 100644 --- a/app/client/src/reducers/uiReducers/editorReducer.tsx +++ b/app/client/src/reducers/uiReducers/editorReducer.tsx @@ -88,7 +88,7 @@ const editorReducer = createReducer(initialState, { state.loadingStates.saving = false; return { ...state }; }, - [ReduxActionTypes.SAVE_PAGE_ERROR]: (state: EditorReduxState) => { + [ReduxActionErrorTypes.SAVE_PAGE_ERROR]: (state: EditorReduxState) => { state.loadingStates.saving = false; state.loadingStates.savingError = true; return { ...state }; diff --git a/app/client/src/reducers/uiReducers/explorerReducer.ts b/app/client/src/reducers/uiReducers/explorerReducer.ts index a1ec33ddfd..c630b60b19 100644 --- a/app/client/src/reducers/uiReducers/explorerReducer.ts +++ b/app/client/src/reducers/uiReducers/explorerReducer.ts @@ -103,10 +103,7 @@ const explorerReducer = createReducer(initialState, { ) => { return { editingEntityName: action.payload.id }; }, - [ReduxActionTypes.END_EXPLORER_ENTITY_NAME_EDIT]: ( - state: ExplorerReduxState, - action: ReduxAction<{ id: string }>, - ) => { + [ReduxActionTypes.END_EXPLORER_ENTITY_NAME_EDIT]: () => { return {}; }, }); diff --git a/app/client/src/reducers/uiReducers/index.tsx b/app/client/src/reducers/uiReducers/index.tsx index 876ed06d42..3780a5f40e 100644 --- a/app/client/src/reducers/uiReducers/index.tsx +++ b/app/client/src/reducers/uiReducers/index.tsx @@ -24,6 +24,7 @@ import datasourceNameReducer from "./datasourceNameReducer"; import pageCanvasStructureReducer from "./pageCanvasStructure"; import pageWidgetsReducer from "./pageWidgetsReducer"; import onBoardingReducer from "./onBoardingReducer"; +import releasesReducer from "./releasesReducer"; const uiReducer = combineReducers({ widgetSidebar: widgetSidebarReducer, @@ -51,5 +52,6 @@ const uiReducer = combineReducers({ theme: themeReducer, confirmRunAction: confirmRunActionReducer, onBoarding: onBoardingReducer, + releases: releasesReducer, }); export default uiReducer; diff --git a/app/client/src/reducers/uiReducers/pageWidgetsReducer.ts b/app/client/src/reducers/uiReducers/pageWidgetsReducer.ts index bf31cc12e4..a88f40af1d 100644 --- a/app/client/src/reducers/uiReducers/pageWidgetsReducer.ts +++ b/app/client/src/reducers/uiReducers/pageWidgetsReducer.ts @@ -1,9 +1,5 @@ import { createImmerReducer } from "utils/AppsmithUtils"; -import { - ReduxActionTypes, - ReduxActionErrorTypes, - ReduxAction, -} from "constants/ReduxActionConstants"; +import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; import { DSL } from "./pageCanvasStructure"; import { WidgetProps } from "widgets/BaseWidget"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; diff --git a/app/client/src/reducers/uiReducers/queryPaneReducer.ts b/app/client/src/reducers/uiReducers/queryPaneReducer.ts index 3bb7bd6606..d4e9aef6d4 100644 --- a/app/client/src/reducers/uiReducers/queryPaneReducer.ts +++ b/app/client/src/reducers/uiReducers/queryPaneReducer.ts @@ -5,7 +5,7 @@ import { ReduxAction, } from "constants/ReduxActionConstants"; import _ from "lodash"; -import { RestAction } from "entities/Action"; +import { Action } from "entities/Action"; import { ActionResponse } from "api/ActionAPI"; const initialState: QueryPaneReduxState = { @@ -66,7 +66,7 @@ const queryPaneReducer = createReducer(initialState, { }), [ReduxActionTypes.UPDATE_ACTION_SUCCESS]: ( state: QueryPaneReduxState, - action: ReduxAction<{ data: RestAction }>, + action: ReduxAction<{ data: Action }>, ) => ({ ...state, isSaving: { diff --git a/app/client/src/reducers/uiReducers/releasesReducer.ts b/app/client/src/reducers/uiReducers/releasesReducer.ts new file mode 100644 index 0000000000..eae135e9c8 --- /dev/null +++ b/app/client/src/reducers/uiReducers/releasesReducer.ts @@ -0,0 +1,25 @@ +import { createReducer } from "utils/AppsmithUtils"; +import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; + +const initialState: ReleasesState = { + newReleasesCount: "", + releaseItems: [], +}; + +const importReducer = createReducer(initialState, { + [ReduxActionTypes.FETCH_RELEASES_SUCCESS]: ( + _state: ReleasesState, + action: ReduxAction<{ payload: Record }>, + ) => action.payload, + [ReduxActionTypes.RESET_UNREAD_RELEASES_COUNT]: (state: ReleasesState) => ({ + ...state, + newReleasesCount: "", + }), +}); + +export type ReleasesState = { + newReleasesCount: string; + releaseItems: any[]; +}; + +export default importReducer; diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 92b8fc48f2..05c6e0ee78 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -9,7 +9,6 @@ import { EventType, ExecuteActionPayload, ExecuteActionPayloadEvent, - EXECUTION_PARAM_KEY, PageAction, } from "constants/ActionConstants"; import * as log from "loglevel"; @@ -47,7 +46,7 @@ import { showRunActionConfirmModal, updateAction, } from "actions/actionActions"; -import { Action, RestAction } from "entities/Action"; +import { Action } from "entities/Action"; import ActionAPI, { ActionApiResponse, ActionResponse, @@ -81,7 +80,11 @@ import { getAppMode, getCurrentApplication, } from "selectors/applicationSelectors"; -import { evaluateDynamicTrigger, evaluateSingleValue } from "./evaluationsSaga"; +import { + evaluateDynamicTrigger, + evaluateActionBindings, +} from "./EvaluationsSaga"; +import copy from "copy-to-clipboard"; function* navigateActionSaga( action: { pageNameOrUrl: string; params: Record }, @@ -173,6 +176,21 @@ async function downloadSaga( } } +function* copySaga( + payload: { + data: string; + options: { debug: boolean; format: string }; + }, + event: ExecuteActionPayloadEvent, +) { + const result = copy(payload.data, payload.options); + if (event.callback) { + if (result) { + event.callback({ success: result }); + } + } +} + function* showAlertSaga( payload: { message: string; style?: TypeOptions }, event: ExecuteActionPayloadEvent, @@ -243,15 +261,6 @@ const isErrorResponse = (response: ActionApiResponse) => { return !response.data.isExecutionSuccess; }; -export function* evaluateDynamicBoundValueSaga( - valueToEvaluate: string, - params?: Record, -): any { - return yield call(evaluateSingleValue, `{{${valueToEvaluate}}}`, params); -} - -const EXECUTION_PARAM_REFERENCE_REGEX = /this.params/g; - /** * Api1 * URL: https://example.com/{{Text1.text}} @@ -285,31 +294,13 @@ export function* evaluateActionParams( bindings: string[] | undefined, executionParams?: Record | string, ) { - if (_.isNil(bindings)) return []; - // We might get execution params as an object or as a string. - // If the user has added a proper object (valid case) it will be an object - // If they have not added any execution params or not an object - // it would be a string (invalid case) - let evaluatedExecutionParams: Record = {}; - if (executionParams && _.isObject(executionParams)) { - evaluatedExecutionParams = yield evaluateDynamicBoundValueSaga( - JSON.stringify(executionParams), - ); - } - // Replace any reference of 'this.params' to 'executionParams' (backwards compatibility) - const bindingsForExecutionParams = bindings.map((binding) => - binding.replace(EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_KEY), - ); + if (_.isNil(bindings) || bindings.length === 0) return []; // Evaluated all bindings of the actions. Pass executionParams if any - const values: any = yield all( - bindingsForExecutionParams.map((binding: string) => { - return call( - evaluateDynamicBoundValueSaga, - binding, - evaluatedExecutionParams, - ); - }), + const values: any = yield call( + evaluateActionBindings, + bindings, + executionParams, ); // Convert to object and transform non string values @@ -346,14 +337,16 @@ export function* executeActionSaga( }, actionId, ); + const appMode = yield select(getAppMode); try { - const api: RestAction = yield select(getAction, actionId); + const api: Action = yield select(getAction, actionId); const currentApp: ApplicationPayload = yield select(getCurrentApplication); AnalyticsUtil.logEvent("EXECUTE_ACTION", { type: api.pluginType, name: api.name, pageId: api.pageId, appId: currentApp.id, + appMode: appMode, appName: currentApp.name, isExampleApp: currentApp.appIsExample, }); @@ -379,7 +372,6 @@ export function* executeActionSaga( : event.type === EventType.ON_PREV_PAGE ? "PREV" : undefined; - const appMode = yield select(getAppMode); const executeActionRequest: ExecuteActionRequest = { actionId: actionId, @@ -508,6 +500,9 @@ function* executeActionTriggers( case "DOWNLOAD": yield call(downloadSaga, trigger.payload, event); break; + case "COPY_TO_CLIPBOARD": + yield call(copySaga, trigger.payload, event); + break; default: yield put( executeActionError({ @@ -665,72 +660,94 @@ function* confirmRunActionSaga() { } function* executePageLoadAction(pageAction: PageAction) { - PerformanceTracker.startAsyncTracking( - PerformanceTransactionName.EXECUTE_ACTION, - { + try { + PerformanceTracker.startAsyncTracking( + PerformanceTransactionName.EXECUTE_ACTION, + { + actionId: pageAction.id, + }, + pageAction.id, + PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS, + ); + const pageId = yield select(getCurrentPageId); + let currentApp: ApplicationPayload = yield select(getCurrentApplication); + currentApp = currentApp || {}; + yield put(executeApiActionRequest({ id: pageAction.id })); + const params: Property[] = yield call( + evaluateActionParams, + pageAction.jsonPathKeys, + ); + const appMode = yield select(getAppMode); + const viewMode = appMode === APP_MODE.PUBLISHED; + const executeActionRequest: ExecuteActionRequest = { actionId: pageAction.id, - }, - pageAction.id, - PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS, - ); - const pageId = yield select(getCurrentPageId); - let currentApp: ApplicationPayload = yield select(getCurrentApplication); - currentApp = currentApp || {}; - yield put(executeApiActionRequest({ id: pageAction.id })); - const params: Property[] = yield call( - evaluateActionParams, - pageAction.jsonPathKeys, - ); - const appMode = yield select(getAppMode); - const viewMode = appMode === APP_MODE.PUBLISHED; - const executeActionRequest: ExecuteActionRequest = { - actionId: pageAction.id, - params, - viewMode, - }; - AnalyticsUtil.logEvent("EXECUTE_ACTION", { - type: pageAction.pluginType, - name: pageAction.name, - pageId: pageId, - appId: currentApp.id, - onPageLoad: true, - appName: currentApp.name, - isExampleApp: currentApp.appIsExample, - }); - const response: ActionApiResponse = yield ActionAPI.executeAction( - executeActionRequest, - pageAction.timeoutInMillisecond, - ); - if (isErrorResponse(response)) { + params, + viewMode, + }; + AnalyticsUtil.logEvent("EXECUTE_ACTION", { + type: pageAction.pluginType, + name: pageAction.name, + pageId: pageId, + appMode: appMode, + appId: currentApp.id, + onPageLoad: true, + appName: currentApp.name, + isExampleApp: currentApp.appIsExample, + }); + const response: ActionApiResponse = yield ActionAPI.executeAction( + executeActionRequest, + pageAction.timeoutInMillisecond, + ); + if (isErrorResponse(response)) { + const body = _.get(response, "data.body"); + let message = `The action "${pageAction.name}" has failed.`; + + if (body) { + message += `\nERROR: "${body}"`; + } + + yield put( + executeActionError({ + actionId: pageAction.id, + isPageLoad: true, + error: _.get(response, "responseMeta.error", { + message, + }), + }), + ); + PerformanceTracker.stopAsyncTracking( + PerformanceTransactionName.EXECUTE_ACTION, + { + failed: true, + }, + pageAction.id, + ); + } else { + const payload = createActionExecutionResponse(response); + PerformanceTracker.stopAsyncTracking( + PerformanceTransactionName.EXECUTE_ACTION, + undefined, + pageAction.id, + ); + yield put( + executeApiActionSuccess({ + id: pageAction.id, + response: payload, + isPageLoad: true, + }), + ); + yield take(ReduxActionTypes.SET_EVALUATED_TREE); + } + } catch (e) { yield put( executeActionError({ actionId: pageAction.id, - error: response.responseMeta.error, isPageLoad: true, + error: { + message: `The action "${pageAction.name}" has failed.`, + }, }), ); - PerformanceTracker.stopAsyncTracking( - PerformanceTransactionName.EXECUTE_ACTION, - { - failed: true, - }, - pageAction.id, - ); - } else { - const payload = createActionExecutionResponse(response); - PerformanceTracker.stopAsyncTracking( - PerformanceTransactionName.EXECUTE_ACTION, - undefined, - pageAction.id, - ); - yield put( - executeApiActionSuccess({ - id: pageAction.id, - response: payload, - isPageLoad: true, - }), - ); - yield take(ReduxActionTypes.SET_EVALUATED_TREE); } } @@ -753,6 +770,7 @@ function* executePageLoadActionsSaga(action: ReduxAction) { ); } catch (e) { log.error(e); + Toaster.show({ text: "Failed to load onPageLoad actions", variant: Variant.danger, diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 5ebbafaab2..c1a3b5e18d 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -11,7 +11,7 @@ import { takeEvery, takeLatest, } from "redux-saga/effects"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import ActionAPI, { ActionCreateUpdateResponse, Property } from "api/ActionAPI"; import _ from "lodash"; import { GenericApiResponse } from "api/ApiResponses"; @@ -44,12 +44,11 @@ import { } from "selectors/editorSelectors"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { QUERY_CONSTANT } from "constants/QueryEditorConstants"; -import { Action, RestAction } from "entities/Action"; +import { Action } from "entities/Action"; import { ActionData } from "reducers/entityReducers/actionsReducer"; import { getAction, getCurrentPageNameByActionId, - getDatasource, getPageNameByPageId, } from "selectors/entitiesSelector"; import { getDataSources } from "selectors/editorSelectors"; @@ -93,16 +92,7 @@ export function* createActionSaga( ...actionPayload.payload.eventData, }); - let newAction = response.data; - - if (newAction.datasource.id) { - const datasource = yield select(getDatasource, newAction.datasource.id); - - newAction = { - ...newAction, - datasource, - }; - } + const newAction = response.data; yield put(createActionSuccess(newAction)); } @@ -121,7 +111,7 @@ export function* fetchActionsSaga(action: ReduxAction) { { mode: "EDITOR", appId: applicationId }, ); try { - const response: GenericApiResponse = yield ActionAPI.fetchActions( + const response: GenericApiResponse = yield ActionAPI.fetchActions( applicationId, ); const isValidResponse = yield validateResponse(response); @@ -155,7 +145,7 @@ export function* fetchActionsForViewModeSaga( { mode: "VIEWER", appId: applicationId }, ); try { - const response: GenericApiResponse = yield ActionAPI.fetchActionsForViewMode( + const response: GenericApiResponse = yield ActionAPI.fetchActionsForViewMode( applicationId, ); const isValidResponse = yield validateResponse(response); @@ -189,7 +179,7 @@ export function* fetchActionsForPageSaga( { pageId: pageId }, ); try { - const response: GenericApiResponse = yield call( + const response: GenericApiResponse = yield call( ActionAPI.fetchActionsByPageId, pageId, ); @@ -218,18 +208,15 @@ export function* updateActionSaga(actionPayload: ReduxAction<{ id: string }>) { PerformanceTransactionName.UPDATE_ACTION_API, { actionid: actionPayload.payload.id }, ); - let action: Action = yield select(getAction, actionPayload.payload.id); + let action = yield select(getAction, actionPayload.payload.id); + if (!action) throw new Error("Could not find action to update"); const isApi = action.pluginType === "API"; - const isDB = action.pluginType === "DB"; if (isApi) { action = transformRestAction(action); } - if (isApi || isDB) { - action = _.omit(action, "name") as RestAction; - } - const response: GenericApiResponse = yield ActionAPI.updateAPI( + const response: GenericApiResponse = yield ActionAPI.updateAPI( action, ); const isValidResponse = yield validateResponse(response); @@ -252,25 +239,11 @@ export function* updateActionSaga(actionPayload: ReduxAction<{ id: string }>) { }); } - let updatedAction = response.data; - - if (updatedAction.datasource.id) { - const datasource = yield select( - getDatasource, - updatedAction.datasource.id, - ); - - updatedAction = { - ...updatedAction, - datasource, - }; - } - PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.UPDATE_ACTION_API, ); - yield put(updateActionSuccess({ data: updatedAction })); + yield put(updateActionSuccess({ data: response.data })); } } catch (error) { PerformanceTracker.stopAsyncTracking( @@ -295,7 +268,7 @@ export function* deleteActionSaga( const isApi = action.pluginType === PLUGIN_TYPE_API; const isQuery = action.pluginType === QUERY_CONSTANT; - const response: GenericApiResponse = yield ActionAPI.deleteAction( + const response: GenericApiResponse = yield ActionAPI.deleteAction( id, ); const isValidResponse = yield validateResponse(response); @@ -344,7 +317,7 @@ function* moveActionSaga( name: string; }>, ) { - const actionObject: RestAction = yield select(getAction, action.payload.id); + const actionObject: Action = yield select(getAction, action.payload.id); const withoutBindings = removeBindingsFromActionObject(actionObject); try { const response = yield ActionAPI.moveAction({ @@ -387,16 +360,18 @@ function* moveActionSaga( function* copyActionSaga( action: ReduxAction<{ id: string; destinationPageId: string; name: string }>, ) { - let actionObject: RestAction = yield select(getAction, action.payload.id); - if (action.payload.destinationPageId !== actionObject.pageId) { - actionObject = removeBindingsFromActionObject(actionObject); - } + let actionObject: Action = yield select(getAction, action.payload.id); try { - const copyAction = { - ...(_.omit(actionObject, "id") as RestAction), + if (!actionObject) throw new Error("Could not find action to copy"); + if (action.payload.destinationPageId !== actionObject.pageId) { + actionObject = removeBindingsFromActionObject(actionObject); + } + + const copyAction = Object.assign({}, actionObject, { name: action.payload.name, pageId: action.payload.destinationPageId, - }; + }) as Partial; + delete copyAction.id; const response = yield ActionAPI.createAPI(copyAction); const datasources = yield select(getDataSources); @@ -429,7 +404,9 @@ function* copyActionSaga( yield put(copyActionSuccess(payload)); } catch (e) { Toaster.show({ - text: `Error while copying action ${actionObject.name}`, + text: `Error while copying action ${ + actionObject ? actionObject.name : "" + }`, variant: Variant.danger, }); yield put(copyActionError(action.payload)); diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts index c33457492b..79631d5f11 100644 --- a/app/client/src/sagas/ApiPaneSagas.ts +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -14,7 +14,7 @@ import { import { getFormData } from "selectors/formSelectors"; import { API_EDITOR_FORM_NAME } from "constants/forms"; import { - DEFAULT_API_ACTION, + DEFAULT_API_ACTION_CONFIG, POST_BODY_FORMAT_OPTIONS, REST_PLUGIN_PACKAGE_NAME, POST_BODY_FORMATS, @@ -44,10 +44,10 @@ import { getPluginIdOfPackageName } from "sagas/selectors"; import { getAction, getActions, getPlugins } from "selectors/entitiesSelector"; import { ActionData } from "reducers/entityReducers/actionsReducer"; import { createActionRequest, setActionProperty } from "actions/actionActions"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { Plugin } from "api/PluginApi"; import { PLUGIN_PACKAGE_DBS } from "constants/QueryEditorConstants"; -import { RestAction } from "entities/Action"; +import { Action, ApiAction } from "entities/Action"; import { getCurrentOrgId } from "selectors/organizationSelectors"; import log from "loglevel"; import PerformanceTracker, { @@ -140,7 +140,7 @@ function* initializeExtraFormDataSaga() { const headers = get( values, "actionConfiguration.headers", - DEFAULT_API_ACTION.actionConfiguration?.headers, + DEFAULT_API_ACTION_CONFIG.headers, ); const queryParameters = get( @@ -157,7 +157,7 @@ function* initializeExtraFormDataSaga() { change( API_EDITOR_FORM_NAME, "actionConfiguration.queryParameters", - DEFAULT_API_ACTION.actionConfiguration?.queryParameters, + DEFAULT_API_ACTION_CONFIG.queryParameters, ), ); } @@ -302,7 +302,7 @@ function* formValueChangeSaga( ]); } -function* handleActionCreatedSaga(actionPayload: ReduxAction) { +function* handleActionCreatedSaga(actionPayload: ReduxAction) { const { id, pluginType } = actionPayload.payload; const action = yield select(getAction, id); const data = { ...action }; @@ -338,7 +338,7 @@ function* handleCreateNewApiActionSaga( const newActionName = createNewApiName(pageActions, pageId); yield put( createActionRequest({ - ...DEFAULT_API_ACTION, + actionConfiguration: DEFAULT_API_ACTION_CONFIG, name: newActionName, datasource: { name: "DEFAULT_REST_DATASOURCE", @@ -350,7 +350,7 @@ function* handleCreateNewApiActionSaga( from: action.payload.from, }, pageId, - }), + } as ApiAction), // We don't have recursive partial in typescript for now. ); history.push( API_EDITOR_URL_WITH_SELECTED_PAGE_ID(applicationId, pageId, pageId), diff --git a/app/client/src/sagas/ApplicationSagas.tsx b/app/client/src/sagas/ApplicationSagas.tsx index 71a5c3763b..144f3a2010 100644 --- a/app/client/src/sagas/ApplicationSagas.tsx +++ b/app/client/src/sagas/ApplicationSagas.tsx @@ -138,6 +138,11 @@ export function* getAllApplicationSaga() { type: ReduxActionTypes.FETCH_USER_APPLICATIONS_ORGS_SUCCESS, payload: organizationApplication, }); + const { newReleasesCount, releaseItems } = response.data || {}; + yield put({ + type: ReduxActionTypes.FETCH_RELEASES_SUCCESS, + payload: { newReleasesCount, releaseItems }, + }); } } catch (error) { yield put({ diff --git a/app/client/src/sagas/CurlImportSagas.ts b/app/client/src/sagas/CurlImportSagas.ts index d892ff0e31..dca470cc9b 100644 --- a/app/client/src/sagas/CurlImportSagas.ts +++ b/app/client/src/sagas/CurlImportSagas.ts @@ -8,7 +8,6 @@ import { validateResponse } from "sagas/ErrorSagas"; import CurlImportApi, { CurlImportRequest } from "api/ImportApi"; import { ApiResponse } from "api/ApiResponses"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import { ToastType } from "react-toastify"; import { CURL_IMPORT_SUCCESS } from "constants/messages"; import { getCurrentApplicationId } from "selectors/editorSelectors"; import { CURL } from "constants/ApiConstants"; diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index 3d51b10774..bf087f866d 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -35,10 +35,8 @@ import { } from "actions/datasourceActions"; import { fetchPluginForm } from "actions/pluginActions"; import { GenericApiResponse } from "api/ApiResponses"; -import DatasourcesApi, { - CreateDatasourceConfig, - Datasource, -} from "api/DatasourcesApi"; +import DatasourcesApi, { CreateDatasourceConfig } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import PluginApi, { DatasourceForm } from "api/PluginApi"; import { diff --git a/app/client/src/sagas/ErrorSagas.tsx b/app/client/src/sagas/ErrorSagas.tsx index e2ede4d34c..8d3d832328 100644 --- a/app/client/src/sagas/ErrorSagas.tsx +++ b/app/client/src/sagas/ErrorSagas.tsx @@ -1,19 +1,23 @@ -import _ from "lodash"; +import { get } from "lodash"; import { ReduxActionTypes, ReduxActionErrorTypes, ReduxAction, } from "constants/ReduxActionConstants"; -import { DEFAULT_ERROR_MESSAGE, DEFAULT_ACTION_ERROR } from "constants/errors"; +import log from "loglevel"; +import history from "utils/history"; import { ApiResponse } from "api/ApiResponses"; -import { put, takeLatest, call, select } from "redux-saga/effects"; -import { ERROR_401, ERROR_500, ERROR_0 } from "constants/messages"; import { Variant } from "components/ads/common"; import { Toaster } from "components/ads/Toast"; -import log from "loglevel"; import { flushErrors } from "actions/errorActions"; -import history from "utils/history"; +import { AUTH_LOGIN_URL } from "constants/routes"; +import { ERROR_CODES } from "constants/ApiConstants"; import { getSafeCrash } from "selectors/errorSelectors"; +import { getCurrentUser } from "selectors/usersSelectors"; +import { ANONYMOUS_USERNAME } from "constants/userConstants"; +import { put, takeLatest, call, select } from "redux-saga/effects"; +import { ERROR_401, ERROR_500, ERROR_0 } from "constants/messages"; +import { DEFAULT_ERROR_MESSAGE, DEFAULT_ACTION_ERROR } from "constants/errors"; export function* callAPI(apiCall: any, requestPayload: any) { try { @@ -80,7 +84,7 @@ Object.keys(ReduxActionErrorTypes).forEach((type: string) => { ActionErrorDisplayMap = { ...ActionErrorDisplayMap, [ReduxActionErrorTypes.API_ERROR]: (error) => - _.get(error, "message", DEFAULT_ERROR_MESSAGE), + get(error, "message", DEFAULT_ERROR_MESSAGE), [ReduxActionErrorTypes.FETCH_PAGE_ERROR]: () => DEFAULT_ACTION_ERROR("fetching the page"), [ReduxActionErrorTypes.SAVE_PAGE_ERROR]: () => @@ -101,19 +105,18 @@ export function* errorSaga( }>, ) { const effects = [ErrorEffectTypes.LOG_ERROR]; - const { - type, - payload: { show = true, error }, - } = errorAction; - const message = - error && error.message ? error.message : ActionErrorDisplayMap[type](error); + const { type, payload } = errorAction; + const { show = true, error } = payload || {}; + const message = get(error, "message", ActionErrorDisplayMap[type](error)); if (show) { effects.push(ErrorEffectTypes.SHOW_ALERT); } + if (error && error.crash) { effects.push(ErrorEffectTypes.SAFE_CRASH); } + for (const effect of effects) { switch (effect) { case ErrorEffectTypes.LOG_ERROR: { @@ -130,6 +133,7 @@ export function* errorSaga( } } } + yield put({ type: ReduxActionTypes.REPORT_ERROR, payload: { @@ -154,6 +158,33 @@ function* crashAppSaga() { }); } +/** + * this saga do some logic before actually setting safeCrash to true + */ +function* safeCrashSagaRequest(action: ReduxAction<{ code?: string }>) { + const user = yield select(getCurrentUser); + const code = get(action, "payload.code"); + + // if user is not logged and the error is "PAGE_NOT_FOUND", + // redirecting user to login page with redirecTo param + if ( + get(user, "email") === ANONYMOUS_USERNAME && + code === ERROR_CODES.PAGE_NOT_FOUND + ) { + window.location.href = `${AUTH_LOGIN_URL}?redirectUrl=${window.location.href}`; + + return false; + } + + // if there is no action to be done, just calling the safe crash action + yield put({ + type: ReduxActionTypes.SAFE_CRASH_APPSMITH, + payload: { + code, + }, + }); +} + /** * flush errors and redirect users to a url * @@ -177,4 +208,8 @@ export default function* errorSagas() { ReduxActionTypes.FLUSH_AND_REDIRECT, flushErrorsAndRedirectSaga, ); + yield takeLatest( + ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, + safeCrashSagaRequest, + ); } diff --git a/app/client/src/sagas/evaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts similarity index 76% rename from app/client/src/sagas/evaluationsSaga.ts rename to app/client/src/sagas/EvaluationsSaga.ts index 289a76bb0c..47b224258d 100644 --- a/app/client/src/sagas/evaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -1,4 +1,11 @@ -import { actionChannel, call, put, select, take } from "redux-saga/effects"; +import { + actionChannel, + call, + fork, + put, + select, + take, +} from "redux-saga/effects"; import { EvaluationReduxAction, @@ -6,10 +13,7 @@ import { ReduxActionErrorTypes, ReduxActionTypes, } from "constants/ReduxActionConstants"; -import { - getDataTree, - getUnevaluatedDataTree, -} from "selectors/dataTreeSelectors"; +import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors"; import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory"; import { GracefulWorkerService } from "../utils/WorkerUtil"; import Worker from "worker-loader!../workers/evaluation.worker"; @@ -19,7 +23,6 @@ import { EvalErrorTypes, } from "../utils/DynamicBindingUtils"; import log from "loglevel"; -import _ from "lodash"; import { WidgetType } from "../constants/WidgetConstants"; import { WidgetProps } from "../widgets/BaseWidget"; import PerformanceTracker, { @@ -28,8 +31,8 @@ import PerformanceTracker, { import { Variant } from "components/ads/common"; import { Toaster } from "components/ads/Toast"; import * as Sentry from "@sentry/react"; -import { EXECUTION_PARAM_KEY } from "../constants/ActionConstants"; import { Action } from "redux"; +import _ from "lodash"; let widgetTypeConfigMap: WidgetTypeConfigMap; @@ -67,26 +70,29 @@ function* evaluateTreeSaga(postEvalActions?: ReduxAction[]) { PerformanceTracker.startAsyncTracking( PerformanceTransactionName.DATA_TREE_EVALUATION, ); - const unEvalTree = yield select(getUnevaluatedDataTree); - log.debug({ unEvalTree }); + const unevalTree = yield select(getUnevaluatedDataTree); + log.debug({ unevalTree }); const workerResponse = yield call( worker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { - dataTree: unEvalTree, + unevalTree, widgetTypeConfigMap, }, ); - const { errors, dataTree, logs } = workerResponse; - const parsedDataTree = JSON.parse(dataTree); + const { errors, dataTree, dependencies, logs } = workerResponse; + log.debug({ dataTree: dataTree }); logs.forEach((evalLog: any) => log.debug(evalLog)); - log.debug({ dataTree: parsedDataTree }); evalErrorHandler(errors); yield put({ type: ReduxActionTypes.SET_EVALUATED_TREE, - payload: parsedDataTree, + payload: dataTree, + }); + yield put({ + type: ReduxActionTypes.SET_EVALUATION_INVERSE_DEPENDENCY_MAP, + payload: { inverseDependencyMap: dependencies }, }); PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.DATA_TREE_EVALUATION, @@ -96,27 +102,23 @@ function* evaluateTreeSaga(postEvalActions?: ReduxAction[]) { } } -export function* evaluateSingleValue( - binding: string, - executionParams: Record = {}, +export function* evaluateActionBindings( + bindings: string[], + executionParams: Record | string = {}, ) { - const dataTree = yield select(getDataTree); - const workerResponse = yield call( worker.request, - EVAL_WORKER_ACTIONS.EVAL_SINGLE, + EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS, { - dataTree: Object.assign({}, dataTree, { - [EXECUTION_PARAM_KEY]: executionParams, - }), - binding, + bindings, + executionParams, }, ); - const { errors, value } = workerResponse; + const { errors, values } = workerResponse; evalErrorHandler(errors); - return value; + return values; } export function* evaluateDynamicTrigger( @@ -170,6 +172,7 @@ export function* validateProperty( props: WidgetProps, ) { return yield call(worker.request, EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY, { + widgetTypeConfigMap, widgetType, property, value, @@ -197,16 +200,13 @@ const EVALUATE_REDUX_ACTIONS = [ ReduxActionTypes.DELETE_ACTION_SUCCESS, ReduxActionTypes.COPY_ACTION_SUCCESS, ReduxActionTypes.MOVE_ACTION_SUCCESS, - ReduxActionTypes.RUN_ACTION_REQUEST, ReduxActionTypes.RUN_ACTION_SUCCESS, ReduxActionErrorTypes.RUN_ACTION_ERROR, - ReduxActionTypes.EXECUTE_API_ACTION_REQUEST, ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS, ReduxActionErrorTypes.EXECUTE_ACTION_ERROR, // App Data ReduxActionTypes.SET_APP_MODE, ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS, - ReduxActionTypes.SET_URL_DATA, ReduxActionTypes.UPDATE_APP_STORE, // Widgets ReduxActionTypes.UPDATE_LAYOUT, @@ -219,20 +219,35 @@ const EVALUATE_REDUX_ACTIONS = [ ReduxActionTypes.BATCH_UPDATES_SUCCESS, ]; +const shouldProcessAction = (action: ReduxAction) => { + // debugger; + if ( + action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS && + Array.isArray(action.payload) + ) { + const batchedActionTypes = action.payload.map( + (batchedAction) => batchedAction.type, + ); + return ( + _.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length > 0 + ); + } + return true; +}; + function evalQueueBuffer() { - let initialised = false; - let takable = false; + let canTake = false; let postEvalActions: any = []; const take = () => { - if (takable) { + if (canTake) { const resp = postEvalActions; postEvalActions = []; - takable = false; - return { postEvalActions: resp, type: "FAKE_ACTION" }; + canTake = false; + return { postEvalActions: resp, type: "BUFFERED_ACTION" }; } }; const flush = () => { - if (takable) { + if (canTake) { return [take() as Action]; } @@ -240,34 +255,11 @@ function evalQueueBuffer() { }; const put = (action: EvaluationReduxAction) => { - if (!initialised) { - if ( - ![ - ReduxActionTypes.FETCH_PAGE_SUCCESS, - ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS, - ].includes(action.type) - ) { - return; - } - initialised = true; - } - // When batching success action happens, we need to only evaluate - // if the batch had any action we need to evaluate properties for - if ( - action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS && - Array.isArray(action.payload) - ) { - const batchedActionTypes = action.payload.map( - (batchedAction: ReduxAction) => batchedAction.type, - ); - if ( - _.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length === 0 - ) { - return; - } + if (!shouldProcessAction(action)) { + return; } + canTake = true; - takable = true; // TODO: If the action is the same as before, we can send only one and ignore duplicates. if (action.postEvalActions) { postEvalActions.push(...action.postEvalActions); @@ -278,7 +270,7 @@ function evalQueueBuffer() { take, put, isEmpty: () => { - return !takable; + return !canTake; }, flush, }; @@ -289,6 +281,8 @@ function* evaluationChangeListenerSaga() { yield call(worker.shutdown); yield call(worker.start); widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap(); + const initAction = yield take(FIRST_EVAL_REDUX_ACTIONS); + yield fork(evaluateTreeSaga, initAction.postEvalActions); const evtActionChannel = yield actionChannel( EVALUATE_REDUX_ACTIONS, evalQueueBuffer(), @@ -297,7 +291,9 @@ function* evaluationChangeListenerSaga() { const action: EvaluationReduxAction = yield take( evtActionChannel, ); - yield call(evaluateTreeSaga, action.postEvalActions); + if (shouldProcessAction(action)) { + yield call(evaluateTreeSaga, action.postEvalActions); + } } // TODO(hetu) need an action to stop listening and evaluate (exit app) } diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index c76b5c6d9b..465fd9109f 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -34,6 +34,8 @@ import { APP_MODE } from "reducers/entityReducers/appReducer"; import { getAppStore } from "constants/AppConstants"; import { getDefaultPageId } from "./selectors"; import { populatePageDSLsSaga } from "./PageSagas"; +import log from "loglevel"; +import * as Sentry from "@sentry/react"; function* initializeEditorSaga( initializeEditorAction: ReduxAction, @@ -67,7 +69,7 @@ function* initializeEditorSaga( if (resultOfPrimaryCalls.failure) { yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH, + type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, payload: { code: get( resultOfPrimaryCalls, @@ -94,7 +96,7 @@ function* initializeEditorSaga( if (resultOfSecondaryCalls.failure) { yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH, + type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, payload: { code: get( resultOfPrimaryCalls, @@ -122,8 +124,10 @@ function* initializeEditorSaga( type: ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, }); } catch (e) { + log.error(e); + Sentry.captureException(e); yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH, + type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, payload: { code: ERROR_CODES.SERVER_ERROR, }, @@ -162,7 +166,7 @@ export function* initializeAppViewerSaga( if (resultOfPrimaryCalls.failure) { yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH, + type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, payload: { code: get( resultOfPrimaryCalls, @@ -188,7 +192,7 @@ export function* initializeAppViewerSaga( if (resultOfFetchPage.failure) { yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH, + type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, payload: { code: get( resultOfFetchPage, diff --git a/app/client/src/sagas/OnboardingSagas.ts b/app/client/src/sagas/OnboardingSagas.ts index 5aa76ec1c7..b65aa962bb 100644 --- a/app/client/src/sagas/OnboardingSagas.ts +++ b/app/client/src/sagas/OnboardingSagas.ts @@ -1,5 +1,6 @@ import { GenericApiResponse } from "api/ApiResponses"; -import DatasourcesApi, { Datasource } from "api/DatasourcesApi"; +import DatasourcesApi from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { Plugin } from "api/PluginApi"; import { ReduxActionErrorTypes, @@ -119,21 +120,24 @@ function* listenForSuccessfullBinding() { const widgetProperties = dataTree[selectedWidget.widgetName]; const dynamicBindingPathList = dataTree[selectedWidget.widgetName].dynamicBindingPathList; + const tableHasData = dataTree[selectedWidget.widgetName].tableData; const hasBinding = dynamicBindingPathList && !!dynamicBindingPathList.length; - if (hasBinding) { - yield put(showTooltip(OnboardingStep.NONE)); - } - - bindSuccessfull = bindSuccessfull && hasBinding; + bindSuccessfull = + bindSuccessfull && hasBinding && tableHasData && tableHasData.length; if (widgetProperties.invalidProps) { bindSuccessfull = - bindSuccessfull && !("tableData" in widgetProperties.invalidProps); + bindSuccessfull && + !( + "tableData" in widgetProperties.invalidProps && + widgetProperties.invalidProps.tableData + ); } if (bindSuccessfull) { + yield put(showTooltip(OnboardingStep.NONE)); AnalyticsUtil.logEvent("ONBOARDING_SUCCESSFUL_BINDING"); yield put(setCurrentStep(OnboardingStep.SUCCESSFUL_BINDING)); diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index a859fca89e..cf0a194798 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -70,7 +70,7 @@ import { setActionsToExecuteOnPageLoad, } from "actions/actionActions"; import { APP_MODE, UrlDataState } from "reducers/entityReducers/appReducer"; -import { clearEvalCache } from "./evaluationsSaga"; +import { clearEvalCache } from "./EvaluationsSaga"; import { getQueryParams } from "utils/AppsmithUtils"; import PerformanceTracker, { PerformanceTransactionName, @@ -341,6 +341,7 @@ function* savePageSaga() { failed: true, }, ); + yield put({ type: ReduxActionErrorTypes.SAVE_PAGE_ERROR, payload: { diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts index 9a61c02a1f..c64bb74d9f 100644 --- a/app/client/src/sagas/QueryPaneSagas.ts +++ b/app/client/src/sagas/QueryPaneSagas.ts @@ -34,7 +34,7 @@ import { getDatasource, getPluginTemplates, } from "selectors/entitiesSelector"; -import { RestAction } from "entities/Action"; +import { QueryAction } from "entities/Action"; import { setActionProperty } from "actions/actionActions"; import { fetchPluginForm } from "actions/pluginActions"; import { getQueryParams } from "utils/AppsmithUtils"; @@ -138,7 +138,7 @@ function* formValueChangeSaga( ); } -function* handleQueryCreatedSaga(actionPayload: ReduxAction) { +function* handleQueryCreatedSaga(actionPayload: ReduxAction) { const { id, pluginType, @@ -164,7 +164,6 @@ function* handleQueryCreatedSaga(actionPayload: ReduxAction) { const showTemplate = !( !!actionConfiguration.body || isEmpty(queryTemplate) ); - history.replace( QUERIES_EDITOR_ID_URL(applicationId, pageId, id, { editName: "true", diff --git a/app/client/src/sagas/WidgetLoadingSaga.ts b/app/client/src/sagas/WidgetLoadingSaga.ts new file mode 100644 index 0000000000..af5a3b514d --- /dev/null +++ b/app/client/src/sagas/WidgetLoadingSaga.ts @@ -0,0 +1,112 @@ +import { DependencyMap } from "../utils/DynamicBindingUtils"; +import { call, fork, put, select, take } from "redux-saga/effects"; +import { getEvaluationInverseDependencyMap } from "../selectors/dataTreeSelectors"; +import { getActions } from "../selectors/entitiesSelector"; +import { ActionData } from "../reducers/entityReducers/actionsReducer"; +import { + ReduxActionErrorTypes, + ReduxActionTypes, +} from "../constants/ReduxActionConstants"; +import log from "loglevel"; +import * as Sentry from "@sentry/react"; + +const createEntityDependencyMap = (dependencyMap: DependencyMap) => { + const entityDepMap: DependencyMap = {}; + Object.entries(dependencyMap).forEach(([dependant, dependencies]) => { + const entityDependant = dependant.split(".")[0]; + const existing = entityDepMap[entityDependant] || []; + entityDepMap[entityDependant] = existing.concat( + dependencies + .map((dep) => { + const value = dep.split(".")[0]; + if (value !== entityDependant) { + return value; + } + return undefined; + }) + .filter((value) => typeof value === "string") as string[], + ); + }); + return entityDepMap; +}; + +const getEntityDependencies = ( + entityNames: string[], + inverseMap: DependencyMap, + visited: Set, +): Set => { + const dependantsEntities: Set = new Set(); + entityNames.forEach((entityName) => { + if (entityName in inverseMap) { + inverseMap[entityName].forEach((dependency) => { + const dependantEntityName = dependency.split(".")[0]; + // Example: For a dependency chain that looks like Dropdown1.selectedOptionValue -> Table1.tableData -> Text1.text -> Dropdown1.options + // Here we're operating on + // Dropdown1 -> Table1 -> Text1 -> Dropdown1 + // It looks like a circle, but isn't + // So we need to mark the visited nodes and avoid infinite recursion in case we've already visited a node once. + if (visited.has(dependantEntityName)) { + return; + } + visited.add(dependantEntityName); + dependantsEntities.add(dependantEntityName); + const childDependencies = getEntityDependencies( + Array.from(dependantsEntities), + inverseMap, + visited, + ); + childDependencies.forEach((entityName) => { + dependantsEntities.add(entityName); + }); + }); + } + }); + return dependantsEntities; +}; + +const ACTION_EXECUTION_REDUX_ACTIONS = [ + ReduxActionTypes.RUN_ACTION_REQUEST, + ReduxActionTypes.RUN_ACTION_SUCCESS, + ReduxActionTypes.EXECUTE_API_ACTION_REQUEST, + ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS, + ReduxActionErrorTypes.EXECUTE_ACTION_ERROR, +]; + +function* setWidgetsLoadingSaga() { + const inverseMap = yield select(getEvaluationInverseDependencyMap); + const entityDependencyMap = createEntityDependencyMap(inverseMap); + const actions = yield select(getActions); + const isLoadingActions: string[] = actions + .filter((action: ActionData) => action.isLoading) + .map((action: ActionData) => action.config.name); + + const loadingEntities = getEntityDependencies( + isLoadingActions, + entityDependencyMap, + new Set(), + ); + + yield put({ + type: ReduxActionTypes.SET_LOADING_ENTITIES, + payload: loadingEntities, + }); +} + +function* actionExecutionChangeListenerSaga() { + while (true) { + yield take(ACTION_EXECUTION_REDUX_ACTIONS); + yield fork(setWidgetsLoadingSaga); + } +} + +export default function* actionExecutionChangeListeners() { + yield take(ReduxActionTypes.START_EVALUATION); + while (true) { + try { + yield call(actionExecutionChangeListenerSaga); + } catch (e) { + log.error(e); + Sentry.captureException(e); + } + } +} diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 3fdba1060a..043ec44edb 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -49,7 +49,7 @@ import { isPathADynamicTrigger, } from "utils/DynamicBindingUtils"; import { WidgetProps } from "widgets/BaseWidget"; -import _, { isString } from "lodash"; +import _ from "lodash"; import WidgetFactory from "utils/WidgetFactory"; import { buildWidgetBlueprint, @@ -88,7 +88,7 @@ import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; import { validateProperty, clearEvalPropertyCacheOfWidget, -} from "./evaluationsSaga"; +} from "./EvaluationsSaga"; import { WidgetBlueprint } from "reducers/entityReducers/widgetConfigReducer"; import { Toaster } from "components/ads/Toast"; import { Variant } from "components/ads/common"; @@ -101,6 +101,7 @@ function getChildWidgetProps( const { leftColumn, topRow, newWidgetId, props, type } = params; let { rows, columns, parentColumnSpace, parentRowSpace, widgetName } = params; let minHeight = undefined; + /* eslint-disable @typescript-eslint/no-unused-vars */ const { blueprint = undefined, ...restDefaultConfig } = { ...(WidgetConfigResponse as any).config[type], }; @@ -286,6 +287,8 @@ export function* addChildrenSaga( defaultConfig.widgetName, widgetNames, ); + // update the list of widget names for the next iteration + widgetNames.push(newWidgetName); widgets[child.widgetId] = { ...child, widgetName: newWidgetName, diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index b3a1ef6c63..b4537c4bdf 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -19,9 +19,9 @@ import queryPaneSagas from "./QueryPaneSagas"; import modalSagas from "./ModalSagas"; import batchSagas from "./BatchSagas"; import themeSagas from "./ThemeSaga"; -import evaluationsSaga from "./evaluationsSaga"; +import evaluationsSaga from "./EvaluationsSaga"; import onboardingSaga from "./OnboardingSagas"; - +import actionExecutionChangeListeners from "./WidgetLoadingSaga"; import log from "loglevel"; import * as sentry from "@sentry/react"; @@ -49,6 +49,7 @@ export function* rootSaga() { themeSagas, evaluationsSaga, onboardingSaga, + actionExecutionChangeListeners, ]; yield all( sagas.map((saga) => diff --git a/app/client/src/sagas/selectors.tsx b/app/client/src/sagas/selectors.tsx index 200812ed0a..05acd6e33d 100644 --- a/app/client/src/sagas/selectors.tsx +++ b/app/client/src/sagas/selectors.tsx @@ -6,6 +6,7 @@ import _ from "lodash"; import { WidgetType } from "constants/WidgetConstants"; import { ActionData } from "reducers/entityReducers/actionsReducer"; import { Page } from "constants/ReduxActionConstants"; +import { getActions } from "../selectors/entitiesSelector"; export const getWidgets = ( state: AppState, @@ -64,9 +65,6 @@ export const getExistingWidgetNames = createSelector( return Object.values(widgets).map((widget) => widget.widgetName); }, ); -export const getActions = (state: AppState) => { - return state.entities.actions; -}; export const currentPageId = (state: AppState) => { return state.entities.pageList.currentPageId; diff --git a/app/client/src/sagas/userSagas.tsx b/app/client/src/sagas/userSagas.tsx index 35bf38f79f..6e3e585473 100644 --- a/app/client/src/sagas/userSagas.tsx +++ b/app/client/src/sagas/userSagas.tsx @@ -36,6 +36,7 @@ import { INVITE_USERS_TO_ORG_FORM } from "constants/forms"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; +import { ERROR_CODES } from "constants/ApiConstants"; import { ANONYMOUS_USERNAME } from "constants/userConstants"; import { flushErrorsAndRedirect } from "actions/errorActions"; @@ -118,6 +119,13 @@ export function* getCurrentUserSaga() { error, }, }); + + yield put({ + type: ReduxActionTypes.SAFE_CRASH_APPSMITH, + payload: { + code: ERROR_CODES.SERVER_ERROR, + }, + }); } } @@ -211,13 +219,11 @@ export function* invitedUserSignupSaga( type InviteUserPayload = { email: string; - groupIds: string[]; + orgId: string; + roleName: string; }; -export function* inviteUser( - payload: { email: string; orgId: string; roleName: string }, - reject: any, -) { +export function* inviteUser(payload: InviteUserPayload, reject: any) { const response: ApiResponse = yield callAPI(UserApi.inviteUser, payload); const isValidResponse = yield validateResponse(response); if (!isValidResponse) { diff --git a/app/client/src/selectors/dataTreeSelectors.ts b/app/client/src/selectors/dataTreeSelectors.ts index be6b42dac4..29866d5c65 100644 --- a/app/client/src/selectors/dataTreeSelectors.ts +++ b/app/client/src/selectors/dataTreeSelectors.ts @@ -33,6 +33,12 @@ export const getUnevaluatedDataTree = createSelector( }, ); +export const getEvaluationInverseDependencyMap = (state: AppState) => + state.evaluations.dependencies.inverseDependencyMap; + +export const getLoadingEntities = (state: AppState) => + state.evaluations.loadingEntities; + /** * returns evaluation tree object * diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 8178cced84..a593cee8a8 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -16,11 +16,11 @@ import { import { PageListReduxState } from "reducers/entityReducers/pageListReducer"; import { OccupiedSpace } from "constants/editorConstants"; -import { getDataTree } from "selectors/dataTreeSelectors"; +import { getDataTree, getLoadingEntities } from "selectors/dataTreeSelectors"; import _ from "lodash"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; import { DataTreeWidget, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; -import { getActions } from "sagas/selectors"; +import { getActions } from "selectors/entitiesSelector"; import PerformanceTracker, { PerformanceTransactionName, @@ -64,6 +64,10 @@ export const getIsPageSaving = (state: AppState) => { return state.ui.editor.loadingStates.saving || areApisSaving; }; +export const getPageSavingError = (state: AppState) => { + return state.ui.editor.loadingStates.savingError; +}; + export const getIsPublishingApplication = (state: AppState) => state.ui.editor.loadingStates.publishing; @@ -113,9 +117,11 @@ export const getWidgetCards = createSelector( export const getCanvasWidgetDsl = createSelector( getCanvasWidgets, getDataTree, + getLoadingEntities, ( canvasWidgets: CanvasWidgetsReduxState, evaluatedDataTree, + loadingEntities, ): ContainerWidgetProps => { PerformanceTracker.startTracking( PerformanceTransactionName.CONSTRUCT_CANVAS_DSL, @@ -123,14 +129,17 @@ export const getCanvasWidgetDsl = createSelector( const widgets: Record = {}; Object.keys(canvasWidgets).forEach((widgetKey) => { const canvasWidget = canvasWidgets[widgetKey]; - const evaluatedWidget = evaluatedDataTree[ - canvasWidget.widgetName - ] as DataTreeWidget; + const evaluatedWidget = _.find(evaluatedDataTree, { + widgetId: widgetKey, + }) as DataTreeWidget; if (evaluatedWidget) { widgets[widgetKey] = createCanvasWidget(canvasWidget, evaluatedWidget); } else { widgets[widgetKey] = createLoadingWidget(canvasWidget); } + widgets[widgetKey].isLoading = loadingEntities.has( + canvasWidget.widgetName, + ); }); const denormalizedWidgets = CanvasWidgetsNormalizer.denormalize("0", { diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 818b917141..f6265172a8 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -6,7 +6,7 @@ import { import { ActionResponse } from "api/ActionAPI"; import { QUERY_CONSTANT } from "constants/QueryEditorConstants"; import { createSelector } from "reselect"; -import { Datasource } from "api/DatasourcesApi"; +import { Datasource } from "entities/Datasource"; import { Action } from "entities/Action"; import { find } from "lodash"; import ImageAlt from "assets/images/placeholder-image.svg"; @@ -205,7 +205,7 @@ export const getActionsForCurrentPage = createSelector( export const getQueryActionsForCurrentPage = createSelector( getActionsForCurrentPage, (actions) => { - return actions.filter((action: ActionData) => { + return actions.filter((action) => { return action.config.pluginType === QUERY_CONSTANT; }); }, diff --git a/app/client/src/selectors/formSelectors.ts b/app/client/src/selectors/formSelectors.ts index 276731c68f..f443df9bbb 100644 --- a/app/client/src/selectors/formSelectors.ts +++ b/app/client/src/selectors/formSelectors.ts @@ -1,6 +1,5 @@ import { getFormValues, isValid, getFormInitialValues } from "redux-form"; import { AppState } from "reducers"; -import { RestAction } from "entities/Action"; import { ActionData } from "reducers/entityReducers/actionsReducer"; type GetFormData = ( @@ -9,8 +8,8 @@ type GetFormData = ( ) => { initialValues: any; values: any; valid: boolean }; export const getFormData: GetFormData = (state, formName) => { - const initialValues = getFormInitialValues(formName)(state) as RestAction; - const values = getFormValues(formName)(state) as RestAction; + const initialValues = getFormInitialValues(formName)(state); + const values = getFormValues(formName)(state); const valid = isValid(formName)(state); return { initialValues, values, valid }; }; diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index 581d5bc333..b3787220cc 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -10,7 +10,6 @@ import { WidgetProps } from "widgets/BaseWidget"; import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory"; import _ from "lodash"; import { getDataTree } from "selectors/dataTreeSelectors"; -import * as log from "loglevel"; import { getCanvasWidgets } from "./entitiesSelector"; const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => diff --git a/app/client/src/transformers/RestActionTransformer.ts b/app/client/src/transformers/RestActionTransformer.ts index 4d5c26736c..59e4f2d5d0 100644 --- a/app/client/src/transformers/RestActionTransformer.ts +++ b/app/client/src/transformers/RestActionTransformer.ts @@ -1,11 +1,8 @@ -import { - CONTENT_TYPE, - HTTP_METHODS, - POST_BODY_FORMAT_OPTIONS, -} from "constants/ApiEditorConstants"; +import { HTTP_METHODS } from "constants/ApiEditorConstants"; +import { ApiAction } from "entities/Action"; import _ from "lodash"; -export const transformRestAction = (data: any): any => { +export const transformRestAction = (data: ApiAction): ApiAction => { let action = { ...data }; // GET actions should not save body if (action.actionConfiguration.httpMethod === HTTP_METHODS[0]) { @@ -29,25 +26,10 @@ export const transformRestAction = (data: any): any => { } // Body should send correct format depending on the content type if (action.actionConfiguration.httpMethod !== HTTP_METHODS[0]) { - let contentType = "raw"; - if ( - action.actionConfiguration.headers && - action.actionConfiguration.headers.length - ) { - const contentTypeHeader = _.find( - action.actionConfiguration.headers, - (header) => { - return header.key.toLowerCase() === CONTENT_TYPE; - }, - ); - if (contentTypeHeader) { - contentType = contentTypeHeader.value; - } - } - let body: any = ""; - if (action.actionConfiguration.body) + if (action.actionConfiguration.body) { body = action.actionConfiguration.body || undefined; + } if (!_.isString(body)) body = JSON.stringify(body); action = { diff --git a/app/client/src/transformers/RestActionTransformers.test.ts b/app/client/src/transformers/RestActionTransformers.test.ts index 4fc2a0cdef..e7757baa59 100644 --- a/app/client/src/transformers/RestActionTransformers.test.ts +++ b/app/client/src/transformers/RestActionTransformers.test.ts @@ -1,10 +1,10 @@ import { transformRestAction } from "transformers/RestActionTransformer"; -import { PluginType, RestAction } from "entities/Action"; +import { PluginType, ApiAction } from "entities/Action"; import { POST_BODY_FORMAT_OPTIONS } from "constants/ApiEditorConstants"; // jest.mock("POST_"); -const BASE_ACTION: RestAction = { +const BASE_ACTION: ApiAction = { dynamicBindingPathList: [], cacheResponse: "", executeOnLoad: false, @@ -22,13 +22,15 @@ const BASE_ACTION: RestAction = { actionConfiguration: { httpMethod: "GET", path: "users", + headers: [], + timeoutInMillisecond: 5000, }, jsonPathKeys: [], }; describe("Api action transformer", () => { it("Removes params from path", () => { - const input: RestAction = { + const input: ApiAction = { ...BASE_ACTION, actionConfiguration: { ...BASE_ACTION.actionConfiguration, @@ -110,7 +112,16 @@ describe("Api action transformer", () => { httpMethod: "POST", headers: [{ key: "content-type", value: "application/json" }], body: "{ name: 'test' }", - bodyFormData: [{ key: "key", value: "value" }], + bodyFormData: [ + { + key: "hey", + value: "ho", + editable: true, + mandatory: false, + description: "I been tryin to do it right", + type: "", + }, + ], }, }; const output = { @@ -120,7 +131,16 @@ describe("Api action transformer", () => { httpMethod: "POST", headers: [{ key: "content-type", value: "application/json" }], body: "{ name: 'test' }", - bodyFormData: [{ key: "key", value: "value" }], + bodyFormData: [ + { + key: "hey", + value: "ho", + editable: true, + mandatory: false, + description: "I been tryin to do it right", + type: "", + }, + ], }, }; const result = transformRestAction(input); @@ -136,7 +156,16 @@ describe("Api action transformer", () => { headers: [ { key: "content-type", value: POST_BODY_FORMAT_OPTIONS[1].value }, ], - bodyFormData: [{ key: "key", value: "value" }], + bodyFormData: [ + { + key: "hey", + value: "ho", + editable: true, + mandatory: false, + description: "I been tryin to do it right", + type: "", + }, + ], body: "{ name: 'test' }", }, }; @@ -149,7 +178,16 @@ describe("Api action transformer", () => { { key: "content-type", value: POST_BODY_FORMAT_OPTIONS[1].value }, ], body: "{ name: 'test' }", - bodyFormData: [{ key: "key", value: "value" }], + bodyFormData: [ + { + key: "hey", + value: "ho", + editable: true, + mandatory: false, + description: "I been tryin to do it right", + type: "", + }, + ], }, }; const result = transformRestAction(input); @@ -165,7 +203,16 @@ describe("Api action transformer", () => { headers: [ { key: "content-type", value: POST_BODY_FORMAT_OPTIONS[1].value }, ], - bodyFormData: [{ key: "hey", value: "ho" }], + bodyFormData: [ + { + key: "hey", + value: "ho", + editable: true, + mandatory: false, + description: "I been tryin to do it right", + type: "", + }, + ], }, }; const output = { @@ -177,7 +224,16 @@ describe("Api action transformer", () => { { key: "content-type", value: POST_BODY_FORMAT_OPTIONS[1].value }, ], body: "", - bodyFormData: [{ key: "hey", value: "ho" }], + bodyFormData: [ + { + key: "hey", + value: "ho", + editable: true, + mandatory: false, + description: "I been tryin to do it right", + type: "", + }, + ], }, }; const result = transformRestAction(input); diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index 1a927e6653..b994bf01a4 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -92,6 +92,7 @@ export type EventName = | "ONBOARDING_EXAMPLE_DATABASE" | "ONBOARDING_ADD_QUERY" | "ONBOARDING_RUN_QUERY" + | "ONBOARDING_ADD_WIDGET_CLICK" | "ONBOARDING_ADD_WIDGET" | "ONBOARDING_SUCCESSFUL_BINDING" | "ONBOARDING_DEPLOY" diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx index 3ae9073a5e..7359340007 100644 --- a/app/client/src/utils/AppsmithUtils.tsx +++ b/app/client/src/utils/AppsmithUtils.tsx @@ -48,7 +48,7 @@ export const appInitializer = () => { if (appsmithConfigs.sentry.enabled) { Sentry.init({ ...appsmithConfigs.sentry, - beforeBreadcrumb(breadcrumb, hint) { + beforeBreadcrumb(breadcrumb) { if (breadcrumb.category === "console" && breadcrumb.level !== "error") { return null; } diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 3e26969a1a..a7e87c2096 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -8,6 +8,8 @@ import moment from "moment-timezone"; import { WidgetProps } from "../widgets/BaseWidget"; import parser from "fast-xml-parser"; +export type DependencyMap = Record>; + export const removeBindingsFromActionObject = (obj: Action) => { const string = JSON.stringify(obj); const withBindings = string.replace(DATA_BIND_REGEX_GLOBAL, "{{ }}"); @@ -87,6 +89,7 @@ export enum EvalErrorTypes { EVAL_TREE_ERROR = "EVAL_TREE_ERROR", UNESCAPE_STRING_ERROR = "UNESCAPE_STRING_ERROR", EVAL_ERROR = "EVAL_ERROR", + UNKNOWN_ERROR = "UNKNOWN_ERROR", BAD_UNEVAL_TREE_ERROR = "BAD_UNEVAL_TREE_ERROR", } @@ -98,7 +101,7 @@ export type EvalError = { export enum EVAL_WORKER_ACTIONS { EVAL_TREE = "EVAL_TREE", - EVAL_SINGLE = "EVAL_SINGLE", + EVAL_ACTION_BINDINGS = "EVAL_ACTION_BINDINGS", EVAL_TRIGGER = "EVAL_TRIGGER", CLEAR_PROPERTY_CACHE = "CLEAR_PROPERTY_CACHE", CLEAR_PROPERTY_CACHE_OF_WIDGET = "CLEAR_PROPERTY_CACHE_OF_WIDGET", diff --git a/app/client/src/utils/DynamicBindingsUtil.test.ts b/app/client/src/utils/DynamicBindingsUtil.test.ts index 05fcfefe32..ef0bea5619 100644 --- a/app/client/src/utils/DynamicBindingsUtil.test.ts +++ b/app/client/src/utils/DynamicBindingsUtil.test.ts @@ -1,213 +1,32 @@ -// import { -// mockExecute, -// mockRegisterLibrary, -// } from "../../test/__mocks__/RealmExecutorMock"; -// import { -// dependencySortedEvaluateDataTree, -// getDynamicValue, -// getEntityDependencies, -// parseDynamicString, -// } from "./DynamicBindingUtils"; -// import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; -// import { RenderModes, WidgetTypes } from "constants/WidgetConstants"; -// -// beforeAll(() => { -// mockRegisterLibrary.mockClear(); -// mockExecute.mockClear(); -// }); -// -// it("Gets the value from the data tree", () => { -// const dynamicBinding = "{{GetUsers.data}}"; -// const nameBindingsWithData: DataTree = { -// GetUsers: { -// data: { text: "correct data" }, -// config: { -// pluginId: "", -// id: "id", -// name: "text", -// actionConfiguration: {}, -// pageId: "", -// jsonPathKeys: [], -// datasource: { id: "" }, -// pluginType: "1", -// }, -// isLoading: false, -// ENTITY_TYPE: ENTITY_TYPE.ACTION, -// run: jest.fn(), -// }, -// }; -// const actualValue = { result: { text: "correct data" } }; -// -// const value = getDynamicValue(dynamicBinding, nameBindingsWithData); -// -// expect(value).toEqual(actualValue); -// }); -// -// describe.each([ -// ["{{A}}", ["{{A}}"]], -// ["A {{B}}", ["A ", "{{B}}"]], -// [ -// "Hello {{Customer.Name}}, the status for your order id {{orderId}} is {{status}}", -// [ -// "Hello ", -// "{{Customer.Name}}", -// ", the status for your order id ", -// "{{orderId}}", -// " is ", -// "{{status}}", -// ], -// ], -// [ -// "{{data.map(datum => {return {id: datum}})}}", -// ["{{data.map(datum => {return {id: datum}})}}"], -// ], -// ["{{}}{{}}}", ["{{}}", "{{}}", "}"]], -// ["{{{}}", ["{{{}}"]], -// ["{{ {{", ["{{ {{"]], -// ["}} }}", ["}} }}"]], -// ["}} {{", ["}} {{"]], -// ])("Parse the dynamic string(%s, %j)", (dynamicString, expected) => { -// test(`returns ${expected}`, () => { -// expect(parseDynamicString(dynamicString as string)).toStrictEqual(expected); -// }); -// }); -// -// const baseWidgetProps = { -// parentColumnSpace: 0, -// parentRowSpace: 0, -// parentId: "0", -// type: WidgetTypes.BUTTON_WIDGET, -// renderMode: RenderModes.CANVAS, -// leftColumn: 0, -// rightColumn: 0, -// topRow: 0, -// bottomRow: 0, -// isLoading: false, -// }; -// -// it("evaluates the data tree", () => { -// const input: DataTree = { -// widget1: { -// ...baseWidgetProps, -// widgetId: "1", -// widgetName: "widget1", -// displayValue: "{{widget2.computedProperty}}", -// ENTITY_TYPE: ENTITY_TYPE.WIDGET, -// }, -// widget2: { -// ...baseWidgetProps, -// widgetId: "2", -// widgetName: "widget2", -// computedProperty: "{{ widget2.data[widget2.index] }}", -// data: "{{ apiData.data }}", -// index: 2, -// ENTITY_TYPE: ENTITY_TYPE.WIDGET, -// }, -// apiData: { -// config: { -// id: "123", -// pageId: "1234", -// datasource: {}, -// name: "api", -// actionConfiguration: {}, -// jsonPathKeys: [], -// pluginId: "plugin", -// }, -// run: (onSuccess, onError) => ({ -// type: "RUN_ACTION", -// payload: { -// actionId: "", -// onSuccess: "", -// onError: "", -// }, -// }), -// isLoading: false, -// data: ["wrong value", "still wrong", "correct"], -// ENTITY_TYPE: ENTITY_TYPE.ACTION, -// }, -// }; -// -// const dynamicBindings = { -// "widget1.displayValue": ["widget2.computedProperty"], -// "widget2.computedProperty": ["widget2.data", "widget2.index"], -// "widget2.data": ["apiData.data"], -// }; -// -// const sortedDeps = [ -// "apiData.data", -// "widget2.data", -// "widget2.index", -// "widget2.computedProperty", -// "widget1.displayValue", -// ]; -// -// const output: DataTree = { -// widget1: { -// ...baseWidgetProps, -// widgetId: "1", -// widgetName: "widget1", -// displayValue: "correct", -// ENTITY_TYPE: ENTITY_TYPE.WIDGET, -// }, -// widget2: { -// ...baseWidgetProps, -// widgetId: "2", -// widgetName: "widget2", -// computedProperty: "correct", -// data: ["wrong value", "still wrong", "correct"], -// index: 2, -// ENTITY_TYPE: ENTITY_TYPE.WIDGET, -// }, -// apiData: { -// config: { -// id: "123", -// pageId: "1234", -// datasource: {}, -// name: "api", -// actionConfiguration: {}, -// jsonPathKeys: [], -// pluginId: "plugin", -// }, -// run: (onSuccess, onError) => ({ -// type: "RUN_ACTION", -// payload: { -// actionId: "", -// onSuccess: "", -// onError: "", -// }, -// }), -// isLoading: false, -// data: ["wrong value", "still wrong", "correct"], -// ENTITY_TYPE: ENTITY_TYPE.ACTION, -// }, -// }; -// -// const result = dependencySortedEvaluateDataTree( -// input, -// dynamicBindings, -// sortedDeps, -// ); -// expect(result).toEqual(output); -// }); -// -// it("finds dependencies of a entity", () => { -// const depMap: Array<[string, string]> = [ -// ["Widget5.text", "Widget2.data.visible"], -// ["Widget1.options", "Action1.data"], -// ["Widget2.text", "Widget1.selectedOption"], -// ["Widget3.text", "Widget4.selectedRow.name"], -// ["Widget6.label", "Action1.data.label"], -// ]; -// const entity = "Action1"; -// const result = ["Widget1", "Widget2", "Widget5", "Widget6"]; -// -// const actual = getEntityDependencies(depMap, entity); -// -// expect(actual).toEqual(result); -// }); +import { getDynamicStringSegments } from "./DynamicBindingUtils"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: No types available -it("does nothing. needs implementing", () => { - expect(1 + 1).toEqual(2); +describe.each([ + ["{{A}}", ["{{A}}"]], + ["A {{B}}", ["A ", "{{B}}"]], + [ + "Hello {{Customer.Name}}, the status for your order id {{orderId}} is {{status}}", + [ + "Hello ", + "{{Customer.Name}}", + ", the status for your order id ", + "{{orderId}}", + " is ", + "{{status}}", + ], + ], + [ + "{{data.map(datum => {return {id: datum}})}}", + ["{{data.map(datum => {return {id: datum}})}}"], + ], + ["{{}}{{}}}", ["{{}}", "{{}}", "}"]], + ["{{{}}", ["{{{}}"]], + ["{{ {{", ["{{ {{"]], + ["}} }}", ["}} }}"]], + ["}} {{", ["}} {{"]], +])("Parse the dynamic string(%s, %j)", (dynamicString, expected) => { + test(`returns ${expected}`, () => { + expect(getDynamicStringSegments(dynamicString as string)).toStrictEqual( + expected, + ); + }); }); diff --git a/app/client/src/utils/PerformanceTracker.ts b/app/client/src/utils/PerformanceTracker.ts index b305332ebb..269f45259a 100644 --- a/app/client/src/utils/PerformanceTracker.ts +++ b/app/client/src/utils/PerformanceTracker.ts @@ -7,6 +7,8 @@ import * as log from "loglevel"; export enum PerformanceTransactionName { DEPLOY_APPLICATION = "DEPLOY_APPLICATION", DATA_TREE_EVALUATION = "DATA_TREE_EVALUATION", + DATA_TREE_WORKER_EVALUATION = "DATA_TREE_WORKER_EVALUATION", + EVAL_REDUX_UPDATE = "EVAL_REDUX_UPDATE", CONSTRUCT_UNEVAL_TREE = "CONSTRUCT_UNEVAL_TREE", CONSTRUCT_CANVAS_DSL = "CONSTRUCT_CANVAS_DSL", CREATE_DEPENDENCIES = "CREATE_DEPENDENCIES", @@ -38,6 +40,7 @@ export enum PerformanceTransactionName { USER_ME_API = "USER_ME_API", SIGN_UP = "SIGN_UP", LOGIN_CLICK = "LOGIN_CLICK", + SET_EVALUATED = "SET_EVALUATED", } export enum PerformanceTagNames { @@ -135,7 +138,7 @@ class PerformanceTracker { if (eventName) { const index = _.findLastIndex( PerformanceTracker.perfLogQueue, - (perfLog, i) => { + (perfLog) => { return perfLog.eventName === eventName; }, ); diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 08259c7591..7bd58033f4 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -258,6 +258,26 @@ const dynamicPathListMigration = ( return currentDSL; }; +const canvasNameConflictMigration = ( + currentDSL: ContainerWidgetProps, + props = { counter: 1 }, +): ContainerWidgetProps => { + if ( + currentDSL.type === WidgetTypes.CANVAS_WIDGET && + currentDSL.widgetName.startsWith("Canvas") + ) { + currentDSL.widgetName = `Canvas${props.counter}`; + // Canvases inside tabs have `name` property as well + if (currentDSL.name) { + currentDSL.name = currentDSL.widgetName; + } + props.counter++; + } + currentDSL.children?.forEach((c) => canvasNameConflictMigration(c, props)); + + return currentDSL; +}; + // A rudimentary transform function which updates the DSL based on its version. // A more modular approach needs to be designed. const transformDSL = (currentDSL: ContainerWidgetProps) => { @@ -310,6 +330,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps) => { currentDSL.version = 7; } + if (currentDSL.version === 7) { + currentDSL = canvasNameConflictMigration(currentDSL); + currentDSL.version = 8; + } + return currentDSL; }; diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts index 53f8eae787..a0cecf10e9 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -67,13 +67,13 @@ export const entityDefinitions = { isVisible: isVisible, searchText: "string", }), - VIDEO_WIDGET: (widget: any) => ({ + VIDEO_WIDGET: { "!doc": "Video widget can be used for playing a variety of URLs, including file paths, YouTube, Facebook, Twitch, SoundCloud, Streamable, Vimeo, Wistia, Mixcloud, and DailyMotion.", "!url": "https://docs.appsmith.com/widget-reference/video", playState: "number", autoPlay: "bool", - }), + }, DROP_DOWN_WIDGET: { "!doc": "Dropdown is used to capture user input/s from a specified list of permitted inputs. A Dropdown can capture a single choice as well as multiple choices", @@ -253,4 +253,8 @@ export const GLOBAL_FUNCTIONS = { "!doc": "Download anything as a file", "!type": "fn(data: any, fileName: string, fileType?: string) -> void", }, + copyToClipboard: { + "!doc": "Copy text to clipboard", + "!type": "fn(data: string, options: object) -> void", + }, }; diff --git a/app/client/src/utils/autocomplete/TernServer.test.ts b/app/client/src/utils/autocomplete/TernServer.test.ts index 3d5b04bf28..b6c0ca68c2 100644 --- a/app/client/src/utils/autocomplete/TernServer.test.ts +++ b/app/client/src/utils/autocomplete/TernServer.test.ts @@ -88,7 +88,7 @@ describe("Tern server", () => { }, ]; - testCases.forEach((testCase, index) => { + testCases.forEach((testCase) => { const request = ternServer.buildRequest(testCase.input, {}); expect(request.query.end).toEqual(testCase.expectedOutput); @@ -140,7 +140,7 @@ describe("Tern server", () => { }, ]; - testCases.forEach((testCase, index) => { + testCases.forEach((testCase) => { MockCodemirrorEditor.getValue.mockReturnValueOnce( testCase.input.codeEditor.value, ); diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts index 992174f963..d443dc8b34 100644 --- a/app/client/src/utils/storage.ts +++ b/app/client/src/utils/storage.ts @@ -17,8 +17,8 @@ export const resetAuthExpiration = () => { const expireBy = moment() .add(1, "h") .format(); - store.setItem(STORAGE_KEYS.AUTH_EXPIRATION, expireBy).catch((error) => { - console.log("Unable to set expiration time"); + store.setItem(STORAGE_KEYS.AUTH_EXPIRATION, expireBy).catch(() => { + console.error("Unable to set expiration time"); }); }; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index a6efc38aae..c0d95ea54c 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -72,7 +72,7 @@ abstract class BaseWidget< static getDefaultPropertiesMap(): Record { return {}; } - + // TODO Find a way to enforce this, (dont let it be set) static getMetaPropertiesMap(): Record { return {}; } diff --git a/app/client/src/widgets/DatePickerWidget.tsx b/app/client/src/widgets/DatePickerWidget.tsx index 3016cb4815..8abaaf1306 100644 --- a/app/client/src/widgets/DatePickerWidget.tsx +++ b/app/client/src/widgets/DatePickerWidget.tsx @@ -1,5 +1,4 @@ import React from "react"; -import moment from "moment-timezone"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; import { EventType } from "constants/ActionConstants"; @@ -14,7 +13,6 @@ import { TriggerPropertiesMap, } from "utils/WidgetFactory"; import * as Sentry from "@sentry/react"; -import { DateRange } from "@blueprintjs/datetime"; import withMeta, { WithMeta } from "./MetaHOC"; class DatePickerWidget extends BaseWidget { diff --git a/app/client/src/widgets/ImageWidget.test.tsx b/app/client/src/widgets/ImageWidget.test.tsx index d58fef38f8..e4dd57c812 100644 --- a/app/client/src/widgets/ImageWidget.test.tsx +++ b/app/client/src/widgets/ImageWidget.test.tsx @@ -123,6 +123,8 @@ // ); // }); // }); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore it("does nothing. needs implementing", () => { expect(1 + 1).toEqual(2); }); diff --git a/app/client/src/widgets/InputWidget.tsx b/app/client/src/widgets/InputWidget.tsx index 3c94422144..f186283381 100644 --- a/app/client/src/widgets/InputWidget.tsx +++ b/app/client/src/widgets/InputWidget.tsx @@ -55,7 +55,7 @@ class InputWidget extends BaseWidget { if (this.regex) { /* * break up the regexp pattern into 4 parts: given regex, regex prefix , regex pattern, regex flags - * Example /appsmith/i will be split into ["/appsmith/gi", "/", "appsmith", "gi"] + * Example /test/i will be split into ["/test/gi", "/", "test", "gi"] */ const regexParts = this.regex.match(/(\\/?)(.+)\\1([a-z]*)/i); diff --git a/app/client/src/widgets/MapWidget.tsx b/app/client/src/widgets/MapWidget.tsx index 2dbcceb995..ba35a190a5 100644 --- a/app/client/src/widgets/MapWidget.tsx +++ b/app/client/src/widgets/MapWidget.tsx @@ -10,6 +10,7 @@ import { getAppsmithConfigs } from "configs"; import styled from "styled-components"; import * as Sentry from "@sentry/react"; import withMeta, { WithMeta } from "./MetaHOC"; +import { DEFAULT_CENTER } from "constants/WidgetConstants"; const { google } = getAppsmithConfigs(); @@ -29,7 +30,13 @@ const DisabledContainer = styled.div` } `; -const DefaultCenter = { lat: -34.397, long: 150.644 }; +const DefaultCenter = { ...DEFAULT_CENTER, long: DEFAULT_CENTER.lng }; + +type Center = { + lat: number; + long: number; + [x: string]: any; +}; class MapWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { return { @@ -41,7 +48,7 @@ class MapWidget extends BaseWidget { enableCreateMarker: VALIDATION_TYPES.BOOLEAN, allowZoom: VALIDATION_TYPES.BOOLEAN, zoomLevel: VALIDATION_TYPES.NUMBER, - mapCenter: VALIDATION_TYPES.OBJECT, + mapCenter: VALIDATION_TYPES.LAT_LONG, }; } @@ -75,8 +82,7 @@ class MapWidget extends BaseWidget { const markers: Array = [...(this.props.markers || [])].map( (marker, i) => { if (index === i) { - marker.lat = lat; - marker.long = long; + marker = { lat, long }; } return marker; }, @@ -122,6 +128,10 @@ class MapWidget extends BaseWidget { }); }; + getCenter(): Center { + return this.props.center || this.props.mapCenter || DefaultCenter; + } + getPageView() { return ( <> @@ -149,7 +159,7 @@ class MapWidget extends BaseWidget { isVisible={this.props.isVisible} zoomLevel={this.props.zoomLevel} allowZoom={this.props.allowZoom} - center={this.props.center || this.props.mapCenter || DefaultCenter} + center={this.getCenter()} enableCreateMarker={this.props.enableCreateMarker} selectedMarker={this.props.selectedMarker} updateCenter={this.updateCenter} @@ -160,7 +170,7 @@ class MapWidget extends BaseWidget { updateMarker={this.updateMarker} selectMarker={this.onMarkerClick} unselectMarker={this.unselectMarker} - markers={this.props.markers || []} + markers={this.props.markers} enableDrag={() => { this.disableDrag(false); }} diff --git a/app/client/src/widgets/MetaHOC.tsx b/app/client/src/widgets/MetaHOC.tsx index ececa772e8..a248aa4ecb 100644 --- a/app/client/src/widgets/MetaHOC.tsx +++ b/app/client/src/widgets/MetaHOC.tsx @@ -2,7 +2,7 @@ import React from "react"; import BaseWidget, { WidgetProps } from "./BaseWidget"; import _ from "lodash"; import { EditorContext } from "../components/editorComponents/EditorContextProvider"; -import { clearEvalPropertyCache } from "sagas/evaluationsSaga"; +import { clearEvalPropertyCache } from "sagas/EvaluationsSaga"; import { ExecuteActionPayload } from "../constants/ActionConstants"; type DebouncedExecuteActionPayload = Omit< diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index 104f7c5a71..dd86f8f347 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -353,7 +353,11 @@ class TableWidget extends BaseWidget { filteredTableData: Array>, selectedRowIndex?: number, ) => { - if (selectedRowIndex === undefined || selectedRowIndex === -1) { + if ( + selectedRowIndex === undefined || + selectedRowIndex === null || + selectedRowIndex === -1 + ) { const columnKeys: string[] = getAllTableColumnKeys(this.props.tableData); const selectedRow: { [key: string]: any } = {}; for (let i = 0; i < columnKeys.length; i++) { diff --git a/app/client/src/widgets/TabsWidget.tsx b/app/client/src/widgets/TabsWidget.tsx index 3fee66d9bf..def8801780 100644 --- a/app/client/src/widgets/TabsWidget.tsx +++ b/app/client/src/widgets/TabsWidget.tsx @@ -38,6 +38,12 @@ class TabsWidget extends BaseWidget< }; } + static getMetaPropertiesMap() { + return { + selectedTabWidgetId: undefined, + }; + } + static getDefaultPropertiesMap(): Record { return {}; } diff --git a/app/client/src/workers/evaluation.test.ts b/app/client/src/workers/evaluation.test.ts new file mode 100644 index 0000000000..06718f2ac7 --- /dev/null +++ b/app/client/src/workers/evaluation.test.ts @@ -0,0 +1,767 @@ +import { DataTreeEvaluator } from "./evaluation.worker"; +import { + DataTreeAction, + DataTreeWidget, + ENTITY_TYPE, +} from "../entities/DataTree/dataTreeFactory"; +import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; +import { RenderModes, WidgetTypes } from "../constants/WidgetConstants"; +import { PluginType } from "../entities/Action"; + +const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = { + CONTAINER_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + TEXT_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + text: "TEXT", + textStyle: "TEXT", + shouldScroll: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: { + value: "{{ this.text }}", + }, + triggerProperties: {}, + metaProperties: {}, + }, + BUTTON_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + text: "TEXT", + buttonStyle: "TEXT", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onClick: true, + }, + metaProperties: {}, + }, + INPUT_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + inputType: "TEXT", + defaultText: "TEXT", + text: "TEXT", + regex: "REGEX", + errorMessage: "TEXT", + placeholderText: "TEXT", + maxChars: "NUMBER", + minNum: "NUMBER", + maxNum: "NUMBER", + label: "TEXT", + inputValidators: "ARRAY", + focusIndex: "NUMBER", + isAutoFocusEnabled: "BOOLEAN", + isRequired: "BOOLEAN", + isValid: "BOOLEAN", + }, + defaultProperties: { + text: "defaultText", + }, + derivedProperties: { + isValid: + '{{\n function(){\n let parsedRegex = null;\n if (this.regex) {\n /*\n * break up the regexp pattern into 4 parts: given regex, regex prefix , regex pattern, regex flags\n * Example /test/i will be split into ["/test/gi", "/", "test", "gi"]\n */\n const regexParts = this.regex.match(/(\\/?)(.+)\\1([a-z]*)/i);\n if (!regexParts) {\n parsedRegex = new RegExp(this.regex);\n } else {\n /*\n * if we don\'t have a regex flags (gmisuy), convert provided string into regexp directly\n /*\n if (regexParts[3] && !/^(?!.*?(.).*?\\1)[gmisuy]+$/.test(regexParts[3])) {\n parsedRegex = RegExp(this.regex);\n }\n /*\n * if we have a regex flags, use it to form regexp\n */\n parsedRegex = new RegExp(regexParts[2], regexParts[3]);\n }\n }\n if (this.inputType === "EMAIL") {\n const emailRegex = new RegExp(/^\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$/);\n return emailRegex.test(this.text);\n }\n else if (this.inputType === "NUMBER") {\n return !isNaN(this.text)\n }\n else if (this.isRequired) {\n if(this.text && this.text.length) {\n if (parsedRegex) {\n return parsedRegex.test(this.text)\n } else {\n return true;\n }\n } else {\n return false;\n }\n } if (parsedRegex) {\n return parsedRegex.test(this.text)\n } else {\n return true;\n }\n }()\n }}', + value: "{{this.text}}", + }, + triggerProperties: { + onTextChanged: true, + }, + metaProperties: { + isFocused: false, + isDirty: false, + }, + }, + CHECKBOX_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + label: "TEXT", + defaultCheckedState: "BOOLEAN", + }, + defaultProperties: { + isChecked: "defaultCheckedState", + }, + derivedProperties: { + value: "{{this.isChecked}}", + }, + triggerProperties: { + onCheckChange: true, + }, + metaProperties: {}, + }, + DROP_DOWN_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + placeholderText: "TEXT", + label: "TEXT", + options: "OPTIONS_DATA", + selectionType: "TEXT", + isRequired: "BOOLEAN", + selectedOptionValues: "ARRAY", + defaultOptionValue: "DEFAULT_OPTION_VALUE", + }, + defaultProperties: { + selectedOptionValue: "defaultOptionValue", + selectedOptionValueArr: "defaultOptionValue", + }, + derivedProperties: { + isValid: + "{{this.isRequired ? this.selectionType === 'SINGLE_SELECT' ? !!this.selectedOption : !!this.selectedIndexArr && this.selectedIndexArr.length > 0 : true}}", + selectedOption: + "{{ this.selectionType === 'SINGLE_SELECT' ? _.find(this.options, { value: this.selectedOptionValue }) : undefined}}", + selectedOptionArr: + '{{this.selectionType === "MULTI_SELECT" ? this.options.filter(opt => _.includes(this.selectedOptionValueArr, opt.value)) : undefined}}', + selectedIndex: + "{{ _.findIndex(this.options, { value: this.selectedOption.value } ) }}", + selectedIndexArr: + "{{ this.selectedOptionValueArr.map(o => _.findIndex(this.options, { value: o })) }}", + value: + "{{ this.selectionType === 'SINGLE_SELECT' ? this.selectedOptionValue : this.selectedOptionValueArr }}", + selectedOptionValues: "{{ this.selectedOptionValueArr }}", + }, + triggerProperties: { + onOptionChange: true, + }, + metaProperties: {}, + }, + RADIO_GROUP_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + label: "TEXT", + options: "OPTIONS_DATA", + selectedOptionValue: "TEXT", + defaultOptionValue: "TEXT", + isRequired: "BOOLEAN", + }, + defaultProperties: { + selectedOptionValue: "defaultOptionValue", + }, + derivedProperties: { + selectedOption: + "{{_.find(this.options, { value: this.selectedOptionValue })}}", + isValid: "{{ this.isRequired ? !!this.selectedOptionValue : true }}", + value: "{{this.selectedOptionValue}}", + }, + triggerProperties: { + onSelectionChange: true, + }, + metaProperties: {}, + }, + IMAGE_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + image: "TEXT", + imageShape: "TEXT", + defaultImage: "TEXT", + maxZoomLevel: "NUMBER", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onClick: true, + }, + metaProperties: {}, + }, + TABLE_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + tableData: "TABLE_DATA", + nextPageKey: "TEXT", + prevPageKey: "TEXT", + label: "TEXT", + searchText: "TEXT", + defaultSearchText: "TEXT", + defaultSelectedRow: "DEFAULT_SELECTED_ROW", + }, + defaultProperties: { + searchText: "defaultSearchText", + selectedRowIndex: "defaultSelectedRow", + selectedRowIndices: "defaultSelectedRow", + }, + derivedProperties: { + selectedRow: `{{ _.get(this.filteredTableData, this.selectedRowIndex, _.mapValues(this.filteredTableData[0], () => undefined)) }}`, + selectedRows: `{{ this.filteredTableData.filter((item, i) => selectedRowIndices.includes(i) }); }}`, + }, + triggerProperties: { + onRowSelected: true, + onPageChange: true, + onSearchTextChanged: true, + columnActions: true, + }, + metaProperties: { + pageNo: 1, + selectedRow: {}, + selectedRows: [], + }, + }, + VIDEO_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + url: "TEXT", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onEnd: true, + onPlay: true, + onPause: true, + }, + metaProperties: { + playState: "NOT_STARTED", + }, + }, + FILE_PICKER_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + label: "TEXT", + maxNumFiles: "NUMBER", + allowedFileTypes: "ARRAY", + files: "ARRAY", + isRequired: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: { + isValid: "{{ this.isRequired ? this.files.length > 0 : true }}", + value: "{{this.files}}", + }, + triggerProperties: { + onFilesSelected: true, + }, + metaProperties: { + files: [], + uploadedFileData: {}, + }, + }, + DATE_PICKER_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + defaultDate: "DATE", + timezone: "TEXT", + enableTimePicker: "BOOLEAN", + dateFormat: "TEXT", + label: "TEXT", + datePickerType: "TEXT", + maxDate: "DATE", + minDate: "DATE", + isRequired: "BOOLEAN", + }, + defaultProperties: { + selectedDate: "defaultDate", + }, + derivedProperties: { + isValid: "{{ this.isRequired ? !!this.selectedDate : true }}", + value: "{{ this.selectedDate }}", + }, + triggerProperties: { + onDateSelected: true, + }, + metaProperties: {}, + }, + TABS_WIDGET: { + validations: { + tabs: "TABS_DATA", + defaultTab: "SELECTED_TAB", + }, + defaultProperties: {}, + derivedProperties: { + selectedTab: + "{{_.find(this.tabs, { widgetId: this.selectedTabWidgetId }).label}}", + }, + triggerProperties: { + onTabSelected: true, + }, + metaProperties: {}, + }, + MODAL_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + RICH_TEXT_EDITOR_WIDGET: { + validations: { + text: "TEXT", + placeholder: "TEXT", + defaultValue: "TEXT", + isDisabled: "BOOLEAN", + isVisible: "BOOLEAN", + }, + defaultProperties: { + text: "defaultText", + }, + derivedProperties: { + value: "{{this.text}}", + }, + triggerProperties: { + onTextChange: true, + }, + metaProperties: {}, + }, + CHART_WIDGET: { + validations: { + xAxisName: "TEXT", + yAxisName: "TEXT", + chartName: "TEXT", + isVisible: "BOOLEAN", + chartData: "CHART_DATA", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + FORM_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + FORM_BUTTON_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + text: "TEXT", + disabledWhenInvalid: "BOOLEAN", + buttonStyle: "TEXT", + buttonType: "TEXT", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onClick: true, + }, + metaProperties: {}, + }, + MAP_WIDGET: { + validations: { + defaultMarkers: "MARKERS", + isDisabled: "BOOLEAN", + isVisible: "BOOLEAN", + enableSearch: "BOOLEAN", + enablePickLocation: "BOOLEAN", + allowZoom: "BOOLEAN", + zoomLevel: "NUMBER", + mapCenter: "OBJECT", + }, + defaultProperties: { + markers: "defaultMarkers", + center: "mapCenter", + }, + derivedProperties: {}, + triggerProperties: { + onMarkerClick: true, + onCreateMarker: true, + }, + metaProperties: {}, + }, + CANVAS_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + ICON_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onClick: true, + }, + metaProperties: {}, + }, + SKELETON_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, +}; + +const BASE_WIDGET: DataTreeWidget = { + widgetId: "randomID", + widgetName: "randomName", + bottomRow: 0, + isLoading: false, + leftColumn: 0, + parentColumnSpace: 0, + parentRowSpace: 0, + renderMode: RenderModes.CANVAS, + rightColumn: 0, + topRow: 0, + type: WidgetTypes.SKELETON_WIDGET, + parentId: "0", + ENTITY_TYPE: ENTITY_TYPE.WIDGET, +}; + +const BASE_ACTION: DataTreeAction = { + actionId: "randomId", + name: "randomName", + config: { + timeoutInMillisecond: 10, + }, + dynamicBindingPathList: [], + isLoading: false, + pluginType: PluginType.API, + run: {}, + data: {}, + ENTITY_TYPE: ENTITY_TYPE.ACTION, +}; + +describe("DataTreeEvaluator", () => { + const unEvalTree = { + Text1: { + ...BASE_WIDGET, + widgetName: "Text1", + text: "Label", + type: WidgetTypes.TEXT_WIDGET, + }, + Text2: { + ...BASE_WIDGET, + widgetName: "Text2", + text: "{{Text1.text}}", + dynamicBindingPathList: [{ key: "text" }], + type: WidgetTypes.TEXT_WIDGET, + }, + Text3: { + ...BASE_WIDGET, + widgetName: "Text3", + text: "{{Text1.text}}", + dynamicBindingPathList: [{ key: "text" }], + type: WidgetTypes.TEXT_WIDGET, + }, + Dropdown1: { + ...BASE_WIDGET, + options: [ + { + label: "test", + value: "valueTest", + }, + { + label: "test2", + value: "valueTest2", + }, + ], + type: WidgetTypes.DROP_DOWN_WIDGET, + }, + Table1: { + ...BASE_WIDGET, + tableData: "{{Api1.data.map(datum => ({ ...datum, raw: Text1.text }) )}}", + dynamicBindingPathList: [{ key: "tableData" }], + type: WidgetTypes.TABLE_WIDGET, + }, + Text4: { + ...BASE_WIDGET, + text: "{{Table1.selectedRow.test}}", + dynamicBindingPathList: [{ key: "text" }], + type: WidgetTypes.TEXT_WIDGET, + }, + }; + const evaluator = new DataTreeEvaluator(WIDGET_CONFIG_MAP); + evaluator.createFirstTree(unEvalTree); + it("Evaluates a binding in first run", () => { + const evaluation = evaluator.evalTree; + const dependencyMap = evaluator.dependencyMap; + + expect(evaluation).toHaveProperty("Text2.text", "Label"); + expect(evaluation).toHaveProperty("Text3.text", "Label"); + expect(dependencyMap).toStrictEqual({ + Text1: ["Text1.text"], + Text2: ["Text2.text"], + Text3: ["Text3.text"], + Text4: ["Text4.text"], + Table1: [ + "Table1.tableData", + "Table1.searchText", + "Table1.selectedRowIndex", + "Table1.selectedRowIndices", + ], + Dropdown1: [ + "Dropdown1.selectedOptionValue", + "Dropdown1.selectedOptionValueArr", + ], + "Text2.text": ["Text1.text"], + "Text3.text": ["Text1.text"], + "Dropdown1.selectedOptionValue": [], + "Dropdown1.selectedOptionValueArr": [], + "Table1.tableData": ["Text1.text"], + "Table1.searchText": [], + "Table1.selectedRowIndex": [], + "Table1.selectedRowIndices": [], + "Text4.text": [], + }); + }); + + it("Evaluates a value change in update run", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Text1: { + ...unEvalTree.Text1, + text: "Hey there", + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + expect(updatedEvalTree).toHaveProperty("Text2.text", "Hey there"); + expect(updatedEvalTree).toHaveProperty("Text3.text", "Hey there"); + }); + + it("Evaluates a dependency change in update run", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Text3: { + ...unEvalTree.Text3, + text: "Label 3", + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const updatedDependencyMap = evaluator.dependencyMap; + expect(updatedEvalTree).toHaveProperty("Text2.text", "Label"); + expect(updatedEvalTree).toHaveProperty("Text3.text", "Label 3"); + expect(updatedDependencyMap).toStrictEqual({ + Text1: ["Text1.text"], + Text2: ["Text2.text"], + Text3: ["Text3.text"], + Text4: ["Text4.text"], + Table1: [ + "Table1.tableData", + "Table1.searchText", + "Table1.selectedRowIndex", + "Table1.selectedRowIndices", + ], + Dropdown1: [ + "Dropdown1.selectedOptionValue", + "Dropdown1.selectedOptionValueArr", + ], + "Text2.text": ["Text1.text"], + "Dropdown1.selectedOptionValue": [], + "Dropdown1.selectedOptionValueArr": [], + "Table1.tableData": ["Text1.text"], + "Table1.searchText": [], + "Table1.selectedRowIndex": [], + "Table1.selectedRowIndices": [], + "Text4.text": [], + }); + }); + + it("Overrides with default value", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Input1: { + ...BASE_WIDGET, + text: undefined, + defaultText: "Default value", + widgetName: "Input1", + type: WidgetTypes.INPUT_WIDGET, + }, + }; + + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + expect(updatedEvalTree).toHaveProperty("Input1.text", "Default value"); + }); + + it("Evaluates for value changes in nested diff paths", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Dropdown1: { + ...BASE_WIDGET, + options: [ + { + label: "newValue", + value: "valueTest", + }, + { + label: "test2", + value: "valueTest2", + }, + ], + type: WidgetTypes.DROP_DOWN_WIDGET, + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + expect(updatedEvalTree).toHaveProperty( + "Dropdown1.options.0.label", + "newValue", + ); + }); + + it("Adds an entity with a complicated binding", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Api1: { + ...BASE_ACTION, + data: [ + { + test: "Hey", + }, + { + test: "Ho", + }, + ], + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const updatedDependencyMap = evaluator.dependencyMap; + expect(updatedEvalTree).toHaveProperty("Table1.tableData", [ + { + test: "Hey", + raw: "Label", + }, + { + test: "Ho", + raw: "Label", + }, + ]); + expect(updatedDependencyMap).toStrictEqual({ + Api1: ["Api1.data"], + Input1: ["Input1.text"], + Text1: ["Text1.text"], + Text2: ["Text2.text"], + Text3: ["Text3.text"], + Text4: ["Text4.text"], + Table1: [ + "Table1.tableData", + "Table1.searchText", + "Table1.selectedRowIndex", + "Table1.selectedRowIndices", + ], + Dropdown1: [ + "Dropdown1.selectedOptionValue", + "Dropdown1.selectedOptionValueArr", + ], + "Text2.text": ["Text1.text"], + "Text3.text": ["Text1.text"], + "Dropdown1.selectedOptionValue": [], + "Dropdown1.selectedOptionValueArr": [], + "Table1.tableData": ["Api1.data", "Text1.text"], + "Table1.searchText": [], + "Table1.selectedRowIndex": [], + "Table1.selectedRowIndices": [], + "Text4.text": [], + "Input1.text": [], + }); + }); + + it("Selects a row", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Table1: { + ...unEvalTree.Table1, + selectedRowIndex: 0, + selectedRow: { + test: "Hey", + raw: "Label", + }, + }, + Api1: { + ...BASE_ACTION, + data: [ + { + test: "Hey", + }, + { + test: "Ho", + }, + ], + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const updatedDependencyMap = evaluator.dependencyMap; + expect(updatedEvalTree).toHaveProperty("Table1.tableData", [ + { + test: "Hey", + raw: "Label", + }, + { + test: "Ho", + raw: "Label", + }, + ]); + expect(updatedEvalTree).toHaveProperty("Text4.text", "Hey"); + expect(updatedDependencyMap).toStrictEqual({ + Api1: ["Api1.data"], + Text1: ["Text1.text"], + Text2: ["Text2.text"], + Text3: ["Text3.text"], + Text4: ["Text4.text"], + Table1: [ + "Table1.tableData", + "Table1.selectedRowIndex", + "Table1.searchText", + "Table1.selectedRowIndices", + "Table1.selectedRow", + ], + "Table1.selectedRow": ["Table1.selectedRow.test"], + Dropdown1: [ + "Dropdown1.selectedOptionValue", + "Dropdown1.selectedOptionValueArr", + ], + Input1: ["Input1.text"], + "Text2.text": ["Text1.text"], + "Text3.text": ["Text1.text"], + "Dropdown1.selectedOptionValue": [], + "Dropdown1.selectedOptionValueArr": [], + "Table1.tableData": ["Api1.data", "Text1.text"], + "Table1.searchText": [], + "Table1.selectedRowIndex": [], + "Table1.selectedRowIndices": [], + "Text4.text": ["Table1.selectedRow.test"], + "Input1.text": [], + }); + }); +}); diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts index 5f6cbf5c91..fbad06a5e0 100644 --- a/app/client/src/workers/evaluation.worker.ts +++ b/app/client/src/workers/evaluation.worker.ts @@ -1,11 +1,3 @@ -/* eslint no-restricted-globals: 0 */ -import { - ISO_DATE_FORMAT, - VALIDATION_TYPES, - ValidationResponse, - ValidationType, - Validator, -} from "../constants/WidgetValidation"; import { ActionDescription, DataTree, @@ -14,43 +6,54 @@ import { DataTreeObjectEntity, DataTreeWidget, ENTITY_TYPE, -} from "../entities/DataTree/dataTreeFactory"; -import equal from "fast-deep-equal/es6"; -import _, { - every, - isBoolean, - isNumber, - isObject, - isPlainObject, - isString, - isUndefined, - toNumber, - toString, -} from "lodash"; -import toposort from "toposort"; -import { DATA_BIND_REGEX } from "../constants/BindingsConstants"; -import unescapeJS from "unescape-js"; -import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; -import { WidgetType } from "../constants/WidgetConstants"; -import { WidgetProps } from "../widgets/BaseWidget"; -import { WIDGET_TYPE_VALIDATION_ERROR } from "../constants/messages"; -import moment from "moment"; +} from "entities/DataTree/dataTreeFactory"; import { + DependencyMap, EVAL_WORKER_ACTIONS, EvalError, EvalErrorTypes, extraLibraries, + getDynamicBindings, getEntityDynamicBindingPathList, - getWidgetDynamicTriggerPathList, + isPathADynamicBinding, isPathADynamicTrigger, unsafeFunctionForEval, } from "../utils/DynamicBindingUtils"; +import _ from "lodash"; +import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; +import toposort from "toposort"; +import { DATA_BIND_REGEX } from "../constants/BindingsConstants"; +import equal from "fast-deep-equal/es6"; +import unescapeJS from "unescape-js"; + +import { applyChange, diff, Diff } from "deep-diff"; +import { + addDependantsOfNestedPropertyPaths, + convertPathToString, + CrashingError, + DataTreeDiffEvent, + getValidatedTree, + isChildPropertyPath, + makeParentsDependOnChildren, + removeFunctions, + removeFunctionsFromDataTree, + translateDiffEventToDataTreeDiffEvent, + validateWidgetProperty, +} from "./evaluationUtils"; +import { + EXECUTION_PARAM_KEY, + EXECUTION_PARAM_REFERENCE_REGEX, +} from "../constants/ActionConstants"; const ctx: Worker = self as any; -let ERRORS: EvalError[] = []; +let dataTreeEvaluator: DataTreeEvaluator | undefined; let LOGS: any[] = []; -let WIDGET_TYPE_CONFIG_MAP: WidgetTypeConfigMap = {}; + +type EvalResult = { + result: any; + triggers?: ActionDescription[]; +}; //TODO: Create a more complete RPC setup in the subtree-eval branch. function messageEventListener( @@ -66,7 +69,6 @@ function messageEventListener( responseData, timeTaken: (endTime - startTime).toFixed(2), }); - ERRORS = []; LOGS = []; }; } @@ -76,313 +78,1194 @@ ctx.addEventListener( messageEventListener((method, requestData: any) => { switch (method) { case EVAL_WORKER_ACTIONS.EVAL_TREE: { - const { widgetTypeConfigMap, dataTree } = requestData; - WIDGET_TYPE_CONFIG_MAP = widgetTypeConfigMap; + const { widgetTypeConfigMap, unevalTree } = requestData; + let dataTree: DataTree = unevalTree; + let errors: EvalError[] = []; + let dependencies: DependencyMap = {}; try { - const response = getEvaluatedDataTree(dataTree); + if (!dataTreeEvaluator) { + dataTreeEvaluator = new DataTreeEvaluator(widgetTypeConfigMap); + dataTreeEvaluator.createFirstTree(unevalTree); + dataTree = dataTreeEvaluator.evalTree; + } else { + dataTree = dataTreeEvaluator.updateDataTree(unevalTree); + } + // We need to clean it to remove any possible functions inside the tree. // If functions exist, it will crash the web worker - const cleanDataTree = JSON.stringify(response); - return { dataTree: cleanDataTree, errors: ERRORS, logs: LOGS }; + dataTree = JSON.parse(JSON.stringify(dataTree)); + dependencies = dataTreeEvaluator.inverseDependencyMap; + errors = dataTreeEvaluator.errors; + dataTreeEvaluator.clearErrors(); } catch (e) { - const cleanDataTree = JSON.stringify(getValidatedTree(dataTree)); - return { dataTree: cleanDataTree, errors: ERRORS, logs: LOGS }; + if (dataTreeEvaluator !== undefined) { + errors = dataTreeEvaluator.errors; + } + if (!(e instanceof CrashingError)) { + errors.push({ + type: EvalErrorTypes.UNKNOWN_ERROR, + message: e.message, + }); + console.error(e); + } + dataTree = getValidatedTree(widgetTypeConfigMap, unevalTree); + dataTreeEvaluator = undefined; } + return { + dataTree, + dependencies, + errors, + logs: LOGS, + }; } - case EVAL_WORKER_ACTIONS.EVAL_SINGLE: { - const { binding, dataTree } = requestData; - const withFunctions = addFunctions(dataTree); - const value = getDynamicValue(binding, withFunctions, false); - const cleanedResponse = removeFunctions(value); - return { value: cleanedResponse, errors: ERRORS }; + case EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS: { + const { bindings, executionParams } = requestData; + if (!dataTreeEvaluator) { + return { value: undefined, errors: [] }; + } + + const values = dataTreeEvaluator.evaluateActionBindings( + bindings, + executionParams, + ); + + const errors = dataTreeEvaluator.errors; + dataTreeEvaluator.clearErrors(); + return { values, errors }; } case EVAL_WORKER_ACTIONS.EVAL_TRIGGER: { const { dynamicTrigger, callbackData, dataTree } = requestData; - const evalTree = getEvaluatedDataTree(dataTree); + if (!dataTreeEvaluator) { + return { triggers: [], errors: [] }; + } + const evalTree = dataTreeEvaluator.updateDataTree(dataTree); const withFunctions = addFunctions(evalTree); - const triggers = getDynamicValue( + const triggers = dataTreeEvaluator.getDynamicValue( dynamicTrigger, withFunctions, true, callbackData, ); - const cleanedResponse = removeFunctions(triggers); - return { triggers: cleanedResponse, errors: ERRORS }; + const errors = dataTreeEvaluator.errors; + dataTreeEvaluator.clearErrors(); + return { triggers, errors }; } case EVAL_WORKER_ACTIONS.CLEAR_CACHE: { - clearCaches(); + dataTreeEvaluator = undefined; return true; } case EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE: { const { propertyPath } = requestData; - clearPropertyCache(propertyPath); + if (!dataTreeEvaluator) { + return true; + } + dataTreeEvaluator.clearPropertyCache(propertyPath); return true; } case EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE_OF_WIDGET: { const { widgetName } = requestData; - clearPropertyCacheOfWidget(widgetName); + if (!dataTreeEvaluator) { + return true; + } + dataTreeEvaluator.clearPropertyCacheOfWidget(widgetName); return true; } case EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY: { - const { widgetType, property, value, props } = requestData; - const result = validateWidgetProperty( + const { + widgetType, + widgetTypeConfigMap, + property, + value, + props, + } = requestData; + return validateWidgetProperty( + widgetTypeConfigMap, widgetType, property, value, props, ); - const cleanedResponse = removeFunctions(result); - return cleanedResponse; } default: { - console.error("Action not registered on worker", method, requestData); + console.error("Action not registered on worker", method); } } }), ); -let dependencyTreeCache: any = {}; -let cachedDataTreeString = ""; - -function getEvaluatedDataTree(dataTree: DataTree): DataTree { - const totalStart = performance.now(); - // Add functions to the tre - const withFunctions = addFunctions(dataTree); - // Create Dependencies DAG - const createDepsStart = performance.now(); - const dataTreeString = JSON.stringify(dataTree); - // Stringify before doing a fast equals because the data tree has functions and fast equal will always treat those as changed values - // Better solve will be to prune functions - if (!equal(dataTreeString, cachedDataTreeString)) { - cachedDataTreeString = dataTreeString; - dependencyTreeCache = createDependencyTree(withFunctions); - } - const createDepsEnd = performance.now(); - const { - dependencyMap, - sortedDependencies, - dependencyTree, - } = dependencyTreeCache; - - // Evaluate Tree - const evaluatedTreeStart = performance.now(); - const evaluatedTree = dependencySortedEvaluateDataTree( - dataTree, - dependencyMap, - sortedDependencies, - ); - const evaluatedTreeEnd = performance.now(); - - // Set Loading Widgets - const loadingTreeStart = performance.now(); - const treeWithLoading = setTreeLoading(evaluatedTree, dependencyTree); - const loadingTreeEnd = performance.now(); - - // Validate Widgets - const validateTreeStart = performance.now(); - const validated = getValidatedTree(treeWithLoading); - const validateTreeEnd = performance.now(); - const withoutFunctions = removeFunctionsFromDataTree(validated); - - // End counting total time - const endStart = performance.now(); - - // Log time taken and count - const timeTaken = { - total: (endStart - totalStart).toFixed(2), - createDeps: (createDepsEnd - createDepsStart).toFixed(2), - evaluate: (evaluatedTreeEnd - evaluatedTreeStart).toFixed(2), - loading: (loadingTreeEnd - loadingTreeStart).toFixed(2), - validate: (validateTreeEnd - validateTreeStart).toFixed(2), - }; - LOGS.push({ timeTaken }); - // dataTreeCache = validated; - return withoutFunctions; -} - -const addFunctions = (dataTree: DataTree): DataTree => { - dataTree.actionPaths = []; - Object.keys(dataTree).forEach((entityName) => { - const entity = dataTree[entityName]; - if (isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.ACTION) { - const runFunction = function( - this: DataTreeAction, - onSuccess: string, - onError: string, - params = "", - ) { - return { - type: "RUN_ACTION", - payload: { - actionId: this.actionId, - onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "", - onError: onError ? `{{${onError.toString()}}}` : "", - params, - }, - }; - }; - _.set(dataTree, `${entityName}.run`, runFunction); - dataTree.actionPaths && dataTree.actionPaths.push(`${entityName}.run`); +export class DataTreeEvaluator { + dependencyMap: DependencyMap = {}; + sortedDependencies: Array = []; + inverseDependencyMap: DependencyMap = {}; + widgetConfigMap: WidgetTypeConfigMap = {}; + evalTree: DataTree = {}; + allKeys: Record = {}; + oldUnEvalTree: DataTree = {}; + errors: EvalError[] = []; + parsedValueCache: Map< + string, + { + value: any; + version: number; } - }); - dataTree.navigateTo = function( - pageNameOrUrl: string, - params: Record, - ) { - return { - type: "NAVIGATE_TO", - payload: { pageNameOrUrl, params }, - }; - }; - dataTree.actionPaths.push("navigateTo"); + > = new Map(); - dataTree.showAlert = function(message: string, style: string) { - return { - type: "SHOW_ALERT", - payload: { message, style }, - }; - }; - dataTree.actionPaths.push("showAlert"); - - dataTree.showModal = function(modalName: string) { - return { - type: "SHOW_MODAL_BY_NAME", - payload: { modalName }, - }; - }; - dataTree.actionPaths.push("showModal"); - - dataTree.closeModal = function(modalName: string) { - return { - type: "CLOSE_MODAL", - payload: { modalName }, - }; - }; - dataTree.actionPaths.push("closeModal"); - - dataTree.storeValue = function(key: string, value: string) { - return { - type: "STORE_VALUE", - payload: { key, value }, - }; - }; - dataTree.actionPaths.push("storeValue"); - - dataTree.download = function(data: string, name: string, type: string) { - return { - type: "DOWNLOAD", - payload: { data, name, type }, - }; - }; - dataTree.actionPaths.push("download"); - return dataTree; -}; - -const removeFunctionsFromDataTree = (dataTree: DataTree) => { - dataTree.actionPaths?.forEach((functionPath) => { - _.set(dataTree, functionPath, {}); - }); - delete dataTree.actionPaths; - return dataTree; -}; - -// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing -// Check issue https://github.com/appsmithorg/appsmith/issues/719 -const removeFunctions = (value: any) => { - if (_.isFunction(value)) { - return "Function call"; - } else if (_.isObject(value)) { - return JSON.parse(JSON.stringify(value)); - } else { - return value; + constructor(widgetConfigMap: WidgetTypeConfigMap) { + this.widgetConfigMap = widgetConfigMap; } -}; -type DynamicDependencyMap = Record>; -const createDependencyTree = ( - dataTree: DataTree, -): { - sortedDependencies: Array; - dependencyTree: Array<[string, string]>; - dependencyMap: DynamicDependencyMap; -} => { - let dependencyMap: DynamicDependencyMap = {}; - const allKeys = getAllPaths(dataTree); - Object.keys(dataTree).forEach((entityKey) => { - const entity = dataTree[entityKey]; - if (isValidEntity(entity)) { - if ( - entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET || - entity.ENTITY_TYPE === ENTITY_TYPE.ACTION - ) { - const dynamicBindingPathList = getEntityDynamicBindingPathList(entity); - if (dynamicBindingPathList.length) { - dynamicBindingPathList.forEach((dynamicPath) => { - const propertyPath = dynamicPath.key; - const unevalPropValue = _.get(entity, propertyPath); - const { jsSnippets } = getDynamicBindings(unevalPropValue); - const existingDeps = - dependencyMap[`${entityKey}.${propertyPath}`] || []; - dependencyMap[`${entityKey}.${propertyPath}`] = existingDeps.concat( - jsSnippets.filter((jsSnippet) => !!jsSnippet), + createFirstTree(unEvalTree: DataTree) { + const totalStart = performance.now(); + // Add functions to the tree + const withFunctions = addFunctions(unEvalTree); + // Create dependency map + const createDependencyStart = performance.now(); + this.dependencyMap = this.createDependencyMap(withFunctions); + const createDependencyEnd = performance.now(); + // Sort + const sortDependenciesStart = performance.now(); + this.sortedDependencies = this.sortDependencies(this.dependencyMap); + const sortDependenciesEnd = performance.now(); + // Inverse + this.inverseDependencyMap = this.getInverseDependencyTree(); + // Evaluate + const evaluateStart = performance.now(); + const evaluatedTree = this.evaluateTree( + withFunctions, + this.sortedDependencies, + ); + const evaluateEnd = performance.now(); + // Validate Widgets + const validateStart = performance.now(); + const validated = getValidatedTree(this.widgetConfigMap, evaluatedTree); + const validateEnd = performance.now(); + // Remove functions + this.evalTree = removeFunctionsFromDataTree(validated); + this.oldUnEvalTree = unEvalTree; + const totalEnd = performance.now(); + const timeTakenForFirstTree = { + total: (totalEnd - totalStart).toFixed(2), + createDependencies: (createDependencyEnd - createDependencyStart).toFixed( + 2, + ), + sortDependencies: (sortDependenciesEnd - sortDependenciesStart).toFixed( + 2, + ), + evaluate: (evaluateEnd - evaluateStart).toFixed(2), + validate: (validateEnd - validateStart).toFixed(2), + dependencies: { + map: JSON.parse(JSON.stringify(this.dependencyMap)), + inverseMap: JSON.parse(JSON.stringify(this.inverseDependencyMap)), + sortedList: JSON.parse(JSON.stringify(this.sortedDependencies)), + }, + }; + LOGS.push({ timeTakenForFirstTree }); + } + + updateDataTree(unEvalTree: DataTree) { + const totalStart = performance.now(); + // Add appsmith internal functions to the tree ex. navigateTo / showModal + const unEvalTreeWithFunctions = addFunctions(unEvalTree); + // Calculate diff + const diffCheckTimeStart = performance.now(); + const differences = diff(this.oldUnEvalTree, unEvalTree) || []; + // Since eval tree is listening to possible events that dont cause differences + // We want to check if no diffs are present and bail out early + if (differences.length === 0) { + return this.evalTree; + } + const diffCheckTimeStop = performance.now(); + // Check if dependencies have changed + const updateDependenciesStart = performance.now(); + + // Find all the paths that have changed as part of the difference and update the + // global dependency map if an existing dynamic binding has now become legal + const { + dependenciesOfRemovedPaths, + removedPaths, + } = this.updateDependencyMap(differences, unEvalTreeWithFunctions); + const updateDependenciesStop = performance.now(); + + const calculateSortOrderStart = performance.now(); + + const subTreeSortOrder: string[] = this.calculateSubTreeSortOrder( + differences, + dependenciesOfRemovedPaths, + removedPaths, + ); + + const calculateSortOrderStop = performance.now(); + + LOGS.push({ + differences, + subTreeSortOrder, + sortedDependencies: this.sortedDependencies, + inverse: this.inverseDependencyMap, + updatedDependencyMap: this.dependencyMap, + }); + + // Evaluate + const evalStart = performance.now(); + // We are setting all values from our uneval tree to the old eval tree we have + // this way we can get away with just evaluating the sort order and nothing else + subTreeSortOrder.forEach((propertyPath) => { + const lastIndexOfDot = propertyPath.lastIndexOf("."); + // Only do this for property paths and not the entity themselves + if (lastIndexOfDot !== -1) { + const unEvalPropValue = _.get(unEvalTree, propertyPath); + // TODO Optimise: Required only if unEvalPropValue is a binding that needs eval + _.set(this.evalTree, propertyPath, unEvalPropValue); + } + }); + + // Remove any deleted paths from the eval tree + removedPaths.forEach((removedPath) => { + _.unset(this.evalTree, removedPath); + }); + + const evaluatedTree = this.evaluateTree(this.evalTree, subTreeSortOrder); + const evalStop = performance.now(); + + const validateStart = performance.now(); + // Validate and parse updated widgets + const updatedWidgets = new Set( + subTreeSortOrder.map((path) => path.split(".")[0]), + ); + + const validatedTree = getValidatedTree( + this.widgetConfigMap, + evaluatedTree, + updatedWidgets, + ); + const validateEnd = performance.now(); + + // Remove functions + this.evalTree = removeFunctionsFromDataTree(validatedTree); + const totalEnd = performance.now(); + // TODO: For some reason we are passing some reference which are getting mutated. + // Need to check why big api responses are getting split between two eval runs + this.oldUnEvalTree = unEvalTree; + const timeTakenForSubTreeEval = { + total: (totalEnd - totalStart).toFixed(2), + findDifferences: (diffCheckTimeStop - diffCheckTimeStart).toFixed(2), + updateDependencies: ( + updateDependenciesStop - updateDependenciesStart + ).toFixed(2), + calculateSortOrder: ( + calculateSortOrderStop - calculateSortOrderStart + ).toFixed(2), + evaluate: (evalStop - evalStart).toFixed(2), + validate: (validateEnd - validateStart).toFixed(2), + }; + LOGS.push({ timeTakenForSubTreeEval }); + return this.evalTree; + } + + getCompleteSortOrder( + changes: Array, + inverseMap: DependencyMap, + ): Array { + let finalSortOrder: Array = []; + let computeSortOrder = true; + // Initialize parents with the current sent of property paths that need to be evaluated + let parents = changes; + let subSortOrderArray: Array; + while (computeSortOrder) { + // Get all the nodes that would be impacted by the evaluation of the nodes in parents array in sorted order + subSortOrderArray = this.getEvaluationSortOrder(parents, inverseMap); + + // Add all the sorted nodes in the final list + finalSortOrder = [...finalSortOrder, ...subSortOrderArray]; + + parents = this.getImmediateParentsOfPropertyPaths(subSortOrderArray); + // If we find parents of the property paths in the sorted array, we should continue finding all the nodes dependent + // on the parents + computeSortOrder = parents.length > 0; + } + + // Remove duplicates from this list. Since we explicitly walk down the tree and implicitly (by fetching parents) walk + // up the tree, there are bound to be many duplicates. + const uniqueKeysInSortOrder = [...new Set(finalSortOrder)]; + + const sortOrderPropertyPaths = Array.from(uniqueKeysInSortOrder); + + //Trim this list to now remove the property paths which are simply entity names + const finalSortOrderArray: Array = []; + sortOrderPropertyPaths.forEach((propertyPath) => { + const lastIndexOfDot = propertyPath.lastIndexOf("."); + // Only do this for property paths and not the entity themselves + if (lastIndexOfDot !== -1) { + finalSortOrderArray.push(propertyPath); + } + }); + + return finalSortOrderArray; + } + + // The idea is to find the immediate parents of the property paths + // e.g. For Table1.selectedRow.email, the parent is Table1.selectedRow + getImmediateParentsOfPropertyPaths( + propertyPaths: Array, + ): Array { + // Use a set to ensure that we dont have duplicates + const parents: Set = new Set(); + + propertyPaths.forEach((path) => { + const parentProperty = path.substr(0, path.lastIndexOf(".")); + + if (parentProperty.length != 0) { + parents.add(parentProperty); + } else { + // We have reached the top of the path. No parent exists + } + }); + + return Array.from(parents); + } + + getEvaluationSortOrder( + changes: Array, + inverseMap: DependencyMap, + ): Array { + const sortOrder: Array = [...changes]; + let iterator = 0; + while (iterator < sortOrder.length) { + // Find all the nodes who are to be evaluated when sortOrder[iterator] changes + const newNodes = inverseMap[sortOrder[iterator]]; + + // If we find more nodes that would be impacted by the evaluation of the node being investigated + // we add these to the sort order. + if (newNodes) { + newNodes.forEach((toBeEvaluatedNode) => { + // Only add the nodes if they haven't been already added for evaluation in the list. Since we are doing + // breadth first traversal, we should be safe in not changing the evaluation order and adding this now at this + // point instead of the previous index found. + if (!sortOrder.includes(toBeEvaluatedNode)) { + sortOrder.push(toBeEvaluatedNode); + } + }); + } + iterator++; + } + return sortOrder; + } + + createDependencyMap(unEvalTree: DataTree): DependencyMap { + let dependencyMap: DependencyMap = {}; + this.allKeys = getAllPaths(unEvalTree); + Object.keys(unEvalTree).forEach((entityName) => { + const entity = unEvalTree[entityName]; + if (isAction(entity) || isWidget(entity)) { + const entityListedDependencies = this.listEntityDependencies( + entity, + entityName, + ); + dependencyMap = { ...dependencyMap, ...entityListedDependencies }; + } + }); + Object.keys(dependencyMap).forEach((key) => { + dependencyMap[key] = _.flatten( + dependencyMap[key].map((path) => + extractReferencesFromBinding(path, this.allKeys), + ), + ); + }); + // TODO make this run only for widgets and not actions + dependencyMap = makeParentsDependOnChildren(dependencyMap); + return dependencyMap; + } + + listEntityDependencies( + entity: DataTreeWidget | DataTreeAction, + entityName: string, + ): DependencyMap { + const dependencies: DependencyMap = {}; + const dynamicBindingPathList = getEntityDynamicBindingPathList(entity); + if (dynamicBindingPathList.length) { + dynamicBindingPathList.forEach((dynamicPath) => { + const propertyPath = dynamicPath.key; + const unevalPropValue = _.get(entity, propertyPath); + const { jsSnippets } = getDynamicBindings(unevalPropValue); + const existingDeps = + dependencies[`${entityName}.${propertyPath}`] || []; + dependencies[`${entityName}.${propertyPath}`] = existingDeps.concat( + jsSnippets.filter((jsSnippet) => !!jsSnippet), + ); + }); + } + if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { + // Set default property dependency + const defaultProperties = this.widgetConfigMap[entity.type] + .defaultProperties; + Object.keys(defaultProperties).forEach((property) => { + dependencies[`${entityName}.${property}`] = [ + `${entityName}.${defaultProperties[property]}`, + ]; + }); + } + return dependencies; + } + + evaluateTree( + oldUnevalTree: DataTree, + sortedDependencies: Array, + ): DataTree { + const tree = _.cloneDeep(oldUnevalTree); + try { + return sortedDependencies.reduce( + (currentTree: DataTree, propertyPath: string) => { + LOGS.push(`evaluating ${propertyPath}`); + const entityName = propertyPath.split(".")[0]; + const entity: DataTreeEntity = currentTree[entityName]; + const unEvalPropertyValue = _.get(currentTree as any, propertyPath); + const isABindingPath = + (isAction(entity) || isWidget(entity)) && + isPathADynamicBinding( + entity, + propertyPath.substring(propertyPath.indexOf(".") + 1), ); + let evalPropertyValue; + const requiresEval = + isABindingPath && isDynamicValue(unEvalPropertyValue); + if (requiresEval) { + try { + evalPropertyValue = this.evaluateDynamicProperty( + propertyPath, + currentTree, + unEvalPropertyValue, + ); + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.EVAL_PROPERTY_ERROR, + message: e.message, + context: { + propertyPath, + }, + }); + evalPropertyValue = undefined; + } + } else { + evalPropertyValue = unEvalPropertyValue; + } + if (isWidget(entity)) { + const widgetEntity = entity; + // TODO fix for nested properties + // For nested properties like Table1.selectedRow.email + // The following line will calculated the property name to be selectedRow + // instead of selectedRow.email + const propertyName = propertyPath.split(".")[1]; + if (propertyName) { + let parsedValue = this.validateAndParseWidgetProperty( + propertyPath, + widgetEntity, + currentTree, + evalPropertyValue, + unEvalPropertyValue, + ); + const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type] + .defaultProperties; + const hasDefaultProperty = propertyName in defaultPropertyMap; + if (hasDefaultProperty) { + const defaultProperty = defaultPropertyMap[propertyName]; + parsedValue = this.overwriteDefaultDependentProps( + defaultProperty, + parsedValue, + propertyPath, + widgetEntity, + ); + } + return _.set(currentTree, propertyPath, parsedValue); + } + return _.set(currentTree, propertyPath, evalPropertyValue); + } else { + return _.set(currentTree, propertyPath, evalPropertyValue); + } + }, + tree, + ); + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.EVAL_TREE_ERROR, + message: e.message, + }); + return tree; + } + } + + sortDependencies(dependencyMap: DependencyMap): Array { + const dependencyTree: Array<[string, string]> = []; + Object.keys(dependencyMap).forEach((key: string) => { + if (dependencyMap[key].length) { + dependencyMap[key].forEach((dep) => dependencyTree.push([key, dep])); + } else { + // Set no dependency + dependencyTree.push([key, ""]); + } + }); + + try { + // sort dependencies and remove empty dependencies + return toposort(dependencyTree) + .reverse() + .filter((d) => !!d); + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.DEPENDENCY_ERROR, + message: e.message, + }); + throw new CrashingError(e.message); + } + } + + getParsedValueCache(propertyPath: string) { + return ( + this.parsedValueCache.get(propertyPath) || { + value: undefined, + version: 0, + } + ); + } + + clearPropertyCache(propertyPath: string) { + this.parsedValueCache.delete(propertyPath); + } + + clearPropertyCacheOfWidget(widgetName: string) { + // TODO check if this loop mutating itself is safe + this.parsedValueCache.forEach((value, key) => { + const match = key.match(`${widgetName}.`); + if (match) { + this.parsedValueCache.delete(key); + } + }); + } + + clearAllCaches() { + this.parsedValueCache.clear(); + this.clearErrors(); + this.dependencyMap = {}; + this.allKeys = {}; + this.inverseDependencyMap = {}; + this.sortedDependencies = []; + this.evalTree = {}; + this.oldUnEvalTree = {}; + } + + getDynamicValue( + dynamicBinding: string, + data: DataTree, + returnTriggers: boolean, + callBackData?: Array, + ) { + // Get the {{binding}} bound values + const { stringSegments, jsSnippets } = getDynamicBindings(dynamicBinding); + if (returnTriggers) { + const result = this.evaluateDynamicBoundValue( + data, + jsSnippets[0], + callBackData, + ); + return result.triggers; + } + if (stringSegments.length) { + // Get the Data Tree value of those "binding "paths + const values = jsSnippets.map((jsSnippet, index) => { + if (jsSnippet) { + const result = this.evaluateDynamicBoundValue( + data, + jsSnippet, + callBackData, + ); + return result.result; + } else { + return stringSegments[index]; + } + }); + + // if it is just one binding, no need to create template string + if (stringSegments.length === 1) return values[0]; + // else return a string template with bindings + return createDynamicValueString(dynamicBinding, stringSegments, values); + } + return undefined; + } + + // Paths are expected to have "{name}.{path}" signature + // Also returns any action triggers found after evaluating value + evaluateDynamicBoundValue( + data: DataTree, + path: string, + callbackData?: Array, + ): EvalResult { + try { + const unescapedJS = unescapeJS(path).replace(/(\r\n|\n|\r)/gm, ""); + return this.evaluate(unescapedJS, data, callbackData); + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.UNESCAPE_STRING_ERROR, + message: e.message, + context: { + path, + }, + }); + return { result: undefined, triggers: [] }; + } + } + + evaluate(js: string, data: DataTree, callbackData?: Array): EvalResult { + const scriptToEvaluate = ` + function closedFunction () { + const result = ${js}; + return { result, triggers: self.triggers } + } + closedFunction() + `; + const scriptWithCallback = ` + function callback (script) { + const userFunction = script; + const result = userFunction.apply(self, CALLBACK_DATA); + return { result, triggers: self.triggers }; + } + callback(${js}); + `; + const script = callbackData ? scriptWithCallback : scriptToEvaluate; + try { + const { result, triggers } = (function() { + /**** Setting the eval context ****/ + const GLOBAL_DATA: Record = {}; + ///// Adding callback data + GLOBAL_DATA.CALLBACK_DATA = callbackData; + ///// Adding Data tree + Object.keys(data).forEach((datum) => { + GLOBAL_DATA[datum] = data[datum]; + }); + ///// Fixing action paths and capturing their execution response + if (data.actionPaths) { + GLOBAL_DATA.triggers = []; + const pusher = function( + this: DataTree, + action: any, + ...payload: any[] + ) { + const actionPayload = action(...payload); + GLOBAL_DATA.triggers.push(actionPayload); + }; + GLOBAL_DATA.actionPaths.forEach((path: string) => { + const action = _.get(GLOBAL_DATA, path); + const entity = _.get(GLOBAL_DATA, path.split(".")[0]); + if (action) { + _.set(GLOBAL_DATA, path, pusher.bind(data, action.bind(entity))); + } }); } - if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { - // Set default property dependency - const defaultProperties = - WIDGET_TYPE_CONFIG_MAP[entity.type].defaultProperties; - Object.keys(defaultProperties).forEach((property) => { - dependencyMap[`${entityKey}.${property}`] = [ - `${entityKey}.${defaultProperties[property]}`, - ]; - }); - const dynamicTriggerPathList = getWidgetDynamicTriggerPathList( - entity, - ); - if (dynamicTriggerPathList.length) { - dynamicTriggerPathList.forEach((dynamicPath) => { - dependencyMap[`${entityKey}.${dynamicPath.key}`] = []; - }); + + // Set it to self + Object.keys(GLOBAL_DATA).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + self[key] = GLOBAL_DATA[key]; + }); + + ///// Adding extra libraries separately + extraLibraries.forEach((library) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + self[library.accessor] = library.lib; + }); + + ///// Remove all unsafe functions + unsafeFunctionForEval.forEach((func) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + self[func] = undefined; + }); + + const evalResult = eval(script); + + // Remove it from self + // This is needed so that next eval can have a clean sheet + Object.keys(GLOBAL_DATA).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + delete self[key]; + }); + + return evalResult; + })(); + return { result, triggers }; + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.EVAL_ERROR, + message: e.message, + context: { + binding: js, + }, + }); + return { result: undefined, triggers: [] }; + } + } + + evaluateDynamicProperty( + propertyPath: string, + currentTree: DataTree, + unEvalPropertyValue: any, + ): any { + return this.getDynamicValue(unEvalPropertyValue, currentTree, false); + } + + validateAndParseWidgetProperty( + propertyPath: string, + widget: DataTreeWidget, + currentTree: DataTree, + evalPropertyValue: any, + unEvalPropertyValue: string, + ): any { + const entityPropertyName = _.drop(propertyPath.split(".")).join("."); + let valueToValidate = evalPropertyValue; + if (isPathADynamicTrigger(widget, propertyPath)) { + const { triggers } = this.getDynamicValue( + unEvalPropertyValue, + currentTree, + true, + undefined, + ); + valueToValidate = triggers; + } + const { parsed, isValid, message, transformed } = validateWidgetProperty( + this.widgetConfigMap, + widget.type, + entityPropertyName, + valueToValidate, + widget, + currentTree, + ); + const evaluatedValue = isValid + ? parsed + : _.isUndefined(transformed) + ? evalPropertyValue + : transformed; + const safeEvaluatedValue = removeFunctions(evaluatedValue); + _.set(widget, `evaluatedValues.${entityPropertyName}`, safeEvaluatedValue); + if (!isValid) { + _.set(widget, `invalidProps.${entityPropertyName}`, true); + _.set(widget, `validationMessages.${entityPropertyName}`, message); + } else { + _.set(widget, `invalidProps.${entityPropertyName}`, false); + _.set(widget, `validationMessages.${entityPropertyName}`, ""); + } + + if (isPathADynamicTrigger(widget, entityPropertyName)) { + return unEvalPropertyValue; + } else { + const parsedCache = this.getParsedValueCache(propertyPath); + if (!equal(parsedCache.value, parsed)) { + this.parsedValueCache.set(propertyPath, { + value: parsed, + version: Date.now(), + }); + } + return parsed; + } + } + + overwriteDefaultDependentProps( + defaultProperty: string, + propertyValue: any, + propertyPath: string, + entity: DataTreeWidget, + ) { + const defaultPropertyCache = this.getParsedValueCache( + `${entity.widgetName}.${defaultProperty}`, + ); + const propertyCache = this.getParsedValueCache(propertyPath); + if ( + propertyValue === undefined || + propertyCache.version < defaultPropertyCache.version + ) { + return defaultPropertyCache.value; + } + return propertyValue; + } + + updateDependencyMap( + differences: Array>, + unEvalDataTree: DataTree, + ): { + dependenciesOfRemovedPaths: Array; + removedPaths: Array; + } { + const diffCalcStart = performance.now(); + let didUpdateDependencyMap = false; + const dependenciesOfRemovedPaths: Array = []; + const removedPaths: Array = []; + + // This is needed for NEW and DELETE events below. + // In worst case, it tends to take ~12.5% of entire diffCalc (8 ms out of 67ms for 132 array of NEW) + // TODO: Optimise by only getting paths of changed node + this.allKeys = getAllPaths(unEvalDataTree); + // Transform the diff library events to Appsmith evaluator events + differences + .map(translateDiffEventToDataTreeDiffEvent) + .forEach((dataTreeDiff) => { + const entityName = dataTreeDiff.payload.propertyPath.split(".")[0]; + const entity = unEvalDataTree[entityName]; + const entityType = isValidEntity(entity) ? entity.ENTITY_TYPE : "noop"; + + if (entityType !== "noop") { + switch (dataTreeDiff.event) { + case DataTreeDiffEvent.NEW: { + // If a new widget was added, add all the internal bindings for this widget to the global dependency map + if ( + isWidget(entity) && + dataTreeDiff.payload.propertyPath === entityName + ) { + const widgetDependencyMap: DependencyMap = this.listEntityDependencies( + entity as DataTreeWidget, + entityName, + ); + if (Object.keys(widgetDependencyMap).length) { + didUpdateDependencyMap = true; + Object.assign(this.dependencyMap, widgetDependencyMap); + } + } + // Either a new entity or a new property path has been added. Go through existing dynamic bindings and + // find out if a new dependency has to be created because the property path used in the binding just became + // eligible + const possibleReferencesInOldBindings: DependencyMap = this.getPropertyPathReferencesInExistingBindings( + unEvalDataTree, + dataTreeDiff.payload.propertyPath, + ); + // We have found some bindings which are related to the new property path and hence should be added to the + // global dependency map + if (Object.keys(possibleReferencesInOldBindings).length) { + didUpdateDependencyMap = true; + Object.assign( + this.dependencyMap, + possibleReferencesInOldBindings, + ); + } + break; + } + case DataTreeDiffEvent.DELETE: { + // Add to removedPaths as they have been deleted from the evalTree + removedPaths.push(dataTreeDiff.payload.propertyPath); + // If an existing widget was deleted, remove all the bindings from the global dependency map + if ( + entityType === ENTITY_TYPE.WIDGET && + dataTreeDiff.payload.propertyPath === entityName + ) { + const entity: DataTreeWidget = unEvalDataTree[ + entityName + ] as DataTreeWidget; + + const widgetBindings = this.listEntityDependencies( + entity, + entityName, + ); + Object.keys(widgetBindings).forEach((widgetDep) => { + didUpdateDependencyMap = true; + delete this.dependencyMap[widgetDep]; + }); + } + // Either an existing entity or an existing property path has been deleted. Update the global dependency map + // by removing the bindings from the same. + Object.keys(this.dependencyMap).forEach((dependencyPath) => { + didUpdateDependencyMap = true; + if ( + isChildPropertyPath( + dataTreeDiff.payload.propertyPath, + dependencyPath, + ) + ) { + delete this.dependencyMap[dependencyPath]; + } else { + const toRemove: Array = []; + this.dependencyMap[dependencyPath].forEach( + (dependantPath) => { + if ( + isChildPropertyPath( + dataTreeDiff.payload.propertyPath, + dependantPath, + ) + ) { + dependenciesOfRemovedPaths.push(dependencyPath); + toRemove.push(dependantPath); + } + }, + ); + this.dependencyMap[dependencyPath] = _.difference( + this.dependencyMap[dependencyPath], + toRemove, + ); + } + }); + break; + } + + case DataTreeDiffEvent.EDIT: { + // We only care about dependencies for a widget. This is because in case a dependency of an action changes, + // that shouldn't trigger an evaluation. + // Also for a widget, we only care if the difference is in dynamic bindings since static values do not need + // an evaluation. + if ( + entityType === ENTITY_TYPE.WIDGET && + typeof dataTreeDiff.payload.value === "string" + ) { + const entity: DataTreeWidget = unEvalDataTree[ + entityName + ] as DataTreeWidget; + const isABindingPath = isPathADynamicBinding( + entity, + dataTreeDiff.payload.propertyPath.substring( + dataTreeDiff.payload.propertyPath.indexOf(".") + 1, + ), + ); + if (isABindingPath) { + didUpdateDependencyMap = true; + + const { jsSnippets } = getDynamicBindings( + dataTreeDiff.payload.value, + ); + const correctSnippets = jsSnippets.filter( + (jsSnippet) => !!jsSnippet, + ); + // We found a new dynamic binding for this property path. We update the dependency map by overwriting the + // dependencies for this property path with the newly found dependencies + if (correctSnippets.length) { + this.dependencyMap[ + dataTreeDiff.payload.propertyPath + ] = correctSnippets; + } else { + // The dependency on this property path has been removed. Delete this property path from the global + // dependency map + delete this.dependencyMap[ + dataTreeDiff.payload.propertyPath + ]; + } + } + } + break; + } + default: { + break; + } } } - } + }); + const diffCalcEnd = performance.now(); + const subDepCalcStart = performance.now(); + if (didUpdateDependencyMap) { + // TODO Optimise + Object.keys(this.dependencyMap).forEach((key) => { + this.dependencyMap[key] = _.flatten( + this.dependencyMap[key].map((path) => + extractReferencesFromBinding(path, this.allKeys), + ), + ); + }); + this.dependencyMap = makeParentsDependOnChildren(this.dependencyMap); } - }); - Object.keys(dependencyMap).forEach((key) => { - dependencyMap[key] = _.flatten( - dependencyMap[key].map((path) => calculateSubDependencies(path, allKeys)), - ); - }); - dependencyMap = makeParentsDependOnChildren(dependencyMap); - const dependencyTree: Array<[string, string]> = []; - Object.keys(dependencyMap).forEach((key: string) => { - if (dependencyMap[key].length) { - dependencyMap[key].forEach((dep) => dependencyTree.push([key, dep])); - } else { - // Set no dependency - dependencyTree.push([key, ""]); + const subDepCalcEnd = performance.now(); + const updateChangedDependenciesStart = performance.now(); + // If the global dependency map has changed, re-calculate the sort order for all entities and the + // global inverse dependency map + if (didUpdateDependencyMap) { + // This is being called purely to test for new circular dependencies that might have been added + this.sortedDependencies = this.sortDependencies(this.dependencyMap); + this.inverseDependencyMap = this.getInverseDependencyTree(); } - }); - try { - // sort dependencies and remove empty dependencies - const sortedDependencies = toposort(dependencyTree) - .reverse() - .filter((d) => !!d); - - return { sortedDependencies, dependencyMap, dependencyTree }; - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.DEPENDENCY_ERROR, - message: e.message, + const updateChangedDependenciesStop = performance.now(); + LOGS.push({ + diffCalcDeps: (diffCalcEnd - diffCalcStart).toFixed(2), + subDepCalc: (subDepCalcEnd - subDepCalcStart).toFixed(2), + updateChangedDependencies: ( + updateChangedDependenciesStop - updateChangedDependenciesStart + ).toFixed(2), }); - throw new Error("Dependency Error"); - //return { sortedDependencies: [], dependencyMap: {}, dependencyTree: [] }; + + return { dependenciesOfRemovedPaths, removedPaths }; } + + calculateSubTreeSortOrder( + differences: Diff[], + dependenciesOfRemovedPaths: Array, + removedPaths: Array, + ) { + const changePaths: Set = new Set(dependenciesOfRemovedPaths); + differences.forEach((d) => { + if (d.path) { + // Apply the changes into the evalTree so that it gets the latest changes + applyChange(this.evalTree, undefined, d); + + // If this is a property path change, simply add for evaluation + if (d.path.length > 1) { + const propertyPath = convertPathToString(d.path); + changePaths.add(propertyPath); + + // If this is an array update, trim the array index and add it to the change paths for evaluation + // This is because sometimes inside an object of array time, if only a particular entry changes, the + // difference comes as propertyPath[0].fieldChanged. Another entity could depend on propertyPath and not + // propertyPath[0]. The said entity must be evaluated. + // To do this, we are trimming the array index + if (propertyPath.lastIndexOf("[") > 0) { + changePaths.add( + propertyPath.substr(0, propertyPath.lastIndexOf("[")), + ); + } + } else if (d.path.length === 1) { + /* + When we see a new widget has been added or or delete an old widget ( d.path.length === 1) + We want to add all the dependencies in the sorted order to make + sure all the bindings are evaluated. + */ + this.sortedDependencies.forEach((dependency) => { + if (d.path && dependency.split(".")[0] === d.path[0]) { + changePaths.add(dependency); + } + }); + } + } + }); + + // If a nested property path has changed and someone (say x) is dependent on the parent of the said property, + // x must also be evaluated. For example, the following relationship exists in dependency map: + // < "Input1.defaultText" : ["Table1.selectedRow.email"] > + // If Table1.selectedRow has changed, then Input1.defaultText must also be evaluated because Table1.selectedRow.email + // is a nested property of Table1.selectedRow + const changePathsWithNestedDependants = addDependantsOfNestedPropertyPaths( + Array.from(changePaths), + this.inverseDependencyMap, + ); + + // Now that we have all the root nodes which have to be evaluated, recursively find all the other paths which + // would get impacted because they are dependent on the said root nodes and add them in order + const completeSortOrder = this.getCompleteSortOrder( + changePathsWithNestedDependants, + this.inverseDependencyMap, + ); + // Remove any paths that do no exist in the data tree any more + return _.difference(completeSortOrder, removedPaths); + } + + getInverseDependencyTree(): DependencyMap { + const inverseDag: DependencyMap = {}; + this.sortedDependencies.forEach((propertyPath) => { + const incomingEdges: Array = this.dependencyMap[propertyPath]; + if (incomingEdges) { + incomingEdges.forEach((edge) => { + const node = inverseDag[edge]; + if (node) { + node.push(propertyPath); + } else { + inverseDag[edge] = [propertyPath]; + } + }); + } + }); + return inverseDag; + } + + // TODO: create the lookup dictionary once + // Response from listEntityDependencies only needs to change if the entity itself changed. + // Check if it is possible to make a flat structure with O(1) or at least O(m) lookup instead of O(n*m) + getPropertyPathReferencesInExistingBindings( + dataTree: DataTree, + propertyPath: string, + ) { + const possibleRefs: DependencyMap = {}; + Object.keys(dataTree).forEach((entityName) => { + const entity = dataTree[entityName]; + if ( + isValidEntity(entity) && + (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION || + entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) + ) { + const entityPropertyBindings = this.listEntityDependencies( + entity, + entityName, + ); + Object.keys(entityPropertyBindings).forEach((path) => { + const propertyBindings = entityPropertyBindings[path]; + const references = _.flatten( + propertyBindings.map((binding) => + extractReferencesFromBinding(binding, this.allKeys), + ), + ); + references.forEach((value) => { + if (isChildPropertyPath(propertyPath, value)) { + possibleRefs[path] = propertyBindings; + } + }); + }); + } + }); + return possibleRefs; + } + + evaluateActionBindings( + bindings: string[], + executionParams?: Record | string, + ) { + // We might get execution params as an object or as a string. + // If the user has added a proper object (valid case) it will be an object + // If they have not added any execution params or not an object + // it would be a string (invalid case) + let evaluatedExecutionParams: Record = {}; + if (executionParams && _.isObject(executionParams)) { + evaluatedExecutionParams = this.getDynamicValue( + `{{${JSON.stringify(executionParams)}}}`, + this.evalTree, + false, + ); + } + + // Replace any reference of 'this.params' to 'executionParams' (backwards compatibility) + const bindingsForExecutionParams: string[] = bindings.map( + (binding: string) => + binding.replace(EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_KEY), + ); + + const dataTreeWithExecutionParams = Object.assign({}, this.evalTree, { + [EXECUTION_PARAM_KEY]: evaluatedExecutionParams, + }); + + return bindingsForExecutionParams.map((binding) => + this.getDynamicValue( + `{{${binding}}}`, + dataTreeWithExecutionParams, + false, + ), + ); + } + + clearErrors() { + this.errors = []; + } +} + +const getAllPaths = ( + tree: Record, + prefix = "", + result: Record = {}, +): Record => { + Object.keys(tree).forEach((el) => { + if (Array.isArray(tree[el])) { + const key = `${prefix}${el}`; + result[key] = true; + } else if (typeof tree[el] === "object" && tree[el] !== null) { + const key = `${prefix}${el}`; + result[key] = true; + getAllPaths(tree[el], `${key}.`, result); + } else { + const key = `${prefix}${el}`; + result[key] = true; + } + }); + return result; }; -const calculateSubDependencies = ( +const extractReferencesFromBinding = ( path: string, all: Record, ): Array => { @@ -409,636 +1292,10 @@ const calculateSubDependencies = ( return _.uniq(subDeps); }; -const setTreeLoading = ( - dataTree: DataTree, - dependencyMap: Array<[string, string]>, -) => { - const widgets: string[] = []; - const isLoadingActions: string[] = []; - - // Fetch all actions that are in loading state - Object.keys(dataTree).forEach((e) => { - const entity = dataTree[e]; - if (isValidEntity(entity)) { - if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { - widgets.push(e); - } else if ( - entity.ENTITY_TYPE === ENTITY_TYPE.ACTION && - entity.isLoading - ) { - isLoadingActions.push(e); - } - } - }); - - // get all widget dependencies of those actions - isLoadingActions - .reduce( - (allEntities: string[], curr) => - allEntities.concat(getEntityDependencies(dependencyMap, curr, widgets)), - [], - ) - // set loading to true for those widgets - .forEach((w) => { - const entity = dataTree[w] as DataTreeWidget; - entity.isLoading = true; - }); - return dataTree; -}; - -const getEntityDependencies = ( - dependencyMap: Array<[string, string]>, - entity: string, - entities: string[], -): Array => { - const entityDeps: Record = dependencyMap - .map((d) => [d[1].split(".")[0], d[0].split(".")[0]]) - .filter((d) => d[0] !== d[1]) - .reduce((deps: Record, dep) => { - const key: string = dep[0]; - const value: string = dep[1]; - return { - ...deps, - [key]: deps[key] ? deps[key].concat(value) : [value], - }; - }, {}); - - if (entity in entityDeps) { - const visited = new Set(); - const recFind = ( - keys: Array, - deps: Record, - ): Array => { - let allDeps: string[] = []; - keys - .filter((k) => entities.includes(k)) - .forEach((e) => { - if (visited.has(e)) { - return; - } - visited.add(e); - allDeps = allDeps.concat([e]); - if (e in deps) { - allDeps = allDeps.concat([...recFind(deps[e], deps)]); - } - }); - return allDeps; - }; - return recFind(entityDeps[entity], entityDeps); - } - return []; -}; - -function dependencySortedEvaluateDataTree( - dataTree: DataTree, - dependencyMap: DynamicDependencyMap, - sortedDependencies: Array, -): DataTree { - const tree = _.cloneDeep(dataTree); - try { - return sortedDependencies.reduce( - (currentTree: DataTree, propertyPath: string) => { - const entityName = propertyPath.split(".")[0]; - const entity: DataTreeEntity = currentTree[entityName]; - const unEvalPropertyValue = _.get(currentTree as any, propertyPath); - let evalPropertyValue; - const propertyDependencies = dependencyMap[propertyPath]; - const currentDependencyValues = getCurrentDependencyValues( - propertyDependencies, - currentTree, - propertyPath, - ); - const cachedDependencyValues = dependencyCache.get(propertyPath); - const requiresEval = isDynamicValue(unEvalPropertyValue); - if (requiresEval) { - try { - evalPropertyValue = evaluateDynamicProperty( - propertyPath, - currentTree, - unEvalPropertyValue, - currentDependencyValues, - cachedDependencyValues, - ); - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.EVAL_PROPERTY_ERROR, - message: e.message, - context: { - propertyPath, - }, - }); - evalPropertyValue = undefined; - } - } else { - evalPropertyValue = unEvalPropertyValue; - // If we have stored any previous dependency cache, clear it - // since it is no longer a binding - if (cachedDependencyValues && cachedDependencyValues.length) { - dependencyCache.set(propertyPath, []); - } - } - if (isWidget(entity)) { - const widgetEntity: DataTreeWidget = entity as DataTreeWidget; - const propertyName = propertyPath.split(".")[1]; - if (propertyName) { - let parsedValue = validateAndParseWidgetProperty( - propertyPath, - widgetEntity, - currentTree, - evalPropertyValue, - unEvalPropertyValue, - currentDependencyValues, - cachedDependencyValues, - ); - const defaultPropertyMap = - WIDGET_TYPE_CONFIG_MAP[widgetEntity.type].defaultProperties; - const hasDefaultProperty = propertyName in defaultPropertyMap; - if (hasDefaultProperty) { - const defaultProperty = defaultPropertyMap[propertyName]; - parsedValue = overwriteDefaultDependentProps( - defaultProperty, - parsedValue, - propertyPath, - widgetEntity, - ); - } - return _.set(currentTree, propertyPath, parsedValue); - } - return _.set(currentTree, propertyPath, evalPropertyValue); - } else { - return _.set(currentTree, propertyPath, evalPropertyValue); - } - }, - tree, - ); - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.EVAL_TREE_ERROR, - message: e.message, - }); - return tree; - } -} - -const overwriteDefaultDependentProps = ( - defaultProperty: string, - propertyValue: any, - propertyPath: string, - entity: DataTreeWidget, -) => { - const defaultPropertyCache = getParsedValueCache( - `${entity.widgetName}.${defaultProperty}`, - ); - const propertyCache = getParsedValueCache(propertyPath); - - if ( - propertyValue === undefined || - propertyCache.version < defaultPropertyCache.version - ) { - return defaultPropertyCache.value; - } - return propertyValue; -}; - -const getValidatedTree = (tree: any) => { - return Object.keys(tree).reduce((tree, entityKey: string) => { - const entity = tree[entityKey]; - if (entity && entity.type) { - const parsedEntity = { ...entity }; - Object.keys(entity).forEach((property: string) => { - const hasEvaluatedValue = _.has( - parsedEntity, - `evaluatedValues.${property}`, - ); - const hasValidation = _.has(parsedEntity, `invalidProps.${property}`); - const isSpecialField = [ - "dynamicBindingPathList", - "dynamicTriggerPathList", - "dynamicPropertyPathList", - "evaluatedValues", - "invalidProps", - "validationMessages", - ].includes(property); - - if (!isSpecialField && (!hasValidation || !hasEvaluatedValue)) { - const value = entity[property]; - // Pass it through parse - const { - parsed, - isValid, - message, - transformed, - } = validateWidgetProperty( - entity.type, - property, - value, - entity, - tree, - ); - parsedEntity[property] = parsed; - if (!hasEvaluatedValue) { - const evaluatedValue = isValid - ? parsed - : _.isUndefined(transformed) - ? value - : transformed; - const safeEvaluatedValue = removeFunctions(evaluatedValue); - _.set( - parsedEntity, - `evaluatedValues.${property}`, - safeEvaluatedValue, - ); - } - - const hasValidation = _.has(parsedEntity, `invalidProps.${property}`); - if (!hasValidation && !isValid) { - _.set(parsedEntity, `invalidProps.${property}`, true); - _.set(parsedEntity, `validationMessages.${property}`, message); - } - } - }); - return { ...tree, [entityKey]: parsedEntity }; - } - return tree; - }, tree); -}; - -const getAllPaths = ( - tree: Record, - prefix = "", -): Record => { - return Object.keys(tree).reduce((res: Record, el): Record< - string, - true - > => { - if (Array.isArray(tree[el])) { - const key = `${prefix}${el}`; - return { ...res, [key]: true }; - } else if (typeof tree[el] === "object" && tree[el] !== null) { - const key = `${prefix}${el}`; - return { ...res, [key]: true, ...getAllPaths(tree[el], `${key}.`) }; - } else { - const key = `${prefix}${el}`; - return { ...res, [key]: true }; - } - }, {}); -}; - -const getDynamicBindings = ( - dynamicString: string, -): { stringSegments: string[]; jsSnippets: string[] } => { - // Protect against bad string parse - if (!dynamicString || !_.isString(dynamicString)) { - return { stringSegments: [], jsSnippets: [] }; - } - const sanitisedString = dynamicString.trim(); - // Get the {{binding}} bound values - const stringSegments = getDynamicStringSegments(sanitisedString); - // Get the "binding" path values - const paths = stringSegments.map((segment) => { - const length = segment.length; - const matches = isDynamicValue(segment); - if (matches) { - return segment.substring(2, length - 2); - } - return ""; - }); - return { stringSegments: stringSegments, jsSnippets: paths }; -}; - -//{{}}{{}}} -function getDynamicStringSegments(dynamicString: string): string[] { - let stringSegments = []; - const indexOfDoubleParanStart = dynamicString.indexOf("{{"); - if (indexOfDoubleParanStart === -1) { - return [dynamicString]; - } - //{{}}{{}}} - const firstString = dynamicString.substring(0, indexOfDoubleParanStart); - firstString && stringSegments.push(firstString); - let rest = dynamicString.substring( - indexOfDoubleParanStart, - dynamicString.length, - ); - //{{}}{{}}} - let sum = 0; - for (let i = 0; i <= rest.length - 1; i++) { - const char = rest[i]; - const prevChar = rest[i - 1]; - - if (char === "{") { - sum++; - } else if (char === "}") { - sum--; - if (prevChar === "}" && sum === 0) { - stringSegments.push(rest.substring(0, i + 1)); - rest = rest.substring(i + 1, rest.length); - if (rest) { - stringSegments = stringSegments.concat( - getDynamicStringSegments(rest), - ); - break; - } - } - } - } - if (sum !== 0 && dynamicString !== "") { - return [dynamicString]; - } - return stringSegments; -} - +// TODO cryptic comment below. Dont know if we still need this. Duplicate function // referencing DATA_BIND_REGEX fails for the value "{{Table1.tableData[Table1.selectedRowIndex]}}" if you run it multiple times and don't recreate const isDynamicValue = (value: string): boolean => DATA_BIND_REGEX.test(value); -function getCurrentDependencyValues( - propertyDependencies: Array, - currentTree: DataTree, - currentPropertyPath: string, -): Array { - return propertyDependencies - ? propertyDependencies - .map((path: string) => { - //*** Remove current path from data tree because cached value contains evaluated version while this contains unevaluated version */ - const cleanDataTree = _.omit(currentTree, [currentPropertyPath]); - return _.get(cleanDataTree, path); - }) - .filter((data: any) => { - return data !== undefined; - }) - : []; -} - -const dynamicPropValueCache: Map< - string, - { - unEvaluated: any; - evaluated: any; - } -> = new Map(); - -const parsedValueCache: Map< - string, - { - value: any; - version: number; - } -> = new Map(); - -const getDynamicPropValueCache = (propertyPath: string) => - dynamicPropValueCache.get(propertyPath); - -const getParsedValueCache = (propertyPath: string) => - parsedValueCache.get(propertyPath) || { - value: undefined, - version: 0, - }; - -const clearPropertyCache = (propertyPath: string) => - parsedValueCache.delete(propertyPath); - -/** - * delete all values of a particular widget - * - * @param propertyPath - */ -export const clearPropertyCacheOfWidget = (widgetName: string) => { - parsedValueCache.forEach((value, key) => { - const match = key.match(`${widgetName}.`); - - if (match) return parsedValueCache.delete(key); - }); -}; - -const dependencyCache: Map = new Map(); - -function isValidEntity(entity: DataTreeEntity): entity is DataTreeObjectEntity { - if (!_.isObject(entity)) { - ERRORS.push({ - type: EvalErrorTypes.BAD_UNEVAL_TREE_ERROR, - message: "Data tree entity is not an object", - context: entity, - }); - return false; - } - return "ENTITY_TYPE" in entity; -} - -function isWidget(entity: DataTreeEntity): boolean { - return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET; -} - -function validateAndParseWidgetProperty( - propertyPath: string, - widget: DataTreeWidget, - currentTree: DataTree, - evalPropertyValue: any, - unEvalPropertyValue: string, - currentDependencyValues: Array, - cachedDependencyValues?: Array, -): any { - const entityPropertyName = _.drop(propertyPath.split(".")).join("."); - let valueToValidate = evalPropertyValue; - if (isPathADynamicTrigger(widget, propertyPath)) { - const { triggers } = getDynamicValue( - unEvalPropertyValue, - currentTree, - true, - undefined, - ); - valueToValidate = triggers; - } - const { parsed, isValid, message, transformed } = validateWidgetProperty( - widget.type, - entityPropertyName, - valueToValidate, - widget, - currentTree, - ); - const evaluatedValue = isValid - ? parsed - : _.isUndefined(transformed) - ? evalPropertyValue - : transformed; - const safeEvaluatedValue = removeFunctions(evaluatedValue); - _.set(widget, `evaluatedValues.${entityPropertyName}`, safeEvaluatedValue); - if (!isValid) { - _.set(widget, `invalidProps.${entityPropertyName}`, true); - _.set(widget, `validationMessages.${entityPropertyName}`, message); - } - - if (isPathADynamicTrigger(widget, entityPropertyName)) { - return unEvalPropertyValue; - } else { - const parsedCache = getParsedValueCache(propertyPath); - if ( - !equal(parsedCache.value, parsed) || - (cachedDependencyValues !== undefined && - !equal(currentDependencyValues, cachedDependencyValues)) - ) { - parsedValueCache.set(propertyPath, { - value: parsed, - version: Date.now(), - }); - } - return parsed; - } -} - -function evaluateDynamicProperty( - propertyPath: string, - currentTree: DataTree, - unEvalPropertyValue: any, - currentDependencyValues: Array, - cachedDependencyValues?: Array, -): any { - const cacheObj = getDynamicPropValueCache(propertyPath); - const isCacheHit = - cacheObj && - equal(cacheObj.unEvaluated, unEvalPropertyValue) && - cachedDependencyValues !== undefined && - equal(currentDependencyValues, cachedDependencyValues); - if (isCacheHit && cacheObj) { - return cacheObj.evaluated; - } else { - LOGS.push("eval " + propertyPath); - const dynamicResult = getDynamicValue( - unEvalPropertyValue, - currentTree, - false, - ); - dynamicPropValueCache.set(propertyPath, { - evaluated: dynamicResult, - unEvaluated: unEvalPropertyValue, - }); - dependencyCache.set(propertyPath, currentDependencyValues); - return dynamicResult; - } -} - -type EvalResult = { - result: any; - triggers?: ActionDescription[]; -}; -// Paths are expected to have "{name}.{path}" signature -// Also returns any action triggers found after evaluating value -const evaluateDynamicBoundValue = ( - data: DataTree, - path: string, - callbackData?: Array, -): EvalResult => { - try { - const unescapedJS = unescapeJS(path).replace(/(\r\n|\n|\r)/gm, ""); - return evaluate(unescapedJS, data, callbackData); - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.UNESCAPE_STRING_ERROR, - message: e.message, - context: { - path, - }, - }); - return { result: undefined, triggers: [] }; - } -}; - -const evaluate = ( - js: string, - data: DataTree, - callbackData?: Array, -): EvalResult => { - const scriptToEvaluate = ` - function closedFunction () { - const result = ${js}; - return { result, triggers: self.triggers } - } - closedFunction() - `; - const scriptWithCallback = ` - function callback (script) { - const userFunction = script; - const result = userFunction.apply(self, CALLBACK_DATA); - return { result, triggers: self.triggers }; - } - callback(${js}); - `; - const script = callbackData ? scriptWithCallback : scriptToEvaluate; - try { - const { result, triggers } = (function() { - /**** Setting the eval context ****/ - const GLOBAL_DATA: Record = {}; - ///// Adding callback data - GLOBAL_DATA.CALLBACK_DATA = callbackData; - ///// Adding Data tree - Object.keys(data).forEach((datum) => { - GLOBAL_DATA[datum] = data[datum]; - }); - ///// Fixing action paths and capturing their execution response - if (data.actionPaths) { - GLOBAL_DATA.triggers = []; - const pusher = function( - this: DataTree, - action: any, - ...payload: any[] - ) { - const actionPayload = action(...payload); - GLOBAL_DATA.triggers.push(actionPayload); - }; - GLOBAL_DATA.actionPaths.forEach((path: string) => { - const action = _.get(GLOBAL_DATA, path); - const entity = _.get(GLOBAL_DATA, path.split(".")[0]); - if (action) { - _.set(GLOBAL_DATA, path, pusher.bind(data, action.bind(entity))); - } - }); - } - - // Set it to self - Object.keys(GLOBAL_DATA).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: No types available - self[key] = GLOBAL_DATA[key]; - }); - - ///// Adding extra libraries separately - extraLibraries.forEach((library) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: No types available - self[library.accessor] = library.lib; - }); - - ///// Remove all unsafe functions - unsafeFunctionForEval.forEach((func) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: No types available - self[func] = undefined; - }); - - const evalResult = eval(script); - - // Remove it from self - // This is needed so that next eval can have a clean sheet - Object.keys(GLOBAL_DATA).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: No types available - delete self[key]; - }); - - return evalResult; - })(); - return { result, triggers }; - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.EVAL_ERROR, - message: e.message, - context: { - binding: js, - }, - }); - return { result: undefined, triggers: [] }; - } -}; - // For creating a final value where bindings could be in a template format const createDynamicValueString = ( binding: string, @@ -1064,756 +1321,117 @@ const createDynamicValueString = ( return finalValue; }; -const getDynamicValue = ( - dynamicBinding: string, - data: DataTree, - returnTriggers: boolean, - callBackData?: Array, -) => { - // Get the {{binding}} bound values - const { stringSegments, jsSnippets } = getDynamicBindings(dynamicBinding); - if (returnTriggers) { - const result = evaluateDynamicBoundValue(data, jsSnippets[0], callBackData); - return result.triggers; +function isValidEntity(entity: DataTreeEntity): entity is DataTreeObjectEntity { + if (!_.isObject(entity)) { + // ERRORS.push({ + // type: EvalErrorTypes.BAD_UNEVAL_TREE_ERROR, + // message: "Data tree entity is not an object", + // context: entity, + // }); + return false; } - if (stringSegments.length) { - // Get the Data Tree value of those "binding "paths - const values = jsSnippets.map((jsSnippet, index) => { - if (jsSnippet) { - const result = evaluateDynamicBoundValue(data, jsSnippet, callBackData); - return result.result; - } else { - return stringSegments[index]; - } - }); + return "ENTITY_TYPE" in entity; +} - // if it is just one binding, no need to create template string - if (stringSegments.length === 1) return values[0]; - // else return a string template with bindings - return createDynamicValueString(dynamicBinding, stringSegments, values); - } - return undefined; -}; +function isWidget(entity: DataTreeEntity): entity is DataTreeWidget { + return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET; +} -const validateWidgetProperty = ( - widgetType: WidgetType, - property: string, - value: any, - props: WidgetProps, - dataTree?: DataTree, -) => { - const propertyValidationTypes = - WIDGET_TYPE_CONFIG_MAP[widgetType].validations; - const validationTypeOrValidator = propertyValidationTypes[property]; - let validator; +function isAction(entity: DataTreeEntity): entity is DataTreeAction { + return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.ACTION; +} - if (typeof validationTypeOrValidator === "function") { - validator = validationTypeOrValidator; - } else { - validator = VALIDATORS[validationTypeOrValidator]; - } - if (validator) { - return validator(value, props, dataTree); - } else { - return { isValid: true, parsed: value }; - } -}; - -const clearCaches = () => { - dynamicPropValueCache.clear(); - dependencyCache.clear(); - parsedValueCache.clear(); -}; - -const VALIDATORS: Record = { - [VALIDATION_TYPES.TEXT]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (isUndefined(value) || value === null) { - return { - isValid: true, - parsed: value, - message: "", - }; - } - if (isObject(value)) { - return { - isValid: false, - parsed: JSON.stringify(value, null, 2), - message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, - }; - } - let isValid = isString(value); - if (!isValid) { - try { - parsed = toString(value); - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to string`); - console.error(e); - return { - isValid: false, - parsed: "", - message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.REGEX]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.TEXT]( - value, - props, - dataTree, - ); - - if (isValid) { - try { - new RegExp(parsed); - } catch (e) { - return { - isValid: false, - parsed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: regex`, - }; - } - } - - return { isValid, parsed, message }; - }, - [VALIDATION_TYPES.NUMBER]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (isUndefined(value)) { - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - let isValid = isNumber(value); - if (!isValid) { - try { - parsed = toNumber(value); - if (isNaN(parsed)) { - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to number`); - console.error(e); - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.BOOLEAN]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (isUndefined(value)) { - return { - isValid: false, - parsed: false, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, - }; - } - const isABoolean = isBoolean(value); - const isStringTrueFalse = value === "true" || value === "false"; - const isValid = isABoolean || isStringTrueFalse; - if (isStringTrueFalse) parsed = value !== "false"; - if (!isValid) { - return { - isValid: isValid, - parsed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.OBJECT]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (isUndefined(value)) { - return { - isValid: false, - parsed: {}, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, - }; - } - let isValid = isObject(value); - if (!isValid) { - try { - parsed = JSON.parse(value); - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to object`); - console.error(e); - return { - isValid: false, - parsed: {}, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.ARRAY]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - try { - if (isUndefined(value)) { - return { - isValid: false, - parsed: [], - transformed: undefined, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - if (isString(value)) { - parsed = JSON.parse(parsed as string); - } - if (!Array.isArray(parsed)) { - return { - isValid: false, - parsed: [], - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - return { isValid: true, parsed, transformed: parsed }; - } catch (e) { - console.error(e); - return { - isValid: false, - parsed: [], - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - }, - [VALIDATION_TYPES.TABS_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, - }; - } else if (!every(parsed, (datum) => isObject(datum))) { - return { - isValid: false, - parsed: [], - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.TABLE_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, transformed, parsed } = VALIDATORS.ARRAY( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed: [], - transformed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, - }; - } - const isValidTableData = every(parsed, (datum) => { - return ( - isPlainObject(datum) && - Object.keys(datum).filter((key) => isString(key) && key.length === 0) - .length === 0 - ); - }); - if (!isValidTableData) { - return { - isValid: false, - parsed: [], - transformed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.CHART_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Chart Data`, - }; - } - let validationMessage = ""; - let index = 0; - const isValidChartData = every( - parsed, - (datum: { name: string; data: any }) => { - const validatedResponse: { - isValid: boolean; - parsed: Array; - message?: string; - } = VALIDATORS[VALIDATION_TYPES.ARRAY](datum.data, props, dataTree); - validationMessage = `${index}##${WIDGET_TYPE_VALIDATION_ERROR}: [{ "x": "val", "y": "val" }]`; - let isValidChart = validatedResponse.isValid; - if (validatedResponse.isValid) { - datum.data = validatedResponse.parsed; - isValidChart = every( - datum.data, - (chartPoint: { x: string; y: any }) => { - return ( - isObject(chartPoint) && - isString(chartPoint.x) && - !isUndefined(chartPoint.y) - ); - }, - ); - } - index++; - return isValidChart; - }, - ); - if (!isValidChartData) { - return { - isValid: false, - parsed: [], - transformed: parsed, - message: validationMessage, - }; - } - return { isValid, parsed, transformed: parsed }; - }, - [VALIDATION_TYPES.MARKERS]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, - }; - } else if (!every(parsed, (datum) => isObject(datum))) { - return { - isValid: false, - parsed: [], - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.OPTIONS_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, - }; - } - try { - const isValidOption = (option: { label: any; value: any }) => - _.isObject(option) && - _.isString(option.label) && - _.isString(option.value) && - !_.isEmpty(option.label) && - !_.isEmpty(option.value); - - const hasOptions = every(parsed, isValidOption); - const validOptions = parsed.filter(isValidOption); - const uniqValidOptions = _.uniqBy(validOptions, "value"); - - if (!hasOptions || uniqValidOptions.length !== validOptions.length) { - return { - isValid: false, - parsed: uniqValidOptions, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, - }; - } - return { isValid, parsed }; - } catch (e) { - console.error(e); - return { - isValid: false, - parsed: [], - transformed: parsed, - }; - } - }, - [VALIDATION_TYPES.DATE]: ( - dateString: string, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const today = moment() - .hour(0) - .minute(0) - .second(0) - .millisecond(0); - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; - - const todayDateString = today.format(dateFormat); - if (dateString === undefined) { - return { - isValid: false, - parsed: "", - message: - `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat - ? props.dateFormat - : "", - }; - } - const isValid = moment(dateString, dateFormat).isValid(); - const parsed = isValid ? dateString : todayDateString; - return { - isValid, - parsed, - message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date`, - }; - }, - [VALIDATION_TYPES.DEFAULT_DATE]: ( - dateString: string, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const today = moment() - .hour(0) - .minute(0) - .second(0) - .millisecond(0); - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; - - const todayDateString = today.format(dateFormat); - if (dateString === undefined) { - return { - isValid: false, - parsed: "", - message: - `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat - ? props.dateFormat - : "", - }; - } - const parsedCurrentDate = moment(dateString, dateFormat); - let isValid = parsedCurrentDate.isValid(); - const parsedMinDate = moment(props.minDate, dateFormat); - const parsedMaxDate = moment(props.maxDate, dateFormat); - - // checking for max/min date range - if (isValid) { - if ( - parsedMinDate.isValid() && - parsedCurrentDate.isBefore(parsedMinDate) +const addFunctions = (dataTree: Readonly): DataTree => { + const withFunction: DataTree = _.cloneDeep(dataTree); + withFunction.actionPaths = []; + Object.keys(withFunction).forEach((entityName) => { + const entity = withFunction[entityName]; + if (isAction(entity)) { + const runFunction = function( + this: DataTreeAction, + onSuccess: string, + onError: string, + params = "", ) { - isValid = false; - } - - if ( - isValid && - parsedMaxDate.isValid() && - parsedCurrentDate.isAfter(parsedMaxDate) - ) { - isValid = false; - } - } - - const parsed = isValid ? dateString : todayDateString; - - return { - isValid, - parsed, - message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date R`, - }; - }, - [VALIDATION_TYPES.ACTION_SELECTOR]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - if (Array.isArray(value) && value.length) { - return { - isValid: true, - parsed: undefined, - transformed: "Function Call", - }; - } - /* - if (_.isString(value)) { - if (value.indexOf("navigateTo") !== -1) { - const pageNameOrUrl = modalGetter(value); - if (dataTree) { - if (isDynamicValue(pageNameOrUrl)) { - return { - isValid: true, - parsed: value, - }; - } - const isPage = - (dataTree.pageList as PageListPayload).findIndex( - page => page.pageName === pageNameOrUrl, - ) !== -1; - const isValidUrl = URL_REGEX.test(pageNameOrUrl); - if (!(isValidUrl || isPage)) { - return { - isValid: false, - parsed: value, - message: `${NAVIGATE_TO_VALIDATION_ERROR}`, - }; - } - } - } - } - */ - return { - isValid: false, - parsed: undefined, - transformed: "undefined", - message: "Not a function call", - }; - }, - [VALIDATION_TYPES.ARRAY_ACTION_SELECTOR]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - let isValidFinal = isValid; - let finalParsed = parsed.slice(); - if (isValid) { - finalParsed = parsed.map((value: any) => { - const { isValid, message } = VALIDATORS[ - VALIDATION_TYPES.ACTION_SELECTOR - ](value.dynamicTrigger, props, dataTree); - - isValidFinal = isValidFinal && isValid; return { - ...value, - message: message, - isValid: isValid, + type: "RUN_ACTION", + payload: { + actionId: this.actionId, + onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "", + onError: onError ? `{{${onError.toString()}}}` : "", + params, + }, }; - }); + }; + _.set(withFunction, `${entityName}.run`, runFunction); + withFunction.actionPaths && + withFunction.actionPaths.push(`${entityName}.run`); } - - return { - isValid: isValidFinal, - parsed: finalParsed, - message: message, - }; - }, - [VALIDATION_TYPES.SELECTED_TAB]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const tabs = - props.tabs && isString(props.tabs) - ? JSON.parse(props.tabs) - : props.tabs && Array.isArray(props.tabs) - ? props.tabs - : []; - const tabNames = tabs.map((i: { label: string; id: string }) => i.label); - const isValidTabName = tabNames.includes(value); - return { - isValid: isValidTabName, - parsed: value, - message: isValidTabName - ? "" - : `${WIDGET_TYPE_VALIDATION_ERROR}: Invalid tab name.`, - }; - }, - [VALIDATION_TYPES.DEFAULT_OPTION_VALUE]: ( - value: string | string[], - props: WidgetProps, - dataTree?: DataTree, - ) => { - let values = value; - - if (props) { - if (props.selectionType === "SINGLE_SELECT") { - return VALIDATORS[VALIDATION_TYPES.TEXT](value, props, dataTree); - } else if (props.selectionType === "MULTI_SELECT") { - if (typeof value === "string") { - try { - values = JSON.parse(value); - if (!Array.isArray(values)) { - throw new Error(); - } - } catch { - values = value.length ? value.split(",") : []; - if (values.length > 0) { - values = values.map((value) => value.trim()); - } - } - } - } - } - - if (Array.isArray(values)) { - values = _.uniq(values); - } - - return { - isValid: true, - parsed: values, - }; - }, - [VALIDATION_TYPES.DEFAULT_SELECTED_ROW]: ( - value: string | string[], - props: WidgetProps, - dataTree?: DataTree, - ) => { - let values = value; - - if (props) { - if (props.multiRowSelection) { - if (typeof value === "string") { - try { - values = JSON.parse(value); - if (!Array.isArray(values)) { - throw new Error(); - } - } catch { - values = value.length ? value.split(",") : []; - if (values.length > 0) { - let numbericValues = values.map((value) => { - return isNumber(value.trim()) ? -1 : Number(value.trim()); - }); - numbericValues = _.uniq(numbericValues); - return { - isValid: true, - parsed: numbericValues, - }; - } - } - } - } else { - try { - if (value === "") { - return { - isValid: true, - parsed: -1, - }; - } - const parsed = toNumber(value); - return { - isValid: true, - parsed: parsed, - }; - } catch (e) { - return { - isValid: true, - parsed: -1, - }; - } - } - } - return { - isValid: true, - parsed: values, - }; - }, -}; - -export const makeParentsDependOnChildren = ( - depMap: DynamicDependencyMap, -): DynamicDependencyMap => { - //return depMap; - // Make all parents depend on child - Object.keys(depMap).forEach((key) => { - depMap = makeParentsDependOnChild(depMap, key); - depMap[key].forEach((path) => { - depMap = makeParentsDependOnChild(depMap, path); - }); }); - return depMap; -}; -export const makeParentsDependOnChild = ( - depMap: DynamicDependencyMap, - child: string, -): DynamicDependencyMap => { - const result: DynamicDependencyMap = depMap; - let curKey = child; - const rgx = /^(.*)\..*$/; - let matches: Array | null; - // Note: The `=` is intentional - // Stops looping when match is null - while ((matches = curKey.match(rgx)) !== null) { - const parentKey = matches[1]; - // Todo: switch everything to set. - const existing = new Set(result[parentKey] || []); - existing.add(curKey); - result[parentKey] = Array.from(existing); - curKey = parentKey; - } - return result; + withFunction.navigateTo = function( + pageNameOrUrl: string, + params: Record, + ) { + return { + type: "NAVIGATE_TO", + payload: { pageNameOrUrl, params }, + }; + }; + withFunction.actionPaths.push("navigateTo"); + + withFunction.showAlert = function(message: string, style: string) { + return { + type: "SHOW_ALERT", + payload: { message, style }, + }; + }; + withFunction.actionPaths.push("showAlert"); + + withFunction.showModal = function(modalName: string) { + return { + type: "SHOW_MODAL_BY_NAME", + payload: { modalName }, + }; + }; + withFunction.actionPaths.push("showModal"); + + withFunction.closeModal = function(modalName: string) { + return { + type: "CLOSE_MODAL", + payload: { modalName }, + }; + }; + withFunction.actionPaths.push("closeModal"); + + withFunction.storeValue = function(key: string, value: string) { + return { + type: "STORE_VALUE", + payload: { key, value }, + }; + }; + withFunction.actionPaths.push("storeValue"); + + withFunction.download = function(data: string, name: string, type: string) { + return { + type: "DOWNLOAD", + payload: { data, name, type }, + }; + }; + withFunction.actionPaths.push("download"); + + withFunction.copyToClipboard = function( + data: string, + options?: { debug?: boolean; format?: string }, + ) { + return { + type: "COPY_TO_CLIPBOARD", + payload: { + data, + options: { debug: options?.debug, format: options?.format }, + }, + }; + }; + withFunction.actionPaths.push("copyToClipboard"); + + return withFunction; }; diff --git a/app/client/src/workers/evaluationUtils.test.ts b/app/client/src/workers/evaluationUtils.test.ts new file mode 100644 index 0000000000..0419fb5ada --- /dev/null +++ b/app/client/src/workers/evaluationUtils.test.ts @@ -0,0 +1,17 @@ +import { isChildPropertyPath } from "./evaluationUtils"; + +describe("isChildPropertyPath function", () => { + it("works", () => { + const cases: Array<[string, string, boolean]> = [ + ["Table1.selectedRow", "Table1.selectedRows", false], + ["Table1.selectedRow", "Table1.selectedRow.email", true], + ["Table1.selectedRow", "1Table1.selectedRow", false], + ["Table1.selectedRow", "Table11selectedRow", false], + ["Table1.selectedRow", "Table1.selectedRow", true], + ]; + cases.forEach((testCase) => { + const result = isChildPropertyPath(testCase[0], testCase[1]); + expect(result).toBe(testCase[2]); + }); + }); +}); diff --git a/app/client/src/workers/evaluationUtils.ts b/app/client/src/workers/evaluationUtils.ts new file mode 100644 index 0000000000..3a94668c83 --- /dev/null +++ b/app/client/src/workers/evaluationUtils.ts @@ -0,0 +1,309 @@ +import { DependencyMap, isDynamicValue } from "../utils/DynamicBindingUtils"; +import { WidgetType } from "../constants/WidgetConstants"; +import { WidgetProps } from "../widgets/BaseWidget"; +import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; +import { VALIDATORS } from "./validations"; +import { Diff } from "deep-diff"; +import { + DataTree, + DataTreeEntity, + DataTreeWidget, + ENTITY_TYPE, +} from "../entities/DataTree/dataTreeFactory"; +import _ from "lodash"; + +export enum DataTreeDiffEvent { + NEW = "NEW", + DELETE = "DELETE", + EDIT = "EDIT", + NOOP = "NOOP", +} + +type DataTreeDiff = { + payload: { + propertyPath: string; + value?: string; + }; + event: DataTreeDiffEvent; +}; + +export class CrashingError extends Error {} + +export const convertPathToString = (arrPath: Array) => { + let string = ""; + arrPath.forEach((segment) => { + if (typeof segment === "string") { + if (string.length !== 0) { + string = string + "."; + } + string = string + segment; + } else { + string = string + "[" + segment + "]"; + } + }); + return string; +}; + +export const translateDiffEventToDataTreeDiffEvent = ( + difference: Diff, +): DataTreeDiff => { + const result: DataTreeDiff = { + payload: { + propertyPath: "", + value: "", + }, + event: DataTreeDiffEvent.NOOP, + }; + if (!difference.path) { + return result; + } + const propertyPath = convertPathToString(difference.path); + switch (difference.kind) { + case "N": { + result.event = DataTreeDiffEvent.NEW; + result.payload = { + propertyPath, + }; + break; + } + case "D": { + result.event = DataTreeDiffEvent.DELETE; + result.payload = { propertyPath }; + break; + } + case "E": { + const rhsChange = + typeof difference.rhs === "string" && isDynamicValue(difference.rhs); + + const lhsChange = + typeof difference.lhs === "string" && isDynamicValue(difference.lhs); + + if (rhsChange || lhsChange) { + result.event = DataTreeDiffEvent.EDIT; + result.payload = { + propertyPath, + value: difference.rhs, + }; + } else { + // Handle static value changes that change structure that can lead to + // old bindings being eligible + if ( + difference.lhs === undefined && + typeof difference.rhs === "object" + ) { + result.event = DataTreeDiffEvent.NEW; + result.payload = { propertyPath }; + } + if ( + difference.rhs === undefined && + typeof difference.lhs === "object" + ) { + result.event = DataTreeDiffEvent.DELETE; + result.payload = { propertyPath }; + } + } + + break; + } + case "A": { + break; + } + default: { + break; + } + } + return result; +}; + +export const isPropertyPathOrNestedPath = ( + path: string, + comparePath: string, +): boolean => { + return path === comparePath || comparePath.startsWith(`${path}.`); +}; + +/* + Table1.selectedRow + Table1.selectedRow.email: ["Input1.defaultText"] + */ + +export const addDependantsOfNestedPropertyPaths = ( + parentPaths: Array, + inverseMap: DependencyMap, +): Array => { + const withNestedPaths: Set = new Set(); + const dependantNodes = Object.keys(inverseMap); + parentPaths.forEach((propertyPath) => { + withNestedPaths.add(propertyPath); + dependantNodes + .filter((dependantNodePath) => + isPropertyPathOrNestedPath(propertyPath, dependantNodePath), + ) + .forEach((dependantNodePath) => { + inverseMap[dependantNodePath].forEach((path) => { + withNestedPaths.add(path); + }); + }); + }); + return [...withNestedPaths.values()]; +}; + +export function isWidget(entity: DataTreeEntity): boolean { + return ( + typeof entity === "object" && + "ENTITY_TYPE" in entity && + entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET + ); +} + +export function isAction(entity: DataTreeEntity): boolean { + return ( + typeof entity === "object" && + "ENTITY_TYPE" in entity && + entity.ENTITY_TYPE === ENTITY_TYPE.ACTION + ); +} + +// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing +// Check issue https://github.com/appsmithorg/appsmith/issues/719 +export const removeFunctions = (value: any) => { + if (_.isFunction(value)) { + return "Function call"; + } else if (_.isObject(value) && _.some(value, _.isFunction)) { + return JSON.parse(JSON.stringify(value)); + } else { + return value; + } +}; + +export const removeFunctionsFromDataTree = (dataTree: DataTree) => { + dataTree.actionPaths?.forEach((functionPath) => { + _.set(dataTree, functionPath, {}); + }); + delete dataTree.actionPaths; + return dataTree; +}; + +export const makeParentsDependOnChildren = ( + depMap: DependencyMap, +): DependencyMap => { + //return depMap; + // Make all parents depend on child + Object.keys(depMap).forEach((key) => { + depMap = makeParentsDependOnChild(depMap, key); + depMap[key].forEach((path) => { + depMap = makeParentsDependOnChild(depMap, path); + }); + }); + return depMap; +}; +export const makeParentsDependOnChild = ( + depMap: DependencyMap, + child: string, +): DependencyMap => { + const result: DependencyMap = depMap; + let curKey = child; + const rgx = /^(.*)\..*$/; + let matches: Array | null; + // Note: The `=` is intentional + // Stops looping when match is null + while ((matches = curKey.match(rgx)) !== null) { + const parentKey = matches[1]; + // Todo: switch everything to set. + const existing = new Set(result[parentKey] || []); + existing.add(curKey); + result[parentKey] = Array.from(existing); + curKey = parentKey; + } + return result; +}; + +export function validateWidgetProperty( + widgetConfigMap: WidgetTypeConfigMap, + widgetType: WidgetType, + property: string, + value: any, + props: WidgetProps, + dataTree?: DataTree, +) { + const propertyValidationTypes = widgetConfigMap[widgetType].validations; + const validationTypeOrValidator = propertyValidationTypes[property]; + let validator; + + if (typeof validationTypeOrValidator === "function") { + validator = validationTypeOrValidator; + } else { + validator = VALIDATORS[validationTypeOrValidator]; + } + if (validator) { + return validator(value, props, dataTree); + } else { + return { isValid: true, parsed: value }; + } +} + +export function getValidatedTree( + widgetConfigMap: WidgetTypeConfigMap, + tree: DataTree, + only?: Set, +) { + return Object.keys(tree).reduce((tree, entityKey: string) => { + if (only && only.size) { + if (!only.has(entityKey)) { + return tree; + } + } + const entity = tree[entityKey] as DataTreeWidget; + if (!isWidget(entity)) { + return tree; + } + const parsedEntity = { ...entity }; + Object.keys(entity).forEach((property: string) => { + const validationProperties = widgetConfigMap[entity.type].validations; + + if (property in validationProperties) { + const value = _.get(entity, property); + // Pass it through parse + const { + parsed, + isValid, + message, + transformed, + } = validateWidgetProperty( + widgetConfigMap, + entity.type, + property, + value, + entity, + tree, + ); + parsedEntity[property] = parsed; + const evaluatedValue = isValid + ? parsed + : _.isUndefined(transformed) + ? value + : transformed; + const safeEvaluatedValue = removeFunctions(evaluatedValue); + _.set(parsedEntity, `evaluatedValues.${property}`, safeEvaluatedValue); + if (!isValid) { + _.set(parsedEntity, `invalidProps.${property}`, true); + _.set(parsedEntity, `validationMessages.${property}`, message); + } else { + _.set(parsedEntity, `invalidProps.${property}`, false); + _.set(parsedEntity, `validationMessages.${property}`, ""); + } + } + }); + return { ...tree, [entityKey]: parsedEntity }; + }, tree); +} + +export const isChildPropertyPath = ( + parentPropertyPath: string, + childPropertyPath: string, +): boolean => { + const regexTest = new RegExp( + `^${parentPropertyPath.replace(".", "\\.")}(\\.\\S+)?$`, + ); + return regexTest.test(childPropertyPath); +}; diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts new file mode 100644 index 0000000000..9b8060627e --- /dev/null +++ b/app/client/src/workers/validations.ts @@ -0,0 +1,684 @@ +import { + ISO_DATE_FORMAT, + VALIDATION_TYPES, + ValidationResponse, + ValidationType, + Validator, +} from "../constants/WidgetValidation"; +import { DataTree } from "../entities/DataTree/dataTreeFactory"; +import _, { + every, + isBoolean, + isNumber, + isObject, + isString, + isUndefined, + toNumber, + toString, +} from "lodash"; +import { WidgetProps } from "../widgets/BaseWidget"; +import { WIDGET_TYPE_VALIDATION_ERROR } from "../constants/messages"; +import moment from "moment"; + +export const VALIDATORS: Record = { + [VALIDATION_TYPES.TEXT]: (value: any): ValidationResponse => { + let parsed = value; + if (isUndefined(value) || value === null) { + return { + isValid: true, + parsed: value, + message: "", + }; + } + if (isObject(value)) { + return { + isValid: false, + parsed: JSON.stringify(value, null, 2), + message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, + }; + } + let isValid = isString(value); + if (!isValid) { + try { + parsed = toString(value); + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to string`); + console.error(e); + return { + isValid: false, + parsed: "", + message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.REGEX]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.TEXT]( + value, + props, + dataTree, + ); + + if (isValid) { + try { + new RegExp(parsed); + } catch (e) { + return { + isValid: false, + parsed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: regex`, + }; + } + } + + return { isValid, parsed, message }; + }, + [VALIDATION_TYPES.NUMBER]: (value: any): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + let isValid = isNumber(value); + if (!isValid) { + try { + parsed = toNumber(value); + if (isNaN(parsed)) { + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to number`); + console.error(e); + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.BOOLEAN]: (value: any): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: false, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, + }; + } + const isABoolean = isBoolean(value); + const isStringTrueFalse = value === "true" || value === "false"; + const isValid = isABoolean || isStringTrueFalse; + if (isStringTrueFalse) parsed = value !== "false"; + if (!isValid) { + return { + isValid: isValid, + parsed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.OBJECT]: (value: any): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: {}, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, + }; + } + let isValid = isObject(value); + if (!isValid) { + try { + parsed = JSON.parse(value); + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to object`); + console.error(e); + return { + isValid: false, + parsed: {}, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.ARRAY]: (value: any): ValidationResponse => { + let parsed = value; + try { + if (isUndefined(value)) { + return { + isValid: false, + parsed: [], + transformed: undefined, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + if (isString(value)) { + parsed = JSON.parse(parsed as string); + } + if (!Array.isArray(parsed)) { + return { + isValid: false, + parsed: [], + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + return { isValid: true, parsed, transformed: parsed }; + } catch (e) { + console.error(e); + return { + isValid: false, + parsed: [], + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + }, + [VALIDATION_TYPES.TABS_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, + }; + } else if (!every(parsed, (datum) => isObject(datum))) { + return { + isValid: false, + parsed: [], + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.TABLE_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, transformed, parsed } = VALIDATORS.ARRAY( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed: [], + transformed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, + }; + } + const isValidTableData = every(parsed, (datum) => { + return ( + isObject(datum) && + Object.keys(datum).filter((key) => isString(key) && key.length === 0) + .length === 0 + ); + }); + if (!isValidTableData) { + return { + isValid: false, + parsed: [], + transformed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.CHART_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Chart Data`, + }; + } + let validationMessage = ""; + let index = 0; + const isValidChartData = every( + parsed, + (datum: { name: string; data: any }) => { + const validatedResponse: { + isValid: boolean; + parsed: Array; + message?: string; + } = VALIDATORS[VALIDATION_TYPES.ARRAY](datum.data, props, dataTree); + validationMessage = `${index}##${WIDGET_TYPE_VALIDATION_ERROR}: [{ "x": "val", "y": "val" }]`; + let isValidChart = validatedResponse.isValid; + if (validatedResponse.isValid) { + datum.data = validatedResponse.parsed; + isValidChart = every( + datum.data, + (chartPoint: { x: string; y: any }) => { + return ( + isObject(chartPoint) && + isString(chartPoint.x) && + !isUndefined(chartPoint.y) + ); + }, + ); + } + index++; + return isValidChart; + }, + ); + if (!isValidChartData) { + return { + isValid: false, + parsed: [], + transformed: parsed, + message: validationMessage, + }; + } + return { isValid, parsed, transformed: parsed }; + }, + [VALIDATION_TYPES.MARKERS]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, + }; + } else if ( + !every( + parsed, + (datum) => VALIDATORS[VALIDATION_TYPES.LAT_LONG](datum, props).isValid, + ) + ) { + return { + isValid: false, + parsed: [], + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.OPTIONS_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, + }; + } + try { + const isValidOption = (option: { label: any; value: any }) => + _.isObject(option) && + _.isString(option.label) && + _.isString(option.value) && + !_.isEmpty(option.label) && + !_.isEmpty(option.value); + + const hasOptions = every(parsed, isValidOption); + const validOptions = parsed.filter(isValidOption); + const uniqValidOptions = _.uniqBy(validOptions, "value"); + + if (!hasOptions || uniqValidOptions.length !== validOptions.length) { + return { + isValid: false, + parsed: uniqValidOptions, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, + }; + } + return { isValid, parsed }; + } catch (e) { + console.error(e); + return { + isValid: false, + parsed: [], + transformed: parsed, + }; + } + }, + [VALIDATION_TYPES.DATE]: ( + dateString: string, + props: WidgetProps, + ): ValidationResponse => { + const today = moment() + .hour(0) + .minute(0) + .second(0) + .millisecond(0); + const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + + const todayDateString = today.format(dateFormat); + if (dateString === undefined) { + return { + isValid: false, + parsed: "", + message: + `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat + ? props.dateFormat + : "", + }; + } + const isValid = moment(dateString, dateFormat).isValid(); + const parsed = isValid ? dateString : todayDateString; + return { + isValid, + parsed, + message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date`, + }; + }, + [VALIDATION_TYPES.DEFAULT_DATE]: ( + dateString: string, + props: WidgetProps, + ): ValidationResponse => { + const today = moment() + .hour(0) + .minute(0) + .second(0) + .millisecond(0); + const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + + const todayDateString = today.format(dateFormat); + if (dateString === undefined) { + return { + isValid: false, + parsed: "", + message: + `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat + ? props.dateFormat + : "", + }; + } + const parsedCurrentDate = moment(dateString, dateFormat); + let isValid = parsedCurrentDate.isValid(); + const parsedMinDate = moment(props.minDate, dateFormat); + const parsedMaxDate = moment(props.maxDate, dateFormat); + + // checking for max/min date range + if (isValid) { + if ( + parsedMinDate.isValid() && + parsedCurrentDate.isBefore(parsedMinDate) + ) { + isValid = false; + } + + if ( + isValid && + parsedMaxDate.isValid() && + parsedCurrentDate.isAfter(parsedMaxDate) + ) { + isValid = false; + } + } + + const parsed = isValid ? dateString : todayDateString; + + return { + isValid, + parsed, + message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date R`, + }; + }, + [VALIDATION_TYPES.ACTION_SELECTOR]: (value: any): ValidationResponse => { + if (Array.isArray(value) && value.length) { + return { + isValid: true, + parsed: undefined, + transformed: "Function Call", + }; + } + /* + if (_.isString(value)) { + if (value.indexOf("navigateTo") !== -1) { + const pageNameOrUrl = modalGetter(value); + if (dataTree) { + if (isDynamicValue(pageNameOrUrl)) { + return { + isValid: true, + parsed: value, + }; + } + const isPage = + (dataTree.pageList as PageListPayload).findIndex( + page => page.pageName === pageNameOrUrl, + ) !== -1; + const isValidUrl = URL_REGEX.test(pageNameOrUrl); + if (!(isValidUrl || isPage)) { + return { + isValid: false, + parsed: value, + message: `${NAVIGATE_TO_VALIDATION_ERROR}`, + }; + } + } + } + } + */ + return { + isValid: false, + parsed: undefined, + transformed: "undefined", + message: "Not a function call", + }; + }, + [VALIDATION_TYPES.ARRAY_ACTION_SELECTOR]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + let isValidFinal = isValid; + let finalParsed = parsed.slice(); + if (isValid) { + finalParsed = parsed.map((value: any) => { + const { isValid, message } = VALIDATORS[ + VALIDATION_TYPES.ACTION_SELECTOR + ](value.dynamicTrigger, props, dataTree); + + isValidFinal = isValidFinal && isValid; + return { + ...value, + message: message, + isValid: isValid, + }; + }); + } + + return { + isValid: isValidFinal, + parsed: finalParsed, + message: message, + }; + }, + [VALIDATION_TYPES.SELECTED_TAB]: ( + value: any, + props: WidgetProps, + ): ValidationResponse => { + const tabs = + props.tabs && isString(props.tabs) + ? JSON.parse(props.tabs) + : props.tabs && Array.isArray(props.tabs) + ? props.tabs + : []; + const tabNames = tabs.map((i: { label: string; id: string }) => i.label); + const isValidTabName = tabNames.includes(value); + return { + isValid: isValidTabName, + parsed: value, + message: isValidTabName + ? "" + : `${WIDGET_TYPE_VALIDATION_ERROR}: Invalid tab name.`, + }; + }, + [VALIDATION_TYPES.DEFAULT_OPTION_VALUE]: ( + value: string | string[], + props: WidgetProps, + dataTree?: DataTree, + ) => { + let values = value; + + if (props) { + if (props.selectionType === "SINGLE_SELECT") { + return VALIDATORS[VALIDATION_TYPES.TEXT](value, props, dataTree); + } else if (props.selectionType === "MULTI_SELECT") { + if (typeof value === "string") { + try { + values = JSON.parse(value); + if (!Array.isArray(values)) { + throw new Error(); + } + } catch { + values = value.length ? value.split(",") : []; + if (values.length > 0) { + values = values.map((value) => value.trim()); + } + } + } + } + } + + if (Array.isArray(values)) { + values = _.uniq(values); + } + + return { + isValid: true, + parsed: values, + }; + }, + [VALIDATION_TYPES.DEFAULT_SELECTED_ROW]: ( + value: string | string[], + props: WidgetProps, + ) => { + let values = value; + + if (props) { + if (props.multiRowSelection) { + if (typeof value === "string") { + try { + values = JSON.parse(value); + if (!Array.isArray(values)) { + throw new Error(); + } + } catch { + values = value.length ? value.split(",") : []; + if (values.length > 0) { + let numericValues = values.map((value) => { + return isNumber(value.trim()) ? -1 : Number(value.trim()); + }); + numericValues = _.uniq(numericValues); + return { + isValid: true, + parsed: numericValues, + }; + } + } + } + } else { + try { + const parsed = toNumber(value); + return { + isValid: true, + parsed: parsed, + }; + } catch (e) { + return { + isValid: true, + parsed: -1, + }; + } + } + } + return { + isValid: true, + parsed: values, + }; + }, + [VALIDATION_TYPES.LAT_LONG]: (unparsedValue: { + lat?: number; + long?: number; + [x: string]: any; + }): ValidationResponse => { + let value = unparsedValue; + const invalidResponse = { + isValid: false, + parsed: undefined, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: { lat: number, long: number }`, + }; + + if (isString(unparsedValue)) { + try { + value = JSON.parse(unparsedValue); + } catch (e) { + console.error(`Error when parsing string as object`); + } + } + + const { lat, long } = value || {}; + const validLat = typeof lat === "number" && lat <= 90 && lat >= -90; + const validLong = typeof long === "number" && long <= 180 && long >= -180; + + if (!validLat || !validLong) { + return invalidResponse; + } + + return { + isValid: true, + parsed: value, + }; + }, +}; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 76d82aeda5..e0ba697db7 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -1901,7 +1901,7 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@blueprintjs/core@^3.18.1", "@blueprintjs/core@^3.33.0": +"@blueprintjs/core@^3.33.0": version "3.33.0" resolved "https://registry.yarnpkg.com/@blueprintjs/core/-/core-3.33.0.tgz#fe0b21029061fb9beee09f6230bab73ff17c087c" integrity sha512-9gccauo44DsrW8IP75Qy7rKrRv3sgOTvs2Lv2DIEH9f+WZQjj37x+3bfiGKrzeWWRS5P1vP1uSgn89P/JZrD8w== @@ -1918,6 +1918,23 @@ resize-observer-polyfill "^1.5.1" tslib "~1.13.0" +"@blueprintjs/core@^3.36.0": + version "3.36.0" + resolved "https://registry.yarnpkg.com/@blueprintjs/core/-/core-3.36.0.tgz#0a271092050c17b84f29426594708180a1b5401a" + integrity sha512-7VUyF+qWelDysajK0Xowlou+iqbGAFfGaM3znpmm7OEEIli5XRWjG9rhNuEk3sP7zbdOJpyqh5PAPDQvm5Sxmg== + dependencies: + "@blueprintjs/icons" "^3.23.0" + "@types/dom4" "^2.0.1" + classnames "^2.2" + dom4 "^2.1.5" + normalize.css "^8.0.1" + popper.js "^1.16.1" + react-lifecycles-compat "^3.0.4" + react-popper "^1.3.7" + react-transition-group "^2.9.0" + resize-observer-polyfill "^1.5.1" + tslib "~1.13.0" + "@blueprintjs/datetime@^3.14.0": version "3.19.2" resolved "https://registry.yarnpkg.com/@blueprintjs/datetime/-/datetime-3.19.2.tgz#245d6243cdc78d76c87b6729a51f35f8193f6151" @@ -1937,6 +1954,14 @@ classnames "^2.2" tslib "~1.13.0" +"@blueprintjs/icons@^3.23.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@blueprintjs/icons/-/icons-3.23.0.tgz#4cfe0db4363971ac5d8a0a59590a6efc16115dc6" + integrity sha512-QOQ3P5bU1FiEwnMBl5Chn433ONSSTIMgC+zZJttyXV0m8R7D1bPBJJqIMuANXtRld/Fj+8IzoQ6jfaVUG16slA== + dependencies: + classnames "^2.2" + tslib "~1.13.0" + "@blueprintjs/select@^3.10.0", "@blueprintjs/select@^3.14.2": version "3.14.2" resolved "https://registry.yarnpkg.com/@blueprintjs/select/-/select-3.14.2.tgz#a62e18e2f6648fa7cbf248d69502c155d4a7c7bd" @@ -2251,6 +2276,11 @@ "@fusioncharts/features" "^1.2.0" "@fusioncharts/utils" "^1.2.0" +"@github/g-emoji-element@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@github/g-emoji-element/-/g-emoji-element-1.1.5.tgz#4fd6b20caf49b88705cf3365d1d1f00a69dfa29a" + integrity sha512-qnp14gzcC4AeQ+AK+Hvi3ZjO1MbfiOl9BzUmL+6+yfPpkT3rYpoQYjKuOftPObjVF5XJ3iotENjuvINANfH3Xw== + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -3835,6 +3865,11 @@ dependencies: "@types/tern" "*" +"@types/deep-diff@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/deep-diff/-/deep-diff-1.0.0.tgz#7eba3202a99b3a207f758f351f7f86387269fc40" + integrity sha512-ENsJcujGbCU/oXhDfQ12mSo/mCBWodT2tpARZKmatoSrf8+cGRCPi0KVj3I0FORhYZfLXkewXu7AoIWqiBLkNw== + "@types/dom4@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.1.tgz#506d5781b9bcab81bd9a878b198aec7dee2a6033" @@ -5426,13 +5461,12 @@ axe-core@^4.0.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.0.2.tgz#c7cf7378378a51fcd272d3c09668002a4990b1cb" integrity sha512-arU1h31OGFu+LPrOLGZ7nB45v940NMDMEJeNmbutu57P+UFDVnkZg3e+J1I2HJRZ9hT7gO8J91dn/PMrAiKakA== -axios@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3" - integrity sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g== +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== dependencies: - follow-redirects "1.5.10" - is-buffer "^2.0.2" + follow-redirects "^1.10.0" axobject-query@^2.2.0: version "2.2.0" @@ -6919,6 +6953,11 @@ commander@^4.0.1, commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -7577,10 +7616,10 @@ cypress-xpath@^1.4.0: resolved "https://registry.yarnpkg.com/cypress-xpath/-/cypress-xpath-1.6.0.tgz#e96a8554ef2e3693ee5f282d66e88462c3e2a10c" integrity sha512-JEe6jJo3fhi+PRwoVazLXo0n1VwWXkBCUicgJoFYHzMMX/gfb2lMn1at32cISX8/wXHaGz8aQxIP5YPiwgIMZw== -cypress@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.3.0.tgz#91122219ae66ab910058970dbf36619ab0fbde6c" - integrity sha512-XgebyqL7Th6/8YenE1ddb7+d4EiCG2Jvg/5c8+HPfFFY/gXnOVhoCVUU3KW8qg3JL7g0B+iJbHd5hxuCqbd1RQ== +cypress@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-6.2.1.tgz#27d5fbcf008c698c390fdb0c03441804176d06c4" + integrity sha512-OYkSgzA4J4Q7eMjZvNf5qWpBLR4RXrkqjL3UZ1UzGGLAskO0nFTi/RomNTG6TKvL3Zp4tw4zFY1gp5MtmkCZrA== dependencies: "@cypress/listr-verbose-renderer" "^0.4.1" "@cypress/request" "^2.88.5" @@ -7594,7 +7633,7 @@ cypress@5.3.0: chalk "^4.1.0" check-more-types "^2.24.0" cli-table3 "~0.6.0" - commander "^4.1.1" + commander "^5.1.0" common-tags "^1.8.0" debug "^4.1.1" eventemitter2 "^6.4.2" @@ -7612,10 +7651,10 @@ cypress@5.3.0: minimist "^1.2.5" moment "^2.27.0" ospath "^1.2.2" - pretty-bytes "^5.3.0" + pretty-bytes "^5.4.1" ramda "~0.26.1" request-progress "^3.0.0" - supports-color "^7.1.0" + supports-color "^7.2.0" tmp "~0.2.1" untildify "^4.0.0" url "^0.11.0" @@ -7681,13 +7720,6 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -7715,6 +7747,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-diff@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" + integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg== + deep-equal@^1.0.1, deep-equal@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -9356,18 +9393,16 @@ focus-lock@^0.7.0: resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.7.0.tgz#b2bfb0ca7beacc8710a1ff74275fe0dc60a1d88a" integrity sha512-LI7v2mH02R55SekHYdv9pRHR9RajVNyIJ2N5IEkWbg7FT5ZmJ9Hw4mWxHeEUcd+dJo0QmzztHvDvWcc7prVFsw== -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - follow-redirects@^1.0.0: version "1.13.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== +follow-redirects@^1.10.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" + integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -10732,7 +10767,7 @@ is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.6: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.0, is-buffer@^2.0.2, is-buffer@~2.0.3: +is-buffer@^2.0.0, is-buffer@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== @@ -14973,6 +15008,11 @@ pretty-bytes@^5.3.0: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.4.1.tgz#cd89f79bbcef21e3d21eb0da68ffe93f803e884b" integrity sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA== +pretty-bytes@^5.4.1: + version "5.5.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" + integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== + pretty-error@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" @@ -17969,7 +18009,7 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== diff --git a/app/server/.gitignore b/app/server/.gitignore index c578211f21..5210afb685 100644 --- a/app/server/.gitignore +++ b/app/server/.gitignore @@ -1,4 +1,3 @@ -.DS_Store dist/** target/** .idea/** diff --git a/app/server/Dockerfile b/app/server/Dockerfile index 5fc20371e3..b193784132 100644 --- a/app/server/Dockerfile +++ b/app/server/Dockerfile @@ -8,7 +8,7 @@ VOLUME /tmp EXPOSE 8080 -ARG JAR_FILE=./appsmith-server/target/server-1.0-SNAPSHOT.jar +ARG JAR_FILE=./appsmith-server/target/server-*.jar ARG PLUGIN_JARS=./appsmith-plugins/*/target/*.jar ARG APPSMITH_SEGMENT_CE_KEY ENV APPSMITH_SEGMENT_CE_KEY=${APPSMITH_SEGMENT_CE_KEY} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/FieldName.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/FieldName.java index b1eb89d5ac..2da1e762ee 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/FieldName.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/FieldName.java @@ -3,6 +3,7 @@ package com.appsmith.external.constants; public class FieldName { public static final String CLIENT_SECRET = "clientSecret"; public static final String TOKEN = "token"; + public static final String TOKEN_RESPONSE = "tokenResponse"; public static final String PASSWORD = "password"; } diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java index f7fd6d5825..5dbc750751 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java @@ -29,7 +29,7 @@ public class AuthenticationDTO { // class and fails. @JsonIgnore - private Boolean isEncrypted; + private Boolean isEncrypted = false; @JsonIgnore public Map getEncryptionFields() { diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java index 72eb6c9dd3..324f08a9f1 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java @@ -30,6 +30,8 @@ public class OAuth2 extends AuthenticationDTO { Type authType; + Boolean isHeader; + String clientId; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @@ -37,11 +39,19 @@ public class OAuth2 extends AuthenticationDTO { String accessTokenUrl; - String scope; + Set scope; + + String headerPrefix = "Bearer"; + + @JsonIgnore + Object tokenResponse; @JsonIgnore String token; + @JsonIgnore + Instant issuedAt; + @JsonIgnore Instant expiresAt; @@ -54,6 +64,9 @@ public class OAuth2 extends AuthenticationDTO { if (this.token != null) { map.put(FieldName.TOKEN, this.token); } + if (this.tokenResponse != null) { + map.put(FieldName.TOKEN_RESPONSE, String.valueOf(this.tokenResponse)); + } return map; } @@ -66,6 +79,9 @@ public class OAuth2 extends AuthenticationDTO { if (encryptedFields.containsKey(FieldName.TOKEN)) { this.token = encryptedFields.get(FieldName.TOKEN); } + if (encryptedFields.containsKey(FieldName.TOKEN_RESPONSE)) { + this.tokenResponse = encryptedFields.get(FieldName.TOKEN_RESPONSE); + } } } @@ -78,6 +94,9 @@ public class OAuth2 extends AuthenticationDTO { if (this.token == null || this.token.isEmpty()) { set.add(FieldName.TOKEN); } + if (this.tokenResponse == null || (String.valueOf(this.token)).isEmpty()) { + set.add(FieldName.TOKEN_RESPONSE); + } return set; } diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/UpdatableConnection.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/UpdatableConnection.java index fbe6ccd5f4..7c91149de7 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/UpdatableConnection.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/UpdatableConnection.java @@ -1,9 +1,5 @@ package com.appsmith.external.models; public interface UpdatableConnection { - void updateDatasource(DatasourceConfiguration datasourceConfiguration); - - default boolean isUpdated() { - return false; - } + public AuthenticationDTO getAuthenticationDTO(AuthenticationDTO authenticationDTO); } diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index 608773285d..4dd5ace902 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -24,5 +24,6 @@ redisPlugin mssqlPlugin firestorePlugin + redshiftPlugin \ No newline at end of file diff --git a/app/server/appsmith-plugins/redshiftPlugin/plugin.properties b/app/server/appsmith-plugins/redshiftPlugin/plugin.properties new file mode 100644 index 0000000000..f8c7748b53 --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=redshift-plugin +plugin.class=com.external.plugins.RedshiftPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= \ No newline at end of file diff --git a/app/server/appsmith-plugins/redshiftPlugin/pom.xml b/app/server/appsmith-plugins/redshiftPlugin/pom.xml new file mode 100644 index 0000000000..b1e43673bf --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/pom.xml @@ -0,0 +1,152 @@ + + + + 4.0.0 + + com.external.plugins + redshiftPlugin + 1.0-SNAPSHOT + + redshiftPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + redshift-plugin + com.external.plugins.RedshiftPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + redshift + http://redshift-maven-repository.s3-website-us-east-1.amazonaws.com/release + + + + + + + org.pf4j + pf4j-spring + 0.6.0 + provided + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + provided + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + com.amazon.redshift + redshift-jdbc42 + 2.0.0.1 + runtime + + + + + junit + junit + 4.13.1 + test + + + + org.hamcrest + hamcrest-all + 1.3 + test + + + + io.projectreactor + reactor-test + 3.2.11.RELEASE + test + + + + org.mockito + mockito-core + 3.1.0 + test + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + + + + + + + package + + shade + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib + + + + + + + + diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java new file mode 100644 index 0000000000..48dadd4ff6 --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java @@ -0,0 +1,621 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.DBAuth; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.SSLDetails; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.pluginExceptions.StaleConnectionException; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.ObjectUtils; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.appsmith.external.models.Connection.Mode.READ_ONLY; + + +public class RedshiftPlugin extends BasePlugin { + static final String JDBC_DRIVER = "com.amazon.redshift.jdbc.Driver"; + private static final String JDBC_PROTOCOL = "jdbc:redshift://"; + private static final String USER = "user"; + private static final String PASSWORD = "password"; + private static final String SSL = "ssl"; + private static final int VALIDITY_CHECK_TIMEOUT = 5; /* must be positive, otherwise may receive exception */ + private static final String DATE_COLUMN_TYPE_NAME = "date"; + + public RedshiftPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Slf4j + @Extension + public static class RedshiftPluginExecutor implements PluginExecutor { + + private final Scheduler scheduler = Schedulers.elastic(); + + private static final String TABLES_QUERY = + "select a.attname as name,\n" + + " t1.typname as column_type,\n" + + " case when a.atthasdef then pg_get_expr(d.adbin, d.adrelid) end as default_expr,\n" + + " c.relkind as kind,\n" + + " c.relname as table_name,\n" + + " n.nspname as schema_name\n" + + "from pg_catalog.pg_attribute a\n" + + " left join pg_catalog.pg_type t1 on t1.oid = a.atttypid\n" + + " inner join pg_catalog.pg_class c on a.attrelid = c.oid\n" + + " left join pg_catalog.pg_namespace n on c.relnamespace = n.oid\n" + + " left join pg_catalog.pg_attrdef d on d.adrelid = c.oid and d.adnum = a.attnum\n" + + "where a.attnum > 0\n" + + " and not a.attisdropped\n" + + " and n.nspname not in ('information_schema', 'pg_catalog')\n" + + " and c.relkind in ('r', 'v')\n" + + " and pg_catalog.pg_table_is_visible(a.attrelid)\n" + + "order by c.relname, a.attnum;"; + + private static final String KEYS_QUERY_PRIMARY_KEY = "select tco.constraint_schema as self_schema,\n" + + " tco.constraint_name,\n" + + " kcu.column_name as self_column,\n" + + " kcu.table_name as self_table,\n" + + " 'p' as constraint_type\n" + + "from information_schema.table_constraints tco\n" + + "join information_schema.key_column_usage kcu \n" + + " on kcu.constraint_name = tco.constraint_name\n" + + " and kcu.constraint_schema = tco.constraint_schema\n" + + " and kcu.constraint_name = tco.constraint_name\n" + + "where tco.constraint_type = 'PRIMARY KEY'\n" + + "order by tco.constraint_schema,\n" + + " tco.constraint_name,\n" + + " kcu.ordinal_position;"; + + private static final String KEYS_QUERY_FOREIGN_KEY = "select kcu.table_schema as self_schema,\n" + + "\t kcu.table_name as self_table,\n" + + " rel_kcu.table_schema as foreign_schema,\n" + + " rel_kcu.table_name as foreign_table,\n" + + " kcu.column_name as self_column,\n" + + " rel_kcu.column_name as foreign_column,\n" + + " kcu.constraint_name,\n" + + " 'f' as constraint_type\n" + + "from information_schema.table_constraints tco\n" + + "left join information_schema.key_column_usage kcu\n" + + " on tco.constraint_schema = kcu.constraint_schema\n" + + " and tco.constraint_name = kcu.constraint_name\n" + + "left join information_schema.referential_constraints rco\n" + + " on tco.constraint_schema = rco.constraint_schema\n" + + " and tco.constraint_name = rco.constraint_name\n" + + "left join information_schema.key_column_usage rel_kcu\n" + + " on rco.unique_constraint_schema = rel_kcu.constraint_schema\n" + + " and rco.unique_constraint_name = rel_kcu.constraint_name\n" + + " and kcu.ordinal_position = rel_kcu.ordinal_position\n" + + "where tco.constraint_type = 'FOREIGN KEY'\n" + + "order by kcu.table_schema,\n" + + " kcu.table_name,\n" + + " kcu.ordinal_position;\n"; + + private void checkResultSetValidity(ResultSet resultSet) throws AppsmithPluginException { + if(resultSet == null) { + System.out.println( + Thread.currentThread().getName() + ": " + + "Redshift plugin: getRow: driver failed to fetch result: resultSet is null." + ); + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "redshift driver failed to fetch result: resultSet is null." + ); + } + } + + private Map getRow(ResultSet resultSet) throws SQLException, AppsmithPluginException { + checkResultSetValidity(resultSet); + + ResultSetMetaData metaData = resultSet.getMetaData(); + + /* + * 1. Ideally metaData is never supposed to be null. Redshift JDBC driver does null check before returning + * ResultSetMetaData. + */ + if(metaData == null) { + System.out.println( + Thread.currentThread().getName() + ": " + + "Redshift plugin: getRow: metaData is null. Ideally this is never supposed to " + + "happen as the Redshift JDBC driver does a null check before passing this object. This means " + + "that something has gone wrong while processing the query result." + ); + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "metaData is null. Ideally this is never supposed to happen as the Redshift JDBC driver " + + "does a null check before passing this object. This means that something has gone wrong " + + "while processing the query result" + ); + } + + int colCount = metaData.getColumnCount(); + // Use `LinkedHashMap` here so that the column ordering is preserved in the response. + Map row = new LinkedHashMap<>(colCount); + + for (int i = 1; i <= colCount; i++) { + Object value; + final String typeName = metaData.getColumnTypeName(i); + + if (resultSet.getObject(i) == null) { + value = null; + + } else if (DATE_COLUMN_TYPE_NAME.equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE.format(resultSet.getDate(i).toLocalDate()); + + } else if ("timestamp".equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE_TIME.format( + LocalDateTime.of( + resultSet.getDate(i).toLocalDate(), + resultSet.getTime(i).toLocalTime() + ) + ) + "Z"; + + } else if ("timestamptz".equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE_TIME.format( + resultSet.getObject(i, OffsetDateTime.class) + ); + } + else if ("time".equalsIgnoreCase(typeName) || "timetz".equalsIgnoreCase(typeName)) { + value = resultSet.getString(i); + } else { + value = resultSet.getObject(i); + } + + row.put(metaData.getColumnName(i), value); + } + + return row; + } + + /* + * 1. This method can throw SQLException via connection.isClosed() or connection.isValid(...) + * 2. StaleConnectionException thrown by this method needs to be propagated to upper layers so that a retry + * can be triggered. + */ + private void checkConnectionValidity(Connection connection) throws SQLException { + if (connection == null || connection.isClosed() || !connection.isValid(VALIDITY_CHECK_TIMEOUT)) { + throw new StaleConnectionException(); + } + } + + @Override + public Mono execute(Connection connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + String query = actionConfiguration.getBody(); + + if (query == null) { + return Mono.error( + new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Missing required parameter: Query." + ) + ); + } + + return (Mono) Mono.fromCallable(() -> { + /* + * 1. StaleConnectionException thrown by checkConnectionValidity(...) needs to be propagated to upper + * layers so that a retry can be triggered. + */ + try { + checkConnectionValidity(connection); + } catch (SQLException error) { + String error_msg = "Error checking validity of Redshift connection. " + error; + System.out.println(Thread.currentThread().getName() + ": " + error_msg); + + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, error_msg)); + } + + List> rowsList = new ArrayList<>(50); + Statement statement = null; + ResultSet resultSet = null; + + try { + statement = connection.createStatement(); + boolean isResultSet = statement.execute(query); + + if (isResultSet) { + resultSet = statement.getResultSet(); + + while (resultSet.next()) { + Map row = getRow(resultSet); + rowsList.add(row); + } + } else { + rowsList.add(Map.of( + "affectedRows", + ObjectUtils.defaultIfNull(statement.getUpdateCount(), 0)) + ); + + } + } catch (SQLException | AppsmithPluginException e) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage())); + } finally { + if (resultSet != null) { + try { + resultSet.close(); + } catch (SQLException e) { + log.warn("Error closing Redshift ResultSet", e); + } + } + + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + log.warn("Error closing Redshift Statement", e); + } + } + } + + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(objectMapper.valueToTree(rowsList)); + result.setIsExecutionSuccess(true); + System.out.println( + Thread.currentThread().getName() + ": " + + "In RedshiftPlugin, got action execution result" + ); + return Mono.just(result); + }) + .flatMap(obj -> obj) + .subscribeOn(scheduler); + } + + @Override + public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + try { + Class.forName(JDBC_DRIVER); + } catch (ClassNotFoundException e) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error loading Redshift JDBC Driver class.")); + } + + String url; + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + + com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection(); + + final boolean isSslEnabled = configurationConnection != null + && configurationConnection.getSsl() != null + && !SSLDetails.AuthType.NO_SSL.equals(configurationConnection.getSsl().getAuthType()); + + Properties properties = new Properties(); + properties.put(SSL, isSslEnabled); + if (authentication.getUsername() != null) { + properties.put(USER, authentication.getUsername()); + } + if (authentication.getPassword() != null) { + properties.put(PASSWORD, authentication.getPassword()); + } + + if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + url = datasourceConfiguration.getUrl(); + + } else { + StringBuilder urlBuilder = new StringBuilder(JDBC_PROTOCOL); + for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + urlBuilder + .append(endpoint.getHost()) + .append(':') + .append(ObjectUtils.defaultIfNull(endpoint.getPort(), 5439L)) + .append('/'); + + if (!StringUtils.isEmpty(authentication.getDatabaseName())) { + urlBuilder.append(authentication.getDatabaseName()); + } + } + url = urlBuilder.toString(); + } + + return Mono.fromCallable(() -> { + try { + System.out.println(Thread.currentThread().getName() + ": Connecting to Redshift db"); + Connection connection = DriverManager.getConnection(url, properties); + connection.setReadOnly( + configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode())); + return Mono.just(connection); + } catch (SQLException e) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error connecting" + + " to Redshift.", e)); + } + }) + .flatMap(obj -> obj) + .map(conn -> (Connection) conn) + .subscribeOn(scheduler); + } + + @Override + public void datasourceDestroy(Connection connection) { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + System.out.println(Thread.currentThread().getName() + ": Error closing Redshift Connection. " + e); + log.error("Error closing Redshift Connection.", e); + } + } + + @Override + public Set validateDatasource(@NonNull DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + + if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + invalids.add("Missing endpoint."); + } else { + for (final Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + if (StringUtils.isEmpty(endpoint.getHost())) { + invalids.add("Missing hostname."); + } else if (endpoint.getHost().contains("/") || endpoint.getHost().contains(":")) { + invalids.add("Host value cannot contain `/` or `:` characters. Found `" + endpoint.getHost() + "`."); + } + } + } + + if (datasourceConfiguration.getConnection() != null + && datasourceConfiguration.getConnection().getMode() == null) { + invalids.add("Missing Connection Mode."); + } + + if (datasourceConfiguration.getAuthentication() == null) { + invalids.add("Missing authentication details."); + + } else { + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + if (StringUtils.isEmpty(authentication.getUsername())) { + invalids.add("Missing username for authentication."); + } + + if (StringUtils.isEmpty(authentication.getPassword())) { + invalids.add("Missing password for authentication."); + } + + if (StringUtils.isEmpty(authentication.getDatabaseName())) { + invalids.add("Missing database name."); + } + } + + return invalids; + } + + @Override + public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { + return datasourceCreate(datasourceConfiguration) + .map(connection -> { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + log.warn("Error closing Redshift connection that was made for testing.", e); + } + + return new DatasourceTestResult(); + }) + .onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage()))); + } + + private void getTablesInfo(ResultSet columnsResultSet, Map tablesByName) + throws SQLException, AppsmithPluginException { + checkResultSetValidity(columnsResultSet); + + while (columnsResultSet.next()) { + final char kind = columnsResultSet.getString("kind").charAt(0); + final String schemaName = columnsResultSet.getString("schema_name"); + final String tableName = columnsResultSet.getString("table_name"); + final String fullTableName = schemaName + "." + tableName; + if (!tablesByName.containsKey(fullTableName)) { + tablesByName.put(fullTableName, new DatasourceStructure.Table( + kind == 'r' ? DatasourceStructure.TableType.TABLE : DatasourceStructure.TableType.VIEW, + fullTableName, + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>() + )); + } + final DatasourceStructure.Table table = tablesByName.get(fullTableName); + table.getColumns().add(new DatasourceStructure.Column( + columnsResultSet.getString("name"), + columnsResultSet.getString("column_type"), + columnsResultSet.getString("default_expr") + )); + } + } + + private void getKeysInfo(ResultSet constraintsResultSet, Map tablesByName, + Map keyRegistry) throws SQLException, AppsmithPluginException { + checkResultSetValidity(constraintsResultSet); + + while (constraintsResultSet.next()) { + final String constraintName = constraintsResultSet.getString("constraint_name"); + final char constraintType = constraintsResultSet.getString("constraint_type").charAt(0); + final String selfSchema = constraintsResultSet.getString("self_schema"); + final String tableName = constraintsResultSet.getString("self_table"); + final String fullTableName = selfSchema + "." + tableName; + + if (!tablesByName.containsKey(fullTableName)) { + /* do nothing */ + return; + } + + final DatasourceStructure.Table table = tablesByName.get(fullTableName); + final String keyFullName = tableName + "." + constraintName; + + if (constraintType == 'p') { + if (!keyRegistry.containsKey(keyFullName)) { + final DatasourceStructure.PrimaryKey key = new DatasourceStructure.PrimaryKey( + constraintName, + new ArrayList<>() + ); + keyRegistry.put(keyFullName, key); + table.getKeys().add(key); + } + ((DatasourceStructure.PrimaryKey) keyRegistry.get(keyFullName)).getColumnNames() + .add(constraintsResultSet.getString("self_column")); + } else if (constraintType == 'f') { + final String foreignSchema = constraintsResultSet.getString("foreign_schema"); + final String prefix = (foreignSchema.equalsIgnoreCase(selfSchema) ? "" : foreignSchema + ".") + + constraintsResultSet.getString("foreign_table") + "."; + + if (!keyRegistry.containsKey(keyFullName)) { + final DatasourceStructure.ForeignKey key = new DatasourceStructure.ForeignKey( + constraintName, + new ArrayList<>(), + new ArrayList<>() + ); + keyRegistry.put(keyFullName, key); + table.getKeys().add(key); + } + + ((DatasourceStructure.ForeignKey) keyRegistry.get(keyFullName)).getFromColumns() + .add(constraintsResultSet.getString("self_column")); + ((DatasourceStructure.ForeignKey) keyRegistry.get(keyFullName)).getToColumns() + .add(prefix + constraintsResultSet.getString("foreign_column")); + } + } + } + + private void getTemplates(Map tablesByName) { + for (DatasourceStructure.Table table : tablesByName.values()) { + final List columnsWithoutDefault = table.getColumns() + .stream() + .filter(column -> column.getDefaultValue() == null) + .collect(Collectors.toList()); + + final List columnNames = new ArrayList<>(); + final List columnValues = new ArrayList<>(); + final StringBuilder setFragments = new StringBuilder(); + + for (DatasourceStructure.Column column : columnsWithoutDefault) { + final String name = column.getName(); + final String type = column.getType(); + String value; + + if (type == null) { + value = "null"; + } else if ("text".equals(type) || "varchar".equals(type)) { + value = "''"; + } else if (type.startsWith("int")) { + value = "1"; + } else if ("date".equals(type)) { + value = "'2019-07-01'"; + } else if ("time".equals(type)) { + value = "'18:32:45'"; + } else if ("timetz".equals(type)) { + value = "'04:05:06 PST'"; + } else if ("timestamp".equals(type)) { + value = "TIMESTAMP '2019-07-01 10:00:00'"; + } else if ("timestamptz".equals(type)) { + value = "TIMESTAMP WITH TIME ZONE '2019-07-01 06:30:00 CET'"; + } else { + value = "''"; + } + + columnNames.add("\"" + name + "\""); + columnValues.add(value); + setFragments.append("\n \"").append(name).append("\" = ").append(value); + } + + final String quotedTableName = table.getName().replaceFirst("\\.(\\w+)", ".\"$1\""); + table.getTemplates().addAll(List.of( + new DatasourceStructure.Template("SELECT", "SELECT * FROM " + quotedTableName + " LIMIT 10;"), + new DatasourceStructure.Template("INSERT", "INSERT INTO " + quotedTableName + + " (" + String.join(", ", columnNames) + ")\n" + + " VALUES (" + String.join(", ", columnValues) + ");"), + new DatasourceStructure.Template("UPDATE", "UPDATE " + quotedTableName + " SET" + + setFragments.toString() + "\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + new DatasourceStructure.Template("DELETE", "DELETE FROM " + quotedTableName + + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!") + )); + } + } + + @Override + public Mono getStructure(Connection connection, DatasourceConfiguration datasourceConfiguration) { + /* + * 1. StaleConnectionException thrown by checkConnectionValidity(...) needs to be propagated to upper + * layers so that a retry can be triggered. + */ + try { + checkConnectionValidity(connection); + } catch (SQLException error) { + String error_msg = "Error checking validity of Redshift connection. " + error; + System.out.println(Thread.currentThread().getName() + ": " + error_msg); + + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, error_msg)); + } + + final DatasourceStructure structure = new DatasourceStructure(); + final Map tablesByName = new LinkedHashMap<>(); + final Map keyRegistry = new HashMap<>(); + + return Mono.fromSupplier(() -> { + // Ref: . + System.out.println(Thread.currentThread().getName() + ": Getting Redshift Db structure"); + try (Statement statement = connection.createStatement()) { + + // Get tables' schema and fill up their columns. + ResultSet columnsResultSet = statement.executeQuery(TABLES_QUERY); + getTablesInfo(columnsResultSet, tablesByName); + + // Get tables' primary key constraints and fill those up. + ResultSet primaryKeyConstraintsResultSet = statement.executeQuery(KEYS_QUERY_PRIMARY_KEY); + getKeysInfo(primaryKeyConstraintsResultSet, tablesByName, keyRegistry); + + // Get tables' foreign key constraints and fill those up. + ResultSet foreignKeyConstraintsResultSet = statement.executeQuery(KEYS_QUERY_FOREIGN_KEY); + getKeysInfo(foreignKeyConstraintsResultSet, tablesByName, keyRegistry); + + // Get templates for each table and put those in. + getTemplates(tablesByName); + } catch (SQLException | AppsmithPluginException throwable) { + return Mono.error(throwable); + } + + structure.setTables(new ArrayList<>(tablesByName.values())); + + for (DatasourceStructure.Table table : structure.getTables()) { + table.getKeys().sort(Comparator.naturalOrder()); + } + + return structure; + }) + .map(resultStructure -> (DatasourceStructure) resultStructure) + .subscribeOn(scheduler); + } + } +} diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/editor.json new file mode 100644 index 0000000000..7896a10ac6 --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/editor.json @@ -0,0 +1,15 @@ +{ + "editor": [ + { + "sectionName": "", + "id": 1, + "children": [ + { + "label": "", + "configProperty": "actionConfiguration.body", + "controlType": "QUERY_DYNAMIC_TEXT" + } + ] + } + ] +} \ No newline at end of file diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/form.json new file mode 100644 index 0000000000..eb58eb62e8 --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/form.json @@ -0,0 +1,155 @@ +{ + "form": [ + { + "sectionName": "Connection", + "id": 1, + "children": [ + { + "label": "Connection Mode", + "configProperty": "datasourceConfiguration.connection.mode", + "controlType": "DROP_DOWN", + "isRequired": true, + "initialValue": "READ_WRITE", + "options": [ + { + "label": "Read Only", + "value": "READ_ONLY" + }, + { + "label": "Read / Write", + "value": "READ_WRITE" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "Host Address", + "configProperty": "datasourceConfiguration.endpoints[*].host", + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" + }, + { + "label": "Port", + "configProperty": "datasourceConfiguration.endpoints[*].port", + "dataType": "NUMBER", + "controlType": "KEYVALUE_ARRAY" + } + ] + }, + { + "label": "Database Name", + "configProperty": "datasourceConfiguration.authentication.databaseName", + "controlType": "INPUT_TEXT", + "placeholderText": "Database name", + "initialValue": "admin" + } + ] + }, + { + "sectionName": "Authentication", + "id": 2, + "children": [ + { + "sectionName": null, + "children": [ + { + "label": "Username", + "configProperty": "datasourceConfiguration.authentication.username", + "controlType": "INPUT_TEXT", + "placeholderText": "Username" + }, + { + "label": "Password", + "configProperty": "datasourceConfiguration.authentication.password", + "dataType": "PASSWORD", + "controlType": "INPUT_TEXT", + "placeholderText": "Password", + "encrypted": true + } + ] + } + ] + }, + { + "id": 3, + "sectionName": "SSL (optional)", + "children": [ + { + "label": "SSL Mode", + "configProperty": "datasourceConfiguration.connection.ssl.authType", + "controlType": "DROP_DOWN", + "options": [ + { + "label": "No SSL", + "value": "NO_SSL" + }, + { + "label": "Allow", + "value": "ALLOW" + }, + { + "label": "Prefer", + "value": "PREFER" + }, + { + "label": "Require", + "value": "REQUIRE" + }, + { + "label": "Disable", + "value": "DISABLE" + }, + { + "label": "Verify-CA", + "value": "VERIFY_CA" + }, + { + "label": "Verify-Full", + "value": "VERIFY_FULL" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "Key File", + "configProperty": "datasourceConfiguration.connection.ssl.keyFile", + "controlType": "FILE_PICKER" + }, + { + "label": "Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.certificateFile", + "controlType": "FILE_PICKER" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "CA Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.caCertificateFile", + "controlType": "FILE_PICKER" + }, + { + "label": "PEM Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.pemCertificate.file", + "controlType": "FILE_PICKER" + }, + { + "label": "PEM Passphrase", + "configProperty": "datasourceConfiguration.connection.ssl.pemCertificate.password", + "dataType": "PASSWORD", + "controlType": "INPUT_TEXT", + "placeholderText": "PEM Passphrase" + } + ] + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/CREATE.sql b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/CREATE.sql new file mode 100644 index 0000000000..5a7c082ce9 --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/CREATE.sql @@ -0,0 +1,8 @@ +INSERT INTO users + (name, gender, email) +VALUES + ( + '{{ nameInput.text }}', + '{{ genderDropdown.selectedOptionValue }}', + '{{ nameInput.text }}' + ); -- nameInput and genderDropdown are example widgets, replace them with your widget names. Read more at http://bit.ly/postgres-widget-docs diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/DELETE.sql b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/DELETE.sql new file mode 100644 index 0000000000..9e871189b6 --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/DELETE.sql @@ -0,0 +1 @@ +DELETE FROM users WHERE id = -1; -- Use widget data in a query by replacing static values with {{ widgetName.property }}. Read more at http://bit.ly/postgres-widget-docs diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/SELECT.sql b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/SELECT.sql new file mode 100644 index 0000000000..4f38a30a09 --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/SELECT.sql @@ -0,0 +1 @@ +SELECT * FROM users where role = 'Admin' ORDER BY id LIMIT 10; -- Use widget data in a query using {{ widgetName.property }}. Read more at http://bit.ly/postgres-widget-docs \ No newline at end of file diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/UPDATE.sql b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/UPDATE.sql new file mode 100644 index 0000000000..fdba4616e1 --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/resources/templates/UPDATE.sql @@ -0,0 +1,4 @@ +UPDATE users + SET status = 'APPROVED' + WHERE id = '{{ usersTable.selectedRow.id }}'; -- usersTable is an example table widget from where the id is being read. Replace it with your own Table widget or a static value. Read more at http://bit.ly/postgres-widget-docs + diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java b/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java new file mode 100644 index 0000000000..9e04f8fe1e --- /dev/null +++ b/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java @@ -0,0 +1,446 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.DBAuth; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.pluginExceptions.StaleConnectionException; +import com.appsmith.external.plugins.PluginExecutor; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the RedshiftPlugin + */ +@Slf4j +public class RedshiftPluginTest { + PluginExecutor pluginExecutor = new RedshiftPlugin.RedshiftPluginExecutor(); + + private static String address; + private static Integer port; + private static String username; + private static String password; + private static String dbName; + + @BeforeClass + public static void setUp() { + address = "address"; + port = 5439; + username = "username"; + password = "password"; + dbName = "dbName"; + } + + private DatasourceConfiguration createDatasourceConfiguration() { + DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); + authDTO.setUsername(username); + authDTO.setPassword(password); + authDTO.setDatabaseName(dbName); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost(address); + endpoint.setPort(port.longValue()); + + DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + dsConfig.setAuthentication(authDTO); + dsConfig.setEndpoints(List.of(endpoint)); + return dsConfig; + } + + @Test + public void testDatasourceCreateConnectionFailure() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithPluginException && throwable.getMessage() + .equals(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error connecting" + + " to Redshift.").getMessage())) + .verify(); + } + + @Test + public void testStaleConnectionCheck() throws SQLException { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show databases"); + + /* Mock java.sql.Connection: + * a. isClosed(): return true + * b. isValid() : return false + */ + Connection mockConnection = mock(Connection.class); + when(mockConnection.isClosed()).thenReturn(true); + when(mockConnection.isValid(Mockito.anyInt())).thenReturn(false); + + Mono resultMono = pluginExecutor.execute(mockConnection, dsConfig, actionConfiguration); + + StepVerifier.create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof StaleConnectionException) + .verify(); + } + + @Test + public void itShouldValidateDatasourceWithEmptyEndpoints() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + dsConfig.setEndpoints(new ArrayList<>()); + + Assert.assertEquals(Set.of("Missing endpoint."), + pluginExecutor.validateDatasource(dsConfig)); + } + + @Test + public void itShouldValidateDatasourceWithEmptyHost() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + dsConfig.getEndpoints().get(0).setHost(""); + + Assert.assertEquals(Set.of("Missing hostname."), + pluginExecutor.validateDatasource(dsConfig)); + } + + @Test + public void itShouldValidateDatasourceWithInvalidHostname() { + String hostname = "jdbc://localhost"; + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + dsConfig.getEndpoints().get(0).setHost("jdbc://localhost"); + + Assert.assertEquals(Set.of("Host value cannot contain `/` or `:` characters. Found `" + hostname + "`."), + pluginExecutor.validateDatasource(dsConfig)); + } + + /* 1. CREATE TABLE users ( + * id INTEGER PRIMARY KEY IDENTITY(1,1), + * username VARCHAR (50) UNIQUE NOT NULL, + * password VARCHAR (50) NOT NULL, + * email VARCHAR (355) UNIQUE NOT NULL, + * spouse_dob DATE, + * dob DATE NOT NULL, + * time1 TIME NOT NULL, + * time_tz TIME WITH TIME ZONE NOT NULL, + * created_on TIMESTAMP NOT NULL, + * created_on_tz TIMESTAMP WITH TIME ZONE NOT NULL + * ); + * 2. INSERT INTO users VALUES ( + * 1, + * 'Jack', + * 'jill', + * 'jack@exemplars.com', + * NULL, + * '2018-12-31', + * '18:32:45', + * '04:05:06 PST', + * TIMESTAMP '2018-11-30 20:45:15', + * TIMESTAMP WITH TIME ZONE '2018-11-30 20:45:15 CET' + * ); + * 3. SELECT * FROM users WHERE id = 1; + */ + @Test + public void testExecute() throws SQLException { + /* Mock java.sql.Connection: + * a. isClosed() + * b. isValid() + */ + Connection mockConnection = mock(Connection.class); + when(mockConnection.isClosed()).thenReturn(false); + when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true); + + /* Mock java.sql.Statement: + * a. execute(...) + * b. close() + */ + Statement mockStatement = mock(Statement.class); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.execute(Mockito.any())).thenReturn(true); + doNothing().when(mockStatement).close(); + + /* Mock java.sql.ResultSet: + * a. getObject(...) + * b. getDate(...) + * c. getTime(...) + * d. getString(...) + * e. getObject(..., ...) + * d. next() + * e. close() + */ + ResultSet mockResultSet = mock(ResultSet.class); + when(mockStatement.getResultSet()).thenReturn(mockResultSet); + when(mockResultSet.getObject(Mockito.anyInt())).thenReturn("", 1, "", "Jack", "", "jill", "", "jack@exemplars.com" + , null, "", "", "", "", ""); + when(mockResultSet.getDate(Mockito.anyInt())).thenReturn(Date.valueOf("2018-12-31"), Date.valueOf("2018-11-30")); + when(mockResultSet.getString(Mockito.anyInt())).thenReturn("18:32:45", "12:05:06+00"); + when(mockResultSet.getTime(Mockito.anyInt())).thenReturn(Time.valueOf("20:45:15")); + when(mockResultSet.getObject(Mockito.anyInt(), Mockito.any(Class.class))).thenReturn(OffsetDateTime.parse( + "2018-11-30T19:45:15+00")); + when(mockResultSet.next()).thenReturn(true).thenReturn(false); + doNothing().when(mockResultSet).close(); + + /* Mock java.sql.ResultSetMetaData: + * a. getColumnCount() + * b. getColumnTypeName(...) + * c. getColumnName(...) + */ + ResultSetMetaData mockResultSetMetaData = mock(ResultSetMetaData.class); + when(mockResultSet.getMetaData()).thenReturn(mockResultSetMetaData); + when(mockResultSetMetaData.getColumnCount()).thenReturn(10); + when(mockResultSetMetaData.getColumnTypeName(Mockito.anyInt())).thenReturn("int4", "varchar", "varchar", + "varchar", "date", "date", "time", "timetz", "timestamp", "timestamptz"); + when(mockResultSetMetaData.getColumnName(Mockito.anyInt())).thenReturn("id", "username", "password", "email", + "spouse_dob", "dob", "time1", "time_tz", "created_on", "created_on_tz"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = Mono.just(mockConnection); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("18:32:45", node.get("time1").asText()); + assertEquals("12:05:06+00", node.get("time_tz").asText()); + assertEquals("2018-11-30T20:45:15Z", node.get("created_on").asText()); + assertEquals("2018-11-30T19:45:15Z", node.get("created_on_tz").asText()); + assertTrue(node.get("spouse_dob").isNull()); + assertArrayEquals( + new String[]{ + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "time1", + "time_tz", + "created_on", + "created_on_tz", + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + }) + .verifyComplete(); + } + + /* 1. CREATE TABLE users ( + * id INTEGER PRIMARY KEY IDENTITY(1,1), + * username VARCHAR (50) UNIQUE NOT NULL, + * password VARCHAR (50) NOT NULL, + * ); + * 2. CREATE TABLE possessions( + * id serial PRIMARY KEY, + * title VARCHAR (50) NOT NULL, + * user_id int NOT NULL, + * constraint user_fk foreign key (user_id) references users(id) + * ); + * 3. CREATE TABLE campus( + * id timestamptz default now(), + * name timestamptz default now() + * ); + * 4. Run TABLES_QUERY + * 5. Run KEYS_QUERY_PRIMARY_KEY + * 6. Run KEYS_QUERY_FOREIGN_KEY + */ + @Test + public void testStructure() throws SQLException { + /* Mock java.sql.Connection: + * a. isClosed() + * b. isValid() + */ + Connection mockConnection = mock(Connection.class); + when(mockConnection.isClosed()).thenReturn(false); + when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true); + + /* Mock java.sql.Statement: + * a. execute(...) + * b. close() + */ + Statement mockStatement = mock(Statement.class); + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.execute(Mockito.any())).thenReturn(true); + doNothing().when(mockStatement).close(); + + /* Mock java.sql.ResultSet: + * d. getString(...) + * d. next() + * e. close() + */ + ResultSet mockResultSet = mock(ResultSet.class); + when(mockStatement.executeQuery(Mockito.anyString())).thenReturn(mockResultSet, mockResultSet, mockResultSet); + when(mockResultSet.next()) + .thenReturn(true, true, true, true, true, true, true, true, false) // TABLES_QUERY + .thenReturn(true, true, false) // KEYS_QUERY_PRIMARY_KEY + .thenReturn(true, false); // KEYS_QUERY_FOREIGN_KEY + when(mockResultSet.getString("kind")).thenReturn("r", "r", "r", "r", "r", "r", "r", "r");// TABLES_QUERY + when(mockResultSet.getString("schema_name")).thenReturn("public", "public", "public", "public", "public", + "public", "public", "public"); // TABLES_QUERY + when(mockResultSet.getString("table_name")).thenReturn("campus", "campus", "possessions", "possessions", + "possessions", "users", "users", "users"); // TABLES_QUERY + when(mockResultSet.getString("name")).thenReturn("id", "name", "id", "title", "user_id", "id", "username", + "password"); // TABLES_QUERY + when(mockResultSet.getString("column_type")).thenReturn("timestamptz", "timestamptz", "int4", "varchar", + "int4", "int4", "varchar", "varchar"); // TABLES_QUERY + when(mockResultSet.getString("default_expr")).thenReturn("now()", "now()", null, null, null, "\"identity\"" + + "(101507, 0, '1,1'::text)", null, null); // TABLES_QUERY + when(mockResultSet.getString("constraint_name")) + .thenReturn("possessions_pkey", "users_pkey") // KEYS_QUERY_PRIMARY_KEY + .thenReturn("user_fk"); // KEYS_QUERY_FOREIGN_KEY + when(mockResultSet.getString("constraint_type")) + .thenReturn("p", "p") // KEYS_QUERY_PRIMARY_KEY + .thenReturn("f"); // KEYS_QUERY_FOREIGN_KEY + when(mockResultSet.getString("self_schema")) + .thenReturn("public", "public") // KEYS_QUERY_PRIMARY_KEY + .thenReturn("public"); // KEYS_QUERY_FOREIGN_KEY + when(mockResultSet.getString("self_table")) + .thenReturn("possessions", "users") // KEYS_QUERY_PRIMARY_KEY + .thenReturn("possessions"); // KEYS_QUERY_FOREIGN_KEY + when(mockResultSet.getString("self_column")) + .thenReturn("id", "id") // KEYS_QUERY_PRIMARY_KEY + .thenReturn("user_id"); // KEYS_QUERY_FOREIGN_KEY + when(mockResultSet.getString("foreign_schema")).thenReturn("public"); // KEYS_QUERY_FOREIGN_KEY + when(mockResultSet.getString("foreign_table")).thenReturn("users"); // KEYS_QUERY_FOREIGN_KEY + when(mockResultSet.getString("foreign_column")).thenReturn("id"); // KEYS_QUERY_FOREIGN_KEY + doNothing().when(mockResultSet).close(); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = Mono.just(mockConnection); + Mono structureMono = dsConnectionMono + .flatMap(connection -> pluginExecutor.getStructure(connection, dsConfig)); + + StepVerifier.create(structureMono) + .assertNext(structure -> { + assertNotNull(structure); + assertEquals(3, structure.getTables().size()); + + final DatasourceStructure.Table campusTable = structure.getTables().get(0); + assertEquals("public.campus", campusTable.getName()); + assertEquals(DatasourceStructure.TableType.TABLE, campusTable.getType()); + assertArrayEquals( + new DatasourceStructure.Column[]{ + new DatasourceStructure.Column("id", "timestamptz", "now()"), + new DatasourceStructure.Column("name", "timestamptz", "now()") + }, + campusTable.getColumns().toArray() + ); + assertEquals(campusTable.getKeys().size(), 0); + + final DatasourceStructure.Table possessionsTable = structure.getTables().get(1); + assertEquals("public.possessions", possessionsTable.getName()); + assertEquals(DatasourceStructure.TableType.TABLE, possessionsTable.getType()); + assertArrayEquals( + new DatasourceStructure.Column[]{ + new DatasourceStructure.Column("id", "int4", null), + new DatasourceStructure.Column("title", "varchar", null), + new DatasourceStructure.Column("user_id", "int4", null), + }, + possessionsTable.getColumns().toArray() + ); + + final DatasourceStructure.PrimaryKey possessionsPrimaryKey = new DatasourceStructure.PrimaryKey("possessions_pkey", new ArrayList<>()); + possessionsPrimaryKey.getColumnNames().add("id"); + final DatasourceStructure.ForeignKey possessionsUserForeignKey = new DatasourceStructure.ForeignKey( + "user_fk", + List.of("user_id"), + List.of("users.id") + ); + assertArrayEquals( + new DatasourceStructure.Key[]{possessionsPrimaryKey, possessionsUserForeignKey}, + possessionsTable.getKeys().toArray() + ); + + assertArrayEquals( + new DatasourceStructure.Template[]{ + new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"possessions\" LIMIT 10;"), + new DatasourceStructure.Template("INSERT", "INSERT INTO public.\"possessions\" " + + "(\"id\", \"title\", \"user_id\")\n VALUES (1, '', 1);"), + new DatasourceStructure.Template("UPDATE", "UPDATE public.\"possessions\" SET\n" + + " \"id\" = 1\n" + + " \"title\" = ''\n" + + " \"user_id\" = 1\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + new DatasourceStructure.Template("DELETE", "DELETE FROM public.\"possessions\"\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + }, + possessionsTable.getTemplates().toArray() + ); + + final DatasourceStructure.Table usersTable = structure.getTables().get(2); + assertEquals("public.users", usersTable.getName()); + assertEquals(DatasourceStructure.TableType.TABLE, usersTable.getType()); + assertArrayEquals( + new DatasourceStructure.Column[]{ + new DatasourceStructure.Column("id", "int4", "\"identity\"(101507, " + + "0, '1,1'::text)"), + new DatasourceStructure.Column("username", "varchar", null), + new DatasourceStructure.Column("password", "varchar", null) + }, + usersTable.getColumns().toArray() + ); + + final DatasourceStructure.PrimaryKey usersPrimaryKey = new DatasourceStructure.PrimaryKey("users_pkey", new ArrayList<>()); + usersPrimaryKey.getColumnNames().add("id"); + assertArrayEquals( + new DatasourceStructure.Key[]{usersPrimaryKey}, + usersTable.getKeys().toArray() + ); + + assertArrayEquals( + new DatasourceStructure.Template[]{ + new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"users\" LIMIT 10;"), + new DatasourceStructure.Template("INSERT", "INSERT INTO public.\"users\" (\"username\", \"password\")\n" + + " VALUES ('', '');"), + new DatasourceStructure.Template("UPDATE", "UPDATE public.\"users\" SET\n" + + " \"username\" = ''\n" + + " \"password\" = ''\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + new DatasourceStructure.Template("DELETE", "DELETE FROM public.\"users\"\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + }, + usersTable.getTemplates().toArray() + ); + }) + .verifyComplete(); + } +} diff --git a/app/server/appsmith-plugins/restApiPlugin/pom.xml b/app/server/appsmith-plugins/restApiPlugin/pom.xml index 876b6fb0c7..b34dbf1947 100644 --- a/app/server/appsmith-plugins/restApiPlugin/pom.xml +++ b/app/server/appsmith-plugins/restApiPlugin/pom.xml @@ -103,6 +103,28 @@ test + + org.assertj + assertj-core + test + + + + + org.powermock + powermock-module-junit4 + 2.0.9 + test + + + + + org.powermock + powermock-api-mockito2 + 2.0.9 + test + +