diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 83c7aa1612e..974f54bc7bb 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -1,15 +1,15 @@ -name: Appsmith Client Workflow +name: Appsmith Client Workflow on: push: - branches: [ release, master ] + branches: [release, master] # Only trigger if files have changed in this specific path paths: - - 'app/client/**' + - "app/client/**" pull_request_target: - branches: [ release, master ] + branches: [release, master] paths: - - 'app/client/**' + - "app/client/**" # Change the working directory for all the jobs in this workflow defaults: run: @@ -24,67 +24,67 @@ jobs: shell: bash steps: - # Checkout the code - - name: Checkout the merged commit from PR and base branch - 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' }} - uses: actions/checkout@v2 - - - name: Figure out the PR number - run: echo ${{ github.event.pull_request.number }} - - - name: Use Node.js 10.16.3 - uses: actions/setup-node@v1 - with: - node-version: '10.16.3' - - # Retrieve npm dependencies from cache. After a successful run, these dependencies are cached again - - name: Cache npm dependencies - uses: actions/cache@v2 - env: - cache-name: cache-yarn-dependencies - with: - # npm dependencies are stored in `~/.npm` on Linux/macOS - path: ~/.npm - key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} - restore-keys: | + # Checkout the code + - name: Checkout the merged commit from PR and base branch + 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' }} + uses: actions/checkout@v2 + + - name: Figure out the PR number + run: echo ${{ github.event.pull_request.number }} + + - name: Use Node.js 10.16.3 + uses: actions/setup-node@v1 + with: + node-version: "10.16.3" + + # Retrieve npm dependencies from cache. After a successful run, these dependencies are cached again + - name: Cache npm dependencies + uses: actions/cache@v2 + env: + cache-name: cache-yarn-dependencies + with: + # npm dependencies are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | ${{ runner.OS }}-node- ${{ runner.OS }}- - # Install all the dependencies - - name: Install dependencies - run: yarn install - - - name: Set the build environment based on the branch - id: vars - run: | - REACT_APP_ENVIRONMENT="DEVELOPMENT" - if [ ${GITHUB_REF} == '/refs/heads/master' ]; then - REACT_APP_ENVIRONMENT="PRODUCTION" - elif [ ${GITHUB_REF} == '/refs/heads/release' ]; then - REACT_APP_ENVIRONMENT="STAGING" - fi - echo ::set-output name=REACT_APP_ENVIRONMENT::${REACT_APP_ENVIRONMENT} - - - name: Run the jest tests - run: REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} yarn run test:unit - - # 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_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} 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 - uses: actions/upload-artifact@v2 - with: - name: build - path: app/client/build/ + # Install all the dependencies + - name: Install dependencies + run: yarn install + + - name: Set the build environment based on the branch + id: vars + run: | + REACT_APP_ENVIRONMENT="DEVELOPMENT" + if [ ${GITHUB_REF} == '/refs/heads/master' ]; then + REACT_APP_ENVIRONMENT="PRODUCTION" + elif [ ${GITHUB_REF} == '/refs/heads/release' ]; then + REACT_APP_ENVIRONMENT="STAGING" + fi + echo ::set-output name=REACT_APP_ENVIRONMENT::${REACT_APP_ENVIRONMENT} + + - name: Run the jest tests + run: REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} yarn run test:unit + + # 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 + + # Upload the build artifact so that it can be used by the test & deploy job in the workflow + - name: Upload react build bundle + uses: actions/upload-artifact@v2 + with: + name: build + path: app/client/build/ ui-test: needs: build @@ -108,134 +108,134 @@ jobs: - 6379:6379 mongo: image: mongo - ports: + ports: - 27017:27017 - + steps: - # Checkout the code - - name: Checkout the merged commit from PR and base branch - 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' }} - uses: actions/checkout@v2 - - - name: Use Node.js 10.16.3 - uses: actions/setup-node@v1 - with: - node-version: '10.16.3' - - # Retrieve npm dependencies from cache. After a successful run, these dependencies are cached again - - name: Cache npm dependencies - uses: actions/cache@v2 - env: - cache-name: cache-yarn-dependencies - with: - # maven dependencies are stored in `~/.m2` on Linux/macOS - path: ~/.npm - key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} - restore-keys: | + # Checkout the code + - name: Checkout the merged commit from PR and base branch + 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' }} + uses: actions/checkout@v2 + + - name: Use Node.js 10.16.3 + uses: actions/setup-node@v1 + with: + node-version: "10.16.3" + + # Retrieve npm dependencies from cache. After a successful run, these dependencies are cached again + - name: Cache npm dependencies + uses: actions/cache@v2 + env: + cache-name: cache-yarn-dependencies + with: + # maven dependencies are stored in `~/.m2` on Linux/macOS + path: ~/.npm + key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | ${{ runner.OS }}-node- ${{ runner.OS }}- - # Install all the dependencies - - name: Install dependencies - run: yarn install - - - name: Download the react build artifact - uses: actions/download-artifact@v2 - with: - name: build - path: app/client/build - - - name: Pull release server docker container and start it locally - if: github.ref == 'refs/heads/release' - shell: bash - run: | - 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 - - - name: Pull master server docker container and start it locally - if: github.ref == 'refs/heads/master' - shell: bash - run: | - 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 - - - name: Installing Yarn serve - run: | - yarn global add serve - echo "$(yarn global bin)" >> $GITHUB_PATH - - - name: Setting up the cypress tests - shell: bash - env: - APPSMITH_SSL_CERTIFICATE: ${{ secrets.APPSMITH_SSL_CERTIFICATE }} - APPSMITH_SSL_KEY: ${{ secrets.APPSMITH_SSL_KEY }} - CYPRESS_URL: ${{ secrets.CYPRESS_URL }} - CYPRESS_USERNAME: ${{ secrets.CYPRESS_USERNAME }} - CYPRESS_PASSWORD: ${{ secrets.CYPRESS_PASSWORD }} - CYPRESS_TESTUSERNAME1: ${{ secrets.CYPRESS_TESTUSERNAME1 }} - CYPRESS_TESTPASSWORD1: ${{ secrets.CYPRESS_TESTPASSWORD1 }} - CYPRESS_TESTUSERNAME2: ${{ secrets.CYPRESS_TESTUSERNAME2 }} - CYPRESS_TESTPASSWORD2: ${{ secrets.CYPRESS_TESTPASSWORD1 }} - APPSMITH_DISABLE_TELEMETRY: true - APPSMITH_GOOGLE_MAPS_API_KEY: ${{ secrets.APPSMITH_GOOGLE_MAPS_API_KEY }} - POSTGRES_PASSWORD: postgres - run: | - ./cypress/setup-test.sh - - - name: Run the cypress test - uses: cypress-io/github-action@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} - CYPRESS_USERNAME: ${{ secrets.CYPRESS_USERNAME }} - CYPRESS_PASSWORD: ${{ secrets.CYPRESS_PASSWORD }} - CYPRESS_TESTUSERNAME1: ${{ secrets.CYPRESS_TESTUSERNAME1 }} - CYPRESS_TESTPASSWORD1: ${{ secrets.CYPRESS_TESTPASSWORD1 }} - CYPRESS_TESTUSERNAME2: ${{ secrets.CYPRESS_TESTUSERNAME2 }} - CYPRESS_TESTPASSWORD2: ${{ secrets.CYPRESS_TESTPASSWORD1 }} - APPSMITH_DISABLE_TELEMETRY: true - APPSMITH_GOOGLE_MAPS_API_KEY: ${{ secrets.APPSMITH_GOOGLE_MAPS_API_KEY }} - COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} - with: - browser: chrome - headless: true - record: true - install: false - parallel: true - group: 'Electrons on Github Action' - spec: 'cypress/integration/Smoke_TestSuite/*/*' - working-directory: app/client - # tag will be either "push" or "pull_request_target" - tag: ${{ github.event_name }} - env: 'NODE_ENV=development' + # Install all the dependencies + - name: Install dependencies + run: yarn install + + - name: Download the react build artifact + uses: actions/download-artifact@v2 + with: + name: build + path: app/client/build - # Upload the screenshots as artifacts if there's a failure - - uses: actions/upload-artifact@v1 - if: failure() - with: - name: cypress-screenshots-${{ matrix.job }} - path: app/client/cypress/screenshots/ + - name: Pull release server docker container and start it locally + if: github.ref == 'refs/heads/release' + shell: bash + run: | + 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 + + - name: Pull master server docker container and start it locally + if: github.ref == 'refs/heads/master' + shell: bash + run: | + 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 + + - name: Installing Yarn serve + run: | + yarn global add serve + echo "$(yarn global bin)" >> $GITHUB_PATH + + - name: Setting up the cypress tests + shell: bash + env: + APPSMITH_SSL_CERTIFICATE: ${{ secrets.APPSMITH_SSL_CERTIFICATE }} + APPSMITH_SSL_KEY: ${{ secrets.APPSMITH_SSL_KEY }} + CYPRESS_URL: ${{ secrets.CYPRESS_URL }} + CYPRESS_USERNAME: ${{ secrets.CYPRESS_USERNAME }} + CYPRESS_PASSWORD: ${{ secrets.CYPRESS_PASSWORD }} + CYPRESS_TESTUSERNAME1: ${{ secrets.CYPRESS_TESTUSERNAME1 }} + CYPRESS_TESTPASSWORD1: ${{ secrets.CYPRESS_TESTPASSWORD1 }} + CYPRESS_TESTUSERNAME2: ${{ secrets.CYPRESS_TESTUSERNAME2 }} + CYPRESS_TESTPASSWORD2: ${{ secrets.CYPRESS_TESTPASSWORD1 }} + APPSMITH_DISABLE_TELEMETRY: true + APPSMITH_GOOGLE_MAPS_API_KEY: ${{ secrets.APPSMITH_GOOGLE_MAPS_API_KEY }} + POSTGRES_PASSWORD: postgres + run: | + ./cypress/setup-test.sh + + - name: Run the cypress test + uses: cypress-io/github-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + CYPRESS_USERNAME: ${{ secrets.CYPRESS_USERNAME }} + CYPRESS_PASSWORD: ${{ secrets.CYPRESS_PASSWORD }} + CYPRESS_TESTUSERNAME1: ${{ secrets.CYPRESS_TESTUSERNAME1 }} + CYPRESS_TESTPASSWORD1: ${{ secrets.CYPRESS_TESTPASSWORD1 }} + CYPRESS_TESTUSERNAME2: ${{ secrets.CYPRESS_TESTUSERNAME2 }} + CYPRESS_TESTPASSWORD2: ${{ secrets.CYPRESS_TESTPASSWORD1 }} + APPSMITH_DISABLE_TELEMETRY: true + APPSMITH_GOOGLE_MAPS_API_KEY: ${{ secrets.APPSMITH_GOOGLE_MAPS_API_KEY }} + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + with: + browser: chrome + headless: true + record: true + install: false + parallel: true + group: "Electrons on Github Action" + spec: "cypress/integration/Smoke_TestSuite/*/*" + working-directory: app/client + # tag will be either "push" or "pull_request_target" + tag: ${{ github.event_name }} + env: "NODE_ENV=development" + + # Upload the screenshots as artifacts if there's a failure + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: cypress-screenshots-${{ matrix.job }} + path: app/client/cypress/screenshots/ package: needs: ui-test @@ -247,43 +247,42 @@ jobs: if: always() && (github.ref == 'refs/heads/release' || github.ref == 'refs/heads/master') steps: - - # Checkout the code - - name: Checkout the merged commit from PR and base branch - 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' }} - uses: actions/checkout@v2 - - - name: Download the react build artifact - uses: actions/download-artifact@v2 - with: - name: build - path: app/client/build - - # 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: branch_name - run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11}) - - # 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' - run: | - docker build -t appsmith/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 - - # 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' - run: | - docker build -t appsmith/appsmith-editor:${GITHUB_SHA} . - docker build -t appsmith/appsmith-editor:nightly . - echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - docker push appsmith/appsmith-editor \ No newline at end of file + # Checkout the code + - name: Checkout the merged commit from PR and base branch + 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' }} + uses: actions/checkout@v2 + + - name: Download the react build artifact + uses: actions/download-artifact@v2 + with: + name: build + path: app/client/build + + # 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: branch_name + run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11}) + + # 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' + run: | + docker build -t appsmith/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 + + # 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' + run: | + docker build -t appsmith/appsmith-editor:${GITHUB_SHA} . + docker build -t appsmith/appsmith-editor:nightly . + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin + docker push appsmith/appsmith-editor diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 0feb8b2d7d0..edb7477e04c 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -1,4 +1,4 @@ -name: Appsmith Github Release Workflow +name: Appsmith Github Release Workflow on: push: @@ -14,56 +14,56 @@ jobs: working-directory: app/client steps: - # Checkout the code - - uses: actions/checkout@v2 - - - name: Use Node.js 10.16.3 - uses: actions/setup-node@v1 - with: - node-version: '10.16.3' - - # Retrieve npm dependencies from cache. After a successful run, these dependencies are cached again - - name: Cache npm dependencies - uses: actions/cache@v2 - env: - cache-name: cache-yarn-dependencies - with: - # npm dependencies are stored in `~/.m2` on Linux/macOS - path: ~/.npm - key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} - restore-keys: | + # Checkout the code + - uses: actions/checkout@v2 + + - name: Use Node.js 10.16.3 + uses: actions/setup-node@v1 + with: + node-version: "10.16.3" + + # Retrieve npm dependencies from cache. After a successful run, these dependencies are cached again + - name: Cache npm dependencies + uses: actions/cache@v2 + env: + cache-name: cache-yarn-dependencies + with: + # npm dependencies are stored in `~/.m2` on Linux/macOS + path: ~/.npm + key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | ${{ runner.OS }}-node- ${{ runner.OS }}- - - # Install all the dependencies - - 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_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/} - - # 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}} . - - # 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 . - fi - - echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - docker push appsmith/appsmith-editor + + # Install all the dependencies + - 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/} + + # 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}} . + + # 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 . + fi + + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin + docker push appsmith/appsmith-editor build-server: runs-on: ubuntu-latest @@ -72,78 +72,77 @@ jobs: working-directory: app/server steps: - # Checkout the code - - uses: actions/checkout@v2 - - # Setup Java - - name: Set up JDK 1.11 - uses: actions/setup-java@v1 - with: - java-version: 1.11 - - # Retrieve maven dependencies from cache. After a successful run, these dependencies are cached again - - name: Cache maven dependencies - uses: actions/cache@v2 - env: - cache-name: cache-maven-dependencies - with: - # maven dependencies are stored in `~/.m2` on Linux/macOS - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - # 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/} - - # 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}} . - - # 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 . - fi - - echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - docker push appsmith/appsmith-server + # Checkout the code + - uses: actions/checkout@v2 + + # Setup Java + - name: Set up JDK 1.11 + uses: actions/setup-java@v1 + with: + java-version: 1.11 + + # Retrieve maven dependencies from cache. After a successful run, these dependencies are cached again + - name: Cache maven dependencies + uses: actions/cache@v2 + env: + cache-name: cache-maven-dependencies + with: + # maven dependencies are stored in `~/.m2` on Linux/macOS + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + # 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/} + + # 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}} . + + # 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 . + fi + + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin + docker push appsmith/appsmith-server create-release: - needs: + 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}} - \ No newline at end of file + # 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}} diff --git a/README.md b/README.md index 42a989e1f1e..2f7418b4a61 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@

Appsmith.com logo +
+ Appsmith is a JavaScript-based visual development platform to build internal tools. +
+

+ +

+ Try Online Sandbox

Documentation · - Latest Release - · Discord

@@ -16,8 +21,9 @@

Appsmith is a JavaScript-based visual development platform to build and launch internal tools quickly. Drag-and-drop pre-built widgets, and connect them using JavaScript to create interactive pages. Connect UI to your APIs and Databases to build complex workflows in minutes.

+**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, & MSFT SQL Server
+**Database Support**: PostgreSQL, MongoDB, MySQL, 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). @@ -33,7 +39,7 @@ Already familiar with Appsmith? [Quickly start building on your own](#%EF%B8%8F- ## 🏭 Features * **5-minute setup**: Deploy Appsmith on your server, or use our cloud version to start building in 5 minutes. [Quick Start](#%EF%B8%8F-quickstart) -* **Frontend as a service**: Drag-and-drop to build sophisticated **dashboards** and **workflows, without writing HTML/CSS**. Write JavaScript anywhere to transform data, and dynamically control widget-properties. +* **Frontend as a service**: Drag-and-drop from pre-built UI widgets like table, form, and image, to build sophisticated **dashboards** and **workflows, without writing HTML/CSS**. Write JavaScript anywhere to transform data, and dynamically control widget-properties. * **Database CRUD**: Query & update your database directly by connecting it to the UI. Connect to **PostgreSQL, MongoDB, MySQL & more!** * **Trigger APIs**: Connect to REST APIs to build custom apps. * **Security**: DB Credentials are AES 256 encrypted and no data is stored by appsmith. Deploy behind your private VPC for additional security! diff --git a/app.json b/app.json index cb74f1a902e..d609fb03acc 100644 --- a/app.json +++ b/app.json @@ -17,6 +17,9 @@ "logo": "https://raw.githubusercontent.com/appsmithorg/appsmith/release/static/logo.png", "success_url": "/", "stack": "container", + "scripts": { + "postdeploy" : "/analytics.sh" + }, "env": { "APPSMITH_MONGODB_URI": { "description": "Your Mongo Database URI", diff --git a/app/client/.gitignore b/app/client/.gitignore index 01f970e3208..fb1f59140fc 100755 --- a/app/client/.gitignore +++ b/app/client/.gitignore @@ -33,9 +33,12 @@ cypress/screenshots /cypress.env.json results/ + /docker/*.pem /docker/nginx.conf /docker/nginx-root.conf storybook-static/* build-storybook.log + +.eslintcache diff --git a/app/client/Dockerfile b/app/client/Dockerfile index ad7ac70c36e..6e82c146882 100644 --- a/app/client/Dockerfile +++ b/app/client/Dockerfile @@ -5,7 +5,7 @@ COPY ./build /var/www/appsmith EXPOSE 80 # This is the default nginx template file inside the container. # This is replaced by the install.sh script during a deployment -COPY ./docker/templates/nginx-linux.conf.template /nginx.conf.template +COPY ./docker/templates/nginx-app.conf.template /nginx.conf.template COPY ./docker/templates/nginx-root.conf.template /nginx-root.conf.template # This is the script that is used to start Nginx when the Docker container starts COPY ./docker/start-nginx.sh /start-nginx.sh diff --git a/app/client/cypress/fixtures/formWidgetdsl.json b/app/client/cypress/fixtures/formWidgetdsl.json index 30996f31da8..7273b8623b1 100644 --- a/app/client/cypress/fixtures/formWidgetdsl.json +++ b/app/client/cypress/fixtures/formWidgetdsl.json @@ -46,6 +46,22 @@ "parentId": "sidaue1kdu", "widgetId": "ac6cc8wmlu" }, + { + "isVisible": true, + "label": "Label", + "defaultCheckedState": true, + "widgetName": "Checkbox1", + "type": "CHECKBOX_WIDGET", + "isLoading": false, + "parentColumnSpace": 71.75, + "parentRowSpace": 38, + "leftColumn": 10, + "rightColumn": 13, + "topRow": 4, + "bottomRow": 5, + "parentId": "e3tq9qwta6", + "widgetId": "szjhneuog5" + }, { "isVisible": true, "widgetName": "FormButton1", diff --git a/app/client/cypress/fixtures/tabInputDsl.json b/app/client/cypress/fixtures/tabInputDsl.json new file mode 100644 index 00000000000..6fb8d652e8f --- /dev/null +++ b/app/client/cypress/fixtures/tabInputDsl.json @@ -0,0 +1,107 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 7, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "inputType": "TEXT", + "label": "", + "widgetName": "Input1", + "type": "INPUT_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 1, + "rightColumn": 6, + "topRow": 7, + "bottomRow": 8, + "parentId": "0", + "widgetId": "mc0qcr7bsb" + }, + { + "isVisible": true, + "shouldScrollContents": false, + "widgetName": "Tabs1", + "tabs": [ + { + "label": "Tab 1", + "id": "tab1", + "widgetId": "o9ody00ep7" + }, + { + "label": "Tab 2", + "id": "tab2", + "widgetId": "plhuaxd4lo" + } + ], + "shouldShowTabs": true, + "defaultTab": "Tab 1", + "type": "TABS_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 1, + "rightColumn": 9, + "topRow": 12, + "bottomRow": 19, + "parentId": "0", + "widgetId": "jd83uvbkmp", + "children": [ + { + "type": "CANVAS_WIDGET", + "tabId": "tab1", + "tabName": "Tab 1", + "widgetId": "o9ody00ep7", + "parentId": "jd83uvbkmp", + "detachFromLayout": true, + "children": [], + "parentRowSpace": 1, + "parentColumnSpace": 1, + "leftColumn": 0, + "rightColumn": 592, + "topRow": 0, + "bottomRow": 280, + "isLoading": false, + "widgetName": "Canvas1", + "renderMode": "CANVAS" + }, + { + "type": "CANVAS_WIDGET", + "tabId": "tab2", + "tabName": "Tab 2", + "widgetId": "plhuaxd4lo", + "parentId": "jd83uvbkmp", + "detachFromLayout": true, + "children": [], + "parentRowSpace": 1, + "parentColumnSpace": 1, + "leftColumn": 0, + "rightColumn": 592, + "topRow": 0, + "bottomRow": 280, + "isLoading": false, + "widgetName": "Canvas1", + "renderMode": "CANVAS" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index ccc5a27b18e..6676bbdd2af 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -179,5 +179,7 @@ "momentInput": "{{moment(new Date).format('yyyy')", "atobInput": "{{atob('QQ==')", "btoaInput": "{{btoa('A')", - "defaultInputBinding": "{{Input2.text" + "defaultInputBinding": "{{Input2.text", + "tabBinding": "{{Tabs1.selectedTab", + "pageloadBinding": "{{PageLoadApi.data.data[1].id}}{{Input1.text}}" } diff --git a/app/client/cypress/integration/Smoke_TestSuite/Applications/UpdateApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Applications/UpdateApplication_spec.js index dbf58962118..3b30176945d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Applications/UpdateApplication_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Applications/UpdateApplication_spec.js @@ -22,7 +22,7 @@ describe("Update Application", function() { cy.get(homePage.appMoreIcon) .first() .click({ force: true }); - cy.get(homePage.applicationName).type(appname + "{enter}"); + cy.get(homePage.applicationName).type(`${appname} updated` + "{enter}"); cy.wait("@updateApplication").should( "have.nested.property", "response.body.responseMeta.status", diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/BindApi_withPageload_Input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/BindApi_withPageload_Input_spec.js new file mode 100644 index 00000000000..14726df59ae --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/BindApi_withPageload_Input_spec.js @@ -0,0 +1,69 @@ +const testdata = require("../../../fixtures/testdata.json"); +const apiwidget = require("../../../locators/apiWidgetslocator.json"); +const commonlocators = require("../../../locators/commonlocators.json"); +const formWidgetsPage = require("../../../locators/FormWidgets.json"); +const dsl = require("../../../fixtures/MultipleInput.json"); +const pages = require("../../../locators/Pages.json"); +const widgetsPage = require("../../../locators/Widgets.json"); +const publish = require("../../../locators/publishWidgetspage.json"); + +describe("Binding the API with pageOnLoad and input Widgets", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Will load an api on load", function() { + cy.NavigateToAPI_Panel(); + cy.CreateAPI("PageLoadApi"); + cy.enterDatasourceAndPath("https://reqres.in/api/", "users"); + cy.WaitAutoSave(); + cy.get(apiwidget.settings).click({ force: true }); + cy.get(apiwidget.onPageLoad) + .find(".bp3-switch") + .click(); + cy.wait("@setExecuteOnLoad"); + cy.reload(); + }); + + it("Input widget updated with deafult data", function() { + cy.SearchEntityandOpen("Input1"); + cy.get(widgetsPage.defaultInput) + .type(testdata.command) + .type("3"); + cy.get(commonlocators.editPropCrossButton).click(); + cy.wait("@updateLayout").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get(publish.inputWidget + " " + "input") + .first() + .invoke("attr", "value") + .should("contain", "3"); + }); + + it("Binding second input widget with API on PageLoad data and default data from input1 widget ", function() { + cy.SearchEntityandOpen("Input3"); + cy.get(widgetsPage.defaultInput).type(testdata.pageloadBinding, { + parseSpecialCharSequences: false, + }); + cy.get(commonlocators.editPropCrossButton).click(); + cy.wait("@updateLayout").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.PublishtheApp(); + cy.get(publish.inputWidget + " " + "input") + .last() + .invoke("attr", "value") + .should("contain", "3"); + cy.get(publish.inputWidget + " " + "input") + .last() + .invoke("attr", "value") + .should("contain", "2"); + cy.get(publish.backToEditor) + .first() + .click(); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TabWidget_Input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TabWidget_Input_spec.js new file mode 100644 index 00000000000..f1ca7ba8578 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TabWidget_Input_spec.js @@ -0,0 +1,44 @@ +const commonlocators = require("../../../locators/commonlocators.json"); +const formWidgetsPage = require("../../../locators/FormWidgets.json"); +const dsl = require("../../../fixtures/tabInputDsl.json"); +const pages = require("../../../locators/Pages.json"); +const widgetsPage = require("../../../locators/Widgets.json"); +const publish = require("../../../locators/publishWidgetspage.json"); +const testdata = require("../../../fixtures/testdata.json"); + +describe("Binding the input Widget with tab Widget", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Input widget test with default value from tab widget", function() { + cy.SearchEntityandOpen("Input1"); + cy.get(widgetsPage.defaultInput).type(testdata.tabBinding); + cy.get(commonlocators.editPropCrossButton).click(); + cy.wait("@updateLayout").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + }); + + it("validation of data displayed in input widgets based on tab selected", function() { + cy.PublishtheApp(); + cy.get(publish.tabWidget) + .contains("Tab 2") + .click({ force: true }) + .should("be.selected"); + cy.get(publish.inputWidget + " " + "input") + .first() + .invoke("attr", "value") + .should("contain", "Tab 2"); + cy.get(publish.tabWidget) + .contains("Tab 1") + .click({ force: true }) + .should("be.selected"); + cy.get(publish.inputWidget + " " + "input") + .first() + .invoke("attr", "value") + .should("contain", "Tab 1"); + }); +}); 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 14a8ad64c35..7a148479953 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 @@ -20,8 +20,8 @@ describe("Binding the button Widgets and validating NavigateTo Page functionalit .children() .contains("Navigate To") .click(); - cy.enterActionValue(testdata.externalPage); - cy.get(commonlocators.editPropCrossButton).click(); + cy.enterNavigatePageName(testdata.externalPage); + cy.get(commonlocators.editPropCrossButton).click({ force: true }); cy.wait(300); }); 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 190b03dc0be..0b5d29e6d19 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 @@ -22,8 +22,8 @@ describe("Binding the multiple Widgets and validating NavigateTo Page", function .children() .contains("Navigate To") .click(); - cy.enterActionValue(pageid); - cy.get(commonlocators.editPropCrossButton).click(); + cy.enterNavigatePageName(pageid); + cy.get(commonlocators.editPropCrossButton).click({ force: true }); cy.wait(300); }); 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 33b7827a4b5..8e1bc81d5c1 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 @@ -22,8 +22,8 @@ describe("Table Widget and Navigate to functionality validation", function() { .children() .contains("Navigate To") .click(); - cy.enterActionValue(pageid); - cy.get(commonlocators.editPropCrossButton).click(); + cy.enterNavigatePageName(pageid); + cy.get(commonlocators.editPropCrossButton).click({ force: true }); }); it("Create MyPage and valdiate if its successfully created", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/Datasources/DatasourceForm_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Datasources/DatasourceForm_spec.js new file mode 100644 index 00000000000..f32900e6ffe --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Datasources/DatasourceForm_spec.js @@ -0,0 +1,21 @@ +describe("Datasource form related tests", function() { + it("Check whether the delete button has the right color", function() { + cy.NavigateToAPI_Panel(); + cy.CreateAPI("Testapi"); + cy.enterDatasourceAndPath("https://reqres.in/api/", "users"); + + cy.get(".t--store-as-datasource-menu").click(); + cy.get(".t--store-as-datasource").click(); + + cy.get(".t--form-control-KEY_VAL_INPUT .t--add-field").click(); + cy.get(".t--form-control-KEY_VAL_INPUT .t--delete-field").should( + "attr", + "color", + "#A3B3BF", + ); + }); + it("Check if save button is disabled", function() { + cy.testDatasource(); + cy.get(".t--save-datasource").should("not.be.disabled"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Datasources/RestApiDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Datasources/RestApiDatasource_spec.js new file mode 100644 index 00000000000..97b742f8526 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Datasources/RestApiDatasource_spec.js @@ -0,0 +1,15 @@ +describe("Create a rest datasource", function() { + it("Create a rest datasource", function() { + cy.NavigateToAPI_Panel(); + cy.CreateAPI("Testapi"); + cy.enterDatasourceAndPath("https://reqres.in/api/", "users"); + + cy.get(".t--store-as-datasource-menu").click(); + cy.get(".t--store-as-datasource").click(); + + cy.saveDatasource(); + cy.contains(".datasource-highlight", "https://reqres.in"); + + cy.SaveAndRunAPI(); + }); +}); 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 e745f426c61..7dd657abf2d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Map_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Map_spec.js @@ -1,128 +1,147 @@ -// const commonlocators = require("../../../locators/commonlocators.json"); -// const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); -// const dsl = require("../../../fixtures/Mapdsl.json"); -// const publishPage = require("../../../locators/publishWidgetspage.json"); -// -// describe("Map Widget Functionality", function() { -// before(() => { -// cy.addDsl(dsl); -// }); -// -// it("Map Widget Functionality", function() { -// cy.openPropertyPane("mapwidget"); -// /** -// * @param{Text} Random Text -// * @param{MapWidget}Mouseover -// * @param{MapPre Css} Assertion -// */ -// cy.widgetText( -// "Maptest", -// viewWidgetsPage.mapWidget, -// viewWidgetsPage.mapInner, -// ); -// cy.get(viewWidgetsPage.mapinitialloc) -// .click({ force: true }) -// .clear() -// .type(this.data.country) -// .type("{enter}"); -// cy.get(viewWidgetsPage.mapInput) -// .click({ force: true }) -// .type(this.data.command) -// .type(JSON.stringify(this.data.marker), { -// parseSpecialCharSequences: false, -// }); -// cy.get(viewWidgetsPage.zoomLevel) -// .eq(0) -// .click({ force: true }); -// cy.get(viewWidgetsPage.zoomLevel) -// .eq(1) -// .click({ force: true }); -// cy.get(viewWidgetsPage.mapSearch) -// .click({ force: true }) -// .clear() -// .type(this.data.location2) -// .type("{enter}"); -// }); -// -// it("Map-Enable Location,Map search and Create Marker Property Validation", function() { -// /** -// * Enable the Search Location checkbox and Validate the same in editor mode -// */ -// cy.CheckWidgetProperties(commonlocators.enableSearchLocCheckbox); -// cy.get(viewWidgetsPage.mapSearch).should("be.visible"); -// cy.get(viewWidgetsPage.mapSearch) -// .invoke("attr", "placeholder") -// .should("contain", "Enter location to search"); -// /** -// * Enable the Pick Location checkbox and Validate the same in editor mode -// */ -// cy.CheckWidgetProperties(commonlocators.enablePickLocCheckbox); -// cy.get(viewWidgetsPage.pickMyLocation).should("exist"); -// -// /** -// * Enable the Createnew Marker checkbox and Validate the same in editor mode -// */ -// cy.CheckWidgetProperties(commonlocators.enableCreateMarkerCheckbox); -// /** -// * Validation will be added when create marker fun is working fine -// */ -// -// cy.PublishtheApp(); -// /** -// * Publish mode Validation -// */ -// cy.get(publishPage.mapSearch).should("be.visible"); -// cy.get(publishPage.mapSearch) -// .invoke("attr", "placeholder") -// .should("contain", "Enter location to search"); -// cy.get(publishPage.pickMyLocation).should("exist"); -// cy.get(publishPage.backToEditor).click(); -// }); -// -// it("Map-Disable Location, Mapsearch and Create Marker Property Validation", function() { -// cy.openPropertyPane("mapwidget"); -// /** -// * Disable the Search Location checkbox and Validate the same in editor mode -// */ -// cy.UncheckWidgetProperties(commonlocators.enableSearchLocCheckbox); -// cy.get(viewWidgetsPage.mapSearch).should("not.be.visible"); -// /** -// * Disable the Pick Location checkbox and Validate the same in editor mode -// */ -// cy.UncheckWidgetProperties(commonlocators.enablePickLocCheckbox); -// cy.get(viewWidgetsPage.pickMyLocation).should("not.exist"); -// -// /** -// * Disable the Createnew Marker checkbox and Validate the same in editor mode -// */ -// cy.UncheckWidgetProperties(commonlocators.enableCreateMarkerCheckbox); -// /** -// * Validation will be added when create marker fun is working fine -// */ -// -// cy.PublishtheApp(); -// /** -// * Publish mode Validation -// */ -// cy.get(publishPage.mapSearch).should("not.be.visible"); -// cy.get(publishPage.pickMyLocation).should("not.exist"); -// cy.get(publishPage.backToEditor).click(); -// }); -// -// it("Map-Check Visible field Validation", function() { -// cy.openPropertyPane("mapwidget"); -// //Check the disableed checkbox and Validate -// cy.CheckWidgetProperties(commonlocators.visibleCheckbox); -// cy.PublishtheApp(); -// cy.get(publishPage.mapWidget).should("be.visible"); -// cy.get(publishPage.backToEditor).click(); -// }); -// -// it("Map-Unckeck Visible field Validation", function() { -// cy.openPropertyPane("mapwidget"); -// //Uncheck the disabled checkbox and validate -// cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); -// cy.PublishtheApp(); -// cy.get(publishPage.mapWidget).should("not.be.visible"); -// }); -// }); +const commonlocators = require("../../../locators/commonlocators.json"); +const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); +const dsl = require("../../../fixtures/Mapdsl.json"); +const publishPage = require("../../../locators/publishWidgetspage.json"); + +if (Cypress.env("APPSMITH_GOOGLE_MAPS_API_KEY")) { + describe("Map Widget Functionality", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Map Widget Functionality", function() { + cy.openPropertyPane("mapwidget"); + /** + * @param{Text} Random Text + * @param{MapWidget}Mouseover + * @param{MapPre Css} Assertion + */ + cy.widgetText( + "Maptest", + viewWidgetsPage.mapWidget, + viewWidgetsPage.mapInner, + ); + cy.get(viewWidgetsPage.mapinitialloc) + .click({ force: true }) + .clear() + .type(this.data.country) + .type("{enter}"); + cy.get(viewWidgetsPage.mapInput) + .click({ force: true }) + .type(this.data.command) + .type(JSON.stringify(this.data.marker), { + parseSpecialCharSequences: false, + }); + cy.get(viewWidgetsPage.zoomLevel) + .eq(0) + .click({ force: true }); + cy.get(viewWidgetsPage.zoomLevel) + .eq(1) + .click({ force: true }); + cy.get(viewWidgetsPage.mapSearch) + .click({ force: true }) + .clear() + .type(this.data.location2) + .type("{enter}"); + }); + + it("Map-Enable Location,Map search and Create Marker Property Validation", function() { + /** + * Enable the Search Location checkbox and Validate the same in editor mode + */ + cy.CheckWidgetProperties(commonlocators.enableSearchLocCheckbox); + cy.get(viewWidgetsPage.mapSearch).should("be.visible"); + cy.get(viewWidgetsPage.mapSearch) + .invoke("attr", "placeholder") + .should("contain", "Enter location to search"); + /** + * Enable the Pick Location checkbox and Validate the same in editor mode + */ + cy.CheckWidgetProperties(commonlocators.enablePickLocCheckbox); + cy.get(viewWidgetsPage.pickMyLocation).should("exist"); + + /** + * Enable the Createnew Marker checkbox and Validate the same in editor mode + */ + cy.CheckWidgetProperties(commonlocators.enableCreateMarkerCheckbox); + /** + * Validation will be added when create marker fun is working fine + */ + + cy.PublishtheApp(); + /** + * Publish mode Validation + */ + cy.get(publishPage.mapSearch).should("be.visible"); + cy.get(publishPage.mapSearch) + .invoke("attr", "placeholder") + .should("contain", "Enter location to search"); + cy.get(publishPage.pickMyLocation).should("exist"); + cy.get(publishPage.backToEditor).click(); + }); + + it("Map-Disable Location, Mapsearch and Create Marker Property Validation", function() { + cy.openPropertyPane("mapwidget"); + /** + * Disable the Search Location checkbox and Validate the same in editor mode + */ + cy.UncheckWidgetProperties(commonlocators.enableSearchLocCheckbox); + cy.get(viewWidgetsPage.mapSearch).should("not.be.visible"); + /** + * Disable the Pick Location checkbox and Validate the same in editor mode + */ + cy.UncheckWidgetProperties(commonlocators.enablePickLocCheckbox); + cy.get(viewWidgetsPage.pickMyLocation).should("not.exist"); + + /** + * Disable the Createnew Marker checkbox and Validate the same in editor mode + */ + cy.UncheckWidgetProperties(commonlocators.enableCreateMarkerCheckbox); + /** + * Validation will be added when create marker fun is working fine + */ + + cy.PublishtheApp(); + /** + * Publish mode Validation + */ + cy.get(publishPage.mapSearch).should("not.be.visible"); + cy.get(publishPage.pickMyLocation).should("not.exist"); + cy.get(publishPage.backToEditor).click(); + }); + + it("Map-Initial location should work", function() { + cy.openPropertyPane("mapwidget"); + + cy.get(viewWidgetsPage.mapinitialloc).should( + "have.value", + this.data.country, + ); + + /** + * Clearing initial location used to reset it, this check makes sure it actually clears + */ + cy.get(viewWidgetsPage.mapinitialloc) + .click({ force: true }) + .clear() + .should("have.value", ""); + }); + + it("Map-Check Visible field Validation", function() { + cy.openPropertyPane("mapwidget"); + //Check the disableed checkbox and Validate + cy.CheckWidgetProperties(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get(publishPage.mapWidget).should("be.visible"); + cy.get(publishPage.backToEditor).click(); + }); + + it("Map-Unckeck Visible field Validation", function() { + cy.openPropertyPane("mapwidget"); + //Uncheck the disabled checkbox and validate + cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get(publishPage.mapWidget).should("not.be.visible"); + }); + }); +} 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 8b11ca3550e..0586b1ec966 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js @@ -23,9 +23,6 @@ describe("Button Widget Functionality", function() { widgetsPage.buttonWidget + " " + commonlocators.widgetNameTag, ); - // changing button to invalid name - cy.invalidWidgetText(); - //Changing the text on the Button cy.testCodeMirror(this.data.ButtonLabel); cy.EvaluateDataType("string"); 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 62ac7139cf3..3588508fea1 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js @@ -3,6 +3,7 @@ const formWidgetsPage = require("../../../locators/FormWidgets.json"); const widgetsPage = require("../../../locators/Widgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const dsl = require("../../../fixtures/newFormDsl.json"); +const formWidgetDsl = require("../../../fixtures/formWidgetdsl.json"); const pages = require("../../../locators/Pages.json"); describe("Checkbox Widget Functionality", function() { @@ -71,6 +72,24 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.checkboxWidget + " " + "input").should("be.visible"); cy.get(publish.backToEditor).click(); }); + + it("Checkbox Functionality To Check required toggle for form", function() { + cy.addDsl(formWidgetDsl); + cy.openPropertyPane("checkboxwidget"); + cy.togglebar(commonlocators.requiredjs + " " + "input"); + cy.PublishtheApp(); + cy.get(publish.checkboxWidget).click(); + cy.get(widgetsPage.formButtonWidget) + .contains("Submit") + .should("have.attr", "disabled"); + + cy.get(publish.checkboxWidget).click(); + cy.get(widgetsPage.formButtonWidget) + .contains("Submit") + .should("not.have.attr", "disabled"); + + cy.get(publish.backToEditor).click(); + }); }); afterEach(() => { // put your clean up code if any 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 620f79dfbc5..20af27aa1d8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js @@ -63,6 +63,57 @@ describe("DatePicker Widget Functionality", function() { ); }); + it("Datepicker min/max date validation", function() { + cy.get(formWidgetsPage.defaultDate).click({ force: true }); + cy.SetDateToToday(); + + cy.get(formWidgetsPage.minDate) + .first() + .click(); + cy.wait(1000); + cy.setDate(-1, "ddd MMM DD YYYY"); + + cy.get(formWidgetsPage.maxDate) + .first() + .click(); + cy.wait(1000); + cy.setDate(1, "ddd MMM DD YYYY"); + + cy.PublishtheApp(); + cy.get(publishPage.datepickerWidget + " .bp3-input").click(); + + const minDate = Cypress.moment() + .add(2, "days") + .format("ddd MMM DD YYYY"); + const maxDate = Cypress.moment() + .add(2, "days") + .format("ddd MMM DD YYYY"); + + cy.get(`.DayPicker-Day[aria-label=\"${minDate}\"]`).should( + "have.attr", + "aria-disabled", + "true", + ); + cy.get(`.DayPicker-Day[aria-label=\"${maxDate}\"]`).should( + "have.attr", + "aria-disabled", + "true", + ); + }); + + it("Datepicker default date validation", function() { + cy.get(formWidgetsPage.defaultDate).click(); + cy.wait(1000); + cy.setDate(-2, "ddd MMM DD YYYY"); + cy.get(formWidgetsPage.defaultDate).should( + "have.css", + "border", + "1px solid rgb(206, 66, 87)", + ); + + cy.PublishtheApp(); + }); + // it("DatePicker-check Required field validation", function() { // // Check the required checkbox // cy.CheckWidgetProperties(commonlocators.requiredCheckbox); 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 d52e69e3029..09963ee5029 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js @@ -15,6 +15,18 @@ describe("FilePicker Widget Functionality", function() { cy.get(commonlocators.editPropCrossButton).click(); }); + it("It checks the loading state of filepicker on call the action", function() { + cy.openPropertyPane("filepickerwidget"); + const fixturePath = "example.json"; + cy.getAlert(commonlocators.filePickerOnFilesSelected); + cy.get(commonlocators.filePickerButton).click(); + cy.get(commonlocators.filePickerInput) + .first() + .attachFile(fixturePath); + cy.get(commonlocators.filePickerUploadButton).click(); + cy.get(".bp3-spinner").should("have.length", 1); + }); + afterEach(() => { // put your clean up code if any }); 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 9efd1ddfeb2..9681263627c 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js @@ -3,11 +3,32 @@ const dsl = require("../../../fixtures/newFormDsl.json"); const widgetsPage = require("../../../locators/Widgets.json"); const publish = require("../../../locators/publishWidgetspage.json"); const pages = require("../../../locators/Pages.json"); +const explorer = require("../../../locators/explorerlocators.json"); describe("Input Widget Functionality", function() { before(() => { cy.addDsl(dsl); }); + + it("Checks if default values are not persisted in cache after delete", function() { + cy.openPropertyPane("inputwidget"); + cy.get(widgetsPage.defaultInput) + .type(this.data.command) + .type(this.data.defaultdata); + cy.get(widgetsPage.inputWidget + " " + "input") + .invoke("attr", "value") + .should("contain", this.data.defaultdata); + cy.get(commonlocators.deleteWidget).click(); + cy.get(explorer.addWidget).click(); + cy.dragAndDropToCanvas("inputwidget"); + cy.get(widgetsPage.inputWidget + " " + "input") + .invoke("attr", "value") + .should("not.contain", this.data.defaultdata); + + cy.addDsl(dsl); + cy.reload(); + }); + it("Input Widget Functionality", function() { cy.openPropertyPane("inputwidget"); /** diff --git a/app/client/cypress/locators/FormWidgets.json b/app/client/cypress/locators/FormWidgets.json index 009e00c55a8..cb7fe277513 100644 --- a/app/client/cypress/locators/FormWidgets.json +++ b/app/client/cypress/locators/FormWidgets.json @@ -7,6 +7,8 @@ "nextDayBtn": ".DayPicker-Day[aria-selected='true'] + div.DayPicker-Day", "datepickerWidget": ".t--draggable-datepickerwidget", "defaultDate": ".t--property-control-defaultdate input", + "minDate": ".t--property-control-mindate input", + "maxDate": ".t--property-control-maxdate input", "filepickerWidget": ".t--draggable-filepickerwidget", "formWidget": ".t--draggable-formwidget", "richTextEditorWidget": ".t--draggable-richtexteditorwidget", diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index 8db7aec1f7b..4c432fd2fad 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -47,5 +47,7 @@ "editName": ".single-select >div:contains('Edit Name')", "page": ".single-select >div", "propertyList": ".t--entity-property", - "actionlist": ".action div div" + "actionlist": ".action div div", + "settings": "li:contains('Settings')", + "onPageLoad": "[data-cy=executeOnLoad]" } diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index 724abca5123..57ded168de1 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -79,5 +79,11 @@ "onPause": ".t--property-control-onpause .t--open-dropdown-Select-Action", "changeZoomlevel": ".t--property-control-maxzoomlevel .bp3-button", "selectedZoomlevel": ".t--property-control-maxzoomlevel button span", - "imgWidget": "div[data-testid='styledImage']" -} \ No newline at end of file + "imgWidget": "div[data-testid='styledImage']", + "selectTab": ".t--tab-Tab", + "filePickerButton": ".t--widget-filepickerwidget", + "filePickerInput": ".uppy-Dashboard-input", + "filePickerUploadButton": ".uppy-StatusBar-actionBtn--upload", + "filePickerOnFilesSelected": ".t--property-control-onfilesselected" + +} diff --git a/app/client/cypress/plugins/index.js b/app/client/cypress/plugins/index.js index ba7c6b97206..5460611670c 100644 --- a/app/client/cypress/plugins/index.js +++ b/app/client/cypress/plugins/index.js @@ -1,4 +1,10 @@ /// + +const fs = require("fs"); +const path = require("path"); +const dotenv = require("dotenv"); +const chalk = require("chalk"); + // *********************************************************** // This example plugins/index.js can be used to load plugins // @@ -27,6 +33,62 @@ module.exports = (on, config) => { return launchOptions; }); + /** + * Fallback to APPSMITH_* env variables for Cypress.env if config.env doesn't already have it. + * Note: APPSMITH_* ENV vars have lower precedence than *all* methods mentioned in https://docs.cypress.io/guides/guides/environment-variables.html + * Example #1: + * process.env -> APPSMITH_FOO=bar + * cypress.json -> APPSMITH_FOO=baz + * + * Cypress.env("APPSMITH_FOO") // baz + * + * Example #2: + * process.env -> APPSMITH_FOO=bar + * cypress.json -> APPSMITH_FOO= + * + * Cypress.env("APPSMITH_FOO") // + */ + Object.keys(process.env).forEach(key => { + if ( + key.startsWith("APPSMITH_") && + !Object.prototype.hasOwnProperty.call(config.env, key) + ) { + config.env[key] = process.env[key]; + } + }); + + /** + * Fallback to .env variables for Cypress.env if procecss.env doesn't have it either + * Note: Value in .env file has the lowest precedence, even lower than APPSMITH_* ENV vars. + * Example: + * .env -> APPSMITH_FOO=bar + * process.env -> APPSMITH_FOO= + * + * Cypress.env("APPSMITH_FOO") // + */ + try { + const parsedEnv = dotenv.parse( + fs.readFileSync(path.join(__dirname, "../../../../.env"), { + encoding: "utf-8", + }), + ); + Object.keys(parsedEnv).forEach(key => { + if (!Object.prototype.hasOwnProperty.call(config.env, key)) { + config.env[key] = parsedEnv[key]; + } + }); + } catch (e) { + console.error( + chalk.yellow( + "\n====================================================================================================\n" + + chalk.red(e.message) + + "\n\n" + + "Could not load env variables from .env file, make sure you have one!\n" + + "====================================================================================================\n", + ), + ); + } + /** * This task logs the message on the CLI terminal. Use with care because it can log sensitive details * Example usage: cy.task('log', 'This is the message printed to the terminal') @@ -38,4 +100,6 @@ module.exports = (on, config) => { return null; }, }); + + return config; }; diff --git a/app/client/cypress/setup-test.sh b/app/client/cypress/setup-test.sh index 944f88aa743..22241bfe148 100755 --- a/app/client/cypress/setup-test.sh +++ b/app/client/cypress/setup-test.sh @@ -10,7 +10,7 @@ serve -s build -p 3000 & # Substitute all the env variables in nginx vars_to_substitute=$(printf '\$%s,' $(env | grep -o "^APPSMITH_[A-Z0-9_]\+" | xargs)) -cat ./docker/templates/nginx-linux.conf.template | envsubst ${vars_to_substitute} | sed -e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' > ./docker/nginx.conf +cat ./docker/templates/nginx-app.conf.template | sed -e "s|__APPSMITH_CLIENT_PROXY_PASS__|http://localhost:3000|g" | sed -e "s|__APPSMITH_SERVER_PROXY_PASS__|http://localhost:8080|g" | envsubst ${vars_to_substitute} | sed -e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' > ./docker/nginx.conf cat ./docker/templates/nginx-root.conf.template | envsubst ${vars_to_substitute} | sed -e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' > ./docker/nginx-root.conf # Create the SSL files for Nginx. Required for service workers to work properly. diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index b0a19922247..93823443c7d 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1,5 +1,7 @@ /// +require("cypress-file-upload"); + const loginPage = require("../locators/LoginPage.json"); const homePage = require("../locators/HomePage.json"); const pages = require("../locators/Pages.json"); @@ -911,7 +913,6 @@ Cypress.Commands.add( ); Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { - // checking valid widget name cy.get(commonlocators.editWidgetName) .click({ force: true }) .type(text) @@ -922,15 +923,6 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { cy.get(innercss).should("have.text", text); }); -Cypress.Commands.add("invalidWidgetText", () => { - // checking invalid widget name - cy.get(commonlocators.editWidgetName) - .click({ force: true }) - .type("download") - .type("{enter}"); - cy.get(commonlocators.toastmsg).contains("download is already being used."); -}); - Cypress.Commands.add("EvaluateDataType", dataType => { cy.get(commonlocators.evaluatedType) .should("be.visible") @@ -1070,6 +1062,38 @@ Cypress.Commands.add("enterActionValue", value => { }); }); +Cypress.Commands.add("enterNavigatePageName", value => { + cy.get("ul.tree") + .children() + .first() + .within(() => { + cy.get(".CodeMirror textarea") + .first() + .focus() + .type("{ctrl}{shift}{downarrow}") + .then($cm => { + if ($cm.val() !== "") { + cy.get(".CodeMirror textarea") + .first() + .clear({ + force: true, + }); + } + cy.get(".CodeMirror textarea") + .first() + .type(value, { + force: true, + parseSpecialCharSequences: false, + }); + cy.wait(200); + cy.get(".CodeMirror textarea") + .first() + .should("have.value", value); + }); + cy.root(); + }); +}); + Cypress.Commands.add("ClearDate", () => { cy.get(formWidgetsPage.datepickerFooter) .contains("Clear") diff --git a/app/client/docker/start-nginx.sh b/app/client/docker/start-nginx.sh index 17e69a3dace..cf29bb7e3a6 100755 --- a/app/client/docker/start-nginx.sh +++ b/app/client/docker/start-nginx.sh @@ -2,6 +2,6 @@ # This script is baked into the appsmith-editor Dockerfile and is used to boot Nginx when the Docker container starts # Refer: /app/client/Dockerfile set -ue -cat /nginx.conf.template | envsubst "$(printf '$%s,' $(env | grep -Eo '^APPSMITH_[A-Z0-9_]+'))" | sed -e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' > /etc/nginx/conf.d/default.conf +cat /nginx.conf.template | sed -e "s|__APPSMITH_CLIENT_PROXY_PASS__|http://localhost:3000|g" | sed -e "s|__APPSMITH_SERVER_PROXY_PASS__|http://localhost:8080|g" | envsubst "$(printf '$%s,' $(env | grep -Eo '^APPSMITH_[A-Z0-9_]+'))" | sed -e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' > /etc/nginx/conf.d/default.conf cat /nginx-root.conf.template | envsubst "$(printf '$%s,' $(env | grep -Eo '^APPSMITH_[A-Z0-9_]+'))" | sed -e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' > /etc/nginx/nginx.conf exec nginx -g 'daemon off;' diff --git a/app/client/docker/templates/nginx-linux.conf.template b/app/client/docker/templates/nginx-app.conf.template similarity index 93% rename from app/client/docker/templates/nginx-linux.conf.template rename to app/client/docker/templates/nginx-app.conf.template index 2aff04381c5..b7cb502f935 100644 --- a/app/client/docker/templates/nginx-linux.conf.template +++ b/app/client/docker/templates/nginx-app.conf.template @@ -24,10 +24,9 @@ server { proxy_set_header X-Forwarded-Host $host; proxy_set_header Accept-Encoding ""; - sub_filter_once off; location / { - proxy_pass http://localhost:3000; + proxy_pass __APPSMITH_CLIENT_PROXY_PASS__; sub_filter __APPSMITH_SENTRY_DSN__ '${APPSMITH_SENTRY_DSN}'; sub_filter __APPSMITH_SMART_LOOK_ID__ '${APPSMITH_SMART_LOOK_ID}'; sub_filter __APPSMITH_OAUTH2_GOOGLE_CLIENT_ID__ '${APPSMITH_OAUTH2_GOOGLE_CLIENT_ID}'; @@ -57,19 +56,19 @@ server { location /api { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass http://localhost:8080; + proxy_pass __APPSMITH_SERVER_PROXY_PASS__; } location /oauth2 { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass http://localhost:8080; + proxy_pass __APPSMITH_SERVER_PROXY_PASS__; } location /login { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass http://localhost:8080; + proxy_pass __APPSMITH_SERVER_PROXY_PASS__; } } diff --git a/app/client/docker/templates/nginx-mac.conf.template b/app/client/docker/templates/nginx-mac.conf.template deleted file mode 100644 index 0ae113a45eb..00000000000 --- a/app/client/docker/templates/nginx-mac.conf.template +++ /dev/null @@ -1,76 +0,0 @@ -server { - listen 80; - server_name dev.appsmith.com; - - return 301 https://$host$request_uri; -} - -server { - listen 443 ssl http2; - server_name dev.appsmith.com; - client_max_body_size 10m; - - ssl_certificate /etc/certificate/dev.appsmith.com.pem; - ssl_certificate_key /etc/certificate/dev.appsmith.com-key.pem; - - # include /etc/letsencrypt/options-ssl-nginx.conf; - # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - gzip on; - - proxy_ssl_server_name on; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Accept-Encoding ""; - - index index.html index.htm; - - sub_filter_once off; - location / { - proxy_pass http://host.docker.internal:3000; - sub_filter __APPSMITH_SENTRY_DSN__ '${APPSMITH_SENTRY_DSN}'; - sub_filter __APPSMITH_SMART_LOOK_ID__ '${APPSMITH_SMART_LOOK_ID}'; - sub_filter __APPSMITH_OAUTH2_GOOGLE_CLIENT_ID__ '${APPSMITH_OAUTH2_GOOGLE_CLIENT_ID}'; - sub_filter __APPSMITH_OAUTH2_GITHUB_CLIENT_ID__ '${APPSMITH_OAUTH2_GITHUB_CLIENT_ID}'; - sub_filter __APPSMITH_MARKETPLACE_ENABLED__ '${APPSMITH_MARKETPLACE_ENABLED}'; - sub_filter __APPSMITH_SEGMENT_KEY__ '${APPSMITH_SEGMENT_KEY}'; - sub_filter __APPSMITH_OPTIMIZELY_KEY__ '${APPSMITH_OPTIMIZELY_KEY}'; - sub_filter __APPSMITH_ALGOLIA_API_ID__ '${APPSMITH_ALGOLIA_API_ID}'; - sub_filter __APPSMITH_ALGOLIA_SEARCH_INDEX_NAME__ '${APPSMITH_ALGOLIA_SEARCH_INDEX_NAME}'; - sub_filter __APPSMITH_ALGOLIA_API_KEY__ '${APPSMITH_ALGOLIA_API_KEY}'; - sub_filter __APPSMITH_CLIENT_LOG_LEVEL__ '${APPSMITH_CLIENT_LOG_LEVEL}'; - sub_filter __APPSMITH_GOOGLE_MAPS_API_KEY__ '${APPSMITH_GOOGLE_MAPS_API_KEY}'; - sub_filter __APPSMITH_TNC_PP__ '${APPSMITH_TNC_PP}'; - sub_filter __APPSMITH_SENTRY_RELEASE__ '${APPSMITH_SENTRY_RELEASE}'; - sub_filter __APPSMITH_SENTRY_ENVIRONMENT__ '${APPSMITH_SENTRY_ENVIRONMENT}'; - sub_filter __APPSMITH_VERSION_ID__ '${APPSMITH_VERSION_ID}'; - sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '${APPSMITH_VERSION_RELEASE_DATE}'; - sub_filter __APPSMITH_INTERCOM_APP_ID__ '${APPSMITH_INTERCOM_APP_ID}'; - sub_filter __APPSMITH_MAIL_ENABLED__ '${APPSMITH_MAIL_ENABLED}'; - sub_filter __APPSMITH_DISABLE_TELEMETRY__ '${APPSMITH_DISABLE_TELEMETRY}'; - } - - - location /f { - proxy_pass https://cdn.optimizely.com/; - } - - location /api { - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_pass http://host.docker.internal:8080; - } - - location /oauth2 { - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - - proxy_pass http://host.docker.internal:8080; - } - - location /login { - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - - proxy_pass http://host.docker.internal:8080; - } -} diff --git a/app/client/package.json b/app/client/package.json index 0e7a56540bc..ef055c9dcab 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -94,7 +94,7 @@ "react-redux": "^7.1.3", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", - "react-scripts": "^4.0.0", + "react-scripts": "4.0.1", "react-select": "^3.0.8", "react-spring": "^8.0.27", "react-table": "^7.0.0", @@ -131,7 +131,7 @@ "eject": "react-scripts eject", "start-prod": "REACT_APP_ENVIRONMENT=PRODUCTION craco start", "cytest": "REACT_APP_TESTING=TESTING REACT_APP_ENVIRONMENT=DEVELOPMENT craco start & ./node_modules/.bin/cypress open", - "test:unit": "$(npm bin)/jest -b --colors", + "test:unit": "$(npm bin)/jest -b --colors --no-cache", "storybook": "start-storybook -p 9009 -s public", "build-storybook": "build-storybook -s public" }, @@ -179,6 +179,7 @@ "babel-plugin-styled-components": "^1.10.7", "craco-babel-loader": "^0.1.4", "cypress": "5.3.0", + "cypress-file-upload": "^4.1.1", "cypress-multi-reporters": "^1.2.4", "cypress-xpath": "^1.4.0", "dotenv": "^8.1.0", diff --git a/app/client/public/index.html b/app/client/public/index.html index cf2a03b0a99..f39036d853e 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -98,7 +98,7 @@ } const LOG_LEVELS = ["debug", "error"]; const CONFIG_LOG_LEVEL_INDEX = LOG_LEVELS.indexOf(parseConfig("__APPSMITH_CLIENT_LOG_LEVEL__")); - + const APP_ID = parseConfig("__APPSMITH_INTERCOM_APP_ID__"); const CLOUD_HOSTING = parseConfig("__APPSMITH_CLOUD_HOSTING__").length > 0; const DISABLE_TELEMETRY = parseConfig("__APPSMITH_DISABLE_TELEMETRY__").toLowerCase(); @@ -125,6 +125,9 @@ apiKey: parseConfig("__APPSMITH_SEGMENT_KEY__"), ceKey: parseConfig("__APPSMITH_SEGMENT_CE_KEY__"), }, + fusioncharts: { + licenseKey: parseConfig("__APPSMITH_FUSIONCHARTS_LICENSE_KEY__") + }, optimizely: parseConfig("__APPSMITH_OPTIMIZELY_KEY__"), enableMixpanel: parseConfig("__APPSMITH_SEGMENT_KEY__").length > 0, algolia: { diff --git a/app/client/src/actions/applicationActions.ts b/app/client/src/actions/applicationActions.ts index 3fde355ce3a..e6f8948c18b 100644 --- a/app/client/src/actions/applicationActions.ts +++ b/app/client/src/actions/applicationActions.ts @@ -63,3 +63,9 @@ export const duplicateApplication = (applicationId: string) => { }, }; }; + +export const getAllApplications = () => { + return { + type: ReduxActionTypes.GET_ALL_APPLICATION_INIT, + }; +}; diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index 3ce1af5a2ec..a33b627cd5a 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -1,13 +1,6 @@ import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; import { CreateDatasourceConfig, Datasource } from "api/DatasourcesApi"; -export const createDatasource = (payload: CreateDatasourceConfig) => { - return { - type: ReduxActionTypes.CREATE_DATASOURCE_INIT, - payload, - }; -}; - export const createDatasourceFromForm = (payload: CreateDatasourceConfig) => { return { type: ReduxActionTypes.CREATE_DATASOURCE_FROM_FORM_INIT, @@ -122,7 +115,6 @@ export const storeAsDatasource = () => { }; export default { - createDatasource, fetchDatasources, initDatasourcePane, selectPlugin, diff --git a/app/client/src/actions/orgActions.ts b/app/client/src/actions/orgActions.ts index e18d723cbe9..bae3b69e162 100644 --- a/app/client/src/actions/orgActions.ts +++ b/app/client/src/actions/orgActions.ts @@ -1,5 +1,5 @@ import { ReduxActionTypes } from "constants/ReduxActionConstants"; -import { SaveOrgRequest } from "api/OrgApi"; +import { SaveOrgLogo, SaveOrgRequest } from "api/OrgApi"; export const fetchOrg = (orgId: string) => { return { @@ -66,3 +66,19 @@ export const saveOrg = (orgSettings: SaveOrgRequest) => { payload: orgSettings, }; }; + +export const uploadOrgLogo = (orgLogo: SaveOrgLogo) => { + return { + type: ReduxActionTypes.UPLOAD_ORG_LOGO, + payload: orgLogo, + }; +}; + +export const deleteOrgLogo = (id: string) => { + return { + type: ReduxActionTypes.REMOVE_ORG_LOGO, + payload: { + id: id, + }, + }; +}; diff --git a/app/client/src/api/ApplicationApi.tsx b/app/client/src/api/ApplicationApi.tsx index 693c0940b06..a81002e8215 100644 --- a/app/client/src/api/ApplicationApi.tsx +++ b/app/client/src/api/ApplicationApi.tsx @@ -67,6 +67,7 @@ export type UpdateApplicationPayload = { icon?: string; color?: string; name?: string; + currentApp?: boolean; }; export type UpdateApplicationRequest = UpdateApplicationPayload & { diff --git a/app/client/src/api/OrgApi.ts b/app/client/src/api/OrgApi.ts index 8ea68790e5c..03e7dc5bdce 100644 --- a/app/client/src/api/OrgApi.ts +++ b/app/client/src/api/OrgApi.ts @@ -52,6 +52,12 @@ export interface SaveOrgRequest { email?: string; } +export interface SaveOrgLogo { + id: string; + logo: File; + progress: (progressEvent: ProgressEvent) => void; +} + export interface CreateOrgRequest { name: string; } @@ -100,5 +106,26 @@ class OrgApi extends Api { roleName: null, }); } + static saveOrgLogo(request: SaveOrgLogo): AxiosPromise { + const formData = new FormData(); + if (request.logo) { + formData.append("file", request.logo); + } + + return Api.post( + OrgApi.orgsURL + "/" + request.id + "/logo", + formData, + null, + { + headers: { + "Content-Type": "multipart/form-data", + }, + onUploadProgress: request.progress, + }, + ); + } + static deleteOrgLogo(request: { id: string }): AxiosPromise { + return Api.delete(OrgApi.orgsURL + "/" + request.id + "/logo"); + } } export default OrgApi; diff --git a/app/client/src/assets/icons/ads/upload.svg b/app/client/src/assets/icons/ads/upload.svg new file mode 100644 index 00000000000..295ac7a16f4 --- /dev/null +++ b/app/client/src/assets/icons/ads/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/warning.svg b/app/client/src/assets/icons/ads/warning.svg new file mode 100644 index 00000000000..2a6d5c023bd --- /dev/null +++ b/app/client/src/assets/icons/ads/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx index 164c0798434..80556eb5ca9 100644 --- a/app/client/src/components/ads/Button.tsx +++ b/app/client/src/components/ads/Button.tsx @@ -1,5 +1,11 @@ import React from "react"; -import { CommonComponentProps, hexToRgba, ThemeProp, Classes } from "./common"; +import { + CommonComponentProps, + hexToRgba, + ThemeProp, + Classes, + Variant, +} from "./common"; import styled, { css } from "styled-components"; import Icon, { IconName, IconSize } from "./Icon"; import Spinner from "./Spinner"; @@ -11,13 +17,6 @@ export enum Category { tertiary = "tertiary", } -export enum Variant { - success = "success", - info = "info", - warning = "warning", - danger = "danger", -} - export enum Size { small = "small", medium = "medium", diff --git a/app/client/src/components/ads/Callout.tsx b/app/client/src/components/ads/Callout.tsx index 7c5236dca3d..efd24b278d9 100644 --- a/app/client/src/components/ads/Callout.tsx +++ b/app/client/src/components/ads/Callout.tsx @@ -1,8 +1,7 @@ import React from "react"; -import { CommonComponentProps, Classes } from "./common"; +import { CommonComponentProps, Classes, Variant } from "./common"; import Text, { TextType } from "./Text"; import styled from "styled-components"; -import { Variant } from "./Button"; type CalloutProps = CommonComponentProps & { variant?: Variant; diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx index 3622481d037..89d4bd300a2 100644 --- a/app/client/src/components/ads/EditableText.tsx +++ b/app/client/src/components/ads/EditableText.tsx @@ -1,5 +1,8 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"; -import { EditableText as BlueprintEditableText } from "@blueprintjs/core"; +import { + EditableText as BlueprintEditableText, + Classes as BlueprintClasses, +} from "@blueprintjs/core"; import styled from "styled-components"; import Text, { TextType } from "./Text"; import Spinner from "./Spinner"; @@ -75,15 +78,17 @@ const TextContainer = styled.div<{ display: none; } - &&& .bp3-editable-text-content, - &&& .bp3-editable-text-input { + &&& + .${BlueprintClasses.EDITABLE_TEXT_CONTENT}, + &&& + .${BlueprintClasses.EDITABLE_TEXT_INPUT} { font-size: ${props => props.theme.typography.p1.fontSize}px; line-height: ${props => props.theme.typography.p1.lineHeight}px; letter-spacing: ${props => props.theme.typography.p1.letterSpacing}px; font-weight: ${props => props.theme.typography.p1.fontWeight}px; } - &&& .bp3-editable-text-content { + &&& .${BlueprintClasses.EDITABLE_TEXT_CONTENT} { cursor: pointer; color: ${props => props.theme.colors.editableText.color}; overflow: hidden; @@ -91,7 +96,7 @@ const TextContainer = styled.div<{ ${props => (props.isEditing ? "display: none" : "display: block")}; } - &&& .bp3-editable-text-input { + &&& .${BlueprintClasses.EDITABLE_TEXT_INPUT} { border: none; outline: none; height: ${props => props.theme.spaces[13] + 3}px; @@ -100,7 +105,7 @@ const TextContainer = styled.div<{ border-radius: ${props => props.theme.spaces[0]}px; } - &&& .bp3-editable-text { + &&& .${BlueprintClasses.EDITABLE_TEXT} { overflow: hidden; height: ${props => props.theme.spaces[13] + 3}px; padding: ${props => props.theme.spaces[4]}px @@ -180,7 +185,9 @@ export const EditableText = (props: EditableTextProps) => { } else if (changeStarted) { onTextChanged && onTextChanged(_value); } - onBlur(_value); + if (_value !== defaultValue) { + onBlur(_value); + } setIsEditing(false); setChangeStarted(false); }, diff --git a/app/client/src/components/ads/FilePicker.tsx b/app/client/src/components/ads/FilePicker.tsx new file mode 100644 index 00000000000..087be95f5d9 --- /dev/null +++ b/app/client/src/components/ads/FilePicker.tsx @@ -0,0 +1,353 @@ +import React, { useEffect, useRef, useState } from "react"; +import styled from "styled-components"; +import Button, { Category, Size } from "./Button"; +import axios from "axios"; +import { ReactComponent as UploadIcon } from "../../assets/icons/ads/upload.svg"; +import { DndProvider, useDrop, DropTargetMonitor } from "react-dnd"; +import HTML5Backend, { NativeTypes } from "react-dnd-html5-backend"; +import Text, { TextType } from "./Text"; +import { Classes, Variant } from "./common"; +import { Toaster } from "./Toast"; + +const CLOUDINARY_PRESETS_NAME = ""; +const CLOUDINARY_CLOUD_NAME = ""; + +type FilePickerProps = { + onFileUploaded?: (fileUrl: string) => void; + onFileRemoved?: () => void; + fileUploader?: FileUploader; + url?: string; + logoUploadError?: string; +}; + +const ContainerDiv = styled.div<{ + isUploaded: boolean; + isActive: boolean; + canDrop: boolean; +}>` + width: 320px; + height: 190px; + background-color: ${props => props.theme.colors.filePicker.bg}; + position: relative; + + #fileInput { + display: none; + } + + .drag-drop-text { + margin: ${props => props.theme.spaces[6]}px 0 + ${props => props.theme.spaces[6]}px 0; + color: ${props => props.theme.colors.filePicker.color}; + } + + .bg-image { + width: 100%; + height: 100%; + display: grid; + place-items: center; + background-repeat: no-repeat; + background-position: center; + background-size: contain; + } + + .file-description { + width: 95%; + margin-top: auto; + margin-bottom: ${props => props.theme.spaces[6] + 1}px; + display: none; + } + + .file-spec { + margin-bottom: ${props => props.theme.spaces[2]}px; + span { + margin-right: ${props => props.theme.spaces[4]}px; + } + } + + .progress-container { + width: 100%; + background: ${props => props.theme.colors.filePicker.progress}; + transition: height 0.2s; + } + + .progress-inner { + background-color: ${props => props.theme.colors.success.light}; + transition: width 0.4s ease; + height: ${props => props.theme.spaces[1]}px; + border-radius: ${props => props.theme.spaces[1] - 1}px; + width: 0%; + } + + .button-wrapper { + display: flex; + flex-direction: column; + align-items: center; + } + + .remove-button { + display: none; + position: absolute; + bottom: 0; + right: 0; + background: linear-gradient( + 180deg, + ${props => props.theme.colors.filePicker.shadow.from}, + ${props => props.theme.colors.filePicker.shadow.to} + ); + opacity: 0.6; + width: 100%; + + a { + width: 110px; + margin: ${props => props.theme.spaces[13]}px + ${props => props.theme.spaces[3]}px ${props => props.theme.spaces[3]}px + auto; + .${Classes.ICON} { + margin-right: ${props => props.theme.spaces[2] - 1}px; + } + } + } + + &:hover { + .remove-button { + display: ${props => (props.isUploaded ? "block" : "none")}; + } + } +`; + +export type SetProgress = (percentage: number) => void; +export type UploadCallback = (url: string) => void; +export type FileUploader = ( + file: any, + setProgress: SetProgress, + onUpload: UploadCallback, +) => void; + +export function CloudinaryUploader( + file: any, + setProgress: SetProgress, + onUpload: UploadCallback, +) { + const formData = new FormData(); + formData.append("upload_preset", CLOUDINARY_PRESETS_NAME); + if (file) { + formData.append("file", file); + } + axios + .post( + `https://api.cloudinary.com/v1_1/${CLOUDINARY_CLOUD_NAME}/image/upload`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + onUploadProgress: function(progressEvent: ProgressEvent) { + const uploadPercentage = Math.round( + (progressEvent.loaded / progressEvent.total) * 100, + ); + setProgress(uploadPercentage); + }, + }, + ) + .then(data => { + onUpload(data.data.url); + }) + .catch(error => { + console.error("error in file uploading", error); + }); +} + +const FilePickerComponent = (props: FilePickerProps) => { + const { logoUploadError } = props; + const [fileInfo, setFileInfo] = useState<{ name: string; size: number }>({ + name: "", + size: 0, + }); + const [isUploaded, setIsUploaded] = useState(false); + const [fileUrl, setFileUrl] = useState(""); + + const [{ canDrop, isOver }, drop] = useDrop({ + accept: [NativeTypes.FILE], + drop(item, monitor) { + onDrop(monitor); + }, + collect: monitor => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }); + + const inputRef = useRef(null); + const bgRef = useRef(null); + const progressRef = useRef(null); + const fileDescRef = useRef(null); + const fileContainerRef = useRef(null); + + function ButtonClick(event: React.MouseEvent) { + event.preventDefault(); + if (inputRef.current) { + inputRef.current.click(); + } + } + + function onDrop(monitor: DropTargetMonitor) { + if (monitor) { + const files = monitor.getItem().files; + if (files) { + handleFileUpload(files); + } + } + } + + function setProgress(uploadPercentage: number) { + if (progressRef.current) { + progressRef.current.style.width = `${uploadPercentage}%`; + } + if (uploadPercentage === 100) { + setIsUploaded(true); + if (fileDescRef.current && bgRef.current) { + fileDescRef.current.style.display = "none"; + bgRef.current.style.opacity = "1"; + } + } + } + + function onUpload(url: string) { + props.onFileUploaded && props.onFileUploaded(url); + } + + function handleFileUpload(files: FileList | null) { + const file = files && files[0]; + let fileSize = 0; + + if (file) { + fileSize = Math.floor(file.size / 1024); + setFileInfo({ name: file.name, size: fileSize }); + } + + if (fileSize < 250) { + if (bgRef.current) { + bgRef.current.style.backgroundImage = `url(${URL.createObjectURL( + file, + )})`; + bgRef.current.style.opacity = "0.5"; + } + if (fileDescRef.current) { + fileDescRef.current.style.display = "block"; + } + if (fileContainerRef.current) { + fileContainerRef.current.style.display = "none"; + } + + /* set form data and send api request */ + props.fileUploader && props.fileUploader(file, setProgress, onUpload); + } else { + Toaster.show({ + text: "File size should be less than 250kb!", + variant: Variant.warning, + }); + } + } + + function removeFile() { + if (fileContainerRef.current && bgRef.current) { + setFileUrl(""); + fileContainerRef.current.style.display = "flex"; + bgRef.current.style.backgroundImage = "url('')"; + setIsUploaded(false); + props.onFileRemoved && props.onFileRemoved(); + } + } + + const isActive = canDrop && isOver; + + useEffect(() => { + if (props.url) { + const urlKeys = props.url.split("/"); + if (urlKeys[urlKeys.length - 1] !== "null") { + setFileUrl(props.url); + } else { + setFileUrl(""); + } + } + }, [props.url]); + + useEffect(() => { + if (fileUrl && !isUploaded) { + setIsUploaded(true); + if (bgRef.current) { + bgRef.current.style.backgroundImage = `url(${fileUrl})`; + bgRef.current.style.opacity = "1"; + } + if (fileDescRef.current) { + fileDescRef.current.style.display = "none"; + } + if (fileContainerRef.current) { + fileContainerRef.current.style.display = "none"; + } + } + }, [fileUrl, logoUploadError]); + + return ( + +
+
+ + + Drag & Drop files to upload or + +
+ handleFileUpload(el.target.files)} + /> +
+
+
+ {fileInfo.name} + {fileInfo.size}KB +
+
+
+
+
+
+
+
+
+ ); +}; + +const FilePicker = (props: FilePickerProps) => { + return ( + + + + ); +}; + +export default FilePicker; diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index f01ff745943..719fbbd4a22 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -8,6 +8,7 @@ import { ReactComponent as ErrorIcon } from "assets/icons/ads/error.svg"; import { ReactComponent as SuccessIcon } from "assets/icons/ads/success.svg"; import { ReactComponent as SearchIcon } from "assets/icons/ads/search.svg"; import { ReactComponent as CloseIcon } from "assets/icons/ads/close.svg"; +import { ReactComponent as WarningIcon } from "assets/icons/ads/warning.svg"; import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg"; import { ReactComponent as ShareIcon } from "assets/icons/ads/share.svg"; import { ReactComponent as RocketIcon } from "assets/icons/ads/launch.svg"; @@ -86,6 +87,7 @@ export const IconCollection = [ "plus", "invite-user", "view-all", + "warning", "downArrow", "context-menu", "duplicate", @@ -197,6 +199,9 @@ const Icon = forwardRef( case "manage": returnIcon = ; break; + case "warning": + returnIcon = ; + break; default: returnIcon = null; break; diff --git a/app/client/src/components/ads/Menu.tsx b/app/client/src/components/ads/Menu.tsx index 6ace64786b7..30639c06fe7 100644 --- a/app/client/src/components/ads/Menu.tsx +++ b/app/client/src/components/ads/Menu.tsx @@ -32,6 +32,7 @@ const Menu = (props: MenuProps) => { className={props.className} portalClassName={props.className} data-cy={props.cypressSelector} + disabled={props.disabled} > {props.target} diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx index 50d0c7e537b..41c946d70a4 100644 --- a/app/client/src/components/ads/TextInput.tsx +++ b/app/client/src/components/ads/TextInput.tsx @@ -81,7 +81,7 @@ const boxStyles = ( const StyledInput = styled.input< TextInputProps & { inputStyle: boxReturnType; isValid: boolean } >` - width: ${props => (props.fill ? "100%" : "260px")}; + width: ${props => (props.fill ? "100%" : "320px")}; border-radius: 0; outline: 0; box-shadow: none; diff --git a/app/client/src/components/ads/Toast.tsx b/app/client/src/components/ads/Toast.tsx index bf9425130ff..c308ad1aae4 100644 --- a/app/client/src/components/ads/Toast.tsx +++ b/app/client/src/components/ads/Toast.tsx @@ -1,20 +1,176 @@ -import { CommonComponentProps } from "./common"; - -type ToastProps = CommonComponentProps & { - text: string; - duration: number; - variant?: "success" | "info" | "warning" | "danger"; //default info - keepOnHover?: boolean; - onComplete?: any; - position: - | "top-right" - | "top-center" - | "top-left" - | "bottom-right" - | "bottom-center" - | "bottom-left"; +import React from "react"; +import { CommonComponentProps, Classes, Variant } from "./common"; +import styled from "styled-components"; +import Icon, { IconSize } from "./Icon"; +import Text, { TextType } from "./Text"; +import { toast, ToastOptions, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import { ReduxActionType } from "constants/ReduxActionConstants"; +import { useDispatch } from "react-redux"; + +type ToastProps = ToastOptions & + CommonComponentProps & { + text: string; + variant?: Variant; + duration?: number; + onUndo?: () => void; + dispatchableAction?: { type: ReduxActionType; payload: any }; + hideProgressBar?: boolean; + }; + +const WrappedToastContainer = styled.div` + .Toastify__toast-container { + width: auto; + padding: 0px; + } + .Toastify__toast--default { + background: transparent; + } + .Toastify__toast { + cursor: auto; + min-height: auto; + border-radius: 0px !important; + font-family: ${props => props.theme.fonts.text}; + margin-bottom: ${props => props.theme.spaces[4]}px; + } + .Toastify__toast-container--top-right { + top: 4em; + } +`; +export const StyledToastContainer = (props: ToastOptions) => { + return ( + + + + ); }; -export default function Toast(props: ToastProps) { - return null; -} +const ToastBody = styled.div<{ + variant?: Variant; + isUndo?: boolean; + dispatchableAction?: { type: ReduxActionType; payload: any }; +}>` + width: 264px; + background: ${props => props.theme.colors.toast.bg}; + padding: ${props => props.theme.spaces[4]}px + ${props => props.theme.spaces[5]}px; + display: flex; + align-items: center; + justify-content: space-between; + + .${Classes.ICON} { + cursor: auto; + margin-right: ${props => props.theme.spaces[3]}px; + svg { + path { + fill: ${props => + props.variant === Variant.warning + ? props.theme.colors.toast.warningColor + : props.variant === Variant.danger + ? "#FFFFFF" + : "#9F9F9F"}; + } + rect { + ${props => + props.variant === Variant.danger + ? `fill: ${props.theme.colors.toast.dangerColor}` + : null}; + } + } + } + + .${Classes.TEXT} { + color: ${props => props.theme.colors.toast.textColor}; + } + + ${props => + props.isUndo || props.dispatchableAction + ? ` + .undo-section .${Classes.TEXT} { + cursor: pointer; + margin-left: ${props.theme.spaces[3]}px; + color: ${props.theme.colors.toast.undo}; + line-height: 18px; + font-weight: 600; + } + ` + : null} +`; + +const FlexContainer = styled.div` + display: flex; + align-items: center; +`; + +const ToastComponent = (props: ToastProps & { undoAction?: () => void }) => { + const dispatch = useDispatch(); + + return ( + + + {props.variant === Variant.success ? ( + + ) : props.variant === Variant.warning ? ( + + ) : null} + {props.variant === Variant.danger ? ( + + ) : null} + {props.text} + +
+ {props.onUndo || props.dispatchableAction ? ( + { + if (props.dispatchableAction) { + dispatch(props.dispatchableAction); + props.undoAction && props.undoAction(); + } else { + props.undoAction && props.undoAction(); + } + }} + > + UNDO + + ) : null} +
+
+ ); +}; + +export const Toaster = { + show: (config: ToastProps) => { + if (typeof config.text !== "string") { + console.error("Toast message needs to be a string"); + return; + } + if (config.variant && !Object.values(Variant).includes(config.variant)) { + console.error( + "Toast type needs to be a one of " + Object.values(Variant).join(", "), + ); + return; + } + const toastId = toast( + { + toast.dismiss(toastId); + config.onUndo && config.onUndo(); + }} + {...config} + />, + { + pauseOnHover: true, + autoClose: config.duration || 5000, + closeOnClick: false, + hideProgressBar: config.hideProgressBar, + }, + ); + }, + clear: () => toast.dismiss(), +}; diff --git a/app/client/src/components/ads/common.tsx b/app/client/src/components/ads/common.tsx index 6c3d702c91d..1a89afec5a9 100644 --- a/app/client/src/components/ads/common.tsx +++ b/app/client/src/components/ads/common.tsx @@ -1,6 +1,7 @@ import { Theme } from "constants/DefaultTheme"; import tinycolor from "tinycolor2"; import styled from "styled-components"; +import { toast } from "react-toastify"; export interface CommonComponentProps { isLoading?: boolean; //default false @@ -64,3 +65,32 @@ export const StoryWrapper = styled.div` height: 700px; padding: 50px 100px; `; + +export enum Variant { + success = "success", + info = "info", + warning = "warning", + danger = "danger", +} + +export const ToastVariant = (type: any) => { + let variant: Variant; + switch (type) { + case toast.TYPE.ERROR === type: + variant = Variant.danger; + break; + case toast.TYPE.INFO === type: + variant = Variant.info; + break; + case toast.TYPE.SUCCESS === type: + variant = Variant.success; + break; + case toast.TYPE.WARNING === type: + variant = Variant.warning; + break; + default: + variant = Variant.info; + break; + } + return variant; +}; diff --git a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx index d64a38f3021..159bb4d7778 100644 --- a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx @@ -1,14 +1,23 @@ +import _ from "lodash"; import React from "react"; -import { ChartType, ChartData, ChartDataPoint } from "widgets/ChartWidget"; import styled from "styled-components"; + import { invisible } from "constants/DefaultTheme"; -import _ from "lodash"; +import { getAppsmithConfigs } from "configs"; +import { ChartType, ChartData, ChartDataPoint } from "widgets/ChartWidget"; + const FusionCharts = require("fusioncharts"); const Charts = require("fusioncharts/fusioncharts.charts"); const FusionTheme = require("fusioncharts/themes/fusioncharts.theme.fusion"); + +const { fusioncharts } = getAppsmithConfigs(); Charts(FusionCharts); FusionTheme(FusionCharts); -FusionCharts.options.creditLabel = false; + +FusionCharts.options.license({ + key: fusioncharts.licenseKey, + creditLabel: false, +}); export interface ChartComponentProps { chartType: ChartType; @@ -231,7 +240,7 @@ class ChartComponent extends React.Component { componentDidMount() { this.createGraph(); FusionCharts.ready(() => { - /* Component could be unmounted before FusionCharts is ready, + /* Component could be unmounted before FusionCharts is ready, this check ensure we don't render on unmounted component */ if (this.chartInstance) { this.chartInstance.render(); diff --git a/app/client/src/components/designSystems/appsmith/MapComponent.tsx b/app/client/src/components/designSystems/appsmith/MapComponent.tsx index 974edfff8fb..59d6921f7e6 100644 --- a/app/client/src/components/designSystems/appsmith/MapComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/MapComponent.tsx @@ -1,14 +1,10 @@ import React, { useEffect } from "react"; -import { - withScriptjs, - withGoogleMap, - GoogleMap, - Marker, -} from "react-google-maps"; +import { withGoogleMap, GoogleMap, Marker } from "react-google-maps"; import SearchBox from "react-google-maps/lib/components/places/SearchBox"; import { MarkerProps } from "widgets/MapWidget"; import PickMyLocation from "./PickMyLocation"; import styled from "styled-components"; +import { useScript, ScriptStatus, AddScriptTo } from "utils/hooks/useScript"; interface MapComponentProps { apiKey: string; @@ -34,7 +30,7 @@ interface MapComponentProps { updateMarker: (lat: number, long: number, index: number) => void; saveMarker: (lat: number, long: number) => void; selectMarker: (lat: number, long: number, title: string) => void; - disableDrag: (e: any) => void; + enableDrag: (e: any) => void; unselectMarker: () => void; } @@ -73,127 +69,128 @@ const PickMyLocationWrapper = styled.div` width: 140px; `; -const MyMapComponent = withScriptjs( - withGoogleMap((props: any) => { - const [mapCenter, setMapCenter] = React.useState< - | { - lat: number; - lng: number; - title?: string; - description?: string; - } - | undefined - >({ - ...props.center, - lng: props.center.long, - }); - const searchBox = React.createRef(); - const onPlacesChanged = () => { - const node: any = searchBox.current; - if (node) { - const places: any = node.getPlaces(); - if ( - places && - places.length && - places[0].geometry && - places[0].geometry.location - ) { - const location = places[0].geometry.location; - const lat = location.lat(); - const long = location.lng(); - setMapCenter({ lat, lng: long }); - props.updateCenter(lat, long); - props.unselectMarker(); - } +const MyMapComponent = withGoogleMap((props: any) => { + const [mapCenter, setMapCenter] = React.useState< + | { + lat: number; + lng: number; + title?: string; + description?: string; } - }; - useEffect(() => { - if (!props.selectedMarker) { - setMapCenter({ - ...props.center, - lng: props.center.long, - }); + | undefined + >({ + ...props.center, + lng: props.center.long, + }); + const searchBox = React.createRef(); + const onPlacesChanged = () => { + const node: any = searchBox.current; + if (node) { + const places: any = node.getPlaces(); + if ( + places && + places.length && + places[0].geometry && + places[0].geometry.location + ) { + const location = places[0].geometry.location; + const lat = location.lat(); + const long = location.lng(); + setMapCenter({ lat, lng: long }); + props.updateCenter(lat, long); + props.unselectMarker(); } - }, [props.center, props.selectedMarker]); - return ( - { - if (props.enableCreateMarker) { - props.saveMarker(e.latLng.lat(), e.latLng.lng()); + } + }; + useEffect(() => { + if (!props.selectedMarker) { + setMapCenter({ + ...props.center, + lng: props.center.long, + }); + } + }, [props.center, props.selectedMarker]); + return ( + { + if (props.enableCreateMarker) { + props.saveMarker(e.latLng.lat(), e.latLng.lng()); + } + }} + > + {props.enableSearch && ( + + + + )} + {props.markers.map((marker: any, index: number) => ( + - {props.enableSearch && ( - - - - )} - {props.markers.map((marker: any, index: number) => ( - { - setMapCenter({ - ...marker, - lng: marker.long, - }); - props.selectMarker(marker.lat, marker.long, marker.title); - }} - onDragEnd={de => { - props.updateMarker(de.latLng.lat(), de.latLng.lng(), index); - }} - /> - ))} - {props.enablePickLocation && ( - - - - )} - - ); - }), -); + onClick={e => { + setMapCenter({ + ...marker, + lng: marker.long, + }); + props.selectMarker(marker.lat, marker.long, marker.title); + }} + onDragEnd={de => { + props.updateMarker(de.latLng.lat(), de.latLng.lng(), index); + }} + /> + ))} + {props.enablePickLocation && ( + + + + )} + + ); +}); -class MapComponent extends React.Component { - render() { - const zoom = Math.floor(this.props.zoomLevel / 5); - return ( - +const MapComponent = (props: MapComponentProps) => { + const zoom = Math.floor(props.zoomLevel / 5); + const status = useScript( + `https://maps.googleapis.com/maps/api/js?key=${props.apiKey}&v=3.exp&libraries=geometry,drawing,places`, + AddScriptTo.HEAD, + ); + return ( + + {status === ScriptStatus.READY && ( } containerElement={} mapElement={} - {...this.props} + {...props} zoom={zoom} /> - - ); - } -} + )} + + ); +}; export default MapComponent; diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx index 8e854b45a40..af5a35e2e7b 100644 --- a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -12,6 +12,7 @@ import { ReactTableFilter, } from "widgets/TableWidget"; import { EventType } from "constants/ActionConstants"; +import produce from "immer"; export interface ColumnMenuOptionProps { content: string | JSX.Element; @@ -146,10 +147,9 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { header.parentElement.className = "th header-reorder"; if (i !== dragged && dragged !== -1) { e.preventDefault(); - let columnOrder = props.columnOrder; - if (columnOrder === undefined) { - columnOrder = props.columns.map(item => item.accessor); - } + const columnOrder = props.columnOrder + ? [...props.columnOrder] + : props.columns.map(item => item.accessor); const draggedColumn = props.columns[dragged].accessor; columnOrder.splice(dragged, 1); columnOrder.splice(i, 0, draggedColumn); @@ -275,9 +275,15 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { const handleResizeColumn = (columnIndex: number, columnWidth: string) => { const column = props.columns[columnIndex]; - const columnSizeMap = props.columnSizeMap || {}; const width = Number(columnWidth.split("px")[0]); - columnSizeMap[column.accessor] = width; + const columnSizeMap = props.columnSizeMap + ? { + ...props.columnSizeMap, + [column.accessor]: width, + } + : { + [column.accessor]: width, + }; props.handleResizeColumn(columnSizeMap); }; diff --git a/app/client/src/components/designSystems/appsmith/TableHeader.tsx b/app/client/src/components/designSystems/appsmith/TableHeader.tsx index e3b8c3ed02a..8e94ede01db 100644 --- a/app/client/src/components/designSystems/appsmith/TableHeader.tsx +++ b/app/client/src/components/designSystems/appsmith/TableHeader.tsx @@ -173,7 +173,7 @@ const TableHeader = (props: TableHeaderProps) => { {!props.serverSidePaginationEnabled && ( - Showing {props.currentPageIndex + 1}-{props.tableData?.length} items + {props.tableData?.length} Records { + if (column.isResizing) return; let columnIndex = props.columnIndex; if (props.isAscOrder === true) { columnIndex = -1; @@ -760,6 +761,10 @@ export const TableHeaderCell = (props: {
) => { + e.preventDefault(); + e.stopPropagation(); + }} />
); diff --git a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx index fe1fcb13ed0..37673e8e3a3 100644 --- a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx @@ -127,6 +127,7 @@ const TabsComponent = (props: TabsComponentProps) => { {props.tabs && props.tabs.map((tab, index) => ( ) => { props.onTabChange(tab.widgetId); event.stopPropagation(); diff --git a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx index d2146327da7..29a69549f18 100644 --- a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx @@ -10,12 +10,13 @@ import { ButtonStyle } from "widgets/ButtonWidget"; import { Theme, darkenHover, darkenActive } from "constants/DefaultTheme"; import _ from "lodash"; import { ComponentProps } from "components/designSystems/appsmith/BaseComponent"; -import useScript from "utils/hooks/useScript"; -import { AppToaster } from "components/editorComponents/ToastComponent"; +import { useScript, ScriptStatus } from "utils/hooks/useScript"; import { GOOGLE_RECAPTCHA_KEY_ERROR, GOOGLE_RECAPTCHA_DOMAIN_ERROR, } from "constants/messages"; +import { Variant } from "components/ads/common"; +import { Toaster } from "components/ads/Toast"; const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => { if (props.filled) return props.theme.colors.textOnDarkBG; @@ -166,9 +167,9 @@ const RecaptchaComponent = ( } & RecaptchaProps, ) => { function handleError(event: React.MouseEvent, error: string) { - AppToaster.show({ - message: error, - type: "error", + Toaster.show({ + text: error, + variant: Variant.danger, }); props.onClick && props.onClick(event); } @@ -178,7 +179,7 @@ const RecaptchaComponent = ( return (
) => { - if (status === "ready") { + if (status === ScriptStatus.READY) { (window as any).grecaptcha.ready(() => { try { (window as any).grecaptcha diff --git a/app/client/src/components/designSystems/blueprint/CheckboxComponent.tsx b/app/client/src/components/designSystems/blueprint/CheckboxComponent.tsx index d6238dc53ae..e18c1781b2e 100644 --- a/app/client/src/components/designSystems/blueprint/CheckboxComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/CheckboxComponent.tsx @@ -4,23 +4,36 @@ import { ComponentProps } from "components/designSystems/appsmith/BaseComponent" import { Checkbox, Classes } from "@blueprintjs/core"; import { BlueprintControlTransform } from "constants/DefaultTheme"; -const CheckboxContainer = styled.div` +const CheckboxContainer = styled.div<{ isValid: boolean }>` && { width: 100%; height: 100%; display: flex; justify-content: flex-start; align-items: center; + + .bp3-control-indicator { + border: ${props => + !props.isValid + ? `1px solid ${props.theme.colors.error} !important` + : `1px solid transparent`}; + } + label { margin: 0; + color: ${props => + !props.isValid ? `${props.theme.colors.error}` : `inherit`}; } } ${BlueprintControlTransform} `; class CheckboxComponent extends React.Component { render() { + console.log({ props: this.props }); return ( - + void; isLoading: boolean; + isRequired?: boolean; } export default CheckboxComponent; diff --git a/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx b/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx index 72f288e3ad2..524c11d0c81 100644 --- a/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx @@ -81,8 +81,12 @@ class DatePickerComponent extends React.Component< render() { const now = moment(); const year = now.get("year"); - const minDate = now.clone().set({ month: 0, date: 1, year: year - 100 }); - const maxDate = now.clone().set({ month: 11, date: 31, year: year + 5 }); + const minDate = this.props.minDate + ? moment(this.props.minDate) + : now.clone().set({ month: 0, date: 1, year: year - 100 }); + const maxDate = this.props.maxDate + ? moment(this.props.maxDate) + : now.clone().set({ month: 11, date: 31, year: year + 5 }); return ( ) : ( )} @@ -170,8 +182,8 @@ interface DatePickerComponentProps extends ComponentProps { dateFormat: string; enableTimePicker?: boolean; selectedDate?: string; - minDate?: Date; - maxDate?: Date; + minDate?: string; + maxDate?: string; timezone?: string; datePickerType: DatePickerType; isDisabled: boolean; diff --git a/app/client/src/components/editorComponents/DragLayerComponent.tsx b/app/client/src/components/editorComponents/DragLayerComponent.tsx index 305f8bb89eb..2da56aab1b6 100644 --- a/app/client/src/components/editorComponents/DragLayerComponent.tsx +++ b/app/client/src/components/editorComponents/DragLayerComponent.tsx @@ -1,10 +1,4 @@ -import React, { - useContext, - useEffect, - useLayoutEffect, - RefObject, - useRef, -} from "react"; +import React, { useContext, useEffect, RefObject, useRef } from "react"; import styled from "styled-components"; import { useDragLayer, XYCoord } from "react-dnd"; import DropZone from "./Dropzone"; @@ -116,7 +110,7 @@ const DragLayerComponent = (props: DragLayerProps) => { : widget.rightColumn - widget.leftColumn; widgetHeight = widget.rows ? widget.rows : widget.bottomRow - widget.topRow; } - useLayoutEffect(() => { + useEffect(() => { const el = dropTargetMask.current; if (el) { const rect = el.getBoundingClientRect(); diff --git a/app/client/src/components/editorComponents/EditableText.tsx b/app/client/src/components/editorComponents/EditableText.tsx index b65892182ed..ac469ee618f 100644 --- a/app/client/src/components/editorComponents/EditableText.tsx +++ b/app/client/src/components/editorComponents/EditableText.tsx @@ -8,7 +8,8 @@ import _ from "lodash"; import Edit from "assets/images/EditPen.svg"; import ErrorTooltip from "./ErrorTooltip"; import { Colors } from "constants/Colors"; -import { AppToaster } from "components/editorComponents/ToastComponent"; +import { Toaster } from "components/ads/Toast"; +import { Variant } from "components/ads/common"; export enum EditInteractionKind { SINGLE, @@ -111,9 +112,9 @@ export const EditableText = (props: EditableTextProps) => { props.onTextChanged(_value); setIsEditing(false); } else { - AppToaster.show({ - message: "Invalid name", - type: "error", + Toaster.show({ + text: "Invalid name", + variant: Variant.danger, }); } }; diff --git a/app/client/src/components/editorComponents/StoreAsDatasource.tsx b/app/client/src/components/editorComponents/StoreAsDatasource.tsx index 8cb02ee0cd8..6bed9885b3a 100644 --- a/app/client/src/components/editorComponents/StoreAsDatasource.tsx +++ b/app/client/src/components/editorComponents/StoreAsDatasource.tsx @@ -41,6 +41,7 @@ const StoreAsDatasource = () => { const MenuContainer = ( { portalClassName="helper-tooltip" >
{ e.stopPropagation(); }} diff --git a/app/client/src/components/editorComponents/ToastComponent.tsx b/app/client/src/components/editorComponents/ToastComponent.tsx deleted file mode 100644 index 1d8161bc430..00000000000 --- a/app/client/src/components/editorComponents/ToastComponent.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from "react"; -import { toast, ToastOptions, TypeOptions, ToastType } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import styled from "styled-components"; -import { theme } from "constants/DefaultTheme"; -import { AlertIcons } from "icons/AlertIcons"; -import { ReduxAction } from "constants/ReduxActionConstants"; -import { useDispatch } from "react-redux"; - -const ToastBody = styled.div<{ type: TypeOptions; action: string }>` - height: 100%; - border-left: 4px solid ${({ type }) => theme.alert[type].color}; - border-radius: 4px; - background-color: white; - color: black; - padding-left: 5px; - display: grid; - grid-template-columns: ${props => - props.action === "enabled" ? "20px 212px 60px" : "20px auto"}; - align-items: center; -`; - -const ToastMessage = styled.span` - font-size: ${props => props.theme.fontSizes[3]}px; - margin: 0 5px; -`; - -const ToastAction = styled.button` - border: none; - background: rgba(214, 65, 95, 0.08); - - color: #d6415f; - font-size: 12px; - font-weight: bold; - padding: 10px; - text-transform: uppercase; - cursor: pointer; - float: right; - &:hover { - background: rgba(214, 65, 95, 0.2); - } -`; - -export const ToastTypeOptions = [ - "info", - "success", - "warning", - "error", - "default", -]; - -const ToastIcon = { - info: AlertIcons.INFO, - success: AlertIcons.SUCCESS, - error: AlertIcons.ERROR, - warning: AlertIcons.WARNING, - default: AlertIcons.INFO, -}; - -type Props = ToastOptions & { - message: string; - closeToast?: () => void; - action?: { text: string; dispatchableAction: ReduxAction }; -}; - -const ToastComponent = (props: Props) => { - const dispatch = useDispatch(); - const alertType = props.type || ToastType.INFO; - const Icon = ToastIcon[alertType]; - return ( - - - {props.message} - {props.action && ( - { - dispatch(props.action?.dispatchableAction); - props.closeToast && props.closeToast(); - }} - > - {props.action.text} - - )} - - ); -}; - -const Toaster = { - show: (config: Props) => { - if (typeof config.message !== "string") { - console.error("Toast message needs to be a string"); - return; - } - if (config.type && !ToastTypeOptions.includes(config.type)) { - console.error( - "Toast type needs to be a one of " + ToastTypeOptions.join(", "), - ); - return; - } - toast( - , - { - pauseOnHover: false, - pauseOnFocusLoss: false, - autoClose: config.autoClose || 4000, - hideProgressBar: - config.hideProgressBar === undefined ? true : config.hideProgressBar, - }, - ); - }, - clear: () => toast.dismiss(), -}; - -export const AppToaster = Toaster; diff --git a/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx b/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx index eb5d7a70e4a..e1ab8efe3ea 100644 --- a/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx +++ b/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx @@ -47,7 +47,7 @@ const FILE_TYPE_OPTIONS = [ { label: "SVG", value: "'image/svg+xml'", id: "image/svg+xml" }, ]; -const FUNC_ARGS_REGEX = /((["][^"]*["])|(['][^']*['])|([\(].*[\)[=][>][{].*[}])|([^'",][^,"+]*[^'",]*))*/gi; +const FUNC_ARGS_REGEX = /((["][^"]*["])|([\[].*[\]])|([\{].*[\}])|(['][^']*['])|([\(].*[\)[=][>][{].*[}])|([^'",][^,"+]*[^'",]*))*/gi; const ACTION_TRIGGER_REGEX = /^{{([\s\S]*?)\(([\s\S]*?)\)}}$/g; //Old Regex:: /\(\) => ([\s\S]*?)(\([\s\S]*?\))/g; const ACTION_ANONYMOUS_FUNC_REGEX = /\(\) => (({[\s\S]*?})|([\s\S]*?)(\([\s\S]*?\)))/g; @@ -189,6 +189,40 @@ 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; @@ -265,7 +299,7 @@ const views = { props.set(pageParams)} /> @@ -310,6 +344,7 @@ const FieldType = { ALERT_TYPE_SELECTOR_FIELD: "ALERT_TYPE_SELECTOR_FIELD", KEY_TEXT_FIELD: "KEY_TEXT_FIELD", VALUE_TEXT_FIELD: "VALUE_TEXT_FIELD", + QUERY_PARAMS_FIELD: "QUERY_PARAMS_FIELD", DOWNLOAD_DATA_FIELD: "DOWNLOAD_DATA_FIELD", DOWNLOAD_FILE_NAME_FIELD: "DOWNLOAD_FILE_NAME_FIELD", DOWNLOAD_FILE_TYPE_FIELD: "DOWNLOAD_FILE_TYPE_FIELD", @@ -443,6 +478,15 @@ const fieldConfigs: FieldConfigs = { }, view: ViewTypes.TEXT_VIEW, }, + [FieldType.QUERY_PARAMS_FIELD]: { + getter: (value: any) => { + return textGetter(value, 1); + }, + setter: (value: any, currentValue: string) => { + return textSetter(value, currentValue, 1); + }, + view: ViewTypes.TEXT_VIEW, + }, [FieldType.DOWNLOAD_DATA_FIELD]: { getter: (value: any) => { return textGetter(value, 0); @@ -604,6 +648,9 @@ function getFieldFromValue( fields.push({ field: FieldType.URL_FIELD, }); + fields.push({ + field: FieldType.QUERY_PARAMS_FIELD, + }); } if (value.indexOf("showModal") !== -1) { @@ -783,6 +830,7 @@ function renderField(props: { case FieldType.URL_FIELD: case FieldType.KEY_TEXT_FIELD: case FieldType.VALUE_TEXT_FIELD: + case FieldType.QUERY_PARAMS_FIELD: case FieldType.DOWNLOAD_DATA_FIELD: case FieldType.DOWNLOAD_FILE_NAME_FIELD: let fieldLabel = ""; @@ -794,6 +842,8 @@ function renderField(props: { fieldLabel = "Key"; } else if (fieldType === FieldType.VALUE_TEXT_FIELD) { fieldLabel = "Value"; + } else if (fieldType === FieldType.QUERY_PARAMS_FIELD) { + fieldLabel = "Query Params"; } else if (fieldType === FieldType.DOWNLOAD_DATA_FIELD) { fieldLabel = "Data to download"; } else if (fieldType === FieldType.DOWNLOAD_FILE_NAME_FIELD) { diff --git a/app/client/src/components/formControls/KeyValueInputControl.tsx b/app/client/src/components/formControls/KeyValueInputControl.tsx index 119c1fc2b17..1a0fe7f9ff6 100644 --- a/app/client/src/components/formControls/KeyValueInputControl.tsx +++ b/app/client/src/components/formControls/KeyValueInputControl.tsx @@ -7,6 +7,7 @@ import BaseControl, { ControlProps, ControlData } from "./BaseControl"; import TextField from "components/editorComponents/form/fields/TextField"; import { ControlType } from "constants/PropertyControlConstants"; import FormLabel from "components/editorComponents/FormLabel"; +import { Colors } from "constants/Colors"; const FormRowWithLabel = styled.div` display: flex; @@ -49,6 +50,7 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => {
{index === props.fields.length - 1 ? ( props.fields.push({ key: "", value: "" })} @@ -57,8 +59,10 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { /> ) : ( props.fields.remove(index)} style={{ alignSelf: "center" }} /> diff --git a/app/client/src/components/propertyControls/DatePickerControl.tsx b/app/client/src/components/propertyControls/DatePickerControl.tsx index d17df874f00..5fd9317c289 100644 --- a/app/client/src/components/propertyControls/DatePickerControl.tsx +++ b/app/client/src/components/propertyControls/DatePickerControl.tsx @@ -5,8 +5,10 @@ import moment from "moment-timezone"; import styled from "styled-components"; import { TimePrecision } from "@blueprintjs/datetime"; import { WidgetProps } from "widgets/BaseWidget"; +import { Toaster } from "components/ads/Toast"; +import { Variant } from "components/ads/common"; -const DatePickerControlWrapper = styled.div` +const DatePickerControlWrapper = styled.div<{ isValid: boolean }>` display: flex; flex-direction: column; margin: 8px 0 0 0; @@ -16,6 +18,10 @@ const DatePickerControlWrapper = styled.div` color: ${props => props.theme.colors.textOnDarkBG}; font-size: ${props => props.theme.fontSizes[3]}px; box-shadow: none; + border: ${props => + !props.isValid + ? `1px solid ${props.theme.colors.error}` + : `1px solid transparent`}; } } .vertical-center { @@ -57,7 +63,7 @@ class DatePickerControl extends BaseControl< render() { return ( - + { const selectedDate = date ? this.formatDate(date) : undefined; + const isValid = this.validateDate(date); + + if (!isValid) return; + + // if everything is ok, put date in state this.setState({ selectedDate: selectedDate }); this.updateProperty(this.props.propertyName, selectedDate); }; + /** + * checks: + * 1. if max date is greater than the default date + * 2. if default date is in range of min and max date + */ + validateDate = (date: Date): boolean => { + const parsedSelectedDate = moment( + date, + this.props.widgetProperties.dateFormat, + ); + + if (this.props.widgetProperties?.evaluatedValues?.value) { + const parsedWidgetDate = moment( + this.props.widgetProperties.evaluatedValues.value, + this.props.widgetProperties.dateFormat, + ); + + // checking if widget date is after min date + if (this.props.propertyName === "minDate") { + if ( + parsedSelectedDate.isValid() && + parsedWidgetDate.isBefore(parsedSelectedDate) + ) { + Toaster.show({ + text: "Min date cannot be greater than current widget value.", + variant: Variant.danger, + }); + + return false; + } + } + + // checking if widget date is before max date + if (this.props.propertyName === "maxDate") { + if ( + parsedSelectedDate.isValid() && + parsedWidgetDate.isAfter(parsedSelectedDate) + ) { + Toaster.show({ + text: "Max date cannot be less than current widget value.", + variant: Variant.danger, + }); + + return false; + } + } + } + + return true; + }; + formatDate = (date: Date): string => { return moment(date).format( this.props.widgetProperties.dateFormat || "DD/MM/YYYY HH:mm", diff --git a/app/client/src/components/propertyControls/LocationSearchControl.tsx b/app/client/src/components/propertyControls/LocationSearchControl.tsx index 73dbce4f9b0..35f97dbb4b8 100644 --- a/app/client/src/components/propertyControls/LocationSearchControl.tsx +++ b/app/client/src/components/propertyControls/LocationSearchControl.tsx @@ -1,12 +1,10 @@ -import React from "react"; +import React, { useState } from "react"; import BaseControl, { ControlProps } from "./BaseControl"; import styled from "styled-components"; import SearchBox from "react-google-maps/lib/components/places/SearchBox"; import StandaloneSearchBox from "react-google-maps/lib/components/places/StandaloneSearchBox"; import { getAppsmithConfigs } from "configs"; - -const { compose, withProps, lifecycle } = require("recompose"); -const { withScriptjs } = require("react-google-maps"); +import { useScript, ScriptStatus, AddScriptTo } from "utils/hooks/useScript"; const StyledInput = styled.input` box-sizing: border-box; @@ -22,67 +20,40 @@ const StyledInput = styled.input` color: ${props => props.theme.colors.textOnDarkBG}; `; -interface StandaloneSearchBoxProps { - onSearchBoxMounted: (ref: any) => void; - onPlacesChanged: () => void; -} - const { google } = getAppsmithConfigs(); -const PlacesWithStandaloneSearchBox = compose( - withProps({ - googleMapURL: `https://maps.googleapis.com/maps/api/js?key=${google.apiKey}&v=3.exp&libraries=geometry,drawing,places`, - loadingElement:
, - containerElement:
, - }), - lifecycle({ - componentWillMount() { - let searchBox: any = React.createRef(); - this.setState({ - places: [], - onSearchBoxMounted: (ref: any) => { - searchBox = ref; - }, - onPlacesChanged: () => { - if (searchBox === null) return; - if (searchBox.getPlaces === null) return; - const places = searchBox.getPlaces(); - this.setState({ - places, - }); - this.props.onLocationSelection(places); - }, - }); - }, - }), - withScriptjs, -)((props: any) => ( -
- - - -
-)); - class LocationSearchControl extends BaseControl { - onLocationSelection = ( - places: Array<{ - geometry: { location: { lat: () => number; lng: () => number } }; - }>, - ) => { + searchBox: any = null; + + clearLocation = () => { + this.updateProperty(this.props.propertyName, { + lat: -34.397, + long: 150.644, + title: "", + }); + }; + + onLocationSelection = () => { + const places = this.searchBox.getPlaces(); const location = places[0].geometry.location; + const title = places[0].formatted_address; const lat = location.lat(); const long = location.lng(); - const value = { lat, long }; + const value = { lat, long, title }; this.updateProperty(this.props.propertyName, value); }; + + onSearchBoxMounted = (ref: SearchBox) => { + this.searchBox = ref; + }; + render() { return ( - ); } @@ -92,4 +63,45 @@ class LocationSearchControl extends BaseControl { } } +interface MapScriptWrapperProps { + onSearchBoxMounted: (ref: SearchBox) => void; + onPlacesChanged: () => void; + clearLocation: () => void; + propertyValue: any; +} + +const MapScriptWrapper = (props: MapScriptWrapperProps) => { + const status = useScript( + `https://maps.googleapis.com/maps/api/js?key=${google.apiKey}&v=3.exp&libraries=geometry,drawing,places`, + AddScriptTo.HEAD, + ); + const [title, setTitle] = useState(""); + return ( +
+ {status === ScriptStatus.READY && ( + { + props.onPlacesChanged(); + setTitle(""); + }} + > + { + const val = ev.target.value; + if (val === "") { + props.clearLocation(); + } + setTitle(val); + }} + /> + + )} +
+ ); +}; + export default LocationSearchControl; diff --git a/app/client/src/components/propertyControls/StyledControls.tsx b/app/client/src/components/propertyControls/StyledControls.tsx index 5acaabf1913..79b933a6bb2 100644 --- a/app/client/src/components/propertyControls/StyledControls.tsx +++ b/app/client/src/components/propertyControls/StyledControls.tsx @@ -11,7 +11,7 @@ import { } from "@blueprintjs/core"; import { DropdownOption } from "widgets/DropdownWidget"; import { ContainerOrientation } from "constants/WidgetConstants"; -import { DateInput } from "@blueprintjs/datetime"; +import { DateInput, DateRangeInput } from "@blueprintjs/datetime"; import { Colors } from "constants/Colors"; import { Skin } from "constants/DefaultTheme"; @@ -297,6 +297,14 @@ export const StyledDatePicker = styled(DateInput)` } `; +export const StyledDateRangePicker = styled(DateRangeInput)` + > input { + color: ${props => props.theme.colors.textOnDarkBG}; + background: ${props => props.theme.colors.paneInputBG}; + border: 1px solid green; + } +`; + export const StyledPropertyPaneButton = styled(Button)` &&&& { background-color: ${props => props.theme.colors.infoOld}; diff --git a/app/client/src/components/propertyControls/TabControl.tsx b/app/client/src/components/propertyControls/TabControl.tsx index 3404318c943..bf4fea64832 100644 --- a/app/client/src/components/propertyControls/TabControl.tsx +++ b/app/client/src/components/propertyControls/TabControl.tsx @@ -9,6 +9,7 @@ import { generateReactKey } from "utils/generators"; import { DroppableComponent } from "../designSystems/appsmith/DraggableListComponent"; import { getNextEntityName } from "utils/AppsmithUtils"; import _ from "lodash"; +import * as Sentry from "@sentry/react"; const StyledDeleteIcon = styled(FormIcons.DELETE_ICON as AnyStyledComponent)` padding: 0; @@ -98,17 +99,46 @@ function TabControlComponent(props: RenderComponentProps) { } class TabControl extends BaseControl { + componentDidMount() { + this.migrateTabData(this.props.propertyValue); + } + + migrateTabData( + tabData: Array<{ + id: string; + label: string; + }>, + ) { + // Added a migration script for older tab data that was strings + // deprecate after enough tabs have moved to the new format + if (_.isString(tabData)) { + try { + const parsedData: Array<{ + sid: string; + label: string; + }> = JSON.parse(tabData); + this.updateProperty(this.props.propertyName, parsedData); + return parsedData; + } catch (error) { + Sentry.captureException({ + message: "Tab Migration Failed", + oldData: this.props.propertyValue, + }); + } + } else { + return this.props.propertyValue; + } + } + updateItems = (items: Array>) => { - this.updateProperty(this.props.propertyName, JSON.stringify(items)); + this.updateProperty(this.props.propertyName, items); }; render() { const tabs: Array<{ id: string; label: string; - }> = _.isString(this.props.propertyValue) - ? JSON.parse(this.props.propertyValue) - : this.props.propertyValue; + }> = _.isString(this.props.propertyValue) ? [] : this.props.propertyValue; return ( { } deleteOption = (index: number) => { - let tabs: Array> = _.isString( - this.props.propertyValue, - ) - ? JSON.parse(this.props.propertyValue).slice() - : this.props.propertyValue.slice(); + let tabs: Array> = this.props.propertyValue.slice(); if (tabs.length === 1) return; delete tabs[index]; tabs = tabs.filter(Boolean); - this.updateProperty(this.props.propertyName, JSON.stringify(tabs)); + this.updateProperty(this.props.propertyName, tabs); }; updateOption = (index: number, updatedLabel: string) => { const tabs: Array<{ id: string; label: string; - }> = _.isString(this.props.propertyValue) - ? JSON.parse(this.props.propertyValue) - : this.props.propertyValue; + }> = this.props.propertyValue; const updatedTabs = tabs.map((tab, tabIndex) => { if (index === tabIndex) { - tab.label = updatedLabel; + return { + ...tab, + label: updatedLabel, + }; } return tab; }); - this.updateProperty(this.props.propertyName, JSON.stringify(updatedTabs)); + this.updateProperty(this.props.propertyName, updatedTabs); }; addOption = () => { @@ -163,9 +190,7 @@ class TabControl extends BaseControl { id: string; label: string; widgetId: string; - }> = _.isString(this.props.propertyValue) - ? JSON.parse(this.props.propertyValue) - : this.props.propertyValue; + }> = this.props.propertyValue; const newTabId = generateReactKey({ prefix: "tab" }); const newTabLabel = getNextEntityName( "Tab ", @@ -176,7 +201,7 @@ class TabControl extends BaseControl { { id: newTabId, label: newTabLabel, widgetId: generateReactKey() }, ]; - this.updateProperty(this.props.propertyName, JSON.stringify(tabs)); + this.updateProperty(this.props.propertyName, tabs); }; static getControlType() { diff --git a/app/client/src/components/stories/Button.stories.tsx b/app/client/src/components/stories/Button.stories.tsx index 4f6a7f7931f..9cf2e82c1cd 100644 --- a/app/client/src/components/stories/Button.stories.tsx +++ b/app/client/src/components/stories/Button.stories.tsx @@ -1,8 +1,8 @@ import React from "react"; -import Button, { Size, Category, Variant } from "components/ads/Button"; +import Button, { Size, Category } from "components/ads/Button"; import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; -import { StoryWrapper } from "components/ads/common"; +import { StoryWrapper, Variant } from "components/ads/common"; import { IconCollection, IconName } from "components/ads/Icon"; export default { diff --git a/app/client/src/components/stories/Callout.stories.tsx b/app/client/src/components/stories/Callout.stories.tsx index 38c8cf36bf6..8da26a91393 100644 --- a/app/client/src/components/stories/Callout.stories.tsx +++ b/app/client/src/components/stories/Callout.stories.tsx @@ -1,8 +1,7 @@ import React from "react"; import { withKnobs, select, text, boolean } from "@storybook/addon-knobs"; import Callout from "components/ads/Callout"; -import { StoryWrapper } from "components/ads/common"; -import { Variant } from "components/ads/Button"; +import { StoryWrapper, Variant } from "components/ads/common"; export default { title: "Callout", diff --git a/app/client/src/components/stories/FilePicker.stories.tsx b/app/client/src/components/stories/FilePicker.stories.tsx new file mode 100644 index 00000000000..c218b28d215 --- /dev/null +++ b/app/client/src/components/stories/FilePicker.stories.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import FilePicker, { CloudinaryUploader } from "../ads/FilePicker"; + +export default { + title: "FilePicker", + component: FilePicker, +}; + +function ShowUploadedFile(data: any) { + console.log(data); +} + +export const withDynamicProps = () => ( + ShowUploadedFile(data)} + fileUploader={CloudinaryUploader} + /> +); diff --git a/app/client/src/components/stories/Icon.stories.tsx b/app/client/src/components/stories/Icon.stories.tsx index 526bbf9cb12..9341b21c9f4 100644 --- a/app/client/src/components/stories/Icon.stories.tsx +++ b/app/client/src/components/stories/Icon.stories.tsx @@ -1,10 +1,10 @@ import React from "react"; import Icon, { IconSize, IconCollection } from "components/ads/Icon"; -import Button, { Size, Category, Variant } from "components/ads/Button"; +import Button, { Size, Category } from "components/ads/Button"; import { withKnobs, select, boolean } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; import AppIcon, { AppIconCollection } from "components/ads/AppIcon"; -import { StoryWrapper } from "components/ads/common"; +import { StoryWrapper, Variant } from "components/ads/common"; export default { title: "Icon", diff --git a/app/client/src/components/stories/Table.stories.tsx b/app/client/src/components/stories/Table.stories.tsx index 7b21131027b..e89c46d2031 100644 --- a/app/client/src/components/stories/Table.stories.tsx +++ b/app/client/src/components/stories/Table.stories.tsx @@ -1,10 +1,10 @@ import React from "react"; import Table from "components/ads/Table"; -import Button, { Category, Variant, Size } from "components/ads/Button"; +import Button, { Category, Size } from "components/ads/Button"; import Icon, { IconSize } from "components/ads/Icon"; import TableDropdown from "components/ads/TableDropdown"; import { Position } from "@blueprintjs/core/lib/esm/common/position"; -import { StoryWrapper } from "components/ads/common"; +import { StoryWrapper, Variant } from "components/ads/common"; export default { title: "Table", diff --git a/app/client/src/components/stories/Toast.stories.tsx b/app/client/src/components/stories/Toast.stories.tsx new file mode 100644 index 00000000000..55ee91dc694 --- /dev/null +++ b/app/client/src/components/stories/Toast.stories.tsx @@ -0,0 +1,51 @@ +import React, { useEffect } from "react"; +import { withKnobs, text, number, select } from "@storybook/addon-knobs"; +import { Toaster, StyledToastContainer } from "components/ads/Toast"; +import Button, { Size, Category } from "components/ads/Button"; +import { action } from "@storybook/addon-actions"; +import { Slide } from "react-toastify"; +import { StoryWrapper, Variant } from "components/ads/common"; + +export default { + title: "Toast", + component: Toaster, + decorators: [withKnobs], +}; + +export const ToastStory = () => { + useEffect(() => { + Toaster.show({ + text: text("message", "Archived successfully"), + duration: number("duration", 5000), + variant: select("variant", Object.values(Variant), Variant.info), + onUndo: action("on-undo"), + }); + }, []); + + return ( + + +