diff --git a/.all-contributorsrc b/.all-contributorsrc
index 0ef793ed77..d7b219ee17 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -343,6 +343,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "aswathkk",
+ "name": "Aswath K",
+ "avatar_url": "https://avatars.githubusercontent.com/u/10436935?v=4",
+ "profile": "https://github.com/aswathkk",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/.env.example b/.env.example
index ef48c3bc9d..6dfdfab187 100644
--- a/.env.example
+++ b/.env.example
@@ -57,4 +57,9 @@ APPSMITH_MAIL_SMTP_TLS_ENABLED=
#APPSMITH_SENTRY_ENVIRONMENT=
# Configure cloud services
-# APPSMITH_CLOUD_SERVICES_BASE_URL
\ No newline at end of file
+# APPSMITH_CLOUD_SERVICES_BASE_URL="https://release-cs.appsmith.com"
+
+# Google Recaptcha Config
+APPSMITH_RECAPTCHA_SITE_KEY=
+APPSMITH_RECAPTCHA_SECRET_KEY=
+APPSMITH_RECAPTCHA_ENABLED=
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/----feature-request.md b/.github/ISSUE_TEMPLATE/----feature-request.md
index 7f7ff5b05d..be54588e4d 100644
--- a/.github/ISSUE_TEMPLATE/----feature-request.md
+++ b/.github/ISSUE_TEMPLATE/----feature-request.md
@@ -2,7 +2,7 @@
name: "\U0001F6E0οΈ Feature request"
about: Suggest an idea to improve appsmith
title: "[Feature]"
-labels: ''
+labels: Enhancement
assignees: Nikhil-Nandagopal
---
diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md
index c9820c7041..4694040b0e 100644
--- a/.github/ISSUE_TEMPLATE/---bug-report.md
+++ b/.github/ISSUE_TEMPLATE/---bug-report.md
@@ -4,7 +4,6 @@ about: Create a bug report to help us improve appsmith
title: "[Bug]"
labels: Bug, Needs Triaging
assignees: Nikhil-Nandagopal
-
---
## Description
@@ -12,13 +11,17 @@ assignees: Nikhil-Nandagopal
[What happened]
### Steps to reproduce the behaviour:
+
Add steps to reproduce this behaviour, include console / network logs & screenshots
+
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
### Important Details
- - Version: [Cloud / Self-Hosted vx.x]
- - OS: [e.g.MacOSX]
- - Browser [e.g. chrome, safari]
+
+- Version: [Cloud / Self-Hosted vx.x]
+- OS: [e.g.MacOSX]
+- Browser [e.g. chrome, safari]
+- Environment [production, release, deploy preview]
diff --git a/.github/ISSUE_TEMPLATE/---epic.md b/.github/ISSUE_TEMPLATE/---epic.md
index 2a132b4026..ebd598123a 100644
--- a/.github/ISSUE_TEMPLATE/---epic.md
+++ b/.github/ISSUE_TEMPLATE/---epic.md
@@ -4,6 +4,7 @@ about: Describe the problem, solution and related issues
title: "[Epic]"
labels: Epic, Product Note
assignees: Nikhil-Nandagopal
+
---
# Objective
diff --git a/.github/config.json b/.github/config.json
index 48b03bac49..cbc906c204 100644
--- a/.github/config.json
+++ b/.github/config.json
@@ -170,6 +170,10 @@
"color": "50ba23",
"description": ""
},
+ "Import-Export-App": {
+ "name": "Import-Export-App",
+ "color": "50ba23"
+ },
"JS": {
"name": "JS",
"color": "ffc1c2",
@@ -619,6 +623,11 @@
"type": "hasLabel",
"value": true
},
+ {
+ "label": "iFrame",
+ "type": "hasLabel",
+ "value": true
+ },
{
"label": "Widgets",
"type": "hasLabel",
@@ -648,6 +657,11 @@
"type": "hasLabel",
"label": "UI Building",
"value": true
+ },
+ {
+ "type": "hasLabel",
+ "label": "UI Performance",
+ "value": true
}
]
},
@@ -664,6 +678,11 @@
"label": "Debugger",
"value": true
},
+ {
+ "type": "hasLabel",
+ "label": "Import-Export-App",
+ "value": true
+ },
{
"type": "hasLabel",
"label": "Omnibar",
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index df2e2f8f51..162b9ead0c 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -3,10 +3,12 @@
> Use this template to quickly create a well written pull request. Delete all quotes before creating the pull request.
## Description
-> Please include a summary of the changes and which issue has been fixed. Please also include relevant motivation
+
+> Please include a summary of the changes and which issue has been fixed. Please also include relevant motivation
> and context. List any dependencies that are required for this change.
Fixes # (issue)
+
> if no issue exists, please create an issue and ask the maintainers about this first
## Type of change
@@ -20,7 +22,7 @@ Fixes # (issue)
## How Has This Been Tested?
-> Please describe the tests that you ran to verify your changes. Provide instructions, so we can reproduce.
+> Please describe the tests that you ran to verify your changes. Provide instructions, so we can reproduce.
> Please also list any relevant details for your test configuration.
- Test A
diff --git a/.github/release-drafter-template.yml b/.github/release-drafter-template.yml
index ff28150475..e087624d0a 100644
--- a/.github/release-drafter-template.yml
+++ b/.github/release-drafter-template.yml
@@ -9,6 +9,9 @@ categories:
- 'Bug'
- title: 'π Documentation'
label: 'Documentation'
+# Any PR with the label 'skip-changelog' will not be inserted into the release notes
+exclude-labels:
+ - 'skip-changelog'
change-template: '- $TITLE (#$NUMBER)'
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
@@ -25,4 +28,4 @@ version-resolver:
template: |
## What's new?
- $CHANGES
\ No newline at end of file
+ $CHANGES
diff --git a/.github/workflows/build-rts.yml b/.github/workflows/build-rts.yml
index 6ee9672092..9fc4aaca53 100644
--- a/.github/workflows/build-rts.yml
+++ b/.github/workflows/build-rts.yml
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
push:
- branches: [release, master]
+ branches: [release, release-frozen, master]
# Only trigger if files have changed in this specific path
paths:
- "app/rts/**"
@@ -68,6 +68,14 @@ jobs:
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-rts:${{steps.vars.outputs.tag}}
+ # Build release-frozen Docker image and push to Docker Hub
+ - name: Push release-frozen image to Docker Hub
+ if: success() && github.ref == 'refs/heads/release-frozen'
+ run: |
+ docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-rts:${{steps.vars.outputs.tag}} .
+ echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
+ docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-rts:${{steps.vars.outputs.tag}}
+
# Build master Docker image and push to Docker Hub
- name: Push master image to Docker Hub with commit tag
if: success() && github.ref == 'refs/heads/master'
diff --git a/.github/workflows/client-test.yml b/.github/workflows/client-test.yml
index e55457c5d2..d6f2b89ac4 100644
--- a/.github/workflows/client-test.yml
+++ b/.github/workflows/client-test.yml
@@ -13,7 +13,7 @@ on:
# trigger for pushes to release and master
push:
- branches: [release, master]
+ branches: [release, release-frozen, master]
paths:
- "app/client/**"
- "!app/client/cypress/manual_TestSuite/**"
@@ -27,13 +27,11 @@ jobs:
build:
# If the build has been triggered manually via workflow_dispatch or via a push to protected branches
# then we don't check for the PR approved state
- # Only PR approvals of internally created PRs should trigger this workflow
if: |
- github.event_name == 'workflow_dispatch' ||
- github.event_name == 'push' ||
- (github.event_name == 'pull_request_review' &&
- github.event.review.state == 'approved' &&
- github.event.pull_request.head.repo.full_name == github.repository)
+ github.event_name == 'workflow_dispatch' ||
+ github.event_name == 'push' ||
+ (github.event_name == 'pull_request_review' &&
+ github.event.review.state == 'approved')
runs-on: ubuntu-latest
defaults:
run:
@@ -116,6 +114,7 @@ jobs:
REACT_APP_VERSION_ID=${{ steps.vars.outputs.version }} \
REACT_APP_VERSION_RELEASE_DATE=$(date -u '+%Y-%m-%dT%H:%M:%SZ') \
REACT_APP_GOOGLE_ANALYTICS_ID=${{ secrets.GOOGLE_TAG_MANAGER_ID }} \
+ REACT_APP_SHOW_ONBOARDING_FORM=true \
yarn build
# Upload the build artifact so that it can be used by the test & deploy job in the workflow
@@ -197,7 +196,22 @@ jobs:
path: app/client/build
- name: Pull release server docker container and start it locally
- if: github.ref == 'refs/heads/release' || github.event.pull_request.base.ref == 'release'
+ if: github.ref == 'refs/heads/release' || github.event.pull_request.base.ref == 'release' || github.event.pull_request.head.ref == 'release'
+ shell: bash
+ run: |
+ 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 \
+ --env APPSMITH_CLOUD_SERVICES_BASE_URL=https://release-cs.appsmith.com \
+ --env APPSMITH_CLOUD_SERVICES_USERNAME= \
+ --env APPSMITH_CLOUD_SERVICES_PASSWORD= \
+ ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:release
+
+ - name: Pull release-frozen server docker container and start it locally
+ if: github.ref == 'refs/heads/release-frozen' || github.event.pull_request.head.ref == 'release-frozen'
shell: bash
run: |
docker run -d --net=host --name appsmith-internal-server -p 8080:8080 \
@@ -209,10 +223,10 @@ jobs:
--env APPSMITH_CLOUD_SERVICES_BASE_URL= \
--env APPSMITH_CLOUD_SERVICES_USERNAME= \
--env APPSMITH_CLOUD_SERVICES_PASSWORD= \
- ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:release
+ ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:release-frozen
- name: Pull master server docker container and start it locally
- if: github.ref == 'refs/heads/master' || github.event.pull_request.base.ref == 'master'
+ if: github.ref == 'refs/heads/master'
shell: bash
run: |
docker run -d --net=host --name appsmith-internal-server -p 8080:8080 \
@@ -221,7 +235,7 @@ jobs:
--env APPSMITH_ENCRYPTION_PASSWORD=password \
--env APPSMITH_ENCRYPTION_SALT=salt \
--env APPSMITH_IS_SELF_HOSTED=false \
- --env APPSMITH_CLOUD_SERVICES_BASE_URL= \
+ --env APPSMITH_CLOUD_SERVICES_BASE_URL=https://release-cs.appsmith.com \
--env APPSMITH_CLOUD_SERVICES_USERNAME= \
--env APPSMITH_CLOUD_SERVICES_PASSWORD= \
${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:nightly
@@ -287,7 +301,7 @@ jobs:
ui-test-result:
needs: ui-test
# Only run if the ui-test with matrices step is successful
- if: success()
+ if: always()
runs-on: ubuntu-latest
defaults:
run:
@@ -295,6 +309,19 @@ jobs:
steps:
- run: echo "All ui-test matrices completed"
+ - name: Return status for ui-matrix
+ run: |
+ if [[ "${{ needs.ui-test.result }}" == "success" ]]; then
+ echo "Integration tests completed successfully!";
+ exit 0;
+ elif [[ "${{ needs.ui-test.result }}" == "skipped" ]]; then
+ echo "Integration tests were skipped";
+ exit 1;
+ else
+ echo "Integration tests have failed";
+ exit 1;
+ fi
+
package:
needs: ui-test
runs-on: ubuntu-latest
@@ -302,7 +329,7 @@ jobs:
run:
working-directory: app/client
# Run this job only if all the previous steps are a success and the reference if the release or master branch
- if: success() && (github.ref == 'refs/heads/release' || github.ref == 'refs/heads/master')
+ if: (success() && github.ref == 'refs/heads/release') || github.ref == 'refs/heads/master'
steps:
# Checkout the code
@@ -336,6 +363,14 @@ jobs:
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${{steps.branch_name.outputs.tag}}
+ # Build release-frozen Docker image and push to Docker Hub
+ - name: Push release-frozen image to Docker Hub
+ if: success() && github.ref == 'refs/heads/release-frozen' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
+ run: |
+ docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${{steps.branch_name.outputs.tag}} .
+ echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
+ docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${{steps.branch_name.outputs.tag}}
+
# 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' || github.event_name == 'workflow_dispatch')
diff --git a/.github/workflows/external-client-test.yml b/.github/workflows/external-client-test.yml
index f83389788e..28523fdbca 100644
--- a/.github/workflows/external-client-test.yml
+++ b/.github/workflows/external-client-test.yml
@@ -71,6 +71,7 @@ jobs:
- name: Build and test
env:
APPSMITH_MONGODB_URI: "mongodb://localhost:27017/mobtools"
+ APPSMITH_CLOUD_SERVICES_BASE_URL: "https://release-cs.appsmith.com"
APPSMITH_REDIS_URL: "redis://127.0.0.1:6379"
APPSMITH_ENCRYPTION_PASSWORD: "password"
APPSMITH_ENCRYPTION_SALT: "salt"
@@ -199,7 +200,7 @@ jobs:
if(check.length == 0) {
const head_sha = pull.head.sha;
const { data: completed_at } = await github.checks.create({
- owner: context.repo.owner,
+ owner: context.repo.owner,
repo: context.repo.repo,
head_sha: head_sha,
name: process.env.job,
@@ -294,7 +295,24 @@ jobs:
path: app/client/build
- name: Pull release server docker container and start it locally
- if: github.ref == 'refs/heads/release' || github.event.pull_request.base.ref == 'release'
+ if: github.ref == 'refs/heads/release' || github.event.pull_request.base.ref == 'release'|| github.event.pull_request.head.ref == '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 \
+ --env APPSMITH_CLOUD_SERVICES_BASE_URL=https://release-cs.appsmith.com \
+ --env APPSMITH_CLOUD_SERVICES_USERNAME= \
+ --env APPSMITH_CLOUD_SERVICES_PASSWORD= \
+ ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:release
+
+ - name: Pull release-frozen server docker container and start it locally
+ if: github.ref == 'refs/heads/release-frozen' || github.event.pull_request.head.ref == 'release-frozen'
shell: bash
run: |
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
@@ -308,10 +326,10 @@ jobs:
--env APPSMITH_CLOUD_SERVICES_BASE_URL= \
--env APPSMITH_CLOUD_SERVICES_USERNAME= \
--env APPSMITH_CLOUD_SERVICES_PASSWORD= \
- ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:release
+ ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:release-frozen
- name: Pull master server docker container and start it locally
- if: github.ref == 'refs/heads/master' || github.event.pull_request.base.ref == 'master'
+ if: github.ref == 'refs/heads/master'
shell: bash
run: |
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
@@ -322,7 +340,7 @@ jobs:
--env APPSMITH_ENCRYPTION_PASSWORD=password \
--env APPSMITH_ENCRYPTION_SALT=salt \
--env APPSMITH_IS_SELF_HOSTED=false \
- --env APPSMITH_CLOUD_SERVICES_BASE_URL= \
+ --env APPSMITH_CLOUD_SERVICES_BASE_URL=https://release-cs.appsmith.com \
--env APPSMITH_CLOUD_SERVICES_USERNAME= \
--env APPSMITH_CLOUD_SERVICES_PASSWORD= \
${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:nightly
@@ -426,7 +444,7 @@ jobs:
if(check.length == 0) {
const head_sha = pull.head.sha;
const { data: completed_at } = await github.checks.create({
- owner: context.repo.owner,
+ owner: context.repo.owner,
repo: context.repo.repo,
head_sha: head_sha,
name: process.env.job,
@@ -461,45 +479,9 @@ jobs:
run:
working-directory: app/client
# Run this job only if all the previous steps are a success and the reference if the release or master branch
- if: success() && (github.ref == 'refs/heads/release' || github.ref == 'refs/heads/master')
+ if: success() && (github.ref == 'refs/heads/release' || github.ref == 'refs/heads/release-frozen' || github.ref == 'refs/heads/master')
steps:
- # Check out merge commit
- - name: Fork based /ok-to-test checkout
- uses: actions/checkout@v2
- with:
- ref: "refs/pull/${{ github.event.client_payload.pull_request.number }}/merge"
-
- - 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' || github.event_name == 'workflow_dispatch')
- run: |
- docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${{steps.branch_name.outputs.tag}} .
- echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
- docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${{steps.branch_name.outputs.tag}}
-
- # 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' || github.event_name == 'workflow_dispatch')
- run: |
- docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${GITHUB_SHA} .
- docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:nightly .
- echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
- docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:${GITHUB_SHA}
- docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-editor:nightly
-
# Update check run called "package"
- name: Mark package job as complete
uses: actions/github-script@v1
@@ -531,7 +513,7 @@ jobs:
if(check.length == 0) {
const head_sha = pull.head.sha;
const { data: completed_at } = await github.checks.create({
- owner: context.repo.owner,
+ owner: context.repo.owner,
repo: context.repo.repo,
head_sha: head_sha,
name: process.env.job,
diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml
index 3ac0df6d93..e4e8110d4e 100644
--- a/.github/workflows/server.yml
+++ b/.github/workflows/server.yml
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
push:
- branches: [release, master]
+ branches: [release, release-frozen, master]
# Only trigger if files have changed in this specific path
paths:
- "app/server/**"
@@ -83,6 +83,7 @@ jobs:
- name: Build and test
env:
APPSMITH_MONGODB_URI: "mongodb://localhost:27017/mobtools"
+ APPSMITH_CLOUD_SERVICES_BASE_URL: "https://release-cs.appsmith.com"
APPSMITH_REDIS_URL: "redis://127.0.0.1:6379"
APPSMITH_ENCRYPTION_PASSWORD: "password"
APPSMITH_ENCRYPTION_SALT: "salt"
@@ -102,6 +103,14 @@ jobs:
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:${{steps.vars.outputs.tag}}
+ # Build release-frozen Docker image and push to Docker Hub
+ - name: Push release-frozen image to Docker Hub
+ if: success() && github.ref == 'refs/heads/release-frozen'
+ run: |
+ docker build --build-arg APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:${{steps.vars.outputs.tag}} .
+ echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
+ docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:${{steps.vars.outputs.tag}}
+
# Build master Docker image and push to Docker Hub
- name: Push master image to Docker Hub with commit tag
if: success() && github.ref == 'refs/heads/master'
diff --git a/LICENSE b/LICENSE
index b09cd7856d..572020df3c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@ Apache License
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright [yyyy] [name of copyright owner]
+ Copyright 2021 Appsmith Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 2f0080c996..78bb17e222 100644
--- a/README.md
+++ b/README.md
@@ -1,114 +1,128 @@
-
-
-
- Build and self-host all your internal apps.
-
-
+
-
- Try Online Sandbox
-
-
- Documentation
- Β·
+
+ β’
+ Templates
+
----
-
-
-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, OAuth 2.0, CURL
-**Database Support**: PostgreSQL, MongoDB, MySQL, Firestore, S3, 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).
-
-
-
-
-
-
-
+Turn any datasource into an internal app in minutes. Appsmith lets you drag-and-drop components to build dashboards, write logic with JavaScript objects and connect to any API, database or GraphQL source.
+
+
+
+
+
+
+
+
+## Quick Start
+
+The fastest way to start with Appsmith is the cloud-hosted version. When you're ready, you can also host it yourself.
+
+- Start building apps _immediately_ with [Appsmith Cloud](https://app.appsmith.com/signup/?utm_source=github&utm_medium=social&utm_content=website&utm_campaign=null&utm_term=website)
+- Install locally or on a private instance with [Docker](https://docs.appsmith.com/setup/docker?utm_source=github&utm_medium=social&utm_content=appsmith_docs&utm_campaign=null&utm_term=appsmith_docs) _(Recommended)_
+- Deploy on a [Kubernetes](https://docs.appsmith.com/setup/kubernetes?utm_source=github&utm_medium=social&utm_content=appsmith_docs&utm_campaign=null&utm_term=appsmith_docs) cluster
+- Deploy to [AWS with an AMI](https://docs.appsmith.com/setup/aws-ami?utm_source=github&utm_medium=social&utm_content=appsmith_docs&utm_campaign=null&utm_term=appsmith_docs) on the marketplace
+- One-click deploy to [Heroku](https://docs.appsmith.com/setup/heroku?utm_source=github&utm_medium=social&utm_content=appsmith_docs&utm_campaign=null&utm_term=appsmith_docs)
+
+
+
+## Features
+
+
+
-## π Features
+Use [ready components](https://docs.appsmith.com/widget-reference/button) to build workflows in record time. Drag common elements like tables, charts, forms and more right into your app.
-- **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 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!
-- **One-click deployment**: Managed deployment of your apps with a click of a button.
-- **Access-control**: Assign users different roles & control who can edit / view your applications.
-- **Supports OAuth**: Allow users to authenticate via Google Auth or GitHub Auth.
+_Includes text, forms, inputs, buttons, tables, images, charts, checkboxes, switches, radio buttons, datepickers, dropdowns, filepickers, containers, maps, modals, rich text editors, tabs & video._
-## πΊ Demo
+
+
+
-Unsure if Appsmith is for you? [Watch it in action here!](http://bit.ly/appsmith-demo-github)
+Appsmith automagically works using any DB, Rest API or GraphQL as a datasource, with support for OAuth 2.0 and CURL. Here's a list of all our [pre-built integrations](https://docs.appsmith.com/core-concepts/connecting-to-data-sources/connecting-to-databases).
-But if youβd rather check out some real applications that can be built with Appsmith, check below:
+_Currently works with PostgreSQL, MongoDB, MySQL, Firestore, S3, Redshift, Elastic Search, DynamoDB, Redis, and MSFT SQL Server._
-- [Customer Support Dashboard](https://bit.ly/cs-dashboard-appsmith)
-- [Job Application Tracker](https://bit.ly/3hbYtTi)
+
+
+
-## πββοΈ Quickstart
+We'll run JS anywhere! Code interactions between elements, update datasources or manipulate SQL queriesβ[we'll parse JS anywhere](https://docs.appsmith.com/core-concepts/writing-code).
-The following steps introduce you to building a simple user-list dashboard on Appsmith.
+
+
+
-1. [Sign up on Appsmith Cloud](https://app.appsmith.com/signup?utm_source=github&utm_medium=social&utm_content=website&utm_campaign=null&utm_term=website) or [Deploy Appsmith](https://docs.appsmith.com/setup).
-2. Create a new app within the organization that has already been created for you.
-3. Click on the `+` icon next to the `Queries` section to add a new query in the mock database
- 1. Name the query `usersQuery`.
- 2. Write the query `select * from users limit 5;`.
- 3. Run the query.
-4. Click on the `+` icon next to the `Widgets` section and drag a table onto the screen
-5. Link the table data property to the `usersQuery` using JavaScript `{{usersQuery.data}}`
-6. Hit the Deploy button and checkout the view mode of the app.
+[Push a button](https://docs.appsmith.com/core-concepts/access-control) to deploy & share apps with your team, or privately invite your team using OAuth 2.0. Each member can be assigned a role with access to certain parts of the app.
-Congratulations π You just built your first app on Appsmith!
-Connect your own data to build apps for your team. [Read more here.](https://docs.appsmith.com/core-concepts)
+#### π Solid 256-bit Encryption
-## π Tutorials
+Appsmith apps are [secure by default](https://docs.appsmith.com/security). All connections are TLS-encrypted, and credentials are encrypted with AES-256βso we can't see any of your data. For added security, try our [self-hosted options](https://docs.appsmith.com/setup).
-1. [Building an Admin Panel on MongoDB using Appsmith](https://blog.appsmith.com/building-an-admin-panel-with-mongodb-using-appsmith) ([Video](https://www.youtube.com/watch?v=tisUaIgI86k))
-2. [Building a Customer Support Dashboard in Appsmith](https://www.youtube.com/watch?v=-O_6OLREEzo&t=272s)
-3. [Building a Store Catalogue Management System using Appsmith and GraphQL](https://blog.appsmith.com/building-a-store-catalogue-management-system-using-appsmith-and-graphql)
-4. [Running CI/CD Jobs Manually using Appsmith](https://blog.appsmith.com/how-to-run-manual-jobs-in-gitlab-cicd) ([Video](https://www.youtube.com/watch?v=CYdeJcD4I8A))
-5. [Building a Calendly Clone in Appsmith](https://blog.appsmith.com/how-to-build-a-calendly-clone-in-30-minutes)
-6. [Building Internal Tools with Appsmith](https://youtu.be/eYYYfuW-kEE) `Community`
-7. [Building an Issue Tracker with Appsmith](https://dev.to/pjmantoss/how-to-build-an-issue-tracker-with-appsmith-204e) `Community`
+#### π Transparent Support
-## π Support & Troubleshooting
+Issues are inevitable. When you have one, our entire team is around to helpβ
-If you encountered a bug or need help troubleshooting an issue, you can use one of the following channels:
+- π¬ Talk to us on [Discord](https://discord.gg/rBTTVJp)
+- π Find a solution in our [Documentation](https://docs.appsmith.com)
+- β οΈ Open an issue right here on [GitHub](https://github.com/appsmithorg/appsmith/issues/new/choose)
+- πΎ Ask for help on our [Forum](https://community.appsmith.com)
+- Join our Beta users program and give us feedback Betasmith Community . Be a part of the community that will help shape the future of Appsmith.
-- Self Help: [Documentation](https://docs.appsmith.com)
-- Community Support: [Discord](https://discord.gg/rBTTVJp)
-- Issue & bug tracking: [GitHub Issues](https://github.com/appsmithorg/appsmith/issues/new/choose)
+## Demos
-## π§βπ€βπ§ Contributing
+Check out some real applications that can be built with Appsmith:
-If you're interested in contributing to Appsmith:
+- π©βπ» [Customer Support Dashboard](https://app.appsmith.com/applications/5f2aeb2580ca1f6faaed4e4a/pages/5f2d61b580ca1f6faaed4e79/?utm_source=github&utm_medium=direct&utm_content=appsmith_apps&utm_campaign=null&utm_term=appsmith_apps)
+- ποΈ [Calendly in Appsmith](https://app.appsmith.com/applications/600186605b452f525458d6d2/pages/600186605b452f525458d6d4/?utm_source=github&utm_medium=direct&utm_content=appsmith_apps&utm_campaign=null&utm_term=appsmith_apps)
+- πΈ [Fundraising CRM](https://app.appsmith.com/applications/6098bdc65864501cc39c3d2f/pages/6098bdc65864501cc39c3d31/?utm_source=github&utm_medium=direct&utm_content=appsmith_apps&utm_campaign=null&utm_term=appsmith_apps)
+- π [Application Tracking System App](https://app.appsmith.com/applications/5f43bce13697fd5df1c52ce4/pages/5f43c39c3697fd5df1c52d50/?utm_source=github&utm_medium=direct&utm_content=appsmith_apps&utm_campaign=null&utm_term=appsmith_apps)
-1. Start by reading our [Contribution Guide](https://github.com/appsmithorg/appsmith/blob/master/CONTRIBUTING.md).
-2. Learn how to set up your local environment, in our [developer-guide](https://github.com/appsmithorg/appsmith/blob/master/contributions/CodeContributionsGuidelines.md#-setup-for-local-development).
-3. Explore our list of [good first issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+First+Issue%22).
+- π Made something cool? [Share it with the community](https://discord.gg/Tx395War)
+
-We are committed to fostering an open and welcoming environment in the community. Please read our [Code of Conduct](CODE_OF_CONDUCT.md).
+## Tutorials
-## π License
+Here are the latest tutorials and videos for you to learn more about Appsmith:
-The Appsmith platform is available under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) (Apache-2.0).
+1. [Build an Investor CRM using Appsmith on Google Sheets](https://blog.appsmith.com/build-an-investor-crm-using-appsmith-on-google-sheets-1)
+2. [Building an Admin Panel on MongoDB using Appsmith](https://blog.appsmith.com/building-an-admin-panel-with-mongodb-using-appsmith) ([Video](https://www.youtube.com/watch?v=tisUaIgI86k))
+3. [Building a Discount Management Dashboard With Postgres](https://blog.appsmith.com/building-a-discount-management-dashboard-with-postgres)
+4. [Building a Customer Support Dashboard in Appsmith](https://www.youtube.com/watch?v=-O_6OLREEzo)
+5. [Building a Store Catalogue Management System using Appsmith and GraphQL](https://blog.appsmith.com/building-a-store-catalogue-management-system-using-appsmith-and-graphql)
+6. [Using the Notion API to Build a Content Management System](https://blog.appsmith.com/using-the-notion-api-to-build-a-content-management-system)
+7. [How to run manual jobs in Gitlab CI/CD](https://blog.appsmith.com/how-to-run-manual-jobs-in-gitlab-cicd) ([Video](https://www.youtube.com/watch?v=CYdeJcD4I8A))
+8. [Building a Calendly Clone in Appsmith](https://blog.appsmith.com/how-to-build-a-calendly-clone-in-30-minutes)
+9. [Building Internal Tools with Appsmith](https://youtu.be/eYYYfuW-kEE) `Community`
+10. [Building an Issue Tracker with Appsmith](https://dev.to/pjmantoss/how-to-build-an-issue-tracker-with-appsmith-204e) `Community`
-## Contributors
+Visit our [blog](https://blog.appsmith.com/) to explore more.
+
+## Contribute
+
+We love our contributors! We're committed to fostering an open and welcoming environment in the community. If you'd like to contribute anything from a bug-fix to a feature update, start hereβ
+
+- π Read our [Contribution Guide](https://github.com/appsmithorg/appsmith/blob/master/CONTRIBUTING.md)
+- π§© [Set up your local environment](https://github.com/appsmithorg/appsmith/blob/master/contributions/CodeContributionsGuidelines.md#-setup-for-local-development)
+- πΎ Explore some [good first issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+First+Issue%22)
+- π Read our [Code of Conduct](CODE_OF_CONDUCT.md)
+
+#### Top Contributors (36)
@@ -161,6 +175,7 @@ The Appsmith platform is available under the [Apache License 2.0](https://www.ap
Navdeep Singh π»
+ Aswath K π»
@@ -168,3 +183,7 @@ The Appsmith platform is available under the [Apache License 2.0](https://www.ap
+
+## License
+
+Appsmith is available under the [Apache License 2.0](https://github.com/appsmithorg/appsmith/blob/release/LICENSE). Use it wisely.
diff --git a/app.json b/app.json
index d6bf73c26b..1a1fb89536 100644
--- a/app.json
+++ b/app.json
@@ -98,6 +98,21 @@
"value": "",
"required": false
},
+ "APPSMITH_RECAPTCHA_SITE_KEY": {
+ "description" : "Google reCAPTCHA v3 site key, it is required if you wish to enable protection against spam/abusive users. Read more at: https://developers.google.com/recaptcha/docs/v3",
+ "value": "",
+ "required": false
+ },
+ "APPSMITH_RECAPTCHA_SECRET_KEY": {
+ "description" : "Google reCAPTCHA v3 verification secret key, it is required if you wish to enable spam protection in your backend server.",
+ "value": "",
+ "required": false
+ },
+ "APPSMITH_RECAPTCHA_ENABLED": {
+ "description" : "Boolean config to enable or disable Google reCAPTCHA v3 verification feature. If set to true, both site key and secret key should be provided.",
+ "value": "",
+ "required": false
+ },
"APPSMITH_DISABLE_TELEMETRY": {
"description" : "We want to be transparent and request that you share anonymous usage data with us. This data is purely statistical in nature and helps us understand your needs & provide better support to your self-hosted instance. You can read more about what information is collected in our documentation https://docs.appsmith.com/v/v1.2.1/setup/telemetry",
"value": "false"
diff --git a/app/client/.eslintrc.json b/app/client/.eslintrc.json
index fe0ab77466..7cf9b9c110 100644
--- a/app/client/.eslintrc.json
+++ b/app/client/.eslintrc.json
@@ -1,7 +1,7 @@
// This JSON file configures the eslint plugin. It supports comments as well as per the JSON5 spec
{
"parser": "@typescript-eslint/parser",
- "plugins": ["react", "@typescript-eslint", "prettier", "react-hooks"],
+ "plugins": ["react", "@typescript-eslint", "prettier", "react-hooks","sort-destructure-keys"],
"extends": [
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
"plugin:@typescript-eslint/recommended",
@@ -34,7 +34,8 @@
"react/self-closing-comp": "error",
"react/jsx-sort-props": "error",
"react/jsx-fragments": "error",
- "react/jsx-no-useless-fragment": "error"
+ "react/jsx-no-useless-fragment": "error",
+ "sort-destructure-keys/sort-destructure-keys": ["error", {"caseSensitive": false}]
},
"settings": {
"react": {
diff --git a/app/client/README.md b/app/client/README.md
index 2a275ea504..4a5ba02f8e 100755
--- a/app/client/README.md
+++ b/app/client/README.md
@@ -6,4 +6,4 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
-For details on setting up your development machine, please refer to the [Setup Guide](https://github.com/appsmithorg/appsmith/blob/release/contributions/ClientSetup.md)
+For details on setting up your development machine, please refer to the [Setup Guide](../../contributions/ClientSetup.md)
diff --git a/app/client/build.sh b/app/client/build.sh
index c668865fc0..2f625ec5c4 100755
--- a/app/client/build.sh
+++ b/app/client/build.sh
@@ -6,7 +6,7 @@ GIT_SHA=$(eval git rev-parse HEAD)
echo $GIT_SHA
echo "Sentry Auth Token: $SENTRY_AUTH_TOKEN"
-REACT_APP_SENTRY_RELEASE=$GIT_SHA EXTEND_ESLINT=true craco --max-old-space-size=4096 build --config craco.build.config.js
+REACT_APP_SENTRY_RELEASE=$GIT_SHA REACT_APP_CLIENT_LOG_LEVEL=ERROR EXTEND_ESLINT=true craco --max-old-space-size=4096 build --config craco.build.config.js
rm ./build/static/js/*.js.map
echo "build finished"
diff --git a/app/client/cypress.json b/app/client/cypress.json
index 92d8371d5f..6ecfd0080f 100644
--- a/app/client/cypress.json
+++ b/app/client/cypress.json
@@ -4,6 +4,7 @@
"requestTimeout": 21000,
"pageLoadTimeout": 20000,
"video": true,
+ "videoUploadOnPasses": false,
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "results",
@@ -18,4 +19,4 @@
"runMode": 2,
"openMode": 0
}
-}
\ No newline at end of file
+}
diff --git a/app/client/cypress/fixtures/CanvasResizeDsl.json b/app/client/cypress/fixtures/CanvasResizeDsl.json
index 0ade4a1ac2..f26b52f98f 100644
--- a/app/client/cypress/fixtures/CanvasResizeDsl.json
+++ b/app/client/cypress/fixtures/CanvasResizeDsl.json
@@ -7,7 +7,7 @@
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
- "bottomRow": 2960,
+ "bottomRow": 740,
"containerStyle": "none",
"snapRows": 33,
"parentRowSpace": 1,
diff --git a/app/client/cypress/fixtures/DividerDsl.json b/app/client/cypress/fixtures/DividerDsl.json
new file mode 100644
index 0000000000..36c0065140
--- /dev/null
+++ b/app/client/cypress/fixtures/DividerDsl.json
@@ -0,0 +1,48 @@
+{
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 1280,
+ "snapColumns": 64,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 1300,
+ "containerStyle": "none",
+ "snapRows": 125,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 23,
+ "minHeight": 1310,
+ "parentColumnSpace": 1,
+ "dynamicTriggerPathList": [],
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "widgetName": "Divider1",
+ "rightColumn": 23,
+ "widgetId": "i5kplngd19",
+ "topRow": 25,
+ "bottomRow": 75,
+ "parentRowSpace": 10,
+ "isVisible": true,
+ "type": "DIVIDER_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "parentColumnSpace": 19.75,
+ "dynamicTriggerPathList": [],
+ "leftColumn": 15,
+ "dynamicBindingPathList": [],
+ "orientation": "horizontal",
+ "capType": "nc",
+ "capSide": -1,
+ "strokeStyle": "solid",
+ "dividerColor": "black",
+ "thickness": 2
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/client/cypress/fixtures/Mapdsl.json b/app/client/cypress/fixtures/Mapdsl.json
index 490ab5f87b..2f75e8c860 100644
--- a/app/client/cypress/fixtures/Mapdsl.json
+++ b/app/client/cypress/fixtures/Mapdsl.json
@@ -144,7 +144,7 @@
"parentRowSpace": 38,
"leftColumn": 3,
"rightColumn": 11,
- "topRow": 0,
+ "topRow": 1,
"bottomRow": 12,
"parentId": "yt4ouwn0sk",
"widgetId": "673etzjyv9",
diff --git a/app/client/cypress/fixtures/application-file.json b/app/client/cypress/fixtures/application-file.json
new file mode 100644
index 0000000000..5a1193ac1d
--- /dev/null
+++ b/app/client/cypress/fixtures/application-file.json
@@ -0,0 +1,174 @@
+{
+ "exportedApplication": {
+ "userPermissions": [
+ "canComment:applications",
+ "manage:applications",
+ "read:applications",
+ "publish:applications",
+ "makePublic:applications"
+ ],
+ "name": "testing app - pk",
+ "isPublic": false,
+ "appIsExample": false,
+ "color": "#FE9F44",
+ "icon": "heart",
+ "new": true
+ },
+ "datasourceList": [],
+ "pageList": [
+ {
+ "userPermissions": [
+ "read:pages",
+ "manage:pages"
+ ],
+ "unpublishedPage": {
+ "name": "Page1",
+ "layouts": [
+ {
+ "id": "60a77186cdbfc9440388285c",
+ "userPermissions": [],
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 1118,
+ "snapColumns": 16,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 1280,
+ "containerStyle": "none",
+ "snapRows": 33,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 18,
+ "minHeight": 1292,
+ "parentColumnSpace": 1,
+ "dynamicTriggerPathList": [],
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "widgetName": "Button1",
+ "rightColumn": 4,
+ "isDefaultClickDisabled": true,
+ "widgetId": "g7jf8v3wkq",
+ "buttonStyle": "PRIMARY_BUTTON",
+ "topRow": 0,
+ "bottomRow": 1,
+ "parentRowSpace": 40,
+ "isVisible": true,
+ "type": "BUTTON_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "parentColumnSpace": 67.375,
+ "leftColumn": 2,
+ "text": "Submit",
+ "isDisabled": false
+ },
+ {
+ "widgetName": "Chart1",
+ "rightColumn": 8,
+ "allowHorizontalScroll": false,
+ "widgetId": "ow55pc4z0z",
+ "topRow": 5,
+ "bottomRow": 13,
+ "parentRowSpace": 40,
+ "isVisible": true,
+ "type": "CHART_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "chartData": {
+ "pftw37090s": {
+ "seriesName": "Sales",
+ "data": [
+ {
+ "x": "Mon",
+ "y": 10000
+ },
+ {
+ "x": "Tue",
+ "y": 12000
+ },
+ {
+ "x": "Wed",
+ "y": 32000
+ },
+ {
+ "x": "Thu",
+ "y": 28000
+ },
+ {
+ "x": "Fri",
+ "y": 14000
+ },
+ {
+ "x": "Sat",
+ "y": 19000
+ },
+ {
+ "x": "Sun",
+ "y": 36000
+ }
+ ]
+ }
+ },
+ "yAxisName": "Total Order Revenue $",
+ "parentColumnSpace": 67.375,
+ "chartName": "Last week's revenue",
+ "leftColumn": 2,
+ "xAxisName": "Last Week",
+ "chartType": "LINE_CHART"
+ }
+ ]
+ },
+ "layoutOnLoadActions": [],
+ "new": false
+ }
+ ],
+ "userPermissions": []
+ },
+ "publishedPage": {
+ "name": "Page1",
+ "layouts": [
+ {
+ "id": "60a77186cdbfc9440388285c",
+ "userPermissions": [],
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 1224,
+ "snapColumns": 16,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 1254,
+ "containerStyle": "none",
+ "snapRows": 33,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 4,
+ "minHeight": 1292,
+ "parentColumnSpace": 1,
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": []
+ },
+ "new": false
+ }
+ ],
+ "userPermissions": []
+ },
+ "new": true
+ }
+ ],
+ "publishedDefaultPageName": "Page1",
+ "unpublishedDefaultPageName": "Page1",
+ "actionList": [],
+ "decryptedFields": {},
+ "publishedLayoutmongoEscapedWidgets": {},
+ "unpublishedLayoutmongoEscapedWidgets": {}
+}
\ No newline at end of file
diff --git a/app/client/cypress/fixtures/basicDsl.json b/app/client/cypress/fixtures/basicDsl.json
new file mode 100644
index 0000000000..a3932623ec
--- /dev/null
+++ b/app/client/cypress/fixtures/basicDsl.json
@@ -0,0 +1 @@
+{"dsl":{"widgetName":"MainContainer","backgroundColor":"none","rightColumn":966,"snapColumns":64,"detachFromLayout":true,"widgetId":"0","topRow":0,"bottomRow":320,"containerStyle":"none","snapRows":125,"parentRowSpace":1,"type":"CANVAS_WIDGET","canExtend":true,"version":23,"minHeight":1292,"parentColumnSpace":1,"dynamicBindingPathList":[],"leftColumn":0,"children":[{"isVisible":true,"inputType":"TEXT","label":"","widgetName":"Input1","version":1,"resetOnSubmit":true,"isRequired":false,"isDisabled":false,"type":"INPUT_WIDGET","isLoading":false,"parentColumnSpace":14.84375,"parentRowSpace":10,"leftColumn":23,"rightColumn":43,"topRow":8,"bottomRow":12,"parentId":"0","widgetId":"ihviqc47ev"}],"dynamicTriggerPathList":[]}}
\ No newline at end of file
diff --git a/app/client/cypress/fixtures/chartUpdatedDsl.json b/app/client/cypress/fixtures/chartUpdatedDsl.json
new file mode 100644
index 0000000000..20a57d6a5c
--- /dev/null
+++ b/app/client/cypress/fixtures/chartUpdatedDsl.json
@@ -0,0 +1,184 @@
+{
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 966,
+ "snapColumns": 64,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 5168,
+ "containerStyle": "none",
+ "snapRows": 129,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 20,
+ "minHeight": 450,
+ "parentColumnSpace": 1,
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "backgroundColor": "#FFFFFF",
+ "widgetName": "Container1",
+ "rightColumn": 32,
+ "orientation": "VERTICAL",
+ "snapColumns": 16,
+ "widgetId": "mxbaasg65u",
+ "containerStyle": "card",
+ "topRow": 0,
+ "bottomRow": 36,
+ "parentRowSpace": 38,
+ "isVisible": true,
+ "type": "CONTAINER_WIDGET",
+ "version": 1,
+ "isLoading": false,
+ "parentColumnSpace": 75.25,
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "backgroundColor": "transparent",
+ "widgetName": "59gdivzv7s",
+ "rightColumn": 2408,
+ "orientation": "VERTICAL",
+ "snapColumns": 16,
+ "detachFromLayout": true,
+ "widgetId": "bxekwxgc1i",
+ "containerStyle": "none",
+ "topRow": 0,
+ "bottomRow": 20,
+ "parentRowSpace": 1,
+ "isVisible": true,
+ "type": "CANVAS_WIDGET",
+ "canExtend": false,
+ "version": 1,
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "leftColumn": 0,
+ "dynamicBindingPathList": [],
+ "children": []
+ }
+ ]
+ },
+ {
+ "backgroundColor": "#FFFFFF",
+ "widgetName": "Container3",
+ "rightColumn": 64,
+ "orientation": "VERTICAL",
+ "snapColumns": 16,
+ "widgetId": "i331vll2mg",
+ "containerStyle": "card",
+ "topRow": 36,
+ "bottomRow": 92,
+ "parentRowSpace": 38,
+ "isVisible": true,
+ "type": "CONTAINER_WIDGET",
+ "version": 1,
+ "isLoading": false,
+ "parentColumnSpace": 75.25,
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "backgroundColor": "transparent",
+ "widgetName": "rhfg2vf1n5",
+ "rightColumn": 4816,
+ "orientation": "VERTICAL",
+ "snapColumns": 16,
+ "detachFromLayout": true,
+ "widgetId": "rglduihhzk",
+ "containerStyle": "none",
+ "topRow": 0,
+ "bottomRow": 20,
+ "parentRowSpace": 1,
+ "isVisible": true,
+ "type": "CANVAS_WIDGET",
+ "canExtend": false,
+ "version": 1,
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "leftColumn": 0,
+ "dynamicBindingPathList": [],
+ "children": [
+ {
+ "isVisible": true,
+ "widgetName": "Chart1",
+ "chartType": "LINE_CHART",
+ "chartName": "Last week's revenue",
+ "allowHorizontalScroll": false,
+ "version": 1,
+ "chartData": {
+ "q4pm3w97mo": {
+ "seriesName": "Sales",
+ "data": ""
+ }
+ },
+ "xAxisName": "Last Week",
+ "yAxisName": "Total Order Revenue $",
+ "type": "CHART_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 14.59375,
+ "parentRowSpace": 10,
+ "leftColumn": 20,
+ "rightColumn": 44,
+ "topRow": 16,
+ "bottomRow": 48,
+ "parentId": "rglduihhzk",
+ "widgetId": "snzfh3qjo8",
+ "dynamicBindingPathList": [],
+ "dynamicTriggerPathList": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "backgroundColor": "#FFFFFF",
+ "widgetName": "Container4",
+ "rightColumn": 64,
+ "orientation": "VERTICAL",
+ "snapColumns": 16,
+ "widgetId": "qznzsquf70",
+ "containerStyle": "card",
+ "topRow": 0,
+ "bottomRow": 36,
+ "parentRowSpace": 38,
+ "isVisible": true,
+ "type": "CONTAINER_WIDGET",
+ "version": 1,
+ "isLoading": false,
+ "parentColumnSpace": 75.25,
+ "dynamicBindingPathList": [],
+ "leftColumn": 32,
+ "children": [
+ {
+ "backgroundColor": "transparent",
+ "widgetName": "3bn6uv0vy4",
+ "rightColumn": 2408,
+ "orientation": "VERTICAL",
+ "snapColumns": 16,
+ "detachFromLayout": true,
+ "widgetId": "7vm5mdu8ey",
+ "containerStyle": "none",
+ "topRow": 0,
+ "bottomRow": 1368,
+ "parentRowSpace": 1,
+ "isVisible": true,
+ "type": "CANVAS_WIDGET",
+ "canExtend": false,
+ "version": 1,
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "leftColumn": 0,
+ "dynamicBindingPathList": [],
+ "children": [
+ null
+ ]
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/client/cypress/fixtures/debuggerDependencyDsl.json b/app/client/cypress/fixtures/debuggerDependencyDsl.json
new file mode 100644
index 0000000000..d023180e24
--- /dev/null
+++ b/app/client/cypress/fixtures/debuggerDependencyDsl.json
@@ -0,0 +1,58 @@
+{
+ "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": 9,
+ "minHeight": 1292,
+ "parentColumnSpace": 1,
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "isVisible": true,
+ "text": "Submit",
+ "buttonStyle": "PRIMARY_BUTTON",
+ "widgetName": "Button1",
+ "isDisabled": false,
+ "isDefaultClickDisabled": true,
+ "type": "BUTTON_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 74,
+ "parentRowSpace": 40,
+ "leftColumn": 5,
+ "rightColumn": 7,
+ "topRow": 2,
+ "bottomRow": 3,
+ "parentId": "0",
+ "widgetId": "3qg87le9t4"
+ },
+ {
+ "isVisible": true,
+ "inputType": "TEXT",
+ "label": "",
+ "widgetName": "Input1",
+ "type": "INPUT_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 74,
+ "parentRowSpace": 40,
+ "leftColumn": 2,
+ "rightColumn": 7,
+ "topRow": 0,
+ "bottomRow": 1,
+ "parentId": "0",
+ "widgetId": "2lhsjdd5sg"
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/app/client/cypress/fixtures/debuggerTableDsl.json b/app/client/cypress/fixtures/debuggerTableDsl.json
new file mode 100644
index 0000000000..afaed3b2cf
--- /dev/null
+++ b/app/client/cypress/fixtures/debuggerTableDsl.json
@@ -0,0 +1,42 @@
+{
+ "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": 9,
+ "minHeight": 1292,
+ "parentColumnSpace": 1,
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "isVisible": true,
+ "label": "Data",
+ "widgetName": "Table1",
+ "searchKey": "",
+ "tableData": "{{TestApi.data.users}}",
+ "type": "TABLE_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 74,
+ "parentRowSpace": 40,
+ "leftColumn": 1,
+ "rightColumn": 9,
+ "topRow": 7,
+ "bottomRow": 14,
+ "parentId": "0",
+ "widgetId": "7miqot30xy",
+ "dynamicBindingPathList": []
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/app/client/cypress/fixtures/displayWidgetDsl.json b/app/client/cypress/fixtures/displayWidgetDsl.json
index 01da343433..e2fbfa257b 100644
--- a/app/client/cypress/fixtures/displayWidgetDsl.json
+++ b/app/client/cypress/fixtures/displayWidgetDsl.json
@@ -14,7 +14,6 @@
"type": "CANVAS_WIDGET",
"canExtend": true,
"dynamicBindingPathList": [],
- "version": 4,
"minHeight": 1292,
"parentColumnSpace": 1,
"leftColumn": 0,
diff --git a/app/client/cypress/fixtures/example.json b/app/client/cypress/fixtures/example.json
index 26d6dd6c75..053ecc28a5 100644
--- a/app/client/cypress/fixtures/example.json
+++ b/app/client/cypress/fixtures/example.json
@@ -201,50 +201,60 @@
"orderAmount": 9.99
}
],
- "ChartCustomConfig": {"type": "area2d",
- "dataSource": {
- "chart": {
- "caption": "Countries With Most Oil Reserves [2017-18]",
- "subCaption": "In MMbbl = One Million barrels",
- "xAxisName": "Country",
- "yAxisName": "Reserves (MMbbl)",
- "numberSuffix": "K"
- },
- "data": [
- {
- "label": "Venezuela",
- "value": "290"
- },
- {
- "label": "Saudi",
- "value": "260"
- },
- {
- "label": "Canada",
- "value": "180"
- },
- {
- "label": "Iran",
- "value": "140"
- },
- {
- "label": "Russia",
- "value": "115"
- },
- {
- "label": "UAE",
- "value": "100"
- },
- {
- "label": "US",
- "value": "30"
- },
- {
- "label": "China",
- "value": "30"
- }
- ]
- }},
+ "ChartCustomConfig": {
+ "type":"column2d",
+ "dataSource":{
+ "chart":{
+ "caption":"Monthly revenue for last year",
+ "subCaption":"Harry's SuperMart",
+ "xAxisName":"Month",
+ "yAxisName":"Revenues (In USD)",
+ "numberPrefix":"$",
+ "theme":"fusion"
+ },
+ "data":[
+ {
+ "label":"Jan",
+ "value":"420000"
+ },
+ {
+ "label":"Feb",
+ "value":"810000"
+ },
+ {
+ "label":"Mar",
+ "value":"720000"
+ },
+ {
+ "label":"Apr",
+ "value":"550000"
+ },
+ {
+ "label":"May",
+ "value":"910000"
+ },
+ {
+ "label":"Jun",
+ "value":"256000"
+ },
+ {
+ "label":"Jul",
+ "value":"512000"
+ }
+ ],
+ "trendlines":[
+ {
+ "line":[
+ {
+ "startvalue":"700000",
+ "valueOnRight":"1",
+ "displayvalue":"Monthly Target"
+ }
+ ]
+ }
+ ]
+ }
+ },
"TableInputWithNull": [
{
"id": 2381224,
diff --git a/app/client/cypress/fixtures/formInputTableDsl.json b/app/client/cypress/fixtures/formInputTableDsl.json
index 1c731ed338..71eba21877 100644
--- a/app/client/cypress/fixtures/formInputTableDsl.json
+++ b/app/client/cypress/fixtures/formInputTableDsl.json
@@ -110,7 +110,9 @@
"topRow": 3,
"bottomRow": 4,
"parentId": "4lvpg75cy5",
- "widgetId": "vwe9oyg9x1"
+ "widgetId": "vwe9oyg9x1",
+ "defaultText": "{{Input1.text}}",
+ "dynamicBindingPathList": [{"key": "defaultText"}]
}
],
"blueprint": {
diff --git a/app/client/cypress/fixtures/formdsl.json b/app/client/cypress/fixtures/formdsl.json
index 866f2a0255..f8859c48a5 100644
--- a/app/client/cypress/fixtures/formdsl.json
+++ b/app/client/cypress/fixtures/formdsl.json
@@ -199,7 +199,7 @@
"backgroundColor": "Gray",
"rightColumn": 11,
"widgetId": "z62mnh15y5",
- "topRow": 0,
+ "topRow": 1,
"bottomRow": 13,
"parentRowSpace": 38,
"isVisible": true,
diff --git a/app/client/cypress/fixtures/listdsl.json b/app/client/cypress/fixtures/listdsl.json
index 25c0fb517a..e0aab6c6ec 100644
--- a/app/client/cypress/fixtures/listdsl.json
+++ b/app/client/cypress/fixtures/listdsl.json
@@ -13,7 +13,7 @@
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
- "version": 9,
+ "version": 23,
"minHeight": 1292,
"parentColumnSpace": 1,
"dynamicBindingPathList": [],
@@ -25,7 +25,7 @@
"backgroundColor": "",
"gridType": "vertical",
"gridGap": 0,
- "items": "[\n {\n \"id\": 1,\n \"email\": \"michael.lawson@reqres.in\",\n \"first_name\": \"Michael\",\n \"last_name\": \"Lawson\",\n \"avatar\": \"https://reqres.in/img/faces/7-image.jpg\"\n },\n {\n \"id\": 2,\n \"email\": \"lindsay.ferguson@reqres.in\",\n \"first_name\": \"Lindsay\",\n \"last_name\": \"Ferguson\",\n \"avatar\": \"https://reqres.in/img/faces/8-image.jpg\"\n },\n {\n \"id\": 3,\n \"email\": \"brock.lesnar@reqres.in\",\n \"first_name\": \"Brock\",\n \"last_name\": \"Lesnar\",\n \"avatar\": \"https://reqres.in/img/faces/8-image.jpg\"\n }\n]",
+ "listData": "[\n {\n \"id\": 1,\n \"email\": \"michael.lawson@reqres.in\",\n \"first_name\": \"Michael\",\n \"last_name\": \"Lawson\",\n \"avatar\": \"https://reqres.in/img/faces/7-image.jpg\"\n },\n {\n \"id\": 2,\n \"email\": \"lindsay.ferguson@reqres.in\",\n \"first_name\": \"Lindsay\",\n \"last_name\": \"Ferguson\",\n \"avatar\": \"https://reqres.in/img/faces/8-image.jpg\"\n },\n {\n \"id\": 3,\n \"email\": \"brock.lesnar@reqres.in\",\n \"first_name\": \"Brock\",\n \"last_name\": \"Lesnar\",\n \"avatar\": \"https://reqres.in/img/faces/8-image.jpg\"\n }\n]",
"widgetName": "List1",
"children": [
{
@@ -60,9 +60,9 @@
"parentColumnSpace": 32,
"parentRowSpace": 40,
"leftColumn": 2,
- "rightColumn": 6,
+ "rightColumn": 24,
"topRow": 0,
- "bottomRow": 1,
+ "bottomRow": 16,
"parentId": "dinv2tsatk",
"widgetId": "k6ct7dxg4w"
},
@@ -73,15 +73,15 @@
"widgetName":"Button1",
"isDisabled":false,
"isDefaultClickDisabled":true,
- "version":1,
+ "version":26,
"type":"BUTTON_WIDGET",
"isLoading":false,
"parentColumnSpace":29.25,
"parentRowSpace":40,
- "leftColumn":6,
- "rightColumn":8,
+ "leftColumn":24,
+ "rightColumn": 32,
"topRow":1,
- "bottomRow":2,
+ "bottomRow":8,
"parentId":"dinv2tsatk",
"widgetId":"fuw9p7cuek"
}
@@ -105,9 +105,9 @@
"type": "CONTAINER_WIDGET",
"isLoading": false,
"leftColumn": 0,
- "rightColumn": 16,
+ "rightColumn": 64,
"topRow": 0,
- "bottomRow": 4,
+ "bottomRow": 16,
"parentId": "0pvmmqr77m",
"widgetId": "4ruj7xl5ri"
}
@@ -130,9 +130,9 @@
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 0,
- "rightColumn": 8,
+ "rightColumn": 32,
"topRow": 0,
- "bottomRow": 10,
+ "bottomRow": 40,
"parentId": "0",
"widgetId": "5bwz8xcvhj",
"dynamicBindingPathList": [],
diff --git a/app/client/cypress/fixtures/listwidgetdsl.json b/app/client/cypress/fixtures/listwidgetdsl.json
new file mode 100644
index 0000000000..5415a2211c
--- /dev/null
+++ b/app/client/cypress/fixtures/listwidgetdsl.json
@@ -0,0 +1,275 @@
+{
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 966,
+ "snapColumns": 16,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 800,
+ "containerStyle": "none",
+ "snapRows": 33,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 18,
+ "minHeight": 240,
+ "parentColumnSpace": 1,
+ "dynamicTriggerPathList": [],
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "isVisible": true,
+ "backgroundColor": "",
+ "itemBackgroundColor": "white",
+ "gridType": "vertical",
+ "enhancements": true,
+ "gridGap": 0,
+ "listData": "",
+ "widgetName": "List1",
+ "children": [
+ {
+ "isVisible": true,
+ "widgetName": "Canvas1",
+ "version": 1,
+ "containerStyle": "none",
+ "canExtend": false,
+ "detachFromLayout": true,
+ "dropDisabled": true,
+ "noPad": true,
+ "children": [
+ {
+ "isVisible": true,
+ "backgroundColor": "white",
+ "widgetName": "Container1",
+ "containerStyle": "card",
+ "children": [
+ {
+ "isVisible": true,
+ "widgetName": "Canvas2",
+ "version": 1,
+ "containerStyle": "none",
+ "canExtend": false,
+ "detachFromLayout": true,
+ "children": [
+ {
+ "isVisible": true,
+ "defaultImage": "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png",
+ "imageShape": "RECTANGLE",
+ "maxZoomLevel": 1,
+ "image": "{{currentItem.img}}",
+ "widgetName": "Image1",
+ "version": 1,
+ "dynamicBindingPathList": [
+ {
+ "key": "image"
+ }
+ ],
+ "dynamicTriggerPathList": [],
+ "type": "IMAGE_WIDGET",
+ "isLoading": false,
+ "leftColumn": 0,
+ "rightColumn": 4,
+ "topRow": 0,
+ "bottomRow": 3,
+ "parentId": "muh6tmsm1f",
+ "widgetId": "vr29m4code"
+ },
+ {
+ "isVisible": true,
+ "text": "{{currentItem.name}}",
+ "fontSize": "PARAGRAPH",
+ "fontStyle": "BOLD",
+ "textAlign": "LEFT",
+ "textColor": "#231F20",
+ "widgetName": "Text1",
+ "version": 1,
+ "textStyle": "HEADING",
+ "dynamicBindingPathList": [
+ {
+ "key": "text"
+ }
+ ],
+ "dynamicTriggerPathList": [],
+ "type": "TEXT_WIDGET",
+ "isLoading": false,
+ "leftColumn": 4,
+ "rightColumn": 10,
+ "topRow": 0,
+ "bottomRow": 1,
+ "parentId": "muh6tmsm1f",
+ "widgetId": "envgv9f2j9"
+ },
+ {
+ "isVisible": true,
+ "text": "{{currentItem.num}}",
+ "fontSize": "PARAGRAPH",
+ "fontStyle": "BOLD",
+ "textAlign": "LEFT",
+ "textColor": "#231F20",
+ "widgetName": "Text2",
+ "version": 1,
+ "textStyle": "BODY",
+ "dynamicBindingPathList": [
+ {
+ "key": "text"
+ }
+ ],
+ "dynamicTriggerPathList": [],
+ "type": "TEXT_WIDGET",
+ "isLoading": false,
+ "leftColumn": 4,
+ "rightColumn": 10,
+ "topRow": 1,
+ "bottomRow": 2,
+ "parentId": "muh6tmsm1f",
+ "widgetId": "37xbmi0bbz"
+ }
+ ],
+ "minHeight": null,
+ "type": "CANVAS_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "parentRowSpace": 1,
+ "leftColumn": 0,
+ "rightColumn": null,
+ "topRow": 0,
+ "bottomRow": null,
+ "parentId": "5q9jzp3d17",
+ "widgetId": "muh6tmsm1f"
+ }
+ ],
+ "version": 1,
+ "dragDisabled": true,
+ "isDeletable": false,
+ "disallowCopy": true,
+ "disablePropertyPane": true,
+ "type": "CONTAINER_WIDGET",
+ "isLoading": false,
+ "leftColumn": 0,
+ "rightColumn": 16,
+ "topRow": 0,
+ "bottomRow": 4,
+ "parentId": "qt3cziyljx",
+ "widgetId": "5q9jzp3d17"
+ }
+ ],
+ "minHeight": 400,
+ "type": "CANVAS_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "parentRowSpace": 1,
+ "leftColumn": 0,
+ "rightColumn": 463,
+ "topRow": 0,
+ "bottomRow": 400,
+ "parentId": "wmuvmnfqm0",
+ "widgetId": "qt3cziyljx"
+ }
+ ],
+ "type": "LIST_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 57.875,
+ "parentRowSpace": 40,
+ "leftColumn": 4,
+ "rightColumn": 12,
+ "topRow": 8,
+ "bottomRow": 18,
+ "parentId": "0",
+ "widgetId": "wmuvmnfqm0",
+ "dynamicBindingPathList": [
+ {
+ "key": "template.Image1.image"
+ },
+ {
+ "key": "template.Text1.text"
+ },
+ {
+ "key": "template.Text2.text"
+ }
+ ],
+ "template": {
+ "Image1": {
+ "isVisible": true,
+ "defaultImage": "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png",
+ "imageShape": "RECTANGLE",
+ "maxZoomLevel": 1,
+ "image": "{{List1.listData.map((currentItem) => currentItem.img)}}",
+ "widgetName": "Image1",
+ "version": 1,
+ "dynamicBindingPathList": [
+ {
+ "key": "image"
+ }
+ ],
+ "dynamicTriggerPathList": [],
+ "type": "IMAGE_WIDGET",
+ "isLoading": false,
+ "leftColumn": 0,
+ "rightColumn": 4,
+ "topRow": 0,
+ "bottomRow": 3,
+ "parentId": "muh6tmsm1f",
+ "widgetId": "vr29m4code"
+ },
+ "Text1": {
+ "isVisible": true,
+ "text": "{{List1.listData.map((currentItem) => currentItem.name)}}",
+ "fontSize": "PARAGRAPH",
+ "fontStyle": "BOLD",
+ "textAlign": "LEFT",
+ "textColor": "#231F20",
+ "widgetName": "Text1",
+ "version": 1,
+ "textStyle": "HEADING",
+ "dynamicBindingPathList": [
+ {
+ "key": "text"
+ }
+ ],
+ "dynamicTriggerPathList": [],
+ "type": "TEXT_WIDGET",
+ "isLoading": false,
+ "leftColumn": 4,
+ "rightColumn": 10,
+ "topRow": 0,
+ "bottomRow": 1,
+ "parentId": "muh6tmsm1f",
+ "widgetId": "envgv9f2j9"
+ },
+ "Text2": {
+ "isVisible": true,
+ "text": "{{List1.listData.map((currentItem) => currentItem.num)}}",
+ "fontSize": "PARAGRAPH",
+ "fontStyle": "BOLD",
+ "textAlign": "LEFT",
+ "textColor": "#231F20",
+ "widgetName": "Text2",
+ "version": 1,
+ "textStyle": "BODY",
+ "dynamicBindingPathList": [
+ {
+ "key": "text"
+ }
+ ],
+ "dynamicTriggerPathList": [],
+ "type": "TEXT_WIDGET",
+ "isLoading": false,
+ "leftColumn": 4,
+ "rightColumn": 10,
+ "topRow": 1,
+ "bottomRow": 2,
+ "parentId": "muh6tmsm1f",
+ "widgetId": "37xbmi0bbz"
+ }
+ },
+ "childAutoComplete": {
+ "currentItem": {}
+ },
+ "dynamicTriggerPathList": []
+ }
+ ]
+ }
+}
diff --git a/app/client/cypress/fixtures/tableResizedColumnsDsl.json b/app/client/cypress/fixtures/tableResizedColumnsDsl.json
new file mode 100644
index 0000000000..85ea1b6a2a
--- /dev/null
+++ b/app/client/cypress/fixtures/tableResizedColumnsDsl.json
@@ -0,0 +1,505 @@
+{
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 1280,
+ "snapColumns": 64,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 1310,
+ "containerStyle": "none",
+ "snapRows": 125,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 23,
+ "minHeight": 1320,
+ "parentColumnSpace": 1,
+ "dynamicTriggerPathList": [],
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "backgroundColor": "#ffffff",
+ "widgetName": "Container1",
+ "rightColumn": 64,
+ "widgetId": "sc2tsag5jw",
+ "containerStyle": "card",
+ "topRow": 0,
+ "bottomRow": 6,
+ "parentRowSpace": 10,
+ "isVisible": true,
+ "type": "CONTAINER_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "parentColumnSpace": 16.432031249999998,
+ "dynamicTriggerPathList": [],
+ "leftColumn": 0,
+ "dynamicBindingPathList": [],
+ "children": [
+ {
+ "widgetName": "Canvas1",
+ "rightColumn": 525.8249999999999,
+ "detachFromLayout": true,
+ "widgetId": "v615d41hf1",
+ "containerStyle": "none",
+ "topRow": 0,
+ "bottomRow": 400,
+ "parentRowSpace": 1,
+ "isVisible": true,
+ "canExtend": false,
+ "type": "CANVAS_WIDGET",
+ "version": 1,
+ "parentId": "sc2tsag5jw",
+ "minHeight": 400,
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "leftColumn": 0,
+ "children": []
+ }
+ ]
+ },
+ {
+ "image": "https://source.unsplash.com/mGFHA_0TWnA/2048x620",
+ "widgetName": "Image1",
+ "rightColumn": 64,
+ "widgetId": "ah4cbb9o2e",
+ "topRow": 6,
+ "bottomRow": 39,
+ "parentRowSpace": 10,
+ "isVisible": true,
+ "type": "IMAGE_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "maxZoomLevel": 1,
+ "parentColumnSpace": 16.432031249999998,
+ "dynamicTriggerPathList": [],
+ "imageShape": "RECTANGLE",
+ "leftColumn": 0,
+ "dynamicBindingPathList": [],
+ "defaultImage": "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png"
+ },
+ {
+ "backgroundColor": "#FFFFFF",
+ "widgetName": "Container2",
+ "rightColumn": 64,
+ "widgetId": "0oh55q30ji",
+ "containerStyle": "card",
+ "topRow": 39,
+ "bottomRow": 85,
+ "parentRowSpace": 10,
+ "isVisible": true,
+ "type": "CONTAINER_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "shouldScrollContents": false,
+ "parentColumnSpace": 16.432031249999998,
+ "dynamicTriggerPathList": [],
+ "leftColumn": 0,
+ "dynamicBindingPathList": [],
+ "children": [
+ {
+ "widgetName": "Canvas2",
+ "rightColumn": 525.8249999999999,
+ "detachFromLayout": true,
+ "widgetId": "9kax26wjp6",
+ "containerStyle": "none",
+ "topRow": 0,
+ "bottomRow": 460,
+ "parentRowSpace": 1,
+ "isVisible": true,
+ "canExtend": false,
+ "type": "CANVAS_WIDGET",
+ "version": 1,
+ "parentId": "0oh55q30ji",
+ "minHeight": 400,
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "dynamicTriggerPathList": [],
+ "leftColumn": 0,
+ "dynamicBindingPathList": [],
+ "children": [
+ {
+ "widgetName": "Table2",
+ "columnOrder": [
+ "data",
+ "customColumn3",
+ "customColumn4",
+ "customColumn5",
+ "customColumn6",
+ "page_id",
+ "customColumn2",
+ "customColumn7",
+ "customColumn8"
+ ],
+ "dynamicPropertyPathList": [],
+ "topRow": 4,
+ "bottomRow": 44,
+ "parentRowSpace": 10,
+ "type": "TABLE_WIDGET",
+ "parentColumnSpace": 16.182031249999998,
+ "dynamicTriggerPathList": [
+ {
+ "key": "primaryColumns.customColumn2.onClick"
+ }
+ ],
+ "dynamicBindingPathList": [
+ {
+ "key": "tableData"
+ },
+ {
+ "key": "primaryColumns.data.computedValue"
+ },
+ {
+ "key": "primaryColumns.page_id.computedValue"
+ },
+ {
+ "key": "primaryColumns.customColumn3.computedValue"
+ },
+ {
+ "key": "primaryColumns.customColumn4.computedValue"
+ },
+ {
+ "key": "primaryColumns.customColumn5.computedValue"
+ },
+ {
+ "key": "primaryColumns.customColumn6.computedValue"
+ },
+ {
+ "key": "primaryColumns.customColumn7.computedValue"
+ },
+ {
+ "key": "primaryColumns.customColumn8.computedValue"
+ },
+ {
+ "key": "primaryColumns.customColumn2.buttonLabel"
+ }
+ ],
+ "leftColumn": 0,
+ "primaryColumns": {
+ "customColumn7": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.author?.email})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 8,
+ "isVisible": true,
+ "label": "Author",
+ "columnType": "text",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn7",
+ "buttonLabelColor": "#FFFFFF",
+ "verticalAlignment": "CENTER"
+ },
+ "page_id": {
+ "isDerived": false,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.page_id})}}",
+ "textSize": "PARAGRAPH2",
+ "index": 1,
+ "isVisible": false,
+ "label": "page_id",
+ "columnType": "text",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "page_id",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn8": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.updated_at?.date.start})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 9,
+ "isVisible": true,
+ "label": "Last Updated",
+ "inputFormat": "YYYY-MM-DD",
+ "columnType": "date",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn8",
+ "buttonLabelColor": "#FFFFFF",
+ "outputFormat": "LL",
+ "verticalAlignment": "CENTER"
+ },
+ "data": {
+ "isDerived": false,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return '#' + currentRow.data.id?.number})}}",
+ "textSize": "PARAGRAPH2",
+ "index": 0,
+ "isVisible": true,
+ "label": "ID",
+ "fontStyle": "BOLD",
+ "columnType": "text",
+ "horizontalAlignment": "RIGHT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "data",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn2": {
+ "isDerived": true,
+ "computedValue": "",
+ "onClick": "{{showModal('Modal2')}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "#38AFF4",
+ "index": 9,
+ "isVisible": true,
+ "label": "Re-Estimate",
+ "buttonLabel": "{{Table2.sanitizedTableData.map((currentRow) => { return 'Update'})}}",
+ "columnType": "button",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn2",
+ "buttonLabelColor": "#F6F7F8",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn3": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.estimate?.number})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 4,
+ "isVisible": true,
+ "label": "Estimate(SP)",
+ "columnType": "number",
+ "horizontalAlignment": "RIGHT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn3",
+ "buttonLabelColor": "#FFFFFF",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn4": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.issue_type?.select.name})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 5,
+ "isVisible": true,
+ "label": "Issue Type",
+ "columnType": "text",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn4",
+ "buttonLabelColor": "#FFFFFF",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn5": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.issue_url?.url})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 6,
+ "isVisible": true,
+ "label": "Issue URL",
+ "columnType": "url",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn5",
+ "buttonLabelColor": "#FFFFFF",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn6": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.created_at?.date.start})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 7,
+ "isVisible": true,
+ "label": "Created At",
+ "inputFormat": "YYYY-MM-DD",
+ "columnType": "date",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn6",
+ "buttonLabelColor": "#FFFFFF",
+ "outputFormat": "LL",
+ "verticalAlignment": "CENTER"
+ }
+ },
+ "derivedColumns": {
+ "customColumn7": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.author?.email})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 8,
+ "isVisible": true,
+ "label": "Author",
+ "columnType": "text",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn7",
+ "buttonLabelColor": "#FFFFFF",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn8": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.updated_at?.date.start})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 9,
+ "isVisible": true,
+ "label": "Last Updated",
+ "inputFormat": "YYYY-MM-DD",
+ "columnType": "date",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn8",
+ "buttonLabelColor": "#FFFFFF",
+ "outputFormat": "LL",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn2": {
+ "isDerived": true,
+ "computedValue": "",
+ "onClick": "{{showModal('Modal2')}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "#38AFF4",
+ "index": 9,
+ "isVisible": true,
+ "label": "Re-Estimate",
+ "buttonLabel": "{{Table1.sanitizedTableData.map((currentRow) => { return 'Update'})}}",
+ "columnType": "button",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn2",
+ "buttonLabelColor": "#F6F7F8",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn3": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.estimate?.number})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 4,
+ "isVisible": true,
+ "label": "Estimate(SP)",
+ "columnType": "number",
+ "horizontalAlignment": "RIGHT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn3",
+ "buttonLabelColor": "#FFFFFF",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn4": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.issue_type?.select.name})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 5,
+ "isVisible": true,
+ "label": "Issue Type",
+ "columnType": "text",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn4",
+ "buttonLabelColor": "#FFFFFF",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn5": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.issue_url?.url})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 6,
+ "isVisible": true,
+ "label": "Issue URL",
+ "columnType": "url",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn5",
+ "buttonLabelColor": "#FFFFFF",
+ "verticalAlignment": "CENTER"
+ },
+ "customColumn6": {
+ "isDerived": true,
+ "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => { return currentRow.data.created_at?.date.start})}}",
+ "textSize": "PARAGRAPH2",
+ "buttonStyle": "rgb(3, 179, 101)",
+ "index": 7,
+ "isVisible": true,
+ "label": "Created At",
+ "inputFormat": "YYYY-MM-DD",
+ "columnType": "date",
+ "horizontalAlignment": "LEFT",
+ "width": 150,
+ "enableFilter": true,
+ "enableSort": true,
+ "id": "customColumn6",
+ "buttonLabelColor": "#FFFFFF",
+ "outputFormat": "LL",
+ "verticalAlignment": "CENTER"
+ }
+ },
+ "rightColumn": 64,
+ "textSize": "PARAGRAPH2",
+ "widgetId": "efhhtr442b",
+ "tableData": "{{[]}}",
+ "isVisible": true,
+ "label": "Data",
+ "searchKey": "",
+ "version": 1,
+ "parentId": "9kax26wjp6",
+ "isLoading": false,
+ "horizontalAlignment": "LEFT",
+ "verticalAlignment": "CENTER",
+ "columnSizeMap": {
+ "parent": 136,
+ "issue_url": 174,
+ "data": 72,
+ "author": 174,
+ "issue_type": 109,
+ "created_at": 117,
+ "customColumn1": 122,
+ "customColumn2": 110,
+ "customColumn3": 110,
+ "customColumn4": 112,
+ "customColumn5": 320,
+ "task": 245,
+ "estimate": 109,
+ "step": 62,
+ "id": 60,
+ "status": 75
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/client/cypress/fixtures/tableWidgetDsl.json b/app/client/cypress/fixtures/tableWidgetDsl.json
index 349f604903..9929225f5e 100644
--- a/app/client/cypress/fixtures/tableWidgetDsl.json
+++ b/app/client/cypress/fixtures/tableWidgetDsl.json
@@ -67,7 +67,11 @@
"bottomRow": 10,
"parentId": "tyiwk4xuq0",
"widgetId": "5up3r2iuvs",
- "dynamicBindingPathList": []
+ "dynamicTriggerPathList": [{
+ "key": "onSearchTextChanged"
+ }],
+ "onSearchTextChanged": "{{Api1.run()}}",
+ "selectedRow": "{{Table1.tableData[Table1.selectedRowIndex]}}"
}
],
"widgetId": "tyiwk4xuq0",
diff --git a/app/client/cypress/fixtures/tabsWithWidgetDsl.json b/app/client/cypress/fixtures/tabsWithWidgetDsl.json
new file mode 100644
index 0000000000..f736348b0b
--- /dev/null
+++ b/app/client/cypress/fixtures/tabsWithWidgetDsl.json
@@ -0,0 +1,208 @@
+{
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 966,
+ "snapColumns": 16,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 1280,
+ "containerStyle": "none",
+ "snapRows": 33,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 18,
+ "minHeight": 1292,
+ "parentColumnSpace": 1,
+ "dynamicTriggerPathList": [],
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "isVisible": true,
+ "shouldScrollContents": false,
+ "widgetName": "Tabs1",
+ "tabsObj": {
+ "tab1": {
+ "label": "Tab 1",
+ "id": "tab1",
+ "widgetId": "78u8fmjgom",
+ "isVisible": true,
+ "index": 0
+ },
+ "tab2": {
+ "label": "Tab 2",
+ "id": "tab2",
+ "widgetId": "tch1asw4ti",
+ "isVisible": true,
+ "index": 1
+ }
+ },
+ "shouldShowTabs": true,
+ "defaultTab": "Tab 1",
+ "version": 2,
+ "type": "TABS_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 57.875,
+ "parentRowSpace": 40,
+ "leftColumn": 3,
+ "rightColumn": 15,
+ "topRow": 1,
+ "bottomRow": 10,
+ "parentId": "0",
+ "widgetId": "wqb436vlj8",
+ "children": [
+ {
+ "type": "CANVAS_WIDGET",
+ "tabId": "tab1",
+ "tabName": "Tab 1",
+ "widgetId": "78u8fmjgom",
+ "parentId": "wqb436vlj8",
+ "detachFromLayout": true,
+ "children": [
+ {
+ "isVisible": true,
+ "isDisabled": false,
+ "datePickerType": "DATE_PICKER",
+ "label": "",
+ "dateFormat": "YYYY-MM-DD HH:mm",
+ "widgetName": "DatePicker1",
+ "defaultDate": "2021-05-09T10:22:42.862Z",
+ "version": 2,
+ "isRequired": false,
+ "type": "DATE_PICKER_WIDGET2",
+ "isLoading": false,
+ "parentColumnSpace": 40.90625,
+ "parentRowSpace": 40,
+ "leftColumn": 2,
+ "rightColumn": 7,
+ "topRow": 0,
+ "bottomRow": 1,
+ "parentId": "78u8fmjgom",
+ "widgetId": "opw72wgf4p"
+ },
+ {
+ "isVisible": true,
+ "text": "Label",
+ "fontSize": "PARAGRAPH",
+ "fontStyle": "BOLD",
+ "textAlign": "LEFT",
+ "textColor": "#231F20",
+ "widgetName": "Text1",
+ "version": 1,
+ "type": "TEXT_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 40.90625,
+ "parentRowSpace": 40,
+ "leftColumn": 2,
+ "rightColumn": 6,
+ "topRow": 2,
+ "bottomRow": 3,
+ "parentId": "78u8fmjgom",
+ "widgetId": "vnm91359up"
+ },
+ {
+ "isVisible": true,
+ "text": "Submit",
+ "buttonStyle": "PRIMARY_BUTTON",
+ "widgetName": "Button1",
+ "isDisabled": false,
+ "isDefaultClickDisabled": true,
+ "version": 1,
+ "type": "BUTTON_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 40.90625,
+ "parentRowSpace": 40,
+ "leftColumn": 3,
+ "rightColumn": 5,
+ "topRow": 4,
+ "bottomRow": 5,
+ "parentId": "78u8fmjgom",
+ "widgetId": "lu7vjd5tq3"
+ }
+ ],
+ "parentRowSpace": 1,
+ "parentColumnSpace": 1,
+ "leftColumn": 0,
+ "rightColumn": 463,
+ "topRow": 0,
+ "bottomRow": 280,
+ "isLoading": false,
+ "widgetName": "Canvas1",
+ "renderMode": "CANVAS"
+ },
+ {
+ "type": "CANVAS_WIDGET",
+ "tabId": "tab2",
+ "tabName": "Tab 2",
+ "widgetId": "tch1asw4ti",
+ "parentId": "wqb436vlj8",
+ "detachFromLayout": true,
+ "children": [
+ {
+ "isVisible": true,
+ "label": "Label",
+ "defaultCheckedState": true,
+ "widgetName": "Checkbox1",
+ "version": 1,
+ "alignWidget": "LEFT",
+ "isDisabled": false,
+ "isRequired": false,
+ "type": "CHECKBOX_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 40.90625,
+ "parentRowSpace": 40,
+ "leftColumn": 3,
+ "rightColumn": 6,
+ "topRow": 0,
+ "bottomRow": 1,
+ "parentId": "tch1asw4ti",
+ "widgetId": "l7ahbr0vww"
+ },
+ {
+ "isVisible": true,
+ "label": "",
+ "options": [
+ {
+ "label": "Yes",
+ "value": "Y"
+ },
+ {
+ "label": "No",
+ "value": "N"
+ }
+ ],
+ "defaultOptionValue": "Y",
+ "widgetName": "RadioGroup1",
+ "version": 1,
+ "isRequired": false,
+ "isDisabled": false,
+ "type": "RADIO_GROUP_WIDGET",
+ "isLoading": false,
+ "parentColumnSpace": 40.90625,
+ "parentRowSpace": 40,
+ "leftColumn": 8,
+ "rightColumn": 11,
+ "topRow": 0,
+ "bottomRow": 2,
+ "parentId": "tch1asw4ti",
+ "widgetId": "k4vwuehj29"
+ }
+ ],
+ "parentRowSpace": 1,
+ "parentColumnSpace": 1,
+ "leftColumn": 0,
+ "rightColumn": 463,
+ "topRow": 0,
+ "bottomRow": 160,
+ "isLoading": false,
+ "widgetName": "Canvas2",
+ "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 622aac0aae..af51c97ad5 100644
--- a/app/client/cypress/fixtures/testdata.json
+++ b/app/client/cypress/fixtures/testdata.json
@@ -112,8 +112,8 @@
"currentRowWithIdOutside": "#{{currentRow.email}}",
"currentRowWithIdInside": "{{\"#\" + currentRow.email}}",
"defaultdataBinding": "{'Test','This'}",
- "dropdownErrorMsg": "This value does not evaluate to type \"Array<{ label: string, value: string }>\". Transform it using JS inside '{{ }}'",
- "tableWidgetErrorMsg": "This value does not evaluate to type \"Array\". Transform it using JS inside '{{ }}'",
+ "dropdownErrorMsg": "This value does not evaluate to type \"Array<{ label: string, value: string }>\".",
+ "tableWidgetErrorMsg": "This value does not evaluate to type \"Array\".",
"bindingDataPoint": "{{JSON.stringify(Chart1.selectedDataPoint)}}",
"bindChartData": "{{Chart1.selectedDataPoint",
"appUrl": "https://oauth.mocklab.io/userinfo",
@@ -121,5 +121,7 @@
"clientSecret": "505dac16a21681f277b5fde97445be18",
"accessTokenUrl": "https://oauth.mocklab.io/oauth/token",
"oauthResponse": "169444434892406",
- "authorizationURL": "https://oauth.mocklab.io/oauth/authorize"
+ "authorizationURL": "https://oauth.mocklab.io/oauth/authorize",
+ "basicURl": "https://envyenksqii9nf3.m.pipedream.net",
+ "methodWithQueryParam": "users?q=mimeType='application/vnd.google-apps.spreadsheet'"
}
diff --git a/app/client/cypress/fixtures/widgetSelection.json b/app/client/cypress/fixtures/widgetSelection.json
new file mode 100644
index 0000000000..2347e0629b
--- /dev/null
+++ b/app/client/cypress/fixtures/widgetSelection.json
@@ -0,0 +1,209 @@
+{
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 1270,
+ "snapColumns": 64,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 820,
+ "containerStyle": "none",
+ "snapRows": 125,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 23,
+ "minHeight": 830,
+ "parentColumnSpace": 1,
+ "dynamicTriggerPathList": [],
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "widgetName": "Chart1",
+ "rightColumn": 26,
+ "allowHorizontalScroll": false,
+ "widgetId": "ypstklohw5",
+ "topRow": 4,
+ "bottomRow": 36,
+ "parentRowSpace": 10,
+ "isVisible": true,
+ "type": "CHART_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "chartData": {
+ "3jzahcrorq": {
+ "seriesName": "Sales",
+ "data": [
+ {
+ "x": "Mon",
+ "y": 10000
+ },
+ {
+ "x": "Tue",
+ "y": 12000
+ },
+ {
+ "x": "Wed",
+ "y": 32000
+ },
+ {
+ "x": "Thu",
+ "y": 28000
+ },
+ {
+ "x": "Fri",
+ "y": 14000
+ },
+ {
+ "x": "Sat",
+ "y": 19000
+ },
+ {
+ "x": "Sun",
+ "y": 36000
+ }
+ ]
+ }
+ },
+ "yAxisName": "Total Order Revenue $",
+ "parentColumnSpace": 19.65625,
+ "chartName": "Last week's revenue",
+ "leftColumn": 2,
+ "xAxisName": "Last Week",
+ "customFusionChartConfig": {
+ "type": "column2d",
+ "dataSource": {
+ "chart": {
+ "caption": "Last week's revenue",
+ "xAxisName": "Last Week",
+ "yAxisName": "Total Order Revenue $",
+ "theme": "fusion"
+ },
+ "data": [
+ {
+ "label": "Mon",
+ "value": 10000
+ },
+ {
+ "label": "Tue",
+ "value": 12000
+ },
+ {
+ "label": "Wed",
+ "value": 32000
+ },
+ {
+ "label": "Thu",
+ "value": 28000
+ },
+ {
+ "label": "Fri",
+ "value": 14000
+ },
+ {
+ "label": "Sat",
+ "value": 19000
+ },
+ {
+ "label": "Sun",
+ "value": 36000
+ }
+ ],
+ "trendlines": [
+ {
+ "line": [
+ {
+ "startvalue": "38000",
+ "valueOnRight": "1",
+ "displayvalue": "Weekly Target"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "chartType": "LINE_CHART"
+ },
+ {
+ "backgroundColor": "#FFFFFF",
+ "widgetName": "Container1",
+ "rightColumn": 62,
+ "widgetId": "6y21iarlp4",
+ "containerStyle": "card",
+ "topRow": 11,
+ "bottomRow": 51,
+ "parentRowSpace": 10,
+ "isVisible": true,
+ "type": "CONTAINER_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "parentColumnSpace": 19.65625,
+ "leftColumn": 30,
+ "children": [
+ {
+ "widgetName": "Canvas1",
+ "rightColumn": 629,
+ "detachFromLayout": true,
+ "widgetId": "jqasr1uss5",
+ "containerStyle": "none",
+ "topRow": 0,
+ "bottomRow": 400,
+ "parentRowSpace": 1,
+ "isVisible": true,
+ "canExtend": false,
+ "type": "CANVAS_WIDGET",
+ "version": 1,
+ "parentId": "6y21iarlp4",
+ "minHeight": 400,
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "leftColumn": 0,
+ "children": []
+ }
+ ]
+ },
+ {
+ "image": "",
+ "widgetName": "Image1",
+ "rightColumn": 22,
+ "widgetId": "1t50avy6f1",
+ "topRow": 58,
+ "bottomRow": 70,
+ "parentRowSpace": 10,
+ "isVisible": true,
+ "type": "IMAGE_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "maxZoomLevel": 1,
+ "parentColumnSpace": 19.65625,
+ "imageShape": "RECTANGLE",
+ "leftColumn": 6,
+ "defaultImage": "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png"
+ },
+ {
+ "widgetName": "Button1",
+ "rightColumn": 18,
+ "isDefaultClickDisabled": true,
+ "widgetId": "41wgbhd5vp",
+ "buttonStyle": "PRIMARY_BUTTON",
+ "topRow": 44,
+ "bottomRow": 48,
+ "parentRowSpace": 10,
+ "isVisible": true,
+ "type": "BUTTON_WIDGET",
+ "version": 1,
+ "parentId": "0",
+ "isLoading": false,
+ "parentColumnSpace": 19.65625,
+ "leftColumn": 10,
+ "text": "Submit",
+ "isDisabled": false
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/ExecutionParams_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/ExecutionParams_spec.js
index 6e4a234a3b..4a50dfc688 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/ExecutionParams_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/ExecutionParams_spec.js
@@ -31,8 +31,12 @@ describe("API Panel Test Functionality", function() {
cy.contains(".t--datasource-name", datasourceName)
.find(queryLocators.createQuery)
.click();
-
cy.get(queryLocators.templateMenu).click();
+ cy.get(queryLocators.settings).click({ force: true });
+ cy.get(queryLocators.switch)
+ .last()
+ .click({ force: true });
+ cy.get(queryLocators.query).click({ force: true });
cy.get(".CodeMirror textarea")
.first()
.focus()
@@ -43,6 +47,7 @@ describe("API Panel Test Functionality", function() {
cy.WaitAutoSave();
cy.runQuery();
});
+
it("Will pass execution params", function() {
// Bind the table
cy.SearchEntityandOpen("Table1");
@@ -78,7 +83,6 @@ describe("API Panel Test Functionality", function() {
// Publish the app
cy.PublishtheApp();
- cy.wait("@postExecute");
// Assert on load data in table
cy.readTabledataPublish("0", "1").then((cellData) => {
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js
index d46a3b1ec0..7ffd0131ad 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js
@@ -7,11 +7,9 @@ describe("API Panel Test Functionality", function() {
cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful");
cy.CreateAPI("FirstAPI");
- cy.get("textarea").should(
- "have.attr",
- "placeholder",
- "https://mock-api.appsmith.com/users",
- );
+ cy.get(".CodeMirror-placeholder")
+ .first()
+ .should("have.text", "https://mock-api.appsmith.com/users");
cy.log("Creation of FirstAPI Action successful");
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
cy.SaveAndRunAPI();
@@ -29,14 +27,24 @@ describe("API Panel Test Functionality", function() {
cy.NavigateToAPI_Panel();
cy.CreateAPI("CrashTestAPI");
cy.SelectAction(testdata.postAction);
- cy.xpath(apiwidget.headerKey)
+ cy.get(apiwidget.headerKey)
.first()
- .focus({ force: true })
- .type("{uparrow}", { force: true })
- .type("{ctrl}{shift}{downarrow}", { force: true })
- .type("{backspace}", { force: true });
+ .click({ force: true })
+ .type("{uparrow}", { parseSpecialCharSequences: true })
+ .type("{ctrl}{shift}{downarrow}", { parseSpecialCharSequences: true })
+ .type("{backspace}", { parseSpecialCharSequences: true });
// assert so that this fails
- cy.xpath(apiwidget.headerKey).should("be.visible");
- cy.xpath(apiwidget.headerKey).should("have.value", "");
+ cy.get(apiwidget.headerKey).should("be.visible");
+ cy.get(apiwidget.headerKey).should("have.value", "");
+ });
+
+ it("Should correctly parse query params", function() {
+ cy.NavigateToAPI_Panel();
+ cy.CreateAPI("APIWithQueryParams");
+ cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methodWithQueryParam);
+ cy.ValidateQueryParams({
+ key: "q",
+ value: "mimeType='application/vnd.google-apps.spreadsheet'",
+ });
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_RequestBody_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_RequestBody_spec.js
index e7bcd32a12..0651e60673 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_RequestBody_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_RequestBody_spec.js
@@ -12,7 +12,7 @@ describe("API Panel request body", function() {
cy.contains(apiEditor.bodyTab).click();
cy.contains(testdata.apiFormDataBodyType).click();
- cy.xpath(apiwidget.formEncoded).should("be.visible");
+ cy.get(apiwidget.formEncoded).should("be.visible");
cy.DeleteAPI();
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Unique_name_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Unique_name_spec.js
index 8e784f5f56..48655115a4 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Unique_name_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Unique_name_spec.js
@@ -3,9 +3,15 @@ describe("Name uniqueness test", function() {
cy.log("Login Successful");
cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful");
- cy.CreateAPI("UniqueName");
+ cy.CreateAPI("Uni");
cy.log("Creation of UniqueName Action successful");
- cy.CreationOfUniqueAPIcheck("UniqueName");
+ });
+
+ it("Validate Unique Name", () => {
+ cy.CreationOfUniqueAPIcheck("Uni");
+ });
+
+ it("Validate download apiname check", () => {
cy.CreationOfUniqueAPIcheck("download");
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js
index e824916c33..f9aba00e65 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js
@@ -1,8 +1,10 @@
-const dsl = require("../../../../fixtures/displayWidgetDsl.json");
+const dsl = require("../../../../fixtures/basicDsl.json");
const homePage = require("../../../../locators/HomePage.json");
const commonlocators = require("../../../../locators/commonlocators.json");
-const explorerlocators = require("../../../../locators/explorerlocators.json");
+const widgetsPage = require("../../../../locators/Widgets.json");
+
let duplicateApplicationDsl;
+let parentApplicationDsl;
describe("Duplicate application", function() {
before(() => {
@@ -10,12 +12,20 @@ describe("Duplicate application", function() {
});
it("Check whether the duplicate application has the same dsl as the original", function() {
- cy.get(commonlocators.homeIcon).click({ force: true });
const appname = localStorage.getItem("AppName");
+ cy.SearchEntityandOpen("Input1");
+ cy.intercept("PUT", "/api/v1/layouts/*/pages/*").as("inputUpdate");
+ cy.get(widgetsPage.defaultInput).type("A");
+ cy.get(commonlocators.editPropCrossButton).click({ force: true });
+ cy.wait("@inputUpdate").then((response) => {
+ parentApplicationDsl = response.response.body.data.dsl;
+ });
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+ cy.NavigateToHome();
cy.get(homePage.searchInput).type(appname);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
-
cy.get(homePage.applicationCard)
.first()
.trigger("mouseover");
@@ -23,17 +33,20 @@ describe("Duplicate application", function() {
.first()
.click({ force: true });
cy.get(homePage.duplicateApp).click({ force: true });
-
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(4000);
cy.wait("@getPage").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
cy.get("@getPage").then((httpResponse) => {
- const data = httpResponse.response.body.data;
- duplicateApplicationDsl = data.layouts[0].dsl;
-
- expect(duplicateApplicationDsl).to.deep.equal(dsl.dsl);
+ duplicateApplicationDsl = httpResponse.response.body.data.layouts[0].dsl;
+ cy.log(JSON.stringify(duplicateApplicationDsl));
+ cy.log(JSON.stringify(parentApplicationDsl));
+ expect(JSON.stringify(duplicateApplicationDsl)).to.deep.equal(
+ JSON.stringify(parentApplicationDsl),
+ );
});
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ExportApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ExportApplication_spec.js
new file mode 100644
index 0000000000..3b58ec8dde
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ExportApplication_spec.js
@@ -0,0 +1,159 @@
+const dsl = require("../../../../fixtures/displayWidgetDsl.json");
+const homePage = require("../../../../locators/HomePage.json");
+const commonlocators = require("../../../../locators/commonlocators.json");
+
+describe("Export application as a JSON file", function() {
+ let orgid;
+ let appid;
+ let currentUrl;
+ let newOrganizationName;
+ let appname;
+
+ before(() => {
+ cy.addDsl(dsl);
+ });
+
+ it("Check if exporting app flow works as expected", function() {
+ cy.get(commonlocators.homeIcon).click({ force: true });
+ appname = localStorage.getItem("AppName");
+ cy.get(homePage.searchInput).type(appname);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+
+ cy.get(homePage.applicationCard)
+ .first()
+ .trigger("mouseover");
+ cy.get(homePage.appMoreIcon)
+ .first()
+ .click({ force: true });
+ cy.get(homePage.exportAppFromMenu).click({ force: true });
+ cy.get(homePage.toastMessage).should("contain", "Successfully exported");
+ cy.LogOut();
+ });
+
+ it("User with admin access,should be able to export the app", function() {
+ cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
+ cy.NavigateToHome();
+ cy.generateUUID().then((uid) => {
+ orgid = uid;
+ appid = uid;
+ localStorage.setItem("OrgName", orgid);
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ });
+ cy.CreateAppForOrg(orgid, appid);
+ cy.wait("@getPagesForCreateApp").should(
+ "have.nested.property",
+ "response.body.responseMeta.status",
+ 200,
+ );
+ cy.get("h2").contains("Drag and drop a widget here");
+ cy.get(homePage.shareApp).click({ force: true });
+ cy.shareApp(Cypress.env("TESTUSERNAME1"), homePage.adminRole);
+
+ cy.LogOut();
+
+ cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1"));
+ cy.NavigateToHome();
+ cy.wait(2000);
+ cy.log({ appid });
+ cy.get(homePage.searchInput).type(appid);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+
+ cy.get(homePage.applicationCard)
+ .first()
+ .trigger("mouseover");
+ cy.get(homePage.appMoreIcon)
+ .first()
+ .click({ force: true });
+ cy.get(homePage.exportAppFromMenu).should("be.visible");
+ });
+ cy.LogOut();
+ });
+
+ it("User with developer access,should not be able to export the app", function() {
+ cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
+ cy.NavigateToHome();
+ cy.generateUUID().then((uid) => {
+ orgid = uid;
+ appid = uid;
+ localStorage.setItem("OrgName", orgid);
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ });
+ cy.CreateAppForOrg(orgid, appid);
+ cy.wait("@getPagesForCreateApp").should(
+ "have.nested.property",
+ "response.body.responseMeta.status",
+ 200,
+ );
+ cy.get("h2").contains("Drag and drop a widget here");
+ cy.get(homePage.shareApp).click({ force: true });
+ cy.shareApp(Cypress.env("TESTUSERNAME1"), homePage.developerRole);
+
+ cy.LogOut();
+
+ cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1"));
+ cy.NavigateToHome();
+ cy.wait(2000);
+ cy.log({ appid });
+ cy.get(homePage.searchInput).type(appid);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+
+ cy.get(homePage.applicationCard)
+ .first()
+ .trigger("mouseover");
+ cy.get(homePage.appMoreIcon)
+ .first()
+ .click({ force: true });
+ cy.get(homePage.exportAppFromMenu).should("not.exist");
+ });
+ cy.LogOut();
+ });
+
+ it("User with viewer access,should not be able to export the app", function() {
+ cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
+ cy.NavigateToHome();
+ cy.generateUUID().then((uid) => {
+ orgid = uid;
+ appid = uid;
+ localStorage.setItem("OrgName", orgid);
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ });
+ cy.CreateAppForOrg(orgid, appid);
+ cy.wait("@getPagesForCreateApp").should(
+ "have.nested.property",
+ "response.body.responseMeta.status",
+ 200,
+ );
+ cy.get("h2").contains("Drag and drop a widget here");
+ cy.get(homePage.shareApp).click({ force: true });
+ cy.shareApp(Cypress.env("TESTUSERNAME1"), homePage.viewerRole);
+
+ cy.LogOut();
+
+ cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1"));
+ cy.NavigateToHome();
+ cy.wait(2000);
+ cy.log({ appid });
+ cy.get(homePage.searchInput).type(appid);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+
+ cy.get(homePage.applicationCard)
+ .first()
+ .trigger("mouseover");
+ cy.get(homePage.appEditIcon).should("not.exist");
+ });
+ cy.LogOut();
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js
index f0d29008c1..9cc80fe443 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js
@@ -1,7 +1,10 @@
-const dsl = require("../../../../fixtures/displayWidgetDsl.json");
+const dsl = require("../../../../fixtures/basicDsl.json");
const homePage = require("../../../../locators/HomePage.json");
const commonlocators = require("../../../../locators/commonlocators.json");
+const widgetsPage = require("../../../../locators/Widgets.json");
+
let forkedApplicationDsl;
+let parentApplicationDsl;
describe("Fork application across orgs", function() {
before(() => {
@@ -9,12 +12,20 @@ describe("Fork application across orgs", function() {
});
it("Check if the forked application has the same dsl as the original", function() {
- cy.get(commonlocators.homeIcon).click({ force: true });
const appname = localStorage.getItem("AppName");
+ cy.SearchEntityandOpen("Input1");
+ cy.intercept("PUT", "/api/v1/layouts/*/pages/*").as("inputUpdate");
+ cy.get(widgetsPage.defaultInput).type("A");
+ cy.get(commonlocators.editPropCrossButton).click({ force: true });
+ cy.wait("@inputUpdate").then((response) => {
+ parentApplicationDsl = response.response.body.data.dsl;
+ });
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
+ cy.NavigateToHome();
cy.get(homePage.searchInput).type(appname);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
-
cy.get(homePage.applicationCard)
.first()
.trigger("mouseover");
@@ -28,6 +39,8 @@ describe("Fork application across orgs", function() {
.last()
.click({ force: true });
cy.get(homePage.forkAppOrgButton).click({ force: true });
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(4000);
cy.wait("@postForkAppOrg").then((httpResponse) => {
expect(httpResponse.status).to.equal(200);
});
@@ -35,8 +48,11 @@ describe("Fork application across orgs", function() {
cy.get("@getPage").then((httpResponse) => {
const data = httpResponse.response.body.data;
forkedApplicationDsl = data.layouts[0].dsl;
-
- expect(forkedApplicationDsl).to.deep.equal(dsl.dsl);
+ cy.log(JSON.stringify(forkedApplicationDsl));
+ cy.log(JSON.stringify(parentApplicationDsl));
+ expect(JSON.stringify(forkedApplicationDsl)).to.contain(
+ JSON.stringify(parentApplicationDsl),
+ );
});
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/UpdateApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/UpdateApplication_spec.js
index cca7436c89..fa111127a3 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/UpdateApplication_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/UpdateApplication_spec.js
@@ -100,12 +100,13 @@ describe("Update Application", function() {
);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
-
+ cy.get(homePage.searchInput).type(veryLongAppName);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(2000);
cy.get(homePage.applicationCard)
.first()
.find(homePage.applicationCardName)
.trigger("mouseover");
-
cy.get(".bp3-popover-target.bp3-popover-open").should("have.length", 1);
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_API_with_List_Widget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_API_with_List_Widget_spec.js
new file mode 100644
index 0000000000..4015b794e4
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_API_with_List_Widget_spec.js
@@ -0,0 +1,94 @@
+const commonlocators = require("../../../../locators/commonlocators.json");
+const dsl = require("../../../../fixtures/listwidgetdsl.json");
+const pages = require("../../../../locators/Pages.json");
+const apiPage = require("../../../../locators/ApiEditor.json");
+const publishPage = require("../../../../locators/publishWidgetspage.json");
+
+describe("Test Create Api and Bind to Table widget", function() {
+ let apiData;
+ let valueToTest;
+ before(() => {
+ cy.addDsl(dsl);
+ });
+
+ it("Test_Add users api and execute api", function() {
+ cy.createAndFillApi(this.data.userApi, "/users");
+ cy.RunAPI();
+ cy.get(apiPage.responseBody)
+ .contains("name")
+ .siblings("span")
+ .invoke("text")
+ .then((text) => {
+ valueToTest = `${text
+ .match(/"(.*)"/)[0]
+ .split('"')
+ .join("")}`;
+ cy.log(valueToTest);
+ apiData = valueToTest;
+ cy.log("val1:" + valueToTest);
+ });
+ });
+
+ it("Test_Validate the Api data is updated on List widget", function() {
+ cy.SearchEntityandOpen("List1");
+ cy.getCodeMirror().then(($cm) => {
+ cy.get(".CodeMirror textarea")
+ .first()
+ .type(`{{Api1.data.users}}`, {
+ force: true,
+ parseSpecialCharSequences: false,
+ });
+ });
+ cy.get(commonlocators.editPropCrossButton).click({ force: true });
+ cy.get(".t--draggable-textwidget span").should("have.length", 4);
+
+ cy.get(".t--draggable-textwidget span")
+ .first()
+ .invoke("text")
+ .then((text) => {
+ expect(text).to.equal(valueToTest);
+ });
+ cy.PublishtheApp();
+ cy.get(".t--widget-textwidget span").should("have.length", 4);
+ cy.get(".t--widget-textwidget span")
+ .first()
+ .invoke("text")
+ .then((text) => {
+ expect(text).to.equal(valueToTest);
+ });
+ });
+
+ it("Test_Validate the list widget ", function() {
+ cy.get(publishPage.backToEditor).click({ force: true });
+ cy.SearchEntityandOpen("List1");
+
+ cy.getCodeMirror().then(($cm) => {
+ cy.get(".CodeMirror textarea")
+ .last()
+ .type(`50`, {
+ force: true,
+ parseSpecialCharSequences: false,
+ });
+ });
+ cy.get(commonlocators.editPropCrossButton).click({ force: true });
+ cy.get(".t--draggable-textwidget span").should("have.length", 2);
+ cy.get(".t--draggable-textwidget span")
+ .first()
+ .invoke("text")
+ .then((text) => {
+ expect(text).to.equal(valueToTest);
+ });
+ cy.PublishtheApp();
+ cy.get(".t--widget-textwidget span").should("have.length", 2);
+ cy.get(".t--widget-textwidget span")
+ .first()
+ .invoke("text")
+ .then((text) => {
+ expect(text).to.equal(valueToTest);
+ });
+ });
+
+ afterEach(() => {
+ // put your clean up code if any
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_InputWidget_Table_Sorting_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_InputWidget_Table_Sorting_spec.js
index adab4c0052..7583ac1814 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_InputWidget_Table_Sorting_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_InputWidget_Table_Sorting_spec.js
@@ -28,6 +28,7 @@ describe("Binding the Table and input Widget", function() {
cy.get(".draggable-header ")
.first()
.click({ force: true });
+ cy.wait(1000);
cy.readTabledataPublish("0", "0").then((tabData) => {
const tabValue = tabData;
expect(tabValue).to.be.equal("6788734");
@@ -40,6 +41,7 @@ describe("Binding the Table and input Widget", function() {
cy.get(".draggable-header ")
.first()
.click({ force: true });
+ cy.wait(1000);
cy.readTabledataPublish("0", "0").then((tabData) => {
const tabValue = tabData;
expect(tabValue).to.be.equal("2381224");
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js
index 4b128ed56b..707a039195 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js
@@ -46,19 +46,18 @@ describe("Test Create Api and Bind to Table widget", function() {
it("Table-Text, Validate Publish Mode on Server Side Pagination of Paginate with Table Page No", function() {
cy.PublishtheApp();
+ // Make sure onPageLoad action has run before validating the data
+ cy.wait("@postExecute");
cy.ValidatePublishTableData("1");
cy.get(commonlocators.tableNextPage).click({ force: true });
- cy.wait("@postExecute").should(
- "have.nested.property",
- "response.body.responseMeta.status",
- 200,
- );
+ // Make sure net page action is run
+ cy.wait("@postExecute");
cy.validateToastMessage("done");
cy.ValidatePublishTableData("11");
- cy.get(publishPage.backToEditor).click({ force: true });
});
it("Test_Add Paginate with Response URL and Execute the Api", function() {
+ cy.get(publishPage.backToEditor).click({ force: true });
/** Create Api2 of Paginate with Response URL*/
cy.createAndFillApi(this.data.paginationUrl, "users");
cy.RunAPI();
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableWidget_selectedRow_Input_widget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableWidget_selectedRow_Input_widget_spec.js
index 44bc0ccf73..cac307625a 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableWidget_selectedRow_Input_widget_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableWidget_selectedRow_Input_widget_spec.js
@@ -22,17 +22,6 @@ describe("Binding the table widget and input Widget", function() {
);
});
- it("Input widget test with default value from table widget", function() {
- cy.SearchEntityandOpen("Input2");
- cy.get(widgetsPage.defaultInput).type(testdata.defaultRowIndexBinding);
- cy.get(commonlocators.editPropCrossButton).click({ force: true });
- cy.wait("@updateLayout").should(
- "have.nested.property",
- "response.body.responseMeta.status",
- 200,
- );
- });
-
it("validation of data displayed in input widgets based on selected row", function() {
cy.SearchEntityandOpen("Table1");
cy.get(commonlocators.deflautSelectedRow)
@@ -47,10 +36,6 @@ describe("Binding the table widget and input Widget", function() {
.first()
.invoke("attr", "value")
.should("contain", tabValue);
- cy.get(publish.inputWidget + " " + "input")
- .last()
- .invoke("attr", "value")
- .should("contain", 2);
});
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_tableApi_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_tableApi_spec.js
index d65d22e80d..fb8eeb6b6e 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_tableApi_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_tableApi_spec.js
@@ -3,6 +3,7 @@ const dsl = require("../../../../fixtures/tableWidgetDsl.json");
const pages = require("../../../../locators/Pages.json");
const apiPage = require("../../../../locators/ApiEditor.json");
const publishPage = require("../../../../locators/publishWidgetspage.json");
+const widgetsPage = require("../../../../locators/Widgets.json");
describe("Test Create Api and Bind to Table widget", function() {
let apiData;
@@ -43,6 +44,18 @@ describe("Test Create Api and Bind to Table widget", function() {
cy.readTabledataPublish("0", "1").then((tabData) => {
expect(apiData).to.eq(`\"${tabData}\"`);
});
+ cy.get(commonlocators.backToEditor).click();
+ });
+
+ it("Validate onSearchTextChanged function is called when configured for search text", function() {
+ cy.SearchEntityandOpen("Table1");
+ cy.get(".t--widget-tablewidget .t--search-input")
+ .first()
+ .type("Currey");
+ cy.wait(5000);
+ cy.readTabledataPublish("0", "1").then((tabData) => {
+ expect(apiData).to.eq(`\"${tabData}\"`);
+ });
});
afterEach(() => {
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Widgets_form_input_table_default_validation.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Widgets_form_input_table_default_validation.js
index 11f615bf69..d9181f25b4 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Widgets_form_input_table_default_validation.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Widgets_form_input_table_default_validation.js
@@ -22,18 +22,6 @@ describe("Binding the multiple input Widget", function() {
);
});
- it("Binding second input widget with first input widget and validating", function() {
- cy.SearchEntityandOpen("Input2");
- cy.get(widgetsPage.defaultInput).type(testdata.defaultMoustacheData);
- cy.get(commonlocators.editPropCrossButton).click({ force: true });
- cy.wait("@updateLayout").should(
- "have.nested.property",
- "response.body.responseMeta.status",
- 200,
- );
- cy.reload();
- });
-
it("validation of data displayed in all widgets based on row selected", function() {
cy.isSelectRow(1);
cy.readTabledataPublish("1", "0").then((tabData) => {
@@ -45,10 +33,10 @@ describe("Binding the multiple input Widget", function() {
.first()
.invoke("attr", "value")
.should("contain", tabValue);
- cy.get(publish.inputWidget + " " + "input")
- .last()
- .invoke("attr", "value")
- .should("contain", tabValue);
+ // cy.get(publish.inputWidget + " " + "input")
+ // .last()
+ // .invoke("attr", "value")
+ // .should("contain", tabValue);
});
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/aTobAndbToaBasictest_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/aTobAndbToaBasictest_spec.js
index 2243383267..9909e7e9a7 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/aTobAndbToaBasictest_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/aTobAndbToaBasictest_spec.js
@@ -31,7 +31,6 @@ describe("aTob and bToa library tests ", function() {
"response.body.responseMeta.status",
200,
);
- cy.reload();
});
it("publish widget and validate the data displayed in input widgets value for aToB and bToa", function() {
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/loadashBasictest_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/loadashBasictest_spec.js
index 19618536e1..6d30dabefc 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/loadashBasictest_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/loadashBasictest_spec.js
@@ -31,7 +31,6 @@ describe("Loadash basic test with input Widget", function() {
"response.body.responseMeta.status",
200,
);
- cy.reload();
});
it("publish widget and validate the data displayed in input widgets from loadash function", function() {
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/momentBasictest_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/momentBasictest_spec.js
index 2b6ff63311..19fdd3942e 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/momentBasictest_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/momentBasictest_spec.js
@@ -31,7 +31,6 @@ describe("Moment basic test with input Widget", function() {
"response.body.responseMeta.status",
200,
);
- cy.reload();
});
it("publish widget and validate the data displayed in input widgets", function() {
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Resize_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Resize_spec.js
index 6e3009fe9a..1f969e2c13 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Resize_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Resize_spec.js
@@ -7,10 +7,17 @@ describe("Canvas Resize", function() {
});
it("Deleting bottom widget should resize canvas", function() {
const InitHeight = "2960px";
- const FinalHeight = "1292px";
cy.get(commonlocators.dropTarget).should("have.css", "height", InitHeight);
cy.openPropertyPane("textwidget");
+ cy.intercept("PUT", "/api/v1/layouts/*/pages/*").as("deleteUpdate");
cy.get(commonlocators.deleteWidget).click();
- cy.get(commonlocators.dropTarget).should("have.css", "height", FinalHeight);
+ cy.wait("@deleteUpdate").then((response) => {
+ const dsl = response.response.body.data.dsl;
+ cy.get(commonlocators.dropTarget).should(
+ "have.css",
+ "height",
+ `${dsl.bottomRow}px`,
+ );
+ });
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Inspect_Element_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Inspect_Element_spec.js
new file mode 100644
index 0000000000..f565c11a99
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Inspect_Element_spec.js
@@ -0,0 +1,16 @@
+const dsl = require("../../../../fixtures/debuggerDependencyDsl.json");
+
+describe("Inspect Entity", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+ it("Check whether depedencies and references are shown correctly", function() {
+ cy.openPropertyPane("inputwidget");
+ cy.testJsontext("defaulttext", "{{Button1.text}}");
+
+ cy.get(".t--debugger").click();
+ cy.contains(".react-tabs__tab", "Inspect Entity").click();
+ cy.contains(".t--dependencies-item", "Button1").click();
+ cy.contains(".t--references-item", "Input1");
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Logs_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Logs_spec.js
index e884caf535..992fe23a68 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Logs_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Logs_spec.js
@@ -1,4 +1,6 @@
const dsl = require("../../../../fixtures/buttondsl.json");
+const commonlocators = require("../../../../locators/commonlocators.json");
+const debuggerLocators = require("../../../../locators/Debugger.json");
describe("Debugger logs", function() {
before(() => {
@@ -11,4 +13,18 @@ describe("Debugger logs", function() {
cy.get(".t--debugger").click();
cy.get(".t--debugger-log-state").contains("Test");
});
+
+ it("Reset debugger state", function() {
+ cy.get(".t--property-control-visible")
+ .find(".t--js-toggle")
+ .click();
+ cy.testJsontext("visible", "Test");
+
+ cy.get(commonlocators.homeIcon).click({ force: true });
+ cy.generateUUID().then((id) => {
+ cy.CreateAppInFirstListedOrg(id);
+
+ cy.contains(debuggerLocators.debuggerIcon, 0);
+ });
+ });
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/PageOnLoad_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/PageOnLoad_spec.js
new file mode 100644
index 0000000000..2fd3a73d96
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/PageOnLoad_spec.js
@@ -0,0 +1,23 @@
+const dsl = require("../../../../fixtures/debuggerTableDsl.json");
+const explorer = require("../../../../locators/explorerlocators.json");
+const debuggerLocators = require("../../../../locators/Debugger.json");
+const testdata = require("../../../../fixtures/testdata.json");
+
+describe("Check debugger logs state when there are onPageLoad actions", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+ it("Check debugger logs state when there are onPageLoad actions", function() {
+ cy.openPropertyPane("tablewidget");
+ cy.testJsontext("tabledata", "{{TestApi.data.users}}");
+ cy.NavigateToAPI_Panel();
+ cy.CreateAPI("TestApi");
+ cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
+ cy.SaveAndRunAPI();
+
+ cy.get(explorer.addWidget).click();
+
+ cy.reload();
+ cy.contains(debuggerLocators.debuggerIcon, 0);
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js
new file mode 100644
index 0000000000..b90c506c6f
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js
@@ -0,0 +1,25 @@
+const dsl = require("../../../../fixtures/buttondsl.json");
+const debuggerLocators = require("../../../../locators/Debugger.json");
+
+describe("Widget error state", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+ it("Check widget error state", function() {
+ cy.openPropertyPane("buttonwidget");
+
+ cy.get(".t--property-control-visible")
+ .find(".t--js-toggle")
+ .click();
+ cy.testJsontext("visible", "Test");
+
+ cy.contains(".t--widget-error-count", 1);
+ });
+
+ it("Check if the current value is shown in the debugger", function() {
+ cy.get(debuggerLocators.debuggerIcon).click();
+ cy.contains(".react-tabs__tab", "Errors").click();
+
+ cy.get(debuggerLocators.debuggerLogState).contains("Test");
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js
index 261a1741fe..d730e42151 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js
@@ -1,8 +1,10 @@
const commonlocators = require("../../../../locators/commonlocators.json");
const viewWidgetsPage = require("../../../../locators/ViewWidgets.json");
const publish = require("../../../../locators/publishWidgetspage.json");
-const dsl = require("../../../../fixtures/displayWidgetDsl.json");
+const dsl = require("../../../../fixtures/chartUpdatedDsl.json");
const pages = require("../../../../locators/Pages.json");
+const modalWidgetPage = require("../../../../locators/ModalWidget.json");
+const widgetsPage = require("../../../../locators/Widgets.json");
describe("Chart Widget Functionality", function() {
before(() => {
@@ -13,7 +15,8 @@ describe("Chart Widget Functionality", function() {
cy.openPropertyPane("chartwidget");
});
- it("Chart Widget Functionality", function() {
+ it("Fill the Chart Widget Properties.", function() {
+ //changing the Chart Name
/**
* @param{Text} Random Text
* @param{ChartWidget}Mouseover
@@ -24,6 +27,7 @@ describe("Chart Widget Functionality", function() {
viewWidgetsPage.chartWidget,
commonlocators.containerInnerText,
);
+ //changing the Chart Title
/**
* @param{Text} Random Input Value
*/
@@ -32,23 +36,77 @@ describe("Chart Widget Functionality", function() {
.contains("App Sign Up")
.should("have.text", "App Sign Up");
- cy.get(viewWidgetsPage.chartType)
- .last()
- .click({ force: true });
+ //Entering the Chart data
+ cy.testJsontext(
+ "chart-series-data-control",
+ JSON.stringify(this.data.chartInput),
+ );
+ cy.get(".t--propertypane").click("right");
- cy.get(commonlocators.dropdownmenu)
- .children()
- .contains("Column Chart")
- .click();
- cy.get(viewWidgetsPage.chartType)
- .last()
- .should("have.text", "Column Chart");
- cy.testJsontext("chartseries", JSON.stringify(this.data.chartInput));
+ // Asserting Chart Height
cy.get(viewWidgetsPage.chartWidget)
.should("be.visible")
.and((chart) => {
expect(chart.height()).to.be.greaterThan(200);
});
+
+ //Entring the label of x-axis
+ cy.get(viewWidgetsPage.xlabel)
+ .click({ force: true })
+ .type(this.data.command)
+ .type(this.data.plan);
+ //Entring the label of y-axis
+ cy.get(viewWidgetsPage.ylabel)
+ .click({ force: true })
+ .type(this.data.command)
+ .type(this.data.ylabel);
+
+ //Close edit prop
+ cy.get(commonlocators.editPropCrossButton).click({ force: true });
+ cy.PublishtheApp();
+ });
+
+ it("Pie Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.UpdateChartType("Pie Chart");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .last()
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.PieChartLabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Line Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.UpdateChartType("Line Chart");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .last()
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Bar Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.UpdateChartType("Bar Chart");
+
+ //Verifying X-axis labels
cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
[0, 1, 2, 3, 4, 5, 6].forEach((k) => {
@@ -59,74 +117,273 @@ describe("Chart Widget Functionality", function() {
.eq(k)
.should("have.text", labels[k]);
});
- cy.get(viewWidgetsPage.xlabel)
- .click({ force: true })
- .type(this.data.command)
- .type(this.data.plan);
- cy.get(viewWidgetsPage.ylabel)
- .click({ force: true })
- .type(this.data.command)
- .type(this.data.ylabel);
-
- //Close edit prop
- cy.get(commonlocators.editPropCrossButton).click({ force: true });
+ cy.PublishtheApp();
});
- it("Chart Widget Functionality To Unchecked Visible Widget", function() {
+ it("Area Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.UpdateChartType("Area Chart");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .last()
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Column Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.UpdateChartType("Column Chart");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .eq(k)
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Toggle JS - Pie Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.get(widgetsPage.toggleChartType).click({ force: true });
+ cy.testJsontext("charttype", "PIE_CHART");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .last()
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.PieChartLabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Toggle JS - Line Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.testJsontext("charttype", "LINE_CHART");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .last()
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Toggle JS - Bar Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.testJsontext("charttype", "BAR_CHART");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .eq(k)
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Toggle JS - Area Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.testJsontext("charttype", "AREA_CHART");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .last()
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Toggle JS - Column Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.testJsontext("charttype", "COLUMN_CHART");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .eq(k)
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+ cy.PublishtheApp();
+ });
+
+ it("Chart - Show Alert Modal", function() {
+ //creating the Alert Modal and verify Modal name
+ cy.createModal("Alert Modal", this.data.AlertModalName);
+ cy.PublishtheApp();
+ cy.get(widgetsPage.chartPlotGroup)
+ .children()
+ .first()
+ .click();
+ cy.get(modalWidgetPage.modelTextField).should(
+ "have.text",
+ this.data.AlertModalName,
+ );
+ });
+
+ it("Chart - Form Modal Validation", function() {
+ //creating the Form Modal and verify Modal name
+ cy.updateModal("Form Modal", this.data.FormModalName);
+ cy.PublishtheApp();
+ cy.get(widgetsPage.chartPlotGroup)
+ .children()
+ .first()
+ .click();
+ cy.get(modalWidgetPage.modelTextField).should(
+ "have.text",
+ this.data.FormModalName,
+ );
+ });
+
+ it("Chart-Unckeck Visible field Validation", function() {
+ // Making the widget invisible
cy.togglebarDisable(commonlocators.visibleCheckbox);
cy.PublishtheApp();
cy.get(publish.chartWidget).should("not.exist");
- cy.get(publish.backToEditor).click();
});
- it("Chart Widget Functionality To Check Visible Widget", function() {
+
+ it("Chart-Check Visible field Validation", function() {
+ // Making the widget visible
cy.togglebar(commonlocators.visibleCheckbox);
cy.PublishtheApp();
cy.get(publish.chartWidget).should("be.visible");
- cy.get(publish.backToEditor).click();
});
+
+ it("Toggle JS - Chart-Unckeck Visible field Validation", function() {
+ //Uncheck the disabled checkbox using JS and validate
+ cy.get(widgetsPage.toggleVisible).click({ force: true });
+ cy.EditWidgetPropertiesUsingJS(widgetsPage.inputToggleVisible, "false");
+ cy.PublishtheApp();
+ cy.get(publish.chartWidget).should("not.exist");
+ });
+
+ it("Toggle JS - Chart-Check Visible field Validation", function() {
+ //Check the disabled checkbox using JS and Validate
+ cy.EditWidgetPropertiesUsingJS(widgetsPage.inputToggleVisible, "true");
+ cy.PublishtheApp();
+ cy.get(publish.chartWidget).should("be.visible");
+ });
+
it("Chart Widget Functionality To Uncheck Horizontal Scroll Visible", function() {
cy.togglebarDisable(commonlocators.horizontalScroll);
cy.PublishtheApp();
cy.get(publish.horizontalTab).should("not.exist");
- cy.get(publish.backToEditor).click();
});
+
it("Chart Widget Functionality To Check Horizontal Scroll Visible", function() {
cy.togglebar(commonlocators.horizontalScroll);
cy.PublishtheApp();
cy.get(publish.horizontalTab)
.eq(1)
.should("exist");
- cy.get(publish.backToEditor).click();
});
- it("Chart Widget Custom Config Feature", function() {
- // Note: This only checks for crashes in custom config
- cy.get(viewWidgetsPage.chartType)
- .last()
- .click({ force: true });
-
- cy.get(commonlocators.dropdownmenu)
- .children()
- .contains("Custom Chart")
- .click();
- cy.get(viewWidgetsPage.chartType)
- .last()
- .should("have.text", "Custom Chart");
+ it("Custom Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.get(widgetsPage.toggleChartType).click({ force: true });
+ cy.UpdateChartType("Custom Chart");
cy.testJsontext(
"customfusionchartconfiguration",
`{{${JSON.stringify(this.data.ChartCustomConfig)}}}`,
);
- cy.get(viewWidgetsPage.chartWidget)
- .should("be.visible")
- .and((chart) => {
- expect(chart.height()).to.be.greaterThan(200);
- });
+
+ //Verifying X-axis labels
cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .eq(k)
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+
//Close edit prop
cy.get(commonlocators.editPropCrossButton).click();
+ cy.PublishtheApp();
+ });
+
+ it("Toggle JS - Custom Chart Widget Functionality", function() {
+ //changing the Chart type
+ cy.UpdateChartType("Pie Chart");
+ cy.get(widgetsPage.toggleChartType).click({ force: true });
+ cy.testJsontext("charttype", "CUSTOM_FUSION_CHART");
+
+ //Verifying X-axis labels
+ cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
+ const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"];
+ [0, 1, 2, 3, 4, 5, 6].forEach((k) => {
+ cy.get(viewWidgetsPage.rectangleChart)
+ .eq(k)
+ .trigger("mousemove", { force: true });
+ cy.get(viewWidgetsPage.Chartlabel)
+ .eq(k)
+ .should("have.text", labels[k]);
+ });
+
+ //Close edit prop
+ cy.get(commonlocators.editPropCrossButton).click();
+ cy.PublishtheApp();
+ });
+
+ it("Chart-Copy Verification", function() {
+ const modifierKey = Cypress.platform === "darwin" ? "meta" : "ctrl";
+ //Copy Chart and verify all properties
+ cy.copyWidget("chartwidget", viewWidgetsPage.chartWidget);
+
+ cy.PublishtheApp();
+ });
+
+ it("Chart-Delete Verification", function() {
+ // Delete the Chart widget
+ cy.deleteWidget(viewWidgetsPage.chartWidget);
+ cy.PublishtheApp();
+ cy.get(viewWidgetsPage.chartWidget).should("not.exist");
+ });
+
+ afterEach(() => {
+ cy.get(publish.backToEditor).click({ force: true });
});
});
-afterEach(() => {
- // put your clean up code if any
-});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Divider_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Divider_spec.js
new file mode 100644
index 0000000000..05bcec35a9
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Divider_spec.js
@@ -0,0 +1,23 @@
+const dsl = require("../../../../fixtures/DividerDsl.json");
+const explorer = require("../../../../locators/explorerlocators.json");
+
+describe("Divider Widget Functionality", function() {
+ beforeEach(() => {
+ cy.addDsl(dsl);
+ });
+
+ it("Add new Divider", () => {
+ cy.get(explorer.addWidget).click();
+ cy.dragAndDropToCanvas("dividerwidget", { x: 300, y: 300 });
+ cy.get(".t--divider-widget").should("exist");
+ });
+
+ it("Open Existing Divider from created Widgets list", () => {
+ cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Widgets)").click({
+ multiple: true,
+ });
+ cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Divider1)").click({
+ multiple: true,
+ });
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Image_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Image_spec.js
index 747a4e8471..5713afc305 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Image_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Image_spec.js
@@ -23,7 +23,6 @@ describe("Image Widget Functionality", function() {
);
cy.get(viewWidgetsPage.defaultImage)
.click({ force: true })
- .type(this.data.command)
.type(this.data.defaultimage);
/**
* @param{URL} ImageUrl
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Modal_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Modal_spec.js
index ba257c5ae2..e36c3f6648 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Modal_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Modal_spec.js
@@ -8,7 +8,7 @@ describe("Modal Widget Functionality", function() {
it("Add new Modal", () => {
cy.get(explorer.addWidget).click();
- cy.dragAndDropToCanvas("modalwidget", { x: 300, y: -300 });
+ cy.dragAndDropToCanvas("modalwidget", { x: 300, y: 300 });
cy.get(".t--modal-widget").should("exist");
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Column_Resize_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Column_Resize_spec.js
new file mode 100644
index 0000000000..7eb9000870
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Column_Resize_spec.js
@@ -0,0 +1,24 @@
+/* eslint-disable cypress/no-unnecessary-waiting */
+const widgetsPage = require("../../../../locators/Widgets.json");
+const commonlocators = require("../../../../locators/commonlocators.json");
+const publish = require("../../../../locators/publishWidgetspage.json");
+const dsl = require("../../../../fixtures/tableResizedColumnsDsl.json");
+
+describe("Table Widget Functionality with Hidden and Resized Columns", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+
+ it("Table Widget Functionality with Hidden and Resized Columns", function() {
+ cy.PublishtheApp();
+ cy.get(".t--widget-tablewidget")
+ .invoke("outerWidth")
+ .then((tableWidth) => {
+ cy.get(".t--widget-tablewidget .thead .tr")
+ .invoke("outerWidth")
+ .then((width) => {
+ expect(width).to.be.at.least(tableWidth);
+ });
+ });
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Derived_Column_Data_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Derived_Column_Data_validation_spec.js
index 2104fa8e76..d57c23fac3 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Derived_Column_Data_validation_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Derived_Column_Data_validation_spec.js
@@ -43,13 +43,12 @@ describe("Test Create Api and Bind to Table widget", function() {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.toggleJsAndUpdate("tabledata", "Green");
- cy.get(commonlocators.editPropCrossButton).click();
+ cy.get(".t--property-pane-back-btn").click({ force: true });
cy.wait("@updateLayout");
cy.readTabledataValidateCSS("1", "0", "background-color", "rgb(0, 128, 0)");
});
it("Edit column name and validate test for computed value based on column type selected", function() {
- cy.SearchEntityandOpen("Table1");
cy.editColumn("customColumn1");
cy.readTabledataPublish("1", "9").then((tabData) => {
const tabValue = tabData;
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_MultiRowSelect_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_MultiRowSelect_spec.js
new file mode 100644
index 0000000000..fc09a9fbe5
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_MultiRowSelect_spec.js
@@ -0,0 +1,46 @@
+const widgetsPage = require("../../../../locators/Widgets.json");
+const dsl = require("../../../../fixtures/tableNewDsl.json");
+
+describe("Table Widget row multi select validation", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+
+ it("Test multi select column shows when enableMultirowselection is true", function() {
+ cy.openPropertyPane("tablewidget");
+ cy.get(widgetsPage.toggleEnableMultirowselection)
+ .first()
+ .click({ force: true });
+ cy.get(".t--table-multiselect-header")
+ .first()
+ .should("be.visible");
+
+ cy.get(".t--table-multiselect")
+ .first()
+ .should("be.visible");
+ });
+
+ it("Test click on header cell selects all row", function() {
+ // click on header check cell
+ cy.get(".t--table-multiselect-header")
+ .first()
+ .click({ force: true });
+ // check if rows selected
+ cy.get(".tr").should("have.class", "selected-row");
+ });
+
+ it("Test click on single row cell changes header select cell state", function() {
+ // un select all rows
+ cy.get(".t--table-multiselect-header")
+ .first()
+ .click({ force: true });
+ // click on first row select box
+ cy.get(".t--table-multiselect")
+ .first()
+ .click({ force: true });
+ // check if header cell is in half check state
+ cy.get(".t--table-multiselect-header-half-check-svg")
+ .first()
+ .should("be.visible");
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Widget_Copy_Paste_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Widget_Copy_Paste_spec.js
index e398325e27..15a1cff837 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Widget_Copy_Paste_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Widget_Copy_Paste_spec.js
@@ -32,19 +32,24 @@ describe("Test Suite to validate copy/paste table Widget", function() {
200,
);
cy.get(commonlocators.toastAction).should("be.visible");
+
+ //Check after copying new table widget should not have any warnings
+ cy.get('.t--widget-propertypane-toggle [name="warning"]').should(
+ "not.exist",
+ );
/*
cy.get(commonlocators.toastAction)
.contains("UNDO")
.click({ force: true });
*/
- cy.GlobalSearchEntity("Table2");
+ cy.GlobalSearchEntity("Table1Copy");
cy.get(".t--entity-collapse-toggle")
.last()
.click();
cy.get(apiwidget.propertyList).then(function($lis) {
expect($lis).to.have.length(8);
- expect($lis.eq(0)).to.contain("{{Table2.selectedRow}}");
- expect($lis.eq(1)).to.contain("{{Table2.selectedRows}}");
+ expect($lis.eq(0)).to.contain("{{Table1Copy.selectedRow}}");
+ expect($lis.eq(1)).to.contain("{{Table1Copy.selectedRows}}");
});
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js
index 1bfbed6a6b..d46969722f 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js
@@ -3,7 +3,6 @@ const widgetsPage = require("../../../../locators/Widgets.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const publish = require("../../../../locators/publishWidgetspage.json");
const dsl = require("../../../../fixtures/tableWidgetDsl.json");
-const pages = require("../../../../locators/Pages.json");
describe("Table Widget Functionality", function() {
before(() => {
@@ -83,7 +82,9 @@ describe("Table Widget Functionality", function() {
cy.wait(5000);
cy.get(publish.searchInput)
.first()
- .clear()
+ .within(() => {
+ return cy.get("input").clear();
+ })
.type("7434532");
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
@@ -97,7 +98,9 @@ describe("Table Widget Functionality", function() {
it("Table Widget Functionality To Filter The Data", function() {
cy.get(publish.searchInput)
.first()
- .clear();
+ .within(() => {
+ return cy.get("input").clear();
+ });
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.isSelectRow(1);
@@ -261,16 +264,16 @@ describe("Table Widget Functionality", function() {
const tabValue = tabData;
expect(tabValue).to.be.equal("Lindsay Ferguson");
cy.log("the value is" + tabValue);
- cy.get(publish.compactMode).click();
- cy.get(publish.compactOpt)
+ cy.get(publish.rowHeight).click();
+ cy.get(publish.rowHeightOpt)
.contains("Tall")
.click();
cy.scrollTabledataPublish("3", "3").then((tabData) => {
const tabValue = tabData;
expect(tabValue).to.be.equal("Byron Fields");
});
- cy.get(publish.compactMode).click();
- cy.get(publish.compactOpt)
+ cy.get(publish.rowHeight).click();
+ cy.get(publish.rowHeightOpt)
.contains("Short")
.click();
cy.readTabledataPublish("4", "3").then((tabData) => {
@@ -282,7 +285,7 @@ describe("Table Widget Functionality", function() {
/*
To enabled later
-
+
it("Table Widget Functionality To Verify The Visiblity mode functionality", function() {
cy.get(publish.backToEditor)
.first()
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_spec.js
index 2732da5b7c..e0e0885887 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_spec.js
@@ -36,7 +36,19 @@ describe("Text Widget Functionality", function() {
.should("have.css", "font-size", "24px");
});
+ it("Text Email Parsing Validation", function() {
+ cy.testCodeMirror("ab.end@domain.com");
+ cy.wait("@updateLayout");
+ cy.PublishtheApp();
+ cy.get(commonlocators.headingTextStyle + " a").should(
+ "have.attr",
+ "href",
+ "mailto:ab.end@domain.com",
+ );
+ });
+
it("Text-TextStyle Label Validation", function() {
+ cy.testCodeMirror(this.data.TextLabelValue);
//Changing the Text Style's and validating
cy.ChangeTextStyle(
this.data.TextLabel,
@@ -61,33 +73,33 @@ describe("Text Widget Functionality", function() {
.should("have.css", "font-size", "18px");
});
- // it("Text widget depends on itself", function() {
- // cy.getCodeMirror().then(($cm) => {
- // if ($cm.val() !== "") {
- // cy.get(".CodeMirror textarea")
- // .first()
- // .clear({
- // force: true,
- // });
- // }
- //
- // cy.get(".CodeMirror textarea")
- // .first()
- // .type(`{{${this.data.TextName}}}`, {
- // force: true,
- // parseSpecialCharSequences: false,
- // });
- // });
- // cy.get(commonlocators.toastBody)
- // .first()
- // .contains("Cyclic");
- //
- // cy.PublishtheApp();
- // cy.get(commonlocators.bodyTextStyle).should(
- // "have.text",
- // `{{${this.data.TextName}}}`,
- // );
- // });
+ it("Text widget depends on itself", function() {
+ cy.getCodeMirror().then(($cm) => {
+ if ($cm.val() !== "") {
+ cy.get(".CodeMirror textarea")
+ .first()
+ .clear({
+ force: true,
+ });
+ }
+
+ cy.get(".CodeMirror textarea")
+ .first()
+ .type(`{{${this.data.TextName}}}`, {
+ force: true,
+ parseSpecialCharSequences: false,
+ });
+ });
+ cy.get(commonlocators.toastBody)
+ .first()
+ .contains("Cyclic");
+
+ cy.PublishtheApp();
+ cy.get(commonlocators.bodyTextStyle).should(
+ "have.text",
+ `{{${this.data.TextName}}}`,
+ );
+ });
afterEach(() => {
cy.get(publishPage.backToEditor).click({ force: true });
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/video_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/video_spec.js
index 5ee9895da8..1f657612e3 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/video_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/video_spec.js
@@ -51,7 +51,7 @@ describe("Video Widget Functionality", function() {
cy.get(".CodeMirror textarea")
.first()
.blur();
- cy.get(widgetsPage.autoPlay).click();
+ cy.get(widgetsPage.autoPlay).click({ force: true });
cy.wait("@updateLayout").should(
"have.nested.property",
"response.body.responseMeta.status",
@@ -62,7 +62,7 @@ describe("Video Widget Functionality", function() {
cy.get(commonlocators.toastMsg).should("be.visible");
cy.get(commonlocators.toastMsg).contains("Play success");
*/
- cy.get(widgetsPage.autoPlay).click();
+ cy.get(widgetsPage.autoPlay).click({ force: true });
cy.wait("@updateLayout").should(
"have.nested.property",
"response.body.responseMeta.status",
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DynamicInput/autocomplete_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DynamicInput/autocomplete_spec.js
index 6f2e0eebfd..ecc7c5d252 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DynamicInput/autocomplete_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DynamicInput/autocomplete_spec.js
@@ -6,29 +6,23 @@ const dynamicInputLocators = require("../../../../locators/DynamicInput.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
describe("Dynamic input autocomplete", () => {
- beforeEach(() => {
+ before(() => {
cy.addDsl(dsl);
});
it("opens autocomplete for bindings", () => {
cy.openPropertyPane("buttonwidget");
cy.get(dynamicInputLocators.input)
.first()
- .focus()
- .type("{ctrl}{shift}{downarrow}")
- .then(($cm) => {
- if ($cm.val() !== "") {
- cy.get(dynamicInputLocators.input)
- .first()
- .clear({
- force: true,
- });
- }
-
+ .click({ force: true })
+ .type("{uparrow}", { parseSpecialCharSequences: true })
+ .type("{ctrl}{shift}{downarrow}", { parseSpecialCharSequences: true })
+ .type("{backspace}", { parseSpecialCharSequences: true })
+ .then(() => {
cy.get(dynamicInputLocators.input)
.first()
+ .click({ force: true })
.type("{{", {
- force: true,
- parseSpecialCharSequences: false,
+ parseSpecialCharSequences: true,
});
// Tests if autocomplete will open
@@ -36,15 +30,17 @@ describe("Dynamic input autocomplete", () => {
// Tests if data tree entities are sorted
cy.get(`${dynamicInputLocators.hints} li`)
- .first()
- .should("have.text", "Aditya");
+ .eq(1)
+ .should("have.text", "Aditya.backgroundColor");
// Tests if "No suggestions" message will pop if you type any garbage
cy.get(dynamicInputLocators.input)
.first()
- .type("garbage", {
- force: true,
- parseSpecialCharSequences: false,
+ .click({ force: true })
+ .type("{uparrow}", { parseSpecialCharSequences: true })
+ .type("{ctrl}{shift}{downarrow}", { parseSpecialCharSequences: true })
+ .type("{{ garbage", {
+ parseSpecialCharSequences: true,
})
.then(() => {
cy.get(".CodeMirror-Tern-tooltip").should(
@@ -53,21 +49,16 @@ describe("Dynamic input autocomplete", () => {
);
});
});
+ cy.evaluateErrorMessage("ReferenceError: garbage is not defined");
});
it("opens current value popup", () => {
- // Test on widgets pane
- cy.openPropertyPane("buttonwidget");
- cy.get(dynamicInputLocators.input)
- .first()
- .focus();
- cy.assertEvaluatedValuePopup("string");
// Test on api pane
cy.NavigateToAPI_Panel();
cy.get(apiwidget.createapi).click({ force: true });
cy.wait("@createNewApi");
- cy.xpath(apiwidget.headerValue)
- .first()
- .focus();
+ cy.get(apiwidget.headerValue).within(() => {
+ cy.get("textarea").click({ force: true });
+ });
cy.assertEvaluatedValuePopup("string");
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js
index a2d387bd31..ae64c14c37 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js
@@ -1,5 +1,6 @@
const testdata = require("../../../../fixtures/testdata.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
+const widgetsPage = require("../../../../locators/Widgets.json");
const explorer = require("../../../../locators/explorerlocators.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const formWidgetsPage = require("../../../../locators/FormWidgets.json");
@@ -15,7 +16,8 @@ describe("Entity explorer Drag and Drop widgets testcases", function() {
cy.get(commonlocators.entityExplorersearch)
.clear()
.type("form");
- cy.dragAndDropToCanvas("formwidget", { x: 300, y: -300 });
+ cy.dragAndDropToCanvas("formwidget", { x: 300, y: 80 });
+ cy.get(formWidgetsPage.formD).click();
/**
* @param{Text} Random Text
* @param{FormWidget}Mouseover
@@ -29,10 +31,13 @@ describe("Entity explorer Drag and Drop widgets testcases", function() {
/**
* @param{Text} Random Colour
*/
- cy.testCodeMirror(this.data.colour);
+ cy.get(widgetsPage.backgroundcolorPicker)
+ .first()
+ .click({ force: true });
+ cy.xpath(widgetsPage.greenColor).click();
cy.get(formWidgetsPage.formD)
.should("have.css", "background-color")
- .and("eq", this.data.rgbValue);
+ .and("eq", "rgb(3, 179, 101)");
/**
* @param{toggleButton Css} Assert to be checked
*/
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js
index f094bf74d7..01ddf0a9b5 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js
@@ -5,6 +5,7 @@ const datasource = require("../../../../locators/DatasourcesEditor.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const explorer = require("../../../../locators/explorerlocators.json");
+const pages = require("../../../../locators/Pages.json");
const pageid = "MyPage";
let datasourceName;
@@ -59,7 +60,7 @@ describe("Entity explorer tests related to query and datasource", function() {
/* eslint-disable */
cy.wait(2000);
- cy.go("back");
+ cy.NavigateToQueryEditor();
cy.contains(".t--datasource-name", datasourceName)
.find(queryLocators.editDatasourceButton)
@@ -101,10 +102,11 @@ describe("Entity explorer tests related to query and datasource", function() {
.find(explorer.collapse)
.click();
cy.get(apiwidget.propertyList).then(function($lis) {
- expect($lis).to.have.length(3);
+ expect($lis).to.have.length(4);
expect($lis.eq(0)).to.contain("{{Query1.isLoading}}");
expect($lis.eq(1)).to.contain("{{Query1.data}}");
- expect($lis.eq(2)).to.contain("{{Query1.run()}}");
+ expect($lis.eq(2)).to.contain("{{Query1.responseMeta}}");
+ expect($lis.eq(3)).to.contain("{{Query1.run()}}");
});
cy.Createpage(pageid);
cy.GlobalSearchEntity("Query1");
@@ -120,7 +122,12 @@ describe("Entity explorer tests related to query and datasource", function() {
cy.runQuery();
cy.deleteQuery();
-
+ cy.get(commonlocators.entityExplorersearch).clear({ force: true });
+ cy.wait(500);
+ cy.NavigateToQueryEditor();
+ cy.get(pages.integrationActiveTab)
+ .should("be.visible")
+ .click({ force: true });
cy.contains(".t--datasource-name", datasourceName)
.find(".t--edit-datasource")
.click();
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js
index 178635df67..1b0841806c 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js
@@ -31,10 +31,9 @@ describe("Tab widget test", function() {
cy.RenameEntity(tabname);
cy.validateMessage(tabname);
cy.deleteEntity();
- cy.get(commonlocators.entityExplorersearch).should("be.visible");
cy.get(commonlocators.entityExplorersearch)
- .clear()
- .type("Tab 2");
+ .clear({ force: true })
+ .type("Tab 2", { force: true });
cy.get(
commonlocators.entitySearchResult.concat("Tab 2").concat("')"),
).should("not.exist");
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js
index c6627d91bf..2a4774093a 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js
@@ -28,7 +28,8 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() {
cy.wait(500);
cy.get(commonlocators.toastBody)
.first()
- .contains("Copied");
+ .contains("Copied")
+ .click();
cy.get("body").type(`{${modifierKey}}v`, { force: true });
cy.wait("@updateLayout").should(
"have.nested.property",
@@ -45,11 +46,11 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() {
cy.get(commonlocators.toastAction)
.contains("UNDO")
.click({ force: true });
- cy.GlobalSearchEntity("Form1");
+ cy.GlobalSearchEntity("FormTestCopy");
cy.get(apiwidget.propertyList).then(function($lis) {
expect($lis).to.have.length(2);
- expect($lis.eq(0)).to.contain("{{Form1.isVisible}}");
- expect($lis.eq(1)).to.contain("{{Form1.data}}");
+ expect($lis.eq(0)).to.contain("{{FormTestCopy.isVisible}}");
+ expect($lis.eq(1)).to.contain("{{FormTestCopy.data}}");
});
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Button_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Button_spec.js
index f7cecc69a4..3309634c54 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Button_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Button_spec.js
@@ -5,6 +5,8 @@ const homePage = require("../../../../locators/HomePage.json");
const pages = require("../../../../locators/Pages.json");
const publishPage = require("../../../../locators/publishWidgetspage.json");
const modalWidgetPage = require("../../../../locators/ModalWidget.json");
+const datasource = require("../../../../locators/DatasourcesEditor.json");
+const queryLocators = require("../../../../locators/QueryEditor.json");
describe("Button Widget Functionality", function() {
before(() => {
@@ -15,6 +17,20 @@ describe("Button Widget Functionality", function() {
cy.openPropertyPane("buttonwidget");
});
+ it("Button-Style Validation", function() {
+ //Changing the style of the button from the property pane and verify it's color.
+ // Change to Secondary button sytle
+ cy.changeButtonStyle(2, "rgba(0, 0, 0, 0)", "rgba(0, 0, 0, 0)");
+ cy.get(publishPage.backToEditor).click({ force: true });
+ // Change to Danger button sytle
+ cy.openPropertyPane("buttonwidget");
+ cy.changeButtonStyle(3, "rgb(179, 3, 56)", "rgb(139, 2, 43)");
+ cy.get(publishPage.backToEditor).click({ force: true });
+ // Change to Primary button sytle
+ cy.openPropertyPane("buttonwidget");
+ cy.changeButtonStyle(1, "rgb(3, 179, 101)", "rgb(2, 139, 78)");
+ });
+
it("Button-Name validation", function() {
//changing the Button Name
cy.widgetText(
@@ -71,6 +87,35 @@ describe("Button Widget Functionality", function() {
);
});
+ it("Toggle JS - Button-Disable Validation", function() {
+ //Check the disabled checkbox by using JS widget and Validate
+ cy.get(widgetsPage.toggleDisable).click({ force: true });
+ cy.EditWidgetPropertiesUsingJS(widgetsPage.inputToggleDisable, "true");
+ cy.validateDisableWidget(
+ widgetsPage.buttonWidget,
+ commonlocators.disabledField,
+ );
+ cy.PublishtheApp();
+ cy.validateDisableWidget(
+ publishPage.buttonWidget,
+ commonlocators.disabledField,
+ );
+ });
+
+ it("Toggle JS - Button-Enable Validation", function() {
+ //Uncheck the disabled checkbox and validate
+ cy.EditWidgetPropertiesUsingJS(widgetsPage.inputToggleDisable, "false");
+ cy.validateEnableWidget(
+ widgetsPage.buttonWidget,
+ commonlocators.disabledField,
+ );
+ cy.PublishtheApp();
+ cy.validateEnableWidget(
+ publishPage.buttonWidget,
+ commonlocators.disabledField,
+ );
+ });
+
it("Button-Unckeck Visible field Validation", function() {
//Uncheck the disabled checkbox and validate
cy.UncheckWidgetProperties(commonlocators.visibleCheckbox);
@@ -85,6 +130,21 @@ describe("Button Widget Functionality", function() {
cy.get(publishPage.buttonWidget).should("be.visible");
});
+ it("Toggle JS - Button-Unckeck Visible field Validation", function() {
+ //Uncheck the disabled checkbox using JS and validate
+ cy.get(widgetsPage.toggleVisible).click({ force: true });
+ cy.EditWidgetPropertiesUsingJS(widgetsPage.inputToggleVisible, "false");
+ cy.PublishtheApp();
+ cy.get(publishPage.buttonWidget).should("not.exist");
+ });
+
+ it("Toggle JS - Button-Check Visible field Validation", function() {
+ //Check the disabled checkbox using JS and Validate
+ cy.EditWidgetPropertiesUsingJS(widgetsPage.inputToggleVisible, "true");
+ cy.PublishtheApp();
+ cy.get(publishPage.buttonWidget).should("be.visible");
+ });
+
it("Button-AlertModal Validation", function() {
//creating the Alert Modal and verify Modal name
cy.createModal("Alert Modal", this.data.AlertModalName);
@@ -107,6 +167,124 @@ describe("Button Widget Functionality", function() {
);
});
+ it("Button-CallAnApi Validation", function() {
+ //creating an api and calling it from the onClickAction of the button widget.
+ // Creating the api
+ cy.NavigateToAPI_Panel();
+ cy.CreateAPI("buttonApi");
+ cy.log("Creation of buttonApi Action successful");
+ cy.enterDatasourceAndPath(this.data.paginationUrl, "users?page=4&size=3");
+ cy.SaveAndRunAPI();
+
+ // Going to HomePage where the button widget is located and opeing it's property pane.
+ cy.get(widgetsPage.NavHomePage).click({ force: true });
+ cy.reload();
+ cy.openPropertyPane("buttonwidget");
+
+ // Adding the api in the onClickAction of the button widget.
+ cy.addAPIFromLightningMenu("buttonApi");
+ // Filling the messages for success/failure in the onClickAction of the button widget.
+ cy.onClickActions("Success", "Error");
+
+ cy.PublishtheApp();
+
+ // Clicking the button to verify the success message
+ cy.get(publishPage.buttonWidget).click();
+ cy.get(widgetsPage.apiCallToast).should("have.text", "Success");
+ });
+
+ it("Button-Call-Query Validation", function() {
+ //creating a query and calling it from the onClickAction of the button widget.
+ // Creating a mock query
+ // cy.CreateMockQuery("Query1");
+ let postgresDatasourceName;
+
+ cy.startRoutesForDatasource();
+ cy.NavigateToDatasourceEditor();
+ cy.get(datasource.PostgreSQL).click();
+ cy.generateUUID().then((uid) => {
+ postgresDatasourceName = uid;
+
+ cy.get(".t--edit-datasource-name").click();
+ cy.get(".t--edit-datasource-name input")
+ .clear()
+ .type(postgresDatasourceName, { force: true })
+ .should("have.value", postgresDatasourceName)
+ .blur();
+ });
+ cy.wait("@saveDatasource").should(
+ "have.nested.property",
+ "response.body.responseMeta.status",
+ 200,
+ );
+ cy.fillPostgresDatasourceForm();
+ cy.saveDatasource();
+
+ cy.CreateMockQuery("Query1");
+
+ // Going to HomePage where the button widget is located and opeing it's property pane.
+ cy.get(widgetsPage.NavHomePage).click({ force: true });
+ cy.reload();
+ cy.openPropertyPane("buttonwidget");
+
+ // Adding the query in the onClickAction of the button widget.
+ cy.addQueryFromLightningMenu("Query1");
+ // Filling the messages for success/failure in the onClickAction of the button widget.
+ cy.onClickActions("Success", "Error");
+
+ cy.PublishtheApp();
+
+ // Clicking the button to verify the success message
+ cy.get(publishPage.buttonWidget).click();
+ cy.get(widgetsPage.apiCallToast).should("have.text", "Success");
+ });
+
+ it("Toggle JS - Button-CallAnApi Validation", function() {
+ //creating an api and calling it from the onClickAction of the button widget.
+ // calling the existing api
+ cy.get(widgetsPage.toggleOnClick).click({ force: true });
+ cy.testJsontext(
+ "onclick",
+ "{{buttonApi.run(() => showAlert('Success','success'), () => showAlert('Error','error'))}}",
+ );
+
+ cy.PublishtheApp();
+
+ // Clicking the button to verify the success message
+ cy.get(publishPage.buttonWidget).click();
+ cy.get(widgetsPage.apiCallToast).should("have.text", "Success");
+ });
+
+ it("Toggle JS - Button-Call-Query Validation", function() {
+ //creating a query and calling it from the onClickAction of the button widget.
+ // Creating a mock query
+ cy.testJsontext(
+ "onclick",
+ "{{Query1.run(() => showAlert('Success','success'), () => showAlert('Error','error'))}}",
+ );
+
+ cy.PublishtheApp();
+
+ // Clicking the button to verify the success message
+ cy.get(publishPage.buttonWidget).click();
+ cy.get(widgetsPage.apiCallToast).should("have.text", "Success");
+ });
+
+ it("Button-Copy Verification", function() {
+ const modifierKey = Cypress.platform === "darwin" ? "meta" : "ctrl";
+ //Copy button and verify all properties
+ cy.copyWidget("buttonwidget", widgetsPage.buttonWidget);
+
+ cy.PublishtheApp();
+ });
+
+ it("Button-Delete Verification", function() {
+ // Delete the button widget
+ cy.deleteWidget(widgetsPage.buttonWidget);
+ cy.PublishtheApp();
+ cy.get(widgetsPage.buttonWidget).should("not.exist");
+ });
+
afterEach(() => {
cy.get(publishPage.backToEditor).click({ force: true });
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckBox_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckBox_spec.js
index e987207b66..f3503292c7 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckBox_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckBox_spec.js
@@ -81,7 +81,7 @@ describe("Checkbox Widget Functionality", function() {
cy.get(publish.checkboxWidget).click();
cy.get(widgetsPage.formButtonWidget)
.contains("Submit")
- .should("have.attr", "disabled");
+ .should("have.class", "bp3-disabled");
cy.get(publish.checkboxWidget).click();
cy.get(widgetsPage.formButtonWidget)
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_2_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_2_spec.js
index 1f8676f85c..0208539c48 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_2_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_2_spec.js
@@ -74,13 +74,44 @@ describe("DatePicker Widget Property pane tests with js bindings", function() {
cy.get(".t--property-control-defaultdate .bp3-input").type("2020-02-01");
cy.closePropertyPane();
cy.openPropertyPane("datepickerwidget2");
- cy.get(formWidgetsPage.toggleJsMinDate).click();
+ cy.get(formWidgetsPage.toggleJsMinDate).click({ force: true });
cy.get(".t--property-control-mindate .bp3-input").type("2020-01-01");
- cy.get(formWidgetsPage.toggleJsMaxDate).click();
+ cy.get(formWidgetsPage.toggleJsMaxDate).click({ force: true });
cy.get(".t--property-control-maxdate .bp3-input").type("2020-02-10");
cy.closePropertyPane();
});
+ it("Datepicker input value changes to work with selected date formats", function() {
+ cy.openPropertyPane("datepickerwidget2");
+ cy.get(".t--property-control-mindate .bp3-input")
+ .clear()
+ .type("2021-01-01");
+ cy.closePropertyPane();
+ cy.openPropertyPane("datepickerwidget2");
+ cy.get(".t--property-control-maxdate .bp3-input")
+ .clear()
+ .type("2021-10-10");
+ cy.closePropertyPane();
+ cy.openPropertyPane("datepickerwidget2");
+ cy.get(".t--property-control-defaultdate .bp3-input").clear();
+ cy.selectDateFormat("DD/MM/YYYY HH:mm");
+ cy.get(formWidgetsPage.toggleJsDefaultDate).click();
+ cy.testJsontext(
+ "defaultdate",
+ '{{moment("04/05/2021 05:25", "DD/MM/YYYY HH:mm").toISOString()}}',
+ );
+ cy.get(".t--draggable-datepickerwidget2 .bp3-input")
+ .clear({
+ force: true,
+ })
+ .type("04/05/2021 06:25");
+ cy.selectDateFormat("LLL");
+ cy.wait("@updateLayout");
+ cy.get(".t--draggable-textwidget .bp3-ui-text")
+ .first()
+ .should("have.text", "May 4, 2021 6:25 AM");
+ });
+
it("Datepicker default date validation with js binding", function() {
cy.PublishtheApp();
// eslint-disable-next-line cypress/no-unnecessary-waiting
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js
index 50bef3f6c9..70cef20567 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js
@@ -3,6 +3,7 @@ const formWidgetsPage = require("../../../../locators/FormWidgets.json");
const publish = require("../../../../locators/publishWidgetspage.json");
const dsl = require("../../../../fixtures/formdsl.json");
const pages = require("../../../../locators/Pages.json");
+const widgetsPage = require("../../../../locators/Widgets.json");
describe("Form Widget Functionality", function() {
before(() => {
@@ -23,10 +24,13 @@ describe("Form Widget Functionality", function() {
/**
* @param{Text} Random Colour
*/
- cy.testCodeMirror(this.data.colour);
+ cy.get(widgetsPage.backgroundcolorPicker)
+ .first()
+ .click({ force: true });
+ cy.xpath(widgetsPage.greenColor).click();
cy.get(formWidgetsPage.formD)
.should("have.css", "background-color")
- .and("eq", this.data.rgbValue);
+ .and("eq", "rgb(3, 179, 101)");
/**
* @param{toggleButton Css} Assert to be checked
*/
@@ -40,7 +44,7 @@ describe("Form Widget Functionality", function() {
it("Form Widget Functionality To Verify The Colour", function() {
cy.get(formWidgetsPage.formD)
.should("have.css", "background-color")
- .and("eq", this.data.rgbValue);
+ .and("eq", "rgb(3, 179, 101)");
});
it("Form Widget Functionality To Unchecked Visible Widget", function() {
cy.get(publish.backToEditor).click();
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Radio_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Radio_spec.js
index cf11fdd239..32b67709b9 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Radio_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Radio_spec.js
@@ -49,7 +49,6 @@ describe("Radio Widget Functionality", function() {
cy.get(formWidgetsPage.radioOnSelectionChangeDropdown)
.get(commonlocators.dropdownSelectButton)
.click({ force: true })
- .type(this.data.command)
.type("2");
cy.PublishtheApp();
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js
index 6f186627f0..3377ba5c83 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js
@@ -29,17 +29,6 @@ describe("RichTextEditor Widget Functionality", function() {
"h1",
"This is a Heading",
);
-
- // validate after reload
- cy.reload(true);
- // eslint-disable-next-line cypress/no-unnecessary-waiting
- cy.wait(2000);
- cy.validateHTMLText(
- formWidgetsPage.richTextEditorWidget,
- "h1",
- "This is a Heading",
- );
-
cy.PublishtheApp();
cy.validateHTMLText(
publishPage.richTextEditorWidget,
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GlobalSearch/GlobalSearch_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GlobalSearch/GlobalSearch_spec.js
index 3f60f9e819..19986c87ad 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GlobalSearch/GlobalSearch_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GlobalSearch/GlobalSearch_spec.js
@@ -42,8 +42,8 @@ describe("GlobalSearch", function() {
.its("store")
.invoke("getState")
.then((state) => {
- const { selectedWidget } = state.ui.widgetDragResize;
- expect(selectedWidget).to.be.equal(table.widgetId);
+ const { lastSelectedWidget } = state.ui.widgetDragResize;
+ expect(lastSelectedWidget).to.be.equal(table.widgetId);
});
});
@@ -119,5 +119,6 @@ describe("GlobalSearch", function() {
expect(loc.pathname).includes(expectedPage.pageId);
});
});
+ cy.NavigateToHome();
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_new_scenario_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_new_scenario_spec.js
new file mode 100644
index 0000000000..4dddb88105
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_new_scenario_spec.js
@@ -0,0 +1,40 @@
+const commonlocators = require("../../../../locators/commonlocators.json");
+const Layoutpage = require("../../../../locators/Layout.json");
+const widgetsPage = require("../../../../locators/Widgets.json");
+const publish = require("../../../../locators/publishWidgetspage.json");
+const dsl = require("../../../../fixtures/tabsWithWidgetDsl.json");
+const pages = require("../../../../locators/Pages.json");
+const modalWidgetPage = require("../../../../locators/ModalWidget.json");
+
+describe("Tab widget test", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+ it("Tab Widget Functionality Test with Modal on change of selected tab", function() {
+ cy.openPropertyPane("tabswidget");
+ cy.widgetText("tab", Layoutpage.tabWidget, Layoutpage.tabInput);
+ cy.AddActionWithModal();
+ cy.get(".t--widget-buttonwidget:contains('Confirm')").click({
+ force: true,
+ });
+ });
+
+ it("Publih the app and validate the widgets displayed under each tab", function() {
+ cy.PublishtheApp();
+ cy.get(publish.buttonWidget).should("be.visible");
+ cy.get(publish.textWidget).should("be.visible");
+ cy.get(publish.datePickerNew).should("be.visible");
+ cy.get(publish.tab)
+ .contains("Tab 2")
+ .click({ force: true });
+ cy.get(publish.checkboxWidget).should("be.visible");
+ cy.get(publish.radioWidget).should("be.visible");
+ cy.get(publish.buttonWidget)
+ .contains("Confirm")
+ .click({ force: true });
+ });
+});
+
+afterEach(() => {
+ // put your clean up code if any
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_spec.js
index c777b51360..6261817622 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_spec.js
@@ -60,9 +60,9 @@ describe("Tab widget test", function() {
.contains(this.data.tabName)
.click({ force: true })
.should("be.selected");
+ cy.get(publish.backToEditor).click();
});
it("Tab Widget Functionality To Unchecked Visible Widget", function() {
- cy.get(publish.backToEditor).click();
cy.openPropertyPane("tabswidget");
cy.togglebarDisable(commonlocators.visibleCheckbox);
cy.PublishtheApp();
@@ -74,8 +74,71 @@ describe("Tab widget test", function() {
cy.togglebar(commonlocators.visibleCheckbox);
cy.PublishtheApp();
cy.get(publish.tabWidget).should("be.visible");
+ cy.get(publish.backToEditor).click();
});
+
+ it("Tab Widget Functionality To Check tab invisiblity", function() {
+ cy.openPropertyPane("tabswidget");
+ cy.get(Layoutpage.tabEdit)
+ .eq(1)
+ .click({ force: true });
+ cy.get(Layoutpage.tabVisibility)
+ .first()
+ .click({ force: true });
+ cy.get(Layoutpage.tabWidget)
+ .contains("Tab 1")
+ .should("not.exist");
+ cy.PublishtheApp();
+ cy.get(publish.tabWidget)
+ .contains("Tab 1")
+ .should("not.exist");
+ cy.get(publish.backToEditor).click();
+ });
+
+ it("Tab Widget Functionality To Check tab visibility", function() {
+ cy.openPropertyPane("tabswidget");
+ cy.get(Layoutpage.tabEdit)
+ .eq(1)
+ .click({ force: true });
+ cy.get(Layoutpage.tabVisibility)
+ .first()
+ .click({ force: true });
+ cy.get(Layoutpage.tabWidget)
+ .contains("Tab 1")
+ .should("be.visible");
+ cy.PublishtheApp();
+ cy.get(publish.tabWidget)
+ .contains("Tab 1")
+ .should("be.visible");
+ cy.get(publish.backToEditor).click();
+ });
+ /* Test to be revisted as the undo action is inconsistent in automation
+ it("Tab Widget Functionality To Check undo action after delete", function() {
+ cy.openPropertyPane("tabswidget");
+ cy.get(Layoutpage.tabDelete)
+ .eq(1)
+ .click({ force: true });
+ cy.wait(2000);
+ cy.wait("@updateLayout");
+ cy.get(publish.tabWidget)
+ .contains("Tab 1")
+ .should("not.exist");
+ cy.get(".undo-section:contains('UNDO')")
+ .should("be.visible")
+ .click({ force: true });
+ cy.wait(2000);
+ cy.wait("@updateLayout");
+ cy.get(Layoutpage.tabWidget)
+ .contains("Tab 1")
+ .should("be.visible");
+ cy.PublishtheApp();
+ cy.get(publish.tabWidget)
+ .contains("Tab 1")
+ .should("be.visible");
+ });
+ */
});
+
afterEach(() => {
// put your clean up code if any
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/Onboarding_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/Onboarding_spec.js
index d8712160d2..8b4b21a142 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/Onboarding_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/Onboarding_spec.js
@@ -2,6 +2,9 @@
// const explorer = require("../../../../locators/explorerlocators.json");
const homePage = require("../../../../locators/HomePage.json");
const commonlocators = require("../../../../locators/commonlocators.json");
+const pages = require("../../../../locators/Pages.json");
+const datasourceEditor = require("../../../../locators/DatasourcesEditor.json");
+const datasource = require("../../../../locators/DatasourcesEditor.json");
describe("Onboarding", function() {
it("Onboarding flow", function() {
@@ -50,7 +53,7 @@ describe("Onboarding", function() {
// Add widget
cy.get(".t--add-widget").click();
- cy.dragAndDropToCanvas("tablewidget", { x: 30, y: -30 });
+ cy.dragAndDropToCanvas("tablewidget", { x: 360, y: 40 });
// wait for animation duration
// eslint-disable-next-line cypress/no-unnecessary-waiting
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/LeaveOrganizationTest_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/LeaveOrganizationTest_spec.js
new file mode 100644
index 0000000000..d720788c33
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/LeaveOrganizationTest_spec.js
@@ -0,0 +1,62 @@
+///
+
+const homePage = require("../../../../locators/HomePage.json");
+
+describe("Leave organization test spec", function() {
+ let newOrgId;
+ let newOrganizationName;
+
+ it("leave organization menu is visible validation", function() {
+ cy.visit("/applications");
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ newOrgId = interception.response.body.data.name;
+ cy.visit("/applications");
+ cy.openOrgOptionsPopup(newOrganizationName);
+ cy.contains("Leave Organization");
+ });
+ });
+
+ it("Only admin user can not leave organization validation", function() {
+ cy.visit("/applications");
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ newOrgId = interception.response.body.data.name;
+ cy.visit("/applications");
+ cy.openOrgOptionsPopup(newOrganizationName);
+ cy.contains("Leave Organization").click();
+ cy.contains("Are you sure").click();
+ cy.wait("@leaveOrgApiCall").then((httpResponse) => {
+ expect(httpResponse.status).to.equal(400);
+ });
+ cy.contains(newOrganizationName);
+ });
+ });
+
+ it("Non admin users can only access leave organization popup menu validation", function() {
+ cy.visit("/applications");
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ newOrgId = interception.response.body.data.name;
+ cy.visit("/applications");
+ cy.inviteUserForOrg(
+ newOrganizationName,
+ Cypress.env("TESTUSERNAME1"),
+ homePage.viewerRole,
+ );
+ cy.LogOut();
+
+ cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1"));
+ cy.visit("/applications");
+ cy.openOrgOptionsPopup(newOrganizationName);
+ cy.get(homePage.orgNamePopoverContent)
+ .find("a")
+ .should("have.length", 1)
+ .first()
+ .contains("Leave Organization");
+ });
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgImportApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgImportApplication_spec.js
new file mode 100644
index 0000000000..620b05e0aa
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgImportApplication_spec.js
@@ -0,0 +1,39 @@
+const homePage = require("../../../../locators/HomePage.json");
+
+describe("Organization Import Application", function() {
+ let orgid;
+ let newOrganizationName;
+ const fixtureDummyAppPath = "application-file.json";
+ it("Can Import Application", function() {
+ cy.NavigateToHome();
+ cy.generateUUID().then((uid) => {
+ orgid = uid;
+ localStorage.setItem("OrgName", orgid);
+ cy.createOrg();
+ cy.wait("@createOrg").then((createOrgInterception) => {
+ newOrganizationName = createOrgInterception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ cy.get(homePage.orgImportAppOption).click({ force: true });
+
+ cy.get(homePage.orgImportAppModal).should("be.visible");
+ cy.xpath(homePage.uploadLogo).attachFile(fixtureDummyAppPath);
+
+ cy.get(homePage.orgImportAppButton).click({ force: true });
+ cy.wait("@importNewApplication").then((interception) => {
+ let appId = interception.response.body.data.id;
+ let defaultPage = interception.response.body.data.pages.find(
+ (eachPage) => !!eachPage.isDefault,
+ );
+ cy.get(homePage.toastMessage).should(
+ "contain",
+ "Application imported successfully",
+ );
+ cy.url().should(
+ "include",
+ `/applications/${appId}/pages/${defaultPage.id}/edit`,
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgSettings_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgSettings_validation_spec.js
index e606c8376c..5c5ee05712 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgSettings_validation_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgSettings_validation_spec.js
@@ -4,6 +4,7 @@ const homePage = require("../../../../locators/HomePage.json");
describe("Org Settings validation spec", function() {
let orgid;
+ let newOrganizationName;
it("create org with long name should use ellipsis validation", function() {
cy.NavigateToHome();
@@ -13,15 +14,21 @@ describe("Org Settings validation spec", function() {
uid;
localStorage.setItem("OrgName", orgid);
// create org with long name
- cy.createOrg(orgid);
- cy.navigateToOrgSettings(orgid);
- // checking parent's( ) since the child() inherits css from it
- cy.get(homePage.orgHeaderName)
- .parent()
- .then((elem) => {
- assert.isBelow(elem[0].offsetWidth, elem[0].scrollWidth);
- })
- .should("have.css", "text-overflow", "ellipsis");
+ cy.createOrg();
+ // stub the response and
+ // find app name
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ cy.navigateToOrgSettings(orgid);
+ // checking parent's( ) since the child() inherits css from it
+ cy.get(homePage.orgHeaderName)
+ .parent()
+ .then((elem) => {
+ assert.isBelow(elem[0].offsetWidth, elem[0].scrollWidth);
+ })
+ .should("have.css", "text-overflow", "ellipsis");
+ });
});
cy.LogOut();
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgUserIconTest_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgUserIconTest_spec.js
index e4b0fba386..35ee38be2c 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgUserIconTest_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgUserIconTest_spec.js
@@ -4,21 +4,26 @@ const homePage = require("../../../../locators/HomePage.json");
describe("Check if org has user icons on homepage", function() {
let orgid;
+ let newOrganizationName;
it("create org and check if user icons exists in that org on homepage", function() {
cy.NavigateToHome();
cy.generateUUID().then((uid) => {
orgid = uid;
localStorage.setItem("OrgName", orgid);
- cy.createOrg(orgid);
- cy.get(homePage.orgList.concat(orgid).concat(")"))
- .scrollIntoView()
- .should("be.visible")
- .within(() => {
- cy.get(homePage.shareUserIcons)
- .first()
- .should("be.visible");
- });
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ cy.get(homePage.orgList.concat(orgid).concat(")"))
+ .scrollIntoView()
+ .should("be.visible")
+ .within(() => {
+ cy.get(homePage.shareUserIcons)
+ .first()
+ .should("be.visible");
+ });
+ });
});
cy.LogOut();
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/Orgname_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/Orgname_validation_spec.js
index cefe2baab0..8ab08a7c6a 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/Orgname_validation_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/Orgname_validation_spec.js
@@ -3,28 +3,40 @@
const homePage = require("../../../../locators/HomePage.json");
describe("Org name validation spec", function() {
+ let orgid;
+ let newOrganizationName;
it("create org with leading space validation", function() {
cy.NavigateToHome();
- cy.get(homePage.createOrg)
- .should("be.visible")
- .first()
- .click({ force: true });
- cy.xpath(homePage.inputOrgName)
- .should("be.visible")
- .type(" ");
- cy.get(homePage.submit).should("be.disabled");
- cy.xpath(homePage.cancelBtn).click();
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.NavigateToHome();
+ cy.contains(newOrganizationName).click({ force: true });
+ cy.get(homePage.renameOrgInput)
+ .should("be.visible")
+ .type(" ");
+ cy.get(".error-message").should("be.visible");
+ });
+ });
+ it("creates org and checks that orgname is editable", function() {
+ cy.createOrg();
+ cy.generateUUID().then((uid) => {
+ orgid =
+ "kadjhfkjadsjkfakjdscajdsnckjadsnckadsjcnanakdjsnckjdscnakjdscnnadjkncakjdsnckjadsnckajsdfkjadshfkjsdhfjkasdhfkjasdhfjkasdhjfasdjkfhjhdsfjhdsfjhadasdfasdfadsasdf" +
+ uid;
+ // create org with long name
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ });
+ });
});
-
it("create org with special characters validation", function() {
- cy.get(homePage.createOrg)
- .should("be.visible")
- .first()
- .click({ force: true });
- cy.xpath(homePage.inputOrgName)
- .should("be.visible")
- .type("Test & Org");
- cy.get(homePage.submit).should("be.enabled");
- cy.xpath(homePage.cancelBtn).click();
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, "Test & Org");
+ });
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Pages/Pages_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Pages/Pages_spec.js
index 95133ddc2e..fb7a3de75c 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Pages/Pages_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Pages/Pages_spec.js
@@ -23,7 +23,9 @@ describe("Pages", function() {
cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Page1)").click({
multiple: true,
});
- cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(APIs)").click({
+ cy.get(
+ ".bp3-icon-caret-right ~ .t--entity-name:contains(Datasources)",
+ ).click({
multiple: true,
});
cy.get(`.t--entity-name:contains(${apiName})`).should("have.length", 2);
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ProductUpdates/ProductUpdates_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ProductUpdates/ProductUpdates_spec.js
index db433c36c7..ef124c354f 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ProductUpdates/ProductUpdates_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ProductUpdates/ProductUpdates_spec.js
@@ -10,10 +10,10 @@ describe("Check for product updates button and modal", function() {
.its("store")
.invoke("getState")
.then((state) => {
- const { releaseItems, newReleasesCount } = state.ui.releases;
+ const { newReleasesCount, releaseItems } = state.ui.releases;
if (Array.isArray(releaseItems) && releaseItems.length > 0) {
cy.get("[data-cy=t--product-updates-btn]")
- .contains(newReleasesCount)
+ .contains("What's New?")
.click({ force: true });
//eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500); // modal transition
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneCTA_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneCTA_spec.js
new file mode 100644
index 0000000000..6bdd252016
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneCTA_spec.js
@@ -0,0 +1,21 @@
+const dsl = require("../../../../fixtures/TextTabledsl.json");
+
+describe("Property pane CTA to add an action", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+
+ it("Check if CTA is shown when there is no action", function() {
+ cy.openPropertyPane("tablewidget");
+
+ cy.get(".t--propertypane-connect-cta").should("be.visible");
+ });
+
+ it("Check if CTA does not exist when there is an action", function() {
+ cy.NavigateToAPI_Panel();
+
+ cy.CreateAPI("FirstAPI");
+ cy.SearchEntityandOpen("Table1");
+ cy.get(".t--propertypane-connect-cta").should("not.exist");
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/QueryPaneTests/Datasourcedocs_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/QueryPaneTests/Datasourcedocs_spec.js
index 85badafaa2..8834919119 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/QueryPaneTests/Datasourcedocs_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/QueryPaneTests/Datasourcedocs_spec.js
@@ -1,6 +1,7 @@
const datasource = require("../../../../locators/DatasourcesEditor.json");
const queryLocators = require("../../../../locators/QueryEditor.json");
const commonlocators = require("../../../../locators/commonlocators.json");
+const queryEditor = require("../../../../locators/QueryEditor.json");
describe("Check datasource doc links", function() {
let postgresDatasourceName;
@@ -44,7 +45,8 @@ describe("Check datasource doc links", function() {
});
it("Delete the query and datasources", function() {
- cy.get(queryLocators.deleteQuery).click();
+ cy.get(queryEditor.queryMoreAction).click();
+ cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/QueryPaneTests/SwitchDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/QueryPaneTests/SwitchDatasource_spec.js
index 7e4e07ef32..cdb81f2401 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/QueryPaneTests/SwitchDatasource_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/QueryPaneTests/SwitchDatasource_spec.js
@@ -1,5 +1,6 @@
const datasource = require("../../../../locators/DatasourcesEditor.json");
const queryLocators = require("../../../../locators/QueryEditor.json");
+const queryEditor = require("../../../../locators/QueryEditor.json");
describe("Switch datasource", function() {
let postgresDatasourceName;
@@ -92,7 +93,8 @@ describe("Switch datasource", function() {
});
it("Delete the query and datasources", function() {
- cy.get(queryLocators.deleteQuery).click();
+ cy.get(queryEditor.queryMoreAction).click();
+ cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/WidgetSelection/WidgetSelection_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/WidgetSelection/WidgetSelection_spec.js
new file mode 100644
index 0000000000..54ff3d797d
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/WidgetSelection/WidgetSelection_spec.js
@@ -0,0 +1,29 @@
+const dsl = require("../../../../fixtures/widgetSelection.json");
+
+describe("Widget Selection", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+
+ it("Multi Select widgets using cmd + click", function() {
+ cy.get(`#${dsl.dsl.children[0].widgetId}`).click({
+ ctrlKey: true,
+ });
+ cy.get(`#${dsl.dsl.children[1].widgetId}`).click({
+ ctrlKey: true,
+ });
+ cy.get(`.t--widget-propertypane-toggle`).should("have.length", 2);
+ cy.get(`#${dsl.dsl.children[2].widgetId}`).click({
+ ctrlKey: true,
+ });
+ cy.get(`.t--widget-propertypane-toggle`).should("have.length", 3);
+ cy.get(`#${dsl.dsl.children[0].widgetId}`).click({
+ ctrlKey: true,
+ });
+ cy.get(`.t--widget-propertypane-toggle`)
+ .not(`[style *= "background: rgb(255, 224, 210);"]`) // Excluding focused widgets.
+ .should("have.length", 2);
+
+ cy.get(`.t--multi-selection-box`).should("have.length", 1);
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/List_spec.js b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/List_spec.js
index 8221072b87..063471e6e5 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/List_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/List_spec.js
@@ -4,7 +4,7 @@ const dsl = require("../../../fixtures/listdsl.json");
const publishPage = require("../../../locators/publishWidgetspage.json");
describe("Container Widget Functionality", function() {
- const items = JSON.parse(dsl.dsl.children[0].items);
+ const items = JSON.parse(dsl.dsl.children[0].listData);
before(() => {
cy.addDsl(dsl);
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiFlow/CurlImportFlow_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiFlow/CurlImportFlow_spec.js
index 9ea0d1bad9..d33f1f2480 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiFlow/CurlImportFlow_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiFlow/CurlImportFlow_spec.js
@@ -1,10 +1,14 @@
const ApiEditor = require("../../../../locators/ApiEditor.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
+const pages = require("../../../../locators/Pages.json");
describe("Test curl import flow", function() {
it("Test curl import flow Run and Delete", function() {
localStorage.setItem("ApiPaneV2", "ApiPaneV2");
cy.NavigateToApiEditor();
+ cy.get(pages.integrationCreateNew)
+ .should("be.visible")
+ .click({ force: true });
cy.get(ApiEditor.curlImage).click({ force: true });
cy.get("textarea").type("curl -X GET https://mock-api.appsmith.com/users");
cy.importCurl();
@@ -27,7 +31,5 @@ describe("Test curl import flow", function() {
cy.get("@deleteAction").then((response) => {
cy.expect(response.response.body.responseMeta.success).to.eq(true);
});
- cy.get(ApiEditor.ApiHomePage).should("be.visible");
- cy.get(ApiEditor.formActionButtons).should("not.exist");
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_All_Verb_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_All_Verb_spec.js
index 5411c7745f..cb090494a1 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_All_Verb_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_All_Verb_spec.js
@@ -14,11 +14,8 @@ describe("API Panel Test Functionality", function() {
});
it("PUT Action test API fetaure", function() {
- cy.log("Login Successful");
cy.NavigateToAPI_Panel();
- cy.log("Navigation to API Panel screen successful");
cy.CreateAPI("FirstAPI");
- cy.log("Creation of FirstAPI Action successful");
cy.SelectAction(testdata.putAction);
cy.EnterSourceDetailsWithbody(
testdata.baseUrl,
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_CurlPOSTImport_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_CurlPOSTImport_spec.js
index b23b1b8fdb..0437fef97e 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_CurlPOSTImport_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_CurlPOSTImport_spec.js
@@ -1,10 +1,14 @@
const ApiEditor = require("../../../../locators/ApiEditor.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
+const pages = require("../../../../locators/Pages.json");
describe("Test curl import flow", function() {
- it("Test curl import flow for POST action", function() {
+ it("Test curl import flow for POST action with JSON body", function() {
localStorage.setItem("ApiPaneV2", "ApiPaneV2");
cy.NavigateToApiEditor();
+ cy.get(pages.integrationCreateNew)
+ .should("be.visible")
+ .click({ force: true });
cy.get(ApiEditor.curlImage).click({ force: true });
cy.get("textarea").type(
'curl -d \'{"name":"morpheus","job":"leader"}\' -H Content-Type:application/json -X POST https://mock-api.appsmith.com/echo/post',
@@ -26,4 +30,36 @@ describe("Test curl import flow", function() {
});
});
});
+
+ it("Test curl import flow for POST action with multipart form data", function() {
+ localStorage.setItem("ApiPaneV2", "ApiPaneV2");
+ cy.NavigateToApiEditor();
+ cy.get(pages.integrationCreateNew)
+ .should("be.visible")
+ .click({ force: true });
+ cy.get(ApiEditor.curlImage).click({ force: true });
+ cy.get("textarea").type(
+ "curl --request POST http://httpbin.org/post -F 'randomKey=randomValue' --form 'randomKey2=\"randomValue2\"'",
+ {
+ force: true,
+ parseSpecialCharSequences: false,
+ },
+ );
+ cy.importCurl();
+ cy.RunAPI();
+ cy.ResponseStatusCheck("200 OK");
+ cy.log("Ran the API successfully");
+ cy.get("@postExecute").then((response) => {
+ cy.log(response.response.body);
+ cy.expect(response.response.body.responseMeta.success).to.eq(true);
+ // Asserting if the form key value are returned in the response
+ cy.expect(response.response.body.data.body.form.randomKey).to.eq(
+ "randomValue",
+ );
+ // Asserting the content type header set in curl import is multipart/form-data
+ cy.expect(
+ response.response.body.data.body.headers["Content-Type"],
+ ).contains("multipart/form-data;boundary");
+ });
+ });
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js
new file mode 100644
index 0000000000..aac6a3abef
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js
@@ -0,0 +1,26 @@
+/*
+ * Commenting this spec as its flaky
+ *
+ * */
+// const testdata = require("../../../../fixtures/testdata.json");
+// describe("Create a rest datasource", function() {
+// beforeEach(() => {
+// cy.startRoutesForDatasource();
+// });
+
+// it("Create a rest datasource", function() {
+// cy.NavigateToAPI_Panel();
+// cy.CreateAPI("Testapi");
+// cy.enterDatasource(testdata.basicURl);
+// cy.get(".t--store-as-datasource").click();
+// cy.addBasicProfileDetails("test", "test@123");
+// cy.saveDatasource();
+// cy.contains(".datasource-highlight", "envyenksqii9nf3.m.pipedream.net");
+// cy.SaveAndRunAPI();
+// cy.wait(2000);
+// var encodedStringBtoA = btoa("test:test@123");
+// cy.log(encodedStringBtoA);
+// cy.ResponseStatusCheck(testdata.successStatusCode);
+// cy.ResponseTextCheck("Basic ".concat(encodedStringBtoA));
+// });
+// });
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MongoDataSourceStub_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MongoDataSourceStub_spec.js
index 2ce7ec8e3b..1b63e64319 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MongoDataSourceStub_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MongoDataSourceStub_spec.js
@@ -23,6 +23,7 @@ describe("Create, test, save then delete a mongo datasource", function() {
"response.body.responseMeta.status",
200,
);
+ /*
cy.NavigateToQueryEditor();
cy.get("@createDatasource").then((httpResponse) => {
@@ -39,7 +40,8 @@ describe("Create, test, save then delete a mongo datasource", function() {
"response.body.responseMeta.status",
200,
);
-
+ cy.xpath('//div[contains(text(),"Form Input")]').click({ force: true });
+ cy.xpath('//div[contains(text(),"Raw Input")]').click({ force: true });
cy.get(queryLocators.templateMenu).click();
cy.get(".CodeMirror textarea")
.first()
@@ -94,4 +96,5 @@ describe("Create, test, save then delete a mongo datasource", function() {
);
});
*/
+ });
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/PostgresDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/PostgresDatasource_spec.js
index dabd0e202a..9c0da595b9 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/PostgresDatasource_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/PostgresDatasource_spec.js
@@ -1,5 +1,6 @@
const datasource = require("../../../../locators/DatasourcesEditor.json");
const queryEditor = require("../../../../locators/QueryEditor.json");
+const datasourceEditor = require("../../../../locators/DatasourcesEditor.json");
let datasourceName;
@@ -21,14 +22,18 @@ describe("Postgres datasource test cases", function() {
it("Create a new query from the datasource editor", function() {
cy.saveDatasource();
- cy.get(datasource.createQuerty).click();
+ // cy.get(datasource.createQuerty).click();
+ cy.get(`${datasourceEditor.datasourceCard} ${datasource.createQuerty}`)
+ .last()
+ .click();
cy.wait("@createNewApi").should(
"have.nested.property",
"response.body.responseMeta.status",
201,
);
- cy.get(queryEditor.deleteQuery).click();
+ cy.get(queryEditor.queryMoreAction).click();
+ cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_API_Pane_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_API_Pane_spec.js
index c14b38effc..0e60dda003 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_API_Pane_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_API_Pane_spec.js
@@ -27,10 +27,11 @@ describe("Entity explorer API pane related testcases", function() {
.find(explorer.collapse)
.click();
cy.get(apiwidget.propertyList).then(function($lis) {
- expect($lis).to.have.length(3);
+ expect($lis).to.have.length(4);
expect($lis.eq(0)).to.contain("{{FirstAPI.isLoading}}");
expect($lis.eq(1)).to.contain("{{FirstAPI.data}}");
- expect($lis.eq(2)).to.contain("{{FirstAPI.run()}}");
+ expect($lis.eq(2)).to.contain("{{FirstAPI.responseMeta}}");
+ expect($lis.eq(3)).to.contain("{{FirstAPI.run()}}");
});
cy.get(apiwidget.actionlist)
.contains(testdata.Get)
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js
index 952c0cdaaf..82a484dede 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js
@@ -54,10 +54,11 @@ describe("Entity explorer tests related to copy query", function() {
.find(explorer.collapse)
.click();
cy.get(apiwidget.propertyList).then(function($lis) {
- expect($lis).to.have.length(3);
+ expect($lis).to.have.length(4);
expect($lis.eq(0)).to.contain("{{Query1.isLoading}}");
expect($lis.eq(1)).to.contain("{{Query1.data}}");
- expect($lis.eq(2)).to.contain("{{Query1.run()}}");
+ expect($lis.eq(2)).to.contain("{{Query1.responseMeta}}");
+ expect($lis.eq(3)).to.contain("{{Query1.run()}}");
});
});
});
@@ -71,21 +72,22 @@ describe("Entity explorer tests related to copy query", function() {
.invoke("show")
.click({ force: true });
cy.copyEntityToPage(pageid);
- cy.SearchEntityandOpen("Query1Copy");
+ cy.SearchEntityandOpen("Query1");
cy.runQuery();
- cy.get(`.t--entity.action:contains(Query1Copy)`)
+ cy.get(`.t--entity.action:contains(Query1)`).should("have.length", 2);
+ cy.get(`.t--entity.action:contains(Query1)`)
.find(explorer.collapse)
- .click();
+ .click({ multiple: true });
cy.get(apiwidget.propertyList).then(function($lis) {
- expect($lis).to.have.length(3);
- expect($lis.eq(0)).to.contain("{{Query1Copy.isLoading}}");
- expect($lis.eq(1)).to.contain("{{Query1Copy.data}}");
- expect($lis.eq(2)).to.contain("{{Query1Copy.run()}}");
+ expect($lis.eq(0)).to.contain("{{Query1.isLoading}}");
+ expect($lis.eq(1)).to.contain("{{Query1.data}}");
+ expect($lis.eq(2)).to.contain("{{Query1.responseMeta}}");
+ expect($lis.eq(3)).to.contain("{{Query1.run()}}");
});
});
it("Delete query and rename datasource in explorer", function() {
- cy.get(commonlocators.entityExplorersearch).clear();
+ cy.get(commonlocators.entityExplorersearch).clear({ force: true });
cy.NavigateToDatasourceEditor();
cy.GlobalSearchEntity(`${datasourceName}`);
cy.get(`.t--entity-name:contains(${datasourceName})`)
@@ -98,7 +100,6 @@ describe("Entity explorer tests related to copy query", function() {
cy.log("sliced id :" + updatedName);
cy.EditEntityNameByDoubleClick(datasourceName, updatedName);
cy.SearchEntityandOpen(updatedName);
- cy.testSaveDatasource();
cy.hoverAndClick();
cy.get(apiwidget.delete).click({ force: true });
//This is check to make sure if a datasource is active 409
@@ -109,7 +110,7 @@ describe("Entity explorer tests related to copy query", function() {
);
});
- cy.SearchEntityandOpen("Query1Copy");
+ cy.SearchEntityandOpen("Query1");
cy.deleteQuery();
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js
index ff72c2c1c5..ed2cfb49e6 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js
@@ -63,7 +63,8 @@ describe("Entity explorer datasource structure", function() {
201,
);
- cy.get(queryEditor.deleteQuery).click();
+ cy.get(queryEditor.queryMoreAction).click();
+ cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
@@ -72,14 +73,15 @@ describe("Entity explorer datasource structure", function() {
cy.GlobalSearchEntity("MyQuery");
cy.get(`.t--entity-name:contains(MyQuery)`).click();
- cy.get(queryEditor.deleteQuery).click();
+ cy.get(queryEditor.queryMoreAction).click();
+ cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
- cy.get(commonlocators.entityExplorersearch).clear();
+ cy.get(commonlocators.entityExplorersearch).clear({ force: true });
cy.deleteDatasource(datasourceName);
});
@@ -102,7 +104,7 @@ describe("Entity explorer datasource structure", function() {
200,
);
- cy.get(commonlocators.entityExplorersearch).clear();
+ cy.get(commonlocators.entityExplorersearch).clear({ force: true });
const tableName = Math.random()
.toString(36)
@@ -151,14 +153,15 @@ describe("Entity explorer datasource structure", function() {
200,
);
- cy.get(queryEditor.deleteQuery).click();
+ cy.get(queryEditor.queryMoreAction).click();
+ cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
- cy.get(commonlocators.entityExplorersearch).clear();
+ cy.get(commonlocators.entityExplorersearch).clear({ force: true });
cy.deleteDatasource(datasourceName);
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateAppWithSameNameInOrg_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateAppWithSameNameInOrg_spec.js
index c6f78dbbad..32a444c4f0 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateAppWithSameNameInOrg_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateAppWithSameNameInOrg_spec.js
@@ -3,6 +3,7 @@
describe("Create org and a new app / delete and recreate app", function() {
let orgid;
let appid;
+ let newOrganizationName;
it("create app within an org and delete and re-create another app with same name", function() {
cy.NavigateToHome();
@@ -10,7 +11,11 @@ describe("Create org and a new app / delete and recreate app", function() {
orgid = uid;
appid = uid;
localStorage.setItem("OrgName", orgid);
- cy.createOrg(orgid);
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ });
cy.CreateAppForOrg(orgid, appid);
cy.DeleteAppByApi();
cy.NavigateToHome();
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateOrgTests_spec.js
index 4740cd7378..d50dc6924b 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateOrgTests_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateOrgTests_spec.js
@@ -5,6 +5,7 @@ const homePage = require("../../../../locators/HomePage.json");
describe("Create new org and share with a user", function() {
let orgid;
let appid;
+ let newOrganizationName;
it("create org and then share with a user from UI", function() {
cy.NavigateToHome();
@@ -12,7 +13,11 @@ describe("Create new org and share with a user", function() {
orgid = uid;
appid = uid;
localStorage.setItem("OrgName", orgid);
- cy.createOrg(orgid);
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ });
cy.CheckShareIcon(orgid, 1);
cy.inviteUserForOrg(
orgid,
@@ -51,13 +56,7 @@ describe("Create new org and share with a user", function() {
});
it("login as Org owner and update the invited user role to developer", function() {
- cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
- cy.visit("/applications");
- cy.wait("@applications").should(
- "have.nested.property",
- "response.body.responseMeta.status",
- 200,
- );
+ cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
cy.get(homePage.searchInput).type(appid);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
@@ -90,13 +89,7 @@ describe("Create new org and share with a user", function() {
});
it("login as Org owner and update the invited user role to administrator", function() {
- cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
- cy.visit("/applications");
- cy.wait("@applications").should(
- "have.nested.property",
- "response.body.responseMeta.status",
- 200,
- );
+ cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
cy.get(homePage.searchInput).type(appid);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
@@ -124,13 +117,7 @@ describe("Create new org and share with a user", function() {
});
it("login as Org owner and delete App ", function() {
- cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
- cy.visit("/applications");
- cy.wait("@applications").should(
- "have.nested.property",
- "response.body.responseMeta.status",
- 200,
- );
+ cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD"));
cy.get(homePage.searchInput).type(appid);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateSameAppInDiffOrg_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateSameAppInDiffOrg_spec.js
index 3de68cf260..318d304bc3 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateSameAppInDiffOrg_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateSameAppInDiffOrg_spec.js
@@ -3,6 +3,7 @@
describe("Create app same name in different org", function() {
let orgid;
let appid;
+ let newOrganizationName;
it("create app within a new org", function() {
cy.NavigateToHome();
@@ -10,10 +11,16 @@ describe("Create app same name in different org", function() {
orgid = uid;
appid = uid;
localStorage.setItem("OrgName", orgid);
- cy.createOrg(orgid);
- cy.CreateAppForOrg(orgid, appid);
- cy.NavigateToHome();
- cy.LogOut();
+ cy.createOrg();
+ // stub the response and
+ // find app name
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ cy.CreateAppForOrg(orgid, appid);
+ cy.NavigateToHome();
+ cy.LogOut();
+ });
});
});
@@ -25,9 +32,13 @@ describe("Create app same name in different org", function() {
"response.body.responseMeta.status",
200,
);
- cy.reload();
const newOrgName = orgid + "1";
- cy.createOrg(newOrgName);
- cy.CreateAppForOrg(newOrgName, appid);
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ console.log("createOrganization response: ", interception);
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, newOrgName);
+ cy.CreateAppForOrg(newOrgName, appid);
+ });
});
});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/ShareAppTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/ShareAppTests_spec.js
index 478a3b5522..d791df203e 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/ShareAppTests_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/ShareAppTests_spec.js
@@ -7,6 +7,7 @@ describe("Create new org and share with a user", function() {
let orgid;
let appid;
let currentUrl;
+ let newOrganizationName;
it("create org and then share with a user from Application share option within application", function() {
cy.NavigateToHome();
@@ -14,7 +15,11 @@ describe("Create new org and share with a user", function() {
orgid = uid;
appid = uid;
localStorage.setItem("OrgName", orgid);
- cy.createOrg(orgid);
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ });
cy.CreateAppForOrg(orgid, appid);
cy.wait("@getPagesForCreateApp").should(
"have.nested.property",
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/UpdateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/UpdateOrgTests_spec.js
index fc37647369..f8a63868d0 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/UpdateOrgTests_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/UpdateOrgTests_spec.js
@@ -2,26 +2,24 @@ const homePage = require("../../../../locators/HomePage.json");
describe("Update Organization", function() {
let orgid;
+ let newOrganizationName;
it("Open the org general settings and update org name. The update should reflect in the org. It should also reflect in the org names on the left side and the org dropdown. ", function() {
cy.NavigateToHome();
cy.generateUUID().then((uid) => {
orgid = uid;
localStorage.setItem("OrgName", orgid);
- cy.createOrg(orgid);
- cy.get(homePage.orgList.concat(orgid).concat(")"))
- .scrollIntoView()
- .should("be.visible")
- .within(() => {
- cy.get(".t--org-name")
- .first()
- .click();
- });
- cy.get(homePage.orgSettingOption).click();
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ cy.get(homePage.orgSettingOption).click({ force: true });
+ });
});
cy.generateUUID().then((uid) => {
orgid = uid;
localStorage.setItem("OrgName", orgid);
+ cy.get(homePage.orgNameInput).click({ force: true });
cy.get(homePage.orgNameInput).clear();
cy.get(homePage.orgNameInput).type(orgid);
// eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -37,15 +35,12 @@ describe("Update Organization", function() {
});
it("Open the org general settings and update org email. The update should reflect in the org.", function() {
- cy.get(homePage.orgList.concat(orgid).concat(")"))
- .scrollIntoView()
- .should("be.visible")
- .within(() => {
- cy.get(".t--org-name")
- .first()
- .click();
- });
- cy.get(homePage.orgSettingOption).click();
+ cy.createOrg();
+ cy.wait("@createOrg").then((interception) => {
+ newOrganizationName = interception.response.body.data.name;
+ cy.renameOrg(newOrganizationName, orgid);
+ cy.get(homePage.orgSettingOption).click({ force: true });
+ });
cy.get(homePage.orgEmailInput).clear();
cy.get(homePage.orgEmailInput).type(Cypress.env("TESTUSERNAME2"));
cy.wait("@updateOrganization").should(
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/ConfirmRunAction_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/ConfirmRunAction_spec.js
index 312c09fa0d..19977d12df 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/ConfirmRunAction_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/ConfirmRunAction_spec.js
@@ -41,7 +41,8 @@ describe("Confirm run action", function() {
200,
);
- cy.get(queryEditor.deleteQuery).click();
+ cy.get(queryEditor.queryMoreAction).click();
+ cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/MongoDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/MongoDatasource_spec.js
index d64c91b60c..b5ce52ffb5 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/MongoDatasource_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/MongoDatasource_spec.js
@@ -35,6 +35,10 @@ describe("Create a query with a mongo datasource, run, save and then delete the
200,
);
+ cy.xpath('//div[contains(text(),"Find Document(s)")]').click({
+ force: true,
+ });
+ cy.xpath('//div[contains(text(),"Raw")]').click({ force: true });
cy.get(queryLocators.templateMenu).click();
cy.get(".CodeMirror textarea")
.first()
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/PostgreDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/PostgreDatasource_spec.js
index d291516b41..2eda8700f9 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/PostgreDatasource_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/PostgreDatasource_spec.js
@@ -1,5 +1,6 @@
const queryLocators = require("../../../../locators/QueryEditor.json");
const datasource = require("../../../../locators/DatasourcesEditor.json");
+const pages = require("../../../../locators/Pages.json");
let datasourceName;
@@ -39,6 +40,9 @@ describe("Create a query with a postgres datasource, run, save and then delete t
});
it("Create, runs and delete another query", () => {
cy.NavigateToQueryEditor();
+ cy.get(pages.integrationActiveTab)
+ .should("be.visible")
+ .click({ force: true });
cy.contains(".t--datasource-name", datasourceName)
.find(queryLocators.createQuery)
.click();
@@ -53,6 +57,9 @@ describe("Create a query with a postgres datasource, run, save and then delete t
});
it("Deletes a datasource", () => {
cy.NavigateToQueryEditor();
+ cy.get(pages.integrationActiveTab)
+ .should("be.visible")
+ .click({ force: true });
cy.contains(".t--datasource-name", datasourceName)
.find(".t--edit-datasource")
.click();
diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json
index 47aa855c89..834f6726ac 100644
--- a/app/client/cypress/locators/DatasourcesEditor.json
+++ b/app/client/cypress/locators/DatasourcesEditor.json
@@ -13,6 +13,8 @@
"sectionAuthentication": "[data-cy=section-Authentication]",
"PostgresEntity": ".t--entity-name:contains(PostgreSQL)",
"createQuerty": ".t--create-query",
+ "activeDatasourceList": ".t--active-datasource-list",
+ "datasourceCard": ".t--datasource",
"editDatasource": ".t--edit-datasource",
"defaultDatabaseName": "input[name='datasourceConfiguration.connection.defaultDatabaseName']",
"selConnectionType": "[data-cy='datasourceConfiguration.connection.type']",
@@ -35,5 +37,8 @@
"grantType": "[data-cy='authentication.grantType']",
"authorizationURL":"[data-cy='authentication.authorizationUrl'] input",
"authorisecode": "//div[contains(@class,'option') and text()='Authorization Code']",
- "saveAndAuthorize": "button:contains('Save and Authorize')"
+ "saveAndAuthorize": "button:contains('Save and Authorize')",
+ "basic": "//div[contains(@class,'option') and text()='Basic']",
+ "basicUsername": "input[name='authentication.username']",
+ "basicPassword": "input[name='authentication.password']"
}
diff --git a/app/client/cypress/locators/Debugger.json b/app/client/cypress/locators/Debugger.json
new file mode 100644
index 0000000000..9e5cda0e4c
--- /dev/null
+++ b/app/client/cypress/locators/Debugger.json
@@ -0,0 +1,4 @@
+{
+ "debuggerIcon": ".t--debugger",
+ "debuggerLogState": ".t--debugger-log-state"
+}
\ No newline at end of file
diff --git a/app/client/cypress/locators/DynamicInput.json b/app/client/cypress/locators/DynamicInput.json
index 8f34ce8219..8e5b4bc686 100644
--- a/app/client/cypress/locators/DynamicInput.json
+++ b/app/client/cypress/locators/DynamicInput.json
@@ -1,5 +1,5 @@
{
- "input": ".CodeMirror textarea",
+ "input": ".CodeEditorTarget",
"hints": "ul.CodeMirror-hints",
"evaluatedValue": ".t--CodeEditor-evaluatedValue",
"bindingPrompt": ".t--no-binding-prompt"
diff --git a/app/client/cypress/locators/HomePage.json b/app/client/cypress/locators/HomePage.json
index a936620af6..d6b0808e4b 100644
--- a/app/client/cypress/locators/HomePage.json
+++ b/app/client/cypress/locators/HomePage.json
@@ -9,6 +9,7 @@
"appMoreIcon": ".bp3-popover-wrapper.more .bp3-popover-target",
"duplicateApp": "[data-cy=t--duplicate]",
"forkAppFromMenu": "[data-cy=t--fork-app]",
+ "exportAppFromMenu": "[data-cy=t--export-app]",
"forkAppOrgList": ".radio-group",
"forkAppOrgButton": "[data-cy=t--fork-app-to-org-button]",
"selectAction": "#Base",
@@ -59,7 +60,13 @@
"applicationColorSelector": ".t--color-not-selected",
"applicationBackgroundColor": ".t--application-card-background",
"orgSettingOption": "[data-cy=t--org-setting]",
+ "orgImportAppOption": "[data-cy=t--org-import-app]",
+ "orgImportAppModal": ".t--import-application-modal",
+ "orgImportAppButton": "[data-cy=t--org-import-app-button]",
+ "leaveOrgConfirmModal": ".t--member-delete-confirmation-modal",
+ "leaveOrgConfirmButton": "[data-cy=t--org-leave-button]",
"orgNameInput": "[data-cy=t--org-name-input]",
+ "renameOrgInput": "[data-cy=t--org-rename-input]",
"orgEmailInput": "[data-cy=t--org-email-input]",
"orgWebsiteInput": "[data-cy=t--org-website-input]",
"orgHeaderName": ".t--organization-header",
@@ -72,5 +79,7 @@
"generalTab": "//li//span[text()='General']",
"membersTab": "//li//span[text()='Members']",
"cancelBtn": "//span[text()='Cancel']",
- "submit": "button:contains('Submit')"
+ "submit": "button:contains('Submit')",
+ "orgNamePopover": ".t--org-name",
+ "orgNamePopoverContent": ".bp3-popover-content"
}
diff --git a/app/client/cypress/locators/Layout.json b/app/client/cypress/locators/Layout.json
index 7bd865494f..a9799b5de4 100644
--- a/app/client/cypress/locators/Layout.json
+++ b/app/client/cypress/locators/Layout.json
@@ -5,5 +5,7 @@
"tabDefault": ".t--property-control-defaulttab .CodeMirror-code",
"tabButton": ".t--property-control-tabs button",
"tabDelete": ".t--property-control-tabs .t--delete-tab-btn",
- "tabContainer": "div[type='TABS_WIDGET']"
+ "tabContainer": "div[type='TABS_WIDGET']",
+ "tabEdit": ".t--property-control-tabs .t--edit-column-btn",
+ "tabVisibility": ".t--property-control-visible .bp3-control-indicator"
}
\ No newline at end of file
diff --git a/app/client/cypress/locators/Pages.json b/app/client/cypress/locators/Pages.json
index 2044baf6a8..b68297e32d 100644
--- a/app/client/cypress/locators/Pages.json
+++ b/app/client/cypress/locators/Pages.json
@@ -9,7 +9,9 @@
"Menuaction": ".bp3-overlay-open>.bp3-transition-container",
"Delete": ":nth-child(2) > .bp3-menu-item",
"apiEditorIcon": ".t--nav-link-api-editor",
- "addEntityAPI": ".apis .t--entity-add-btn",
+ "addEntityAPI": ".datasources .t--entity-add-btn",
+ "integrationCreateNew": "[data-cy=t--tab-CREATE_NEW]",
+ "integrationActiveTab": "[data-cy=t--tab-ACTIVE]",
"entityWidget": ".t--entity-name:contains('Widgets')",
"entityTable": ".t--entity-name:contains('Table1')",
"entityText": ".t--entity-name:contains('Text1')",
diff --git a/app/client/cypress/locators/QueryEditor.json b/app/client/cypress/locators/QueryEditor.json
index 28929954ba..1d736eda63 100644
--- a/app/client/cypress/locators/QueryEditor.json
+++ b/app/client/cypress/locators/QueryEditor.json
@@ -1,11 +1,18 @@
{
+ "addNewQueryBtn": ".dbqueries .t--entity-add-btn",
"queryEditorIcon": ".t--nav-link-query-editor",
"templateMenu": ".t--template-menu",
"runQuery": ".t--run-query",
"saveQuery": ".t--save-query",
"deleteQuery": ".t--delete-query",
+ "queryMoreAction": ".t--more-action-menu",
+ "deleteUsingContext": ".t--apiFormDeleteBtn",
"createQuery": ".t--create-query",
"addQueryEntity": ".//div[contains(@class,'t--entity group queries')]//div[contains(@class,'t--entity-add-btn')]",
"addDatasource": ".t--add-datasource",
- "editDatasourceButton": ".t--edit-datasource"
-}
+ "editDatasourceButton": ".t--edit-datasource",
+ "queryNameField": ".t--action-name-edit-field input",
+ "settings": "li:contains('Settings')",
+ "query": "li:contains('Query')",
+ "switch": ".t--form-control-SWITCH input"
+}
\ No newline at end of file
diff --git a/app/client/cypress/locators/ViewWidgets.json b/app/client/cypress/locators/ViewWidgets.json
index f11d43e422..d71bd9d2cc 100644
--- a/app/client/cypress/locators/ViewWidgets.json
+++ b/app/client/cypress/locators/ViewWidgets.json
@@ -17,7 +17,7 @@
"chartInnerText": ".t--property-control-title",
"inputChartValue": ".t--property-control-chartseries .CodeMirror textarea",
"chartButton": ".t--property-control-chartseries button",
- "rectangleChart": ".t--draggable-chartwidget g rect",
+ "rectangleChart": ".t--draggable-chartwidget g rect",
"xlabel": ".t--property-control-x-axislabel .CodeMirror-code",
"ylabel": ".t--property-control-y-axislabel .CodeMirror-code",
"mapInner": ".t--draggable-mapwidget span.t--widget-name",
@@ -27,7 +27,8 @@
"createMarker": ".t--property-control-createnewmarker [type='checkbox']",
"zoomLevel": ".t--property-control-zoomlevel svg",
"sourceImage": ".t--property-control-image .CodeMirror-code",
- "defaultImage": ".t--property-control-defaultimage .CodeMirror-code",
+ "defaultImage": ".t--property-control-defaultimage .CodeMirror textarea",
"Chartlabel": ".t--draggable-chartwidget g:nth-child(5) text",
+ "PieChartLabel": ".t--draggable-chartwidget g g:nth-child(1) g text",
"pickMyLocation": ".t--draggable-mapwidget div[title='Pick My Location']"
}
diff --git a/app/client/cypress/locators/Widgets.json b/app/client/cypress/locators/Widgets.json
index 3e67eecebb..38b87d7465 100644
--- a/app/client/cypress/locators/Widgets.json
+++ b/app/client/cypress/locators/Widgets.json
@@ -1,16 +1,23 @@
{
+ "NavHomePage": "[data-icon='home']",
"containerWidget": ".t--draggable-containerwidget",
"inputWidget": ".t--draggable-inputwidget",
"togglebutton": "input[type='checkbox']",
"inputPropsDataType": ".t--property-control-datatype",
"inputdatatypeplaceholder": ".t--property-control-placeholder",
"buttonWidget": ".t--draggable-buttonwidget",
+ "buttonStyleDropdown": ".t--property-control-buttonstyle [name='downArrow']",
+ "buttonBackground": ".sc-ecQjpJ > div > .bp3-button",
+ "copyWidget": ".t--copy-widget svg",
+ "removeWidget": ".t--delete-widget svg",
+ "propertypaneText": ".t--propertypane .bp3-panel-stack-view",
"formButtonWidget": ".t--widget-formbuttonwidget",
"textWidget": ".t--draggable-textwidget",
"tableWidget": ".t--draggable-tablewidget",
"tableOnRowSelected": ".t--property-control-onrowselected",
"dropdownSelectButton": ".t--open-dropdown-Select",
"buttonOnClick": ".t--property-control-onclick .bp3-popover-target",
+ "buttonCreateApi": "a.t--create-api-btn",
"Scrollbutton": ".t--property-control-scrollcontents input",
"label": ".t--draggable-inputwidget label",
"inputval": ".t--draggable-inputwidget span.t--widget-name",
@@ -38,6 +45,8 @@
"textInputval": ".t--draggable-textwidget span.t--widget-name",
"textCenterAlign": ".t--property-control-textalign .t--icon-tab-CENTER",
"ColumnAction": ".t--property-control-rowbutton button",
+ "SearchTextChangeAction": ".t--property-control-onsearchtextchanged button",
+ "tableSearchTextChangeSelected": ".t--property-control-onsearchtextchanged",
"videoWidget": ".t--draggable-videowidget",
"autoPlay": ".t--property-control-autoplay > .bp3-control > .bp3-control-indicator",
"defaultOption": ".t--property-control-defaultoption .CodeMirror-code",
@@ -66,7 +75,7 @@
"verticalBottom": ".t--icon-tab-BOTTOM",
"textColor": ".t--property-control-textcolor input",
"backgroundcolorPicker": ".t--property-control-backgroundcolor input",
- "greenColor": "//div[@color='rgb(3, 179, 101)']",
+ "greenColor": "//div[@color='#03b365']",
"toggleJsColor": ".t--property-control-textcolor .t--js-toggle",
"backgroundColor": ".t--property-control-cellbackground input",
"toggleJsBcgColor": ".t--property-control-cellbackground .t--js-toggle",
@@ -75,6 +84,13 @@
"textSize": ".t--property-control-textsize .bp3-popover-target",
"toggleTextSize": ".t--property-control-textsize .t--js-toggle",
"toggleVerticalAlig": ".t--property-control-verticalalignment .t--js-toggle",
+ "toggleVisible": ".t--property-control-visible .t--js-toggle",
+ "inputToggleVisible": "div.t--property-control-visible div.CodeMirror-lines",
+ "toggleDisable": ".t--property-control-disabled .t--js-toggle",
+ "inputToggleDisable": "div.t--property-control-disabled div.CodeMirror-lines",
+ "toggleOnClick": ".t--property-control-onclick .t--js-toggle",
+ "toggleChartType": ".t--property-control-charttype .t--js-toggle",
+ "inputToggleOnClick": ".t--property-control-onclick div.CodeMirror-lines",
"tableBtn": ".t--draggable-tablewidget .bp3-button",
"toastAction": ".Toastify__toast-container--top-right .t--toast-action",
"toastActionText": ".Toastify__toast-container--top-right .t--toast-action span",
@@ -82,9 +98,13 @@
"selectWidget": ".t--open-dropdown-Select-Widget",
"switchWidget": ".t--widget-switchwidget",
"toastMsg": ".t--toast-action span",
+ "deleteToast": "div[class = 'undo-section'] span[type='h6']",
+ "apiCallToast": "div.t--toast-action span[type='p1']",
"datepickerInput": ".t--draggable-datepickerwidget2 input",
"selectToday": ".DayPicker-Day--today",
"switchWidgetActive": ".t--switch-widget-active",
"switchWidgetInactive": ".t--switch-widget-inactive",
- "switchWidgetLoading": ".t--switch-widget-loading"
+ "switchWidgetLoading": ".t--switch-widget-loading",
+ "chartPlotGroup": "g.raphael-group-63-plot-group",
+ "toggleEnableMultirowselection": ".t--property-control-enablemultirowselection .bp3-control-indicator"
}
diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json
index 2727f72efe..c49c628b0b 100644
--- a/app/client/cypress/locators/apiWidgetslocator.json
+++ b/app/client/cypress/locators/apiWidgetslocator.json
@@ -12,12 +12,11 @@
"path": ".t--path >div textarea",
"editResourceUrl": ".t--dataSourceField",
"autoSuggest": "//div[contains(@id,'react-select')]",
- "headerKey": "(//div[contains(@class,'t--actionConfiguration.headers[0].key.0')]//textarea)[2]",
- "headerValue": "(//div[contains(@class,'t--actionConfiguration.headers[0].value.0')]//textarea)[2]",
- "postDefaultContentTypeHeaderKey": "(//div[contains(@class,'t--actionConfiguration.headers[2].key.2')]//textarea)[2]",
- "queryKey": "(//div[contains(@class,'t--actionConfiguration.queryParameters[0].key.0')]//textarea)[2]",
- "queryValue": "(//div[contains(@class,'t--actionConfiguration.queryParameters[0].value.0')]//textarea)[2]",
- "formEncoded": "(//div[contains(@class,'t--actionConfiguration.bodyFormData[0].key.0')]//textarea)[2]",
+ "headerKey": ".t--actionConfiguration\\.headers\\[0\\]\\.key\\.0",
+ "headerValue": ".t--actionConfiguration\\.headers\\[0\\]\\.value\\.0",
+ "queryKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.0",
+ "queryValue": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.value\\.0",
+ "formEncoded": ".t--actionConfiguration\\.bodyFormData\\[0\\]\\.key\\.0",
"responseStatus": "//div[@id='root']",
"responseText": ".CodeMirror-line > [role='presentation']",
"createApiOnSideBar": "button:contains('Create new API')",
@@ -52,5 +51,8 @@
"actionlist": ".action div div",
"settings": "li:contains('Settings')",
"onPageLoad": "[data-cy=executeOnLoad]",
- "renameEntity": ".single-select >div:contains('Edit Name')"
+ "renameEntity": ".single-select >div:contains('Edit Name')",
+ "paramsTab": "//li//span[text()='Params']",
+ "paramKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.0",
+ "paramValue": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.value\\.0"
}
diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json
index 7248cf2091..968b5ad27a 100644
--- a/app/client/cypress/locators/commonlocators.json
+++ b/app/client/cypress/locators/commonlocators.json
@@ -13,9 +13,9 @@
"optionchangetextDropdown": " .t--property-control-onoptionchange .CodeMirror-code",
"optionchangetextDatePicker": " .t--property-control-ondateselected .CodeMirror-code",
"optionchangetextCheckbox": ".t--property-control-oncheckchange .CodeMirror-code",
- "optionchangetextSwitch": ".t--property-control-onchange .CodeMirror-code",
+ "optionchangetextSwitch": ".t--property-control-onchange .CodeMirror textarea",
"optionchangetextInput": ".t--property-control-ontextchanged .CodeMirror-code",
- "optionchangeRadioselect": ".t--property-control-onselectionchange .CodeMirror-code",
+ "optionchangeRadioselect": ".t--property-control-onselectionchange .CodeMirror textarea",
"optionalignment": ".t--property-control-alignment .bp3-popover-target",
"onMarkerclick": ".t--property-control-onmarkerclick .CodeMirror-code",
"success": "div[type='success'] span",
diff --git a/app/client/cypress/locators/explorerlocators.json b/app/client/cypress/locators/explorerlocators.json
index 47b927f915..42e0c2bb9b 100644
--- a/app/client/cypress/locators/explorerlocators.json
+++ b/app/client/cypress/locators/explorerlocators.json
@@ -3,7 +3,7 @@
"NoQueryMsg": "p:contains('No DB Queries yet.')",
"NoWidgetsMsg": "p:contains('No widgets yet.')",
"AddPage": ".pages .t--entity-add-btn",
- "addEntityAPI": ".apis .t--entity-add-btn",
+ "addEntityAPI": ".datasources .t--entity-add-btn",
"entityWidget": ".t--entity-name:contains('Widgets')",
"entityTable": ".t--entity-name:contains('Table1')",
"entityText": ".t--entity-name:contains('Text1')",
@@ -13,7 +13,7 @@
"editName": ".single-select >div:contains('Edit Name')",
"deletePage": ".single-select >div:contains('Delete')",
"refreshStructure": ".single-select >div:contains('Refresh')",
- "entityQuery": ".t--entity-name:contains('Queries')",
+ "entityQuery": ".t--entity-name:contains('Datasources')",
"datasourceStructure": ".t--entity.datasourceStructure",
"datasourceColumn": ".t--datasource-column",
"templateMenuIcon": ".t--template-menu-trigger",
@@ -25,6 +25,6 @@
"addWidget":".widgets .t--entity-add-btn",
"dropHere":".appsmith_widget_0",
"closeWidgets":".t--close-widgets-sidebar",
- "addDBQueryEntity": ".dbqueries .t--entity-add-btn",
+ "addDBQueryEntity": ".datasources .t--entity-add-btn",
"editEntity": ".t--entity-name input"
}
\ No newline at end of file
diff --git a/app/client/cypress/locators/publishWidgetspage.json b/app/client/cypress/locators/publishWidgetspage.json
index b4a75d5dad..8e91ef0bfe 100644
--- a/app/client/cypress/locators/publishWidgetspage.json
+++ b/app/client/cypress/locators/publishWidgetspage.json
@@ -22,7 +22,7 @@
"pickMyLocation": ".t--widget-mapwidget div[title='Pick My Location']",
"rectChart": ".t--widget-chartwidget g rect",
"chartLab": ".t--widget-chartwidget g:nth-child(5) text",
- "searchInput": "input",
+ "searchInput": ".t--search-input",
"downloadBtn": ".t--table-download-btn",
"filterBtn": ".t--table-filter-toggle-btn",
"attributeDropdown": ".t--table-filter-columns-dropdown",
@@ -31,11 +31,13 @@
"inputValue": ".t--table-filter-value-input",
"canvas": ".canvas",
"removeFilter": ".t--table-filter-remove-btn",
- "compactMode": ".t--table-compact-mode-toggle-btn",
- "compactOpt": ".t--table-compact-mode-option",
+ "rowHeight": ".t--table-compact-mode-toggle-btn",
+ "rowHeightOpt": ".t--table-compact-mode-option",
"visibilityMode": ".t--table-column-visibility-toggle-btn",
"visibilityOpt": ".option-title",
"containerWidget": ".t--widget-containerwidget",
"pageInfo": ".bp3-heading",
- "inputGrp": ".bp3-input-group input"
+ "inputGrp": ".bp3-input-group input",
+ "datePickerNew": ".t--widget-datepickerwidget2",
+ "tab": ".t--tab-Tab"
}
\ No newline at end of file
diff --git a/app/client/cypress/manual_TestSuite/GoogleSheet_API_Spec.js b/app/client/cypress/manual_TestSuite/GoogleSheet_API_Spec.js
new file mode 100644
index 0000000000..3a43e1db1d
--- /dev/null
+++ b/app/client/cypress/manual_TestSuite/GoogleSheet_API_Spec.js
@@ -0,0 +1,90 @@
+const queryLocators = require("../../../locators/QueryEditor.json");
+const queryEditor = require("../../../locators/QueryEditor.json");
+let datasourceName;
+
+describe("Test Ideas for GooglSheet API", function() {
+ it("Add a Datasource", function() {
+ //Add the datasource to Email Id
+ //Ensure different Email Id can be associate to different Email Id
+ //Ensure Datasource has two action "Read Only" and "Read"
+ //Ensure user can Save the Datasource
+ //Click on "Add API"
+ });
+
+ it("List API", function() {
+ //Select the Method "List Sheet"
+ //Ensure response : URL and Name of the sheet
+ //Ensure "Add Widget" is displayed to user
+ //Ensure click on Add widget the data gets populated on the widget
+ });
+
+ it("Fetch a Sheet", function() {
+ //Select the Method "Fetch Sheet"
+ //Ensure response :id,name,createdTime,modifiedTime,permissions
+ });
+
+ it("Create New Sheet", function() {
+ //Ensure the response is appropriate
+ //Ensure to select the method
+ //Ensure to select a new name to the spreadsheet
+ //Provide an existing name to the new sheet and observe sheet should be created
+ //Send a empty response body and ensure an empty sheet is created in the email Id selected
+ //Enter sheet name as special charaters,alphanumeric keys and number and observe
+ //Send the response body with defined value and ensure the data is added
+ });
+
+ it("Insert a sheet or Update a sheet", function() {
+ //Ensure to select the method
+ //Ensure to add the spreadsheet URL
+ //Ensure add the sheet name in which it needs to be inserted
+ //Ensure add the Table Heading Row Index
+ //Ensure to add appropriate raw object
+ //Ensure to submit one row empty each time and account the error
+ //Ensure to make changes in the raw object to throw error
+ //Add inappropriate data in each field to get appropriate error
+ //Add few inappropritae row name and value and check for partial insertion
+ //Miss an "rowIndex" field and check for and error
+ //Ensure to update the "headername"
+ });
+
+ it("Bulk Insert or Bulk Update", function() {
+ //Ensure to select the method
+ //Ensure to add the spreadsheet URL
+ //Ensure add the Table Heading Row Index
+ //Ensure to add appropriate raw object
+ //Add inappropriate data in each field to get appropriate error
+ //Add few inappropritae row name and value and check for partial insertion
+ //Try inserting without Row Index - Row index is not manadate of insert
+ //Add Different Sheet Url from the same doc and correct sheet name ensure teh correct gets updated
+ //Add doc URL different and name of sheet that doesnt exsit in the doc and ensure the error is displayed
+ });
+
+ it("Delete a Row", function() {
+ //Ensure to select the method
+ //Ensure to add the spreadsheet URL
+ //Ensure to add the sheet name
+ //Ensure add the Table Heading Row Index
+ //Ensure to add the valid index
+ //Enter index as 0 and check if the table header is not deleted
+ //Enter index as on and check the first data value gets deleted
+ });
+
+ it("Delete a Sheet", function() {
+ //Ensure to select the method
+ //Ensure to add the spreadsheet URL
+ //Ensure to add the sheet name
+ //Choose Entity has Single and try to delete the only one sheet which is available in the spreadsheet and check for the error messageEntity
+ //Choose Entity has Entire spreadsheet and try to deleted the sheet
+ });
+
+ it("Fetch Sheet Rows", function() {
+ //Ensure to select the method
+ //Ensure to add the spreadsheet URL
+ //Ensure to add the sheet name
+ //Enter the Table heading row index
+ //Choose the Query format as Query range and enter valid Range for example A2:B
+ //Choose the Query format as Query range and enter invalid Range and check for the error message
+ //Choose the Query format as Query rows and pass the valid Row limit and row offset
+ //Choose the Query format as Query rows and pass the invalid Row limit and row offset and check for the error message
+ });
+});
diff --git a/app/client/cypress/manual_TestSuite/List_Widget_Spec.js b/app/client/cypress/manual_TestSuite/List_Widget_Spec.js
new file mode 100644
index 0000000000..720bd2ab87
--- /dev/null
+++ b/app/client/cypress/manual_TestSuite/List_Widget_Spec.js
@@ -0,0 +1,93 @@
+const dsl = require("../../../fixtures/ListWidgetDsl.json");
+
+describe("List Widget test ideas ", function() {
+ it("List widget background colour and deploy ", function() {
+ // Drag and drop a List widget
+ // Open Property pane
+ // Scroll down to Styles
+ // Add background colour
+ // Add item background colour
+ // Ensure the colour are added appropriately
+ // Click on Deploy and ensure it is deployed appropriately
+ });
+
+ it("Adding large item Spacing for item card", function() {
+ // Drag and drop a List widget
+ // Open Property pane
+ // Scroll down to Styles
+ // Add large item spacing (>100)
+ // Ensure the cards get spaced appropriately
+ });
+
+ it("Binding an API data to list widget ", function() {
+ //Add an API
+ // Drag and drop a List widget
+ // Open list Property pane
+ // Bind the API to list widget
+ // Add Input widget into the list widget
+ // Bind the input widgte to the list widget
+ });
+
+ it("Copy Paste and Delete the List Widget ", function() {
+ // Drag and drop a List widget
+ // Click on the property pane
+ // Click on Copy the widget
+ // Paste(cmd+v) the list widget
+ // Click on the delete option of the Parent widget
+ });
+
+ it("Renaming the widget from Property pane and Entity explorer ", function() {
+ // Drag and drop a List widget
+ // Click on the property pane
+ // Click name of the widget
+ // Rename the widget
+ // Navigate to the Entity Explorer
+ // Click on the Widget expands
+ // Navigate to List widget (Double Click)
+ // Rename the widget
+ // Ensure the name of the widget is possible from both the place
+ });
+
+ it("Verify the Pagination functionlaity within List Widget", function() {
+ // Drag and Drop list Widget
+ // Click on page 2
+ // Ensure list widget will be redirected to page 2
+ // Click on next button
+ // Ensure the list widget will be redirected to page 3
+ // Click on Previous button
+ // Ensure the list widget will be redirected to page 2
+ // Mouse Hover on the next button
+ // Ensure the tool tip message is appropriate
+ // Mouse Hover on the Previous button
+ // Ensure the tool tip message is appropriate
+ });
+
+ it("Add new item in the list widget array object", function() {
+ //Drag and drop list widget
+ //Click to open an property pane
+ //Expand Genearl section
+ //Add the following new item
+ //("id": 7,"num": 007",name": Charizard",img": "http://www.serebii.net/pokemongo/pokemon/006.png")
+ //Ensure the new item gets added to the list widget without any error
+ //Check for the new page is added upon adding new items
+ });
+
+ it("Adding apt widget into the List widget", function() {
+ //Drag and Drop List widget
+ //Expand the section 1 size in the list widget
+ //Ensure by exapdning section inside list widget the page size gets increased
+ //Drag and Drop button widget inside list widget
+ //Ensure Button widget can be placed inside list Widget
+ //Drag and Drop Image widget inside list widget
+ //Ensure Image widget can be placed inside list widget
+ // Drag and drop the text widget inside the list widget
+ // Ensure text widget can be place inside the list widget
+ });
+
+ it("Adding unapt widget to identify the error message", function() {
+ //Drag and Drop List widget
+ //Expand the section 1 size in the list widget
+ //Drag and Drop widgets ie: Chart ,Date Picker radio button etc
+ // Ensure an understandable error message is displayed to user
+ });
+});
diff --git a/app/client/cypress/manual_TestSuite/Mongo_Datasource_Spec.js b/app/client/cypress/manual_TestSuite/Mongo_Datasource_Spec.js
new file mode 100644
index 0000000000..71535487b0
--- /dev/null
+++ b/app/client/cypress/manual_TestSuite/Mongo_Datasource_Spec.js
@@ -0,0 +1,78 @@
+const queryLocators = require("../../../locators/QueryEditor.json");
+const queryEditor = require("../../../locators/QueryEditor.json");
+let datasourceName;
+
+describe("Test Ideas for Mongo DB Form Input", function() {
+ it("Insert a Document", function() {
+ //Ensure by choosing command as 'Insert a Document' the following fields will be displayed 'Collection Name' and 'Documents'
+ //Click on Command and enter command name
+ //Clicking on Collection name field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Click on Document and pass the following valid query
+ //Clicking on Document field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Click on run and ensure query runs successfuly
+ //Try passing the invalid query in the document and run and verfiy the error message
+ });
+
+ it("Find One or More Document", function() {
+ // Ensure by choosing command as 'Find one or more Document' the following fields will be displayed 'Collection Name' , 'Query' , 'Sort' , 'Projection' , 'Limit' and 'Skip'
+ //Click on collection name and enter valid collection name (In which document is inserted ealrier)
+ //Clicking on Collection Name field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Click on Query field and pass {} to get all the documents inside the collection
+ //Pass the following value in the query field to get particular document in an collection {"_id":ObjectId("id")}
+ //Clicking on Query field ensure Evaluvated value popup appears and Evaluvated value should match
+ //click on run and ensure query runs successfully
+ //Compare the response with the document inserted earlier and value should match
+ });
+
+ it("Update One Document", function() {
+ //Ensure by choosing command as 'Update one Document' the following fields will be displayed 'Collection Name' , 'Query' , 'Sort' and 'Update'
+ //Click on collection name and enter valid collection name (In which document is inserted ealrier)
+ //Clicking on Collection Name field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Click on Query field and pass {} to get all the documents inside the collection
+ //Pass the following value in the query field to get particular document in an collection {"_id":ObjectId("id")}
+ //Clicking on Query field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Click on Update field and pass the following query {"set":{"status":"Inactive"}}
+ //Repeat 'Find one or More Document' scenario and verfiy if the update results are matching
+ //click on run and ensure query runs successfully
+ //Click on Update field and pass any invalid query and ensure query response with appropriate error message
+ });
+
+ it("Update One or More Document", function() {
+ //Ensure by choosing command as 'Update one or More Document' the following fields will be displayed 'Collection Name' , 'Query' and 'Update'
+ //Click on collection name and enter valid collection name (In which document is inserted ealrier)
+ //Clicking on Collection Name field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Click on Query field and pass {} to get all the documents inside the collection
+ //Pass the following value in the query field to get particular document in an collection {"_id":ObjectId("id")}
+ //Clicking on Query field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Click on Update field and pass the following query {"set":{"status":"Inactive"}}
+ //Repeat 'Find one or More Document' scenario and verify if the update results are matching
+ //click on run and ensure query runs successfully
+ //Click on Update field and pass any invalid query and ensure query response with appropriate error message
+ });
+
+ it("Delet One or More Document", function() {
+ //Ensure by choosing command as 'Delete one or more Document' the following fields will be displayed 'Collection Name' , 'Query' and 'limit'
+ //Ensure limit has two option 'Single Document' and 'All Matching document'
+ //Click on collection name and enter valid collection name (In which document is inserted ealrier)
+ //Clicking on Collection Name field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Choose limit as single document and in the query pass the document id which needs to be deleted {"_id":ObjectId("id")}
+ //Clicking on Query field ensure Evaluvated value popup appears and Evaluvated value should match
+ //Click on run and ensure the query runs successfully
+ //Repeat 'Find one or More Document' scenario and verify if the specified document is deleted
+ //Choose limit as All Matching document and in the query pass {} to delete all the document or pass multi id's
+ //Click on run and ensure the query runs successfully
+ //Repeat 'Find one or More Document' scenario and verify if the specified document is deleted
+ });
+
+ it("Perform Distinct Operation on a Document", function() {
+ //Ensure by choosing command as 'Count' the following fields will be displayed 'Collection Name' , 'Query' and 'Key/Field'
+ //Click on collection name and enter valid collection name (In which document is inserted ealrier)
+ //Clicking on Collection Name field ensure Evaluvated value popup appears and Evaluvated value should match
+ });
+
+ it("Perform Aggregate Operation on a Document", function() {
+ //Ensure by choosing command as 'Count' the following fields will be displayed 'Collection Name' and 'Array of Pipelines'
+ //Click on collection name and enter valid collection name (In which document is inserted ealrier)
+ //Clicking on Collection Name field ensure Evaluvated value popup appears and Evaluvated value should match
+ });
+});
diff --git a/app/client/cypress/manual_TestSuite/Tab_Widget_Spec.js b/app/client/cypress/manual_TestSuite/Tab_Widget_Spec.js
new file mode 100644
index 0000000000..6e88a6da63
--- /dev/null
+++ b/app/client/cypress/manual_TestSuite/Tab_Widget_Spec.js
@@ -0,0 +1,85 @@
+const dsl = require("../../../fixtures/TabWidgetDsl.json");
+
+describe("Tab widget", function() {
+ it("Movement of tabs inside Tab widget ", function() {
+ // Drag and drop the Tab widget
+ // click on "Add a Tab"
+ // Add multiple Tabs
+ // Hold and move the Tab
+ // and observe if the tab are moved in the same
+ });
+
+ it(" Deletion of Tabs and adding them back with Undo", function() {
+ // Drag and drop the Tab widget
+ // click on "Add a Tab"
+ // Add multiple Tabs
+ // Click on delete option of the Tab
+ // ensure the tab is deleted
+ // Click on delete option of the Tab
+ // Ensure an info message is dispalyed to user
+ // Now click on "UNDO"
+ //and observe that the Tab is added back
+ });
+
+ it("Test Ideas for testing the Visible option for tabs ", function() {
+ // Drag and drop the Tab widget
+ // click on "Add a Tab"
+ // Click on Property pane of the tab widget
+ // Click on the Control Pane of the tab
+ // Now click on JS option
+ // Now add it false
+ // and observe the Tab is blured on edit Mode
+ // Now click on Deploy
+ // Observe the tab is not dispalyed to user
+ // Now come back to Edit mode
+ // Click on the Control Pane of the tab
+ // Now click on JS option
+ // enable the button
+ // Now observe the Tab must be visible and normal
+ });
+
+ it("Test Ideas for testing the Show Tabs Feature ", function() {
+ // Drag and drop the Tab widget
+ // Click on Property pane of the tab widget
+ // Scroll down to Show Tabs option
+ // Now click on JS option
+ // Now add it false
+ // and observe the Tab widget does not show any Tabs
+ // Now click on Deploy
+ // Observe the Tab widget does not show any Tabs
+ // Now come back to Edit mode
+ // Now click on JS option
+ // enable the button
+ // Now observe the Tab must be visible
+ });
+
+ it("Adding multiple widgets inside the Tab widget", function() {
+ // Drag and drop the Tab widget
+ // Ensure default 2 Tabs are dispalyed to user
+ // Add date picker, Text and Button into Tab1
+ // Click on Tab2 and ensure it is empty
+ // Add image widget , radio, Check widget
+ // Click on Deploy
+ // Ensure the Tab widget with widgets are displayed to user
+ });
+
+ it("Adding action while changing the Tab ", function() {
+ // Drag and drop the Tab widget
+ // Click on Property pane of the tab widget
+ // Navigate to Action section
+ // Select "Show a modal"
+ // Click on "Add a Modal"
+ // Assign related action on the modal
+ // Now change the tab
+ // and observe the modal pop up is displayed to user
+ });
+
+ it("Binding the Tab to widget ", function() {
+ // Drag and drop the Tab widget
+ // Click on Property pane of the tab widget
+ // Navigate to control pane
+ // Convert a Visible option to JS
+ // Add binding to the widget
+ // Ensure user is dispalyed on the TAB widget
+ });
+});
diff --git a/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js b/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js
new file mode 100644
index 0000000000..d180857d68
--- /dev/null
+++ b/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js
@@ -0,0 +1,106 @@
+const homePage = require("../../../locators/Textwidget.json");
+
+describe("Test Ideas to test different feature of text widget ", function() {
+ it("Add New Text widget along with BG and text colour ", function() {
+ // Navigate to application
+ // Drag and drop a Text Widget
+ // Navigate to Property Pane
+ // Add a text
+ // Scroll to BG colour and add a colour
+ // Next add a text colour
+ // Click on Deploy
+ });
+
+ it("Enable Scroll feature with text colour ", function() {
+ // Navigate to application
+ // Drag and drop a Text Widget
+ // Add a long text in the "Label"
+ // Enable scroll option
+ // Navigate to Text colour and add a colour
+ // and ensure it is scrolling
+ // Click on deploy and check if it scrollable and colour selected is visible
+ });
+
+ it("Adding text Size to the Text along with BG colour ", function() {
+ // Navigate to application
+ // Drag and drop a Text Widget
+ // Navigate to Property pane
+ // Add a medium text in the "Label"
+ // Increase the area of the Text Widget
+ // Navigate to BG colour and add a colour
+ // Naviaget to "Text Size"
+ // Select Paragarph option
+ // Ensure the text size varies accordingly
+ });
+
+ it("Adding Bold Font style and Centre Text Alignment ", function() {
+ // Navigate to application
+ // Drag and drop a Text Widget
+ // Navigate to Property pane
+ // Add a medium text in the "Label"
+ // Increase the area of the Text Widget
+ // Navigate to Font Style
+ // Make it Bold
+ // and Navigate to Alignment and make it centre
+ // Ensure the changes are visible to user
+ });
+
+ it("Adding Italic Font style and Text Alignment to exsisting text widget ", function() {
+ // Navigate to already exsisting Text widget
+ // Ensure the text is added
+ // Navigate to Property pane
+ // Navigate to Font Style
+ // Make it Italic font
+ // and Navigate to Alignment and make it Right
+ // Ensure the changes are visible to user
+ });
+
+ it("Expand and Contract text widget Property pane", function() {
+ // Navigate to already exsisting Text widget
+ // Navigate to Property pane
+ // Click on collapse option
+ // Observe that the property pane is contracted
+ // Now click again on the arrow
+ //and ensure it collapses
+ });
+
+ it("Copy and paste a text widget", function() {
+ // Navigate to already exsisting Text widget
+ // Ensure Clour and font feature exsists
+ // Copy and paste the widget
+ // Ensure the new widget retrives the feature exsisting from parent widget
+ });
+
+ it("Rename and search a text widget", function() {
+ // Ensure there are multiple Text widget
+ // Navigate to Entity Explorer
+ // Search for "Text" keyword
+ // Click on one of the text widget
+ // Rename the text widget from the Entity explorer
+ // Clear the search keyword
+ // enter the new text widget name
+ // and observe the user is navigated to same text widget and properties of the widget does not change on renaming
+ });
+
+ it("Search and delete a text widget", function() {
+ // Ensure there are multiple Text widget
+ // Navigate to Entity Explorer
+ // Search for "Text" keyword
+ // Click on one of the text widget
+ // Ensure user is navigated to Text widget
+ // Click on Delete option
+ // Ensure the Text widget is delete
+ // Click on Deploy adn ensure the Widget is delete
+ });
+
+ it("Search and delete a text widget", function() {
+ // Ensure there are multiple Text widget
+ // Navigate to Entity Explorer
+ // Search for "Text" keyword
+ // Click on one of the text widget
+ // Ensure user is navigated to Text widget
+ // Click on Delete option
+ // Ensure the Text widget is delete
+ // Click on Deploy adn ensure the Widget is delete
+ });
+});
diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js
index b8ad76996a..c1773bdcd7 100644
--- a/app/client/cypress/support/commands.js
+++ b/app/client/cypress/support/commands.js
@@ -20,23 +20,30 @@ const apiwidget = require("../locators/apiWidgetslocator.json");
const dynamicInputLocators = require("../locators/DynamicInput.json");
const explorer = require("../locators/explorerlocators.json");
const datasource = require("../locators/DatasourcesEditor.json");
+const viewWidgetsPage = require("../locators/ViewWidgets.json");
let pageidcopy = " ";
-Cypress.Commands.add("createOrg", (orgName) => {
+export const initLocalstorage = () => {
+ cy.window().then((window) => {
+ window.localStorage.setItem("ShowCommentsButtonToolTip", "");
+ });
+};
+
+Cypress.Commands.add("createOrg", () => {
cy.get(homePage.createOrg)
.should("be.visible")
.first()
.click({ force: true });
- cy.xpath(homePage.inputOrgName)
+});
+
+Cypress.Commands.add("renameOrg", (orgName, newOrgName) => {
+ cy.contains(orgName).click({ force: true });
+ cy.get(homePage.renameOrgInput)
.should("be.visible")
- .type(orgName);
- cy.xpath(homePage.submitBtn).click();
- cy.wait("@applications").should(
- "have.nested.property",
- "response.body.responseMeta.status",
- 200,
- );
+ .type(newOrgName)
+ .type("{enter}");
+ cy.contains(newOrgName);
});
Cypress.Commands.add(
@@ -66,6 +73,16 @@ Cypress.Commands.add("navigateToOrgSettings", (orgName) => {
cy.get(homePage.inviteUserMembersPage).should("be.visible");
});
+Cypress.Commands.add("openOrgOptionsPopup", (orgName) => {
+ cy.get(homePage.orgList.concat(orgName).concat(")"))
+ .scrollIntoView()
+ .should("be.visible");
+ cy.get(".t--org-name span")
+ .contains(orgName)
+ .first()
+ .click({ force: true });
+});
+
Cypress.Commands.add("inviteUserForOrg", (orgName, email, role) => {
cy.stubPostHeaderReq();
cy.get(homePage.orgList.concat(orgName).concat(")"))
@@ -157,7 +174,11 @@ Cypress.Commands.add("deleteUserFromOrg", (orgName, email) => {
"response.body.responseMeta.status",
200,
);
- cy.get(homePage.DeleteBtn).click({ force: true });
+ cy.get(homePage.DeleteBtn)
+ .last()
+ .click({ force: true });
+ cy.get(homePage.leaveOrgConfirmModal).should("be.visible");
+ cy.get(homePage.leaveOrgConfirmButton).click({ force: true });
cy.xpath(homePage.appHome)
.first()
.should("be.visible")
@@ -259,6 +280,11 @@ Cypress.Commands.add("CreateAppInFirstListedOrg", (appname) => {
"response.body.responseMeta.status",
200,
);
+ /* The server created app always has an old dsl so the layout will migrate
+ * To avoid race conditions between that update layout and this one
+ * we wait for that to finish before updating layout here
+ */
+ cy.wait("@updateLayout");
});
Cypress.Commands.add(
@@ -286,6 +312,13 @@ Cypress.Commands.add(
},
);
+Cypress.Commands.add("addBasicProfileDetails", (username, password) => {
+ cy.get(datasource.authType).click();
+ cy.xpath(datasource.basic).click();
+ cy.get(datasource.basicUsername).type(username);
+ cy.get(datasource.basicPassword).type(password);
+});
+
Cypress.Commands.add("firestoreDatasourceForm", () => {
cy.get(datasourceEditor.datasourceConfigUrl).type(
datasourceFormData["database-url"],
@@ -346,6 +379,8 @@ Cypress.Commands.add("LogintoApp", (uname, pword) => {
"response.body.responseMeta.status",
200,
);
+
+ initLocalstorage();
});
Cypress.Commands.add("LoginFromAPI", (uname, pword) => {
@@ -438,10 +473,9 @@ Cypress.Commands.add("SearchApp", (appname) => {
});
Cypress.Commands.add("SearchEntity", (apiname1, apiname2) => {
- cy.get(commonlocators.entityExplorersearch).should("be.visible");
cy.get(commonlocators.entityExplorersearch)
- .clear()
- .type(apiname1);
+ .clear({ force: true })
+ .type(apiname1, { force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.get(
@@ -453,10 +487,10 @@ Cypress.Commands.add("SearchEntity", (apiname1, apiname2) => {
});
Cypress.Commands.add("GlobalSearchEntity", (apiname1) => {
- cy.get(commonlocators.entityExplorersearch).should("be.visible");
+ // entity explorer search will be hidden
cy.get(commonlocators.entityExplorersearch)
- .clear()
- .type(apiname1);
+ .clear({ force: true })
+ .type(apiname1, { force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.get(
@@ -474,10 +508,18 @@ Cypress.Commands.add("ResponseCheck", (textTocheck) => {
cy.get(apiwidget.responseText).should("be.visible");
});
+Cypress.Commands.add("ResponseTextCheck", (textTocheck) => {
+ cy.ResponseCheck();
+ cy.get(apiwidget.responseText).contains(textTocheck);
+});
+
Cypress.Commands.add("NavigateToAPI_Panel", () => {
cy.get(pages.addEntityAPI)
.should("be.visible")
.click({ force: true });
+ cy.get(pages.integrationCreateNew)
+ .should("be.visible")
+ .click({ force: true });
cy.get("#loading").should("not.exist");
});
@@ -608,8 +650,7 @@ Cypress.Commands.add("SelectAction", (action) => {
});
Cypress.Commands.add("ClearSearch", () => {
- cy.get(commonlocators.entityExplorersearch).should("be.visible");
- cy.get(commonlocators.entityExplorersearch).clear();
+ cy.get(commonlocators.entityExplorersearch).clear({ force: true });
});
Cypress.Commands.add(
@@ -623,8 +664,8 @@ Cypress.Commands.add(
const lastChar = text.slice(-1);
cy.get(commonlocators.entityExplorersearch)
- .clear()
- .click()
+ .clear({ force: true })
+ .click({ force: true })
.then(() => {
$element.text(subString);
$element.val(subString);
@@ -634,10 +675,9 @@ Cypress.Commands.add(
);
Cypress.Commands.add("SearchEntityandOpen", (apiname1) => {
- cy.get(commonlocators.entityExplorersearch).should("be.visible");
cy.get(commonlocators.entityExplorersearch)
- .clear()
- .type(apiname1);
+ .clear({ force: true })
+ .type(apiname1, { force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.get(
@@ -649,21 +689,20 @@ Cypress.Commands.add("SearchEntityandOpen", (apiname1) => {
});
Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => {
- cy.get(apiwidget.resourceUrl)
- .first()
- .click({ force: true })
- .type(datasource);
- /*
- cy.xpath(apiwidget.autoSuggest)
- .first()
- .click({ force: true });
- */
+ cy.enterDatasource(datasource);
cy.get(apiwidget.editResourceUrl)
.first()
.click({ force: true })
.type(path, { parseSpecialCharSequences: false });
});
+Cypress.Commands.add("enterDatasource", (datasource) => {
+ cy.get(apiwidget.resourceUrl)
+ .first()
+ .click({ force: true })
+ .type(datasource);
+});
+
Cypress.Commands.add("changeZoomLevel", (zoomValue) => {
cy.get(commonlocators.changeZoomlevel)
.last()
@@ -714,16 +753,14 @@ Cypress.Commands.add(
"EnterSourceDetailsWithHeader",
(baseUrl, v1method, hKey, hValue) => {
cy.enterDatasourceAndPath(baseUrl, v1method);
- cy.xpath(apiwidget.headerKey)
+ cy.get(apiwidget.headerKey)
.first()
.click({ force: true })
- .type(hKey, { force: true })
- .should("have.value", hKey);
- cy.xpath(apiwidget.headerValue)
+ .type(hKey, { parseSpecialCharSequences: true });
+ cy.get(apiwidget.headerValue)
.first()
.click({ force: true })
- .type(hValue, { force: true })
- .should("have.value", hValue);
+ .type(hValue, { parseSpecialCharSequences: true });
cy.WaitAutoSave();
},
);
@@ -816,22 +853,20 @@ Cypress.Commands.add(
"EnterSourceDetailsWithQueryParam",
(baseUrl, v1method, hKey, hValue, qKey, qValue) => {
cy.enterDatasourceAndPath(baseUrl, v1method);
- cy.xpath(apiwidget.headerKey)
+ cy.get(apiwidget.headerKey)
.first()
.click({ force: true })
- .type(hKey, { force: true })
- .should("have.value", hKey);
- cy.xpath(apiwidget.headerValue)
+ .type(hKey, { parseSpecialCharSequences: true });
+ cy.get(apiwidget.headerValue)
.first()
.click({ force: true })
- .type(hValue, { force: true })
- .should("have.value", hValue);
- cy.xpath(apiwidget.queryKey)
+ .type(hValue, { parseSpecialCharSequences: true });
+ cy.get(apiwidget.queryKey)
.first()
.click({ force: true })
.type(qKey, { force: true })
.should("have.value", qKey);
- cy.xpath(apiwidget.queryValue)
+ cy.get(apiwidget.queryValue)
.first()
.click({ force: true })
.type(qValue, { force: true })
@@ -852,6 +887,9 @@ Cypress.Commands.add(
Cypress.Commands.add("CreationOfUniqueAPIcheck", (apiname) => {
cy.get(pages.addEntityAPI).click();
+ cy.get(pages.integrationCreateNew)
+ .should("be.visible")
+ .click({ force: true });
cy.get(apiwidget.createapi).click({ force: true });
cy.wait("@createNewApi");
// cy.wait("@getUser");
@@ -859,9 +897,9 @@ Cypress.Commands.add("CreationOfUniqueAPIcheck", (apiname) => {
cy.get(apiwidget.ApiName).click({ force: true });
cy.get(apiwidget.apiTxt)
.clear()
- .type(apiname, { force: true })
- .should("have.value", apiname)
- .focus();
+ .focus()
+ .type(apiname, { force: true, delay: 500 })
+ .should("have.value", apiname);
cy.get(".error-message").should(($x) => {
console.log($x);
expect($x).contain(apiname.concat(" is already being used."));
@@ -1000,31 +1038,23 @@ Cypress.Commands.add("DeleteAPI", (apiname) => {
);
});
-// Cypress.Commands.add("CreateModal", () => {
-// cy.get(modalWidgetPage.selectModal).click();
-// cy.get(modalWidgetPage.createModalButton).click({ force: true });
-// cy.get(modalWidgetPage.controlModalType)
-// .find(".bp3-button")
-// .click({ force: true })
-// .get("ul.bp3-menu")
-// .children()
-// .contains("Alert Modal")
-// .click();
-// cy.get(modalWidgetPage.controlModalType)
-// .find(".bp3-button > .bp3-button-text")
-// .should("have.text", "Alert Modal");
-// cy.get(commonlocators.editPropCrossButton).click({force: true});
-// cy.reload();
-// });
+Cypress.Commands.add("AddActionWithModal", () => {
+ cy.get(commonlocators.dropdownSelectButton)
+ .last()
+ .click();
+ cy.get(".single-select")
+ .contains("Open Modal")
+ .click({ force: true });
+ cy.get(modalWidgetPage.selectModal).click();
+ cy.get(modalWidgetPage.createModalButton).click({ force: true });
+ cy.get(commonlocators.editPropCrossButton).click({ force: true });
+});
Cypress.Commands.add("createModal", (modalType, ModalName) => {
- cy.get(widgetsPage.buttonOnClick)
- .last()
+ cy.get(widgetsPage.actionSelect)
+ .first()
.click({ force: true });
- cy.get("ul.bp3-menu")
- .children()
- .contains("Open Modal")
- .click();
+ cy.selectOnClickOption("Open Modal");
cy.get(modalWidgetPage.selectModal).click();
cy.get(modalWidgetPage.createModalButton).click({ force: true });
@@ -1057,14 +1087,18 @@ Cypress.Commands.add("createModal", (modalType, ModalName) => {
cy.get(".bp3-overlay-backdrop").click({ force: true });
});
+Cypress.Commands.add("selectOnClickOption", (option) => {
+ cy.get("ul.bp3-menu div.bp3-fill")
+ .wait(500)
+ .contains(option)
+ .click({ force: true });
+});
+
Cypress.Commands.add("updateModal", (modalType, ModalName) => {
- cy.get(".t--open-dropdown-Select-Action")
+ cy.get(widgetsPage.actionSelect)
.first()
.click({ force: true });
- cy.get("ul.bp3-menu")
- .children()
- .contains("Open Modal")
- .click();
+ cy.selectOnClickOption("Open Modal");
cy.get(modalWidgetPage.selectModal).click();
cy.get(modalWidgetPage.createModalButton).click({ force: true });
@@ -1110,6 +1144,13 @@ Cypress.Commands.add("UncheckWidgetProperties", (checkboxCss) => {
cy.assertPageSave();
});
+Cypress.Commands.add("EditWidgetPropertiesUsingJS", (checkboxCss, inputJS) => {
+ cy.get(checkboxCss)
+ .click()
+ .type(inputJS);
+ cy.assertPageSave();
+});
+
Cypress.Commands.add(
"ChangeTextStyle",
(dropDownValue, textStylecss, labelName) => {
@@ -1132,7 +1173,7 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => {
cy.get(inputcss)
.first()
.trigger("mouseover", { force: true });
- cy.get(innercss).should("have.text", text);
+ cy.contains(innercss, text);
});
Cypress.Commands.add("editColName", (text) => {
@@ -1609,15 +1650,15 @@ Cypress.Commands.add("addDsl", (dsl) => {
//Fetch the layout id
cy.server();
cy.request("GET", "api/v1/pages/" + pageid).then((response) => {
- const len = JSON.stringify(response.body);
- cy.log(len);
- layoutId = JSON.parse(len).data.layouts[0].id;
- // Dumpimg the DSL to the created page
+ const respBody = JSON.stringify(response.body);
+ layoutId = JSON.parse(respBody).data.layouts[0].id;
+ // Dumping the DSL to the created page
cy.request(
"PUT",
"api/v1/layouts/" + layoutId + "/pages/" + pageid,
dsl,
).then((response) => {
+ cy.log(response.body);
expect(response.status).equal(200);
cy.reload();
});
@@ -1691,9 +1732,7 @@ Cypress.Commands.add("getAlert", (alertcss) => {
cy.get(alertcss)
.click({ force: true })
- .type("{command}{A}{del}")
- .type("hello")
- .should("not.to.be.empty");
+ .type("hello");
cy.get(".t--open-dropdown-Select-type").click({ force: true });
cy.get(".bp3-popover-content .bp3-menu li")
.contains("Success")
@@ -1723,9 +1762,7 @@ Cypress.Commands.add(
cy.get(alertcss)
.click({ force: true })
- .type("{command}{A}{del}")
- .type("hello")
- .should("not.to.be.empty");
+ .type("hello");
cy.get(".t--open-dropdown-Select-type").click({ force: true });
cy.get(".bp3-popover-content .bp3-menu li")
.contains("Success")
@@ -1733,17 +1770,20 @@ Cypress.Commands.add(
},
);
+Cypress.Commands.add("addQueryFromLightningMenu", (QueryName) => {
+ cy.get(commonlocators.dropdownSelectButton)
+ .first()
+ .click({ force: true })
+ .selectOnClickOption("Execute a Query")
+ .selectOnClickOption(QueryName);
+});
+
Cypress.Commands.add("addAPIFromLightningMenu", (ApiName) => {
cy.get(commonlocators.dropdownSelectButton)
+ .first()
.click({ force: true })
- .get("ul.bp3-menu")
- .children()
- .contains("Call An API")
- .click({ force: true })
- .get("ul.bp3-menu")
- .children()
- .contains(ApiName)
- .click({ force: true });
+ .selectOnClickOption("Execute a Query")
+ .selectOnClickOption(ApiName);
});
Cypress.Commands.add("radioInput", (index, text) => {
@@ -1815,7 +1855,11 @@ Cypress.Commands.add("testSaveDeleteDatasource", () => {
200,
);
- cy.get(datasourceEditor.editDatasource).click();
+ cy.get(
+ `${datasourceEditor.datasourceCard} ${datasourceEditor.editDatasource}`,
+ )
+ .last()
+ .click();
cy.get(".t--delete-datasource").click();
cy.wait("@deleteDatasource").should(
@@ -1838,7 +1882,9 @@ Cypress.Commands.add("NavigateToDatasourceEditor", () => {
cy.get(explorer.addDBQueryEntity)
.last()
.click({ force: true });
- cy.get(queryEditor.addDatasource).click();
+ cy.get(pages.integrationCreateNew)
+ .should("be.visible")
+ .click({ force: true });
});
Cypress.Commands.add("NavigateToQueryEditor", () => {
@@ -1859,16 +1905,20 @@ Cypress.Commands.add("testDatasource", () => {
Cypress.Commands.add("saveDatasource", () => {
cy.get(".t--save-datasource").click();
- cy.wait("@saveDatasource").should(
- "have.nested.property",
- "response.body.responseMeta.status",
- 200,
- );
+ cy.wait("@saveDatasource")
+ .then((xhr) => {
+ cy.log(JSON.stringify(xhr.response.body));
+ })
+ .should("have.nested.property", "response.body.responseMeta.status", 200);
});
Cypress.Commands.add("testSaveDatasource", () => {
cy.saveDatasource();
- cy.get(datasourceEditor.editDatasource).click();
+ cy.get(
+ `${datasourceEditor.datasourceCard} ${datasourceEditor.editDatasource}`,
+ )
+ .last()
+ .click();
cy.testDatasource();
});
@@ -1926,7 +1976,9 @@ Cypress.Commands.add("createPostgresDatasource", () => {
Cypress.Commands.add("deleteDatasource", (datasourceName) => {
cy.NavigateToQueryEditor();
-
+ cy.get(pages.integrationActiveTab)
+ .should("be.visible")
+ .click({ force: true });
cy.contains(".t--datasource-name", datasourceName)
.find(".t--edit-datasource")
.click();
@@ -1986,7 +2038,8 @@ Cypress.Commands.add("runAndDeleteQuery", () => {
200,
);
- cy.get(queryEditor.deleteQuery).click();
+ cy.get(queryEditor.queryMoreAction).click();
+ cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
@@ -2000,8 +2053,8 @@ Cypress.Commands.add("dragAndDropToCanvas", (widgetType, { x, y }) => {
.trigger("mousedown", { button: 0 }, { force: true })
.trigger("mousemove", x, y, { force: true });
cy.get(explorer.dropHere)
- .click()
- .trigger("mouseup", { force: true });
+ .trigger("mousemove", x, y)
+ .trigger("mouseup", x, y + 20);
});
Cypress.Commands.add("executeDbQuery", (queryName) => {
@@ -2010,7 +2063,7 @@ Cypress.Commands.add("executeDbQuery", (queryName) => {
.click({ force: true })
.get("ul.bp3-menu")
.children()
- .contains("Execute a DB Query")
+ .contains("Execute a Query")
.click({ force: true })
.get("ul.bp3-menu")
.children()
@@ -2018,25 +2071,175 @@ Cypress.Commands.add("executeDbQuery", (queryName) => {
.click({ force: true });
});
+Cypress.Commands.add("CreateMockQuery", (queryName) => {
+ // cy.get(queryEditor.addNewQueryBtn).click({ force: true });
+ cy.get(queryEditor.createQuery)
+ .first()
+ .click({ force: true });
+ cy.get(queryEditor.queryNameField).type(queryName + "{enter}", {
+ force: true,
+ });
+ cy.assertPageSave();
+ cy.get(queryEditor.templateMenu + " div")
+ .contains("Select")
+ .click({ force: true });
+ cy.runQuery();
+ // cy.wait(3000);
+ // cy.get(queryEditor.runQuery)
+ // .click({force: true});
+});
+
Cypress.Commands.add("openPropertyPane", (widgetType) => {
const selector = `.t--draggable-${widgetType}`;
cy.get(selector)
.first()
.trigger("mouseover", { force: true })
.wait(500);
- cy.get(`${selector}:first-of-type .t--widget-propertypane-toggle`)
+ cy.get(
+ `${selector}:first-of-type .t--widget-propertypane-toggle > .t--widget-name`,
+ )
.first()
.click({ force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
});
+Cypress.Commands.add("openPropertyPaneCopy", (widgetType) => {
+ const selector = `.t--draggable-${widgetType}`;
+ cy.get(selector)
+ .last()
+ .trigger("mouseover", { force: true })
+ .wait(500);
+ cy.get(
+ `${selector}:first-of-type .t--widget-propertypane-toggle > .t--widget-name`,
+ )
+ .first()
+ .click({ force: true });
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(1000);
+});
+
+Cypress.Commands.add("changeButtonStyle", (index, buttonColor, hoverColor) => {
+ cy.get(widgetsPage.buttonStyleDropdown).click({ force: true });
+ cy.get(
+ ".bp3-popover-content .t--dropdown-option:nth-child(" + index + ")",
+ ).click({ force: true });
+ cy.PublishtheApp();
+ cy.get(widgetsPage.widgetBtn).should(
+ "have.css",
+ "background-color",
+ buttonColor,
+ );
+ // cy.get(buttonBackground)
+ // .first()
+ // .trigger('mouseover', { force: true });
+ // cy.get(buttonBackground)
+ // .should('have.css', 'background-color', hoverColor);
+ cy.wait(1000);
+});
+
Cypress.Commands.add("closePropertyPane", () => {
cy.get(commonlocators.editPropCrossButton).click({ force: true });
});
+Cypress.Commands.add("onClickActions", (forSuccess, forFailure) => {
+ // Filling the messages for success/failure in the onClickAction of the button widget.
+ // For Success
+ cy.get(".code-highlight")
+ .children()
+ .contains("No Action")
+ .first()
+ .click({ force: true })
+ .selectOnClickOption("Show Message")
+ .get("div.t--property-control-onclick div.CodeMirror-lines")
+ .click()
+ .type(forSuccess)
+ .get("button.t--open-dropdown-Select-type")
+ .click()
+ .get("a.single-select div")
+ .contains(forSuccess)
+ .click();
+
+ cy.wait(2000);
+ // For Failure
+ cy.get(".code-highlight")
+ .children()
+ .contains("No Action")
+ .last()
+ .click({ force: true })
+ .selectOnClickOption("Show Message")
+ .get("div.t--property-control-onclick div.CodeMirror-lines")
+ .last()
+ .click()
+ .type(forFailure)
+ .get("button.t--open-dropdown-Select-type")
+ .last()
+ .click()
+ .get("a.single-select div")
+ .contains(forFailure)
+ .click();
+});
+
+Cypress.Commands.add("copyWidget", (widget, widgetLocator) => {
+ const modifierKey = Cypress.platform === "darwin" ? "meta" : "ctrl";
+ //Copy widget and verify all properties
+ cy.get(widgetsPage.propertypaneText)
+ .children()
+ .last()
+ .invoke("text")
+ .then((x) => {
+ cy.log(x);
+ let originalWidget = x.replaceAll("x", "");
+ originalWidget = originalWidget.replaceAll(/\u200B/g, "");
+ cy.log(originalWidget);
+ cy.get(widgetsPage.copyWidget).click({ force: true });
+ cy.reload();
+ // Wait for the widget to be appear in the DOM and press Ctrl/Cmd + V to paste the button.
+ cy.get(widgetLocator).should("be.visible");
+ cy.get("body").type(`{${modifierKey}}v`);
+ cy.wait(2000);
+ cy.openPropertyPaneCopy(widget);
+ cy.get(widgetsPage.propertypaneText)
+ .children()
+ .last()
+ .invoke("text")
+ .then((y) => {
+ cy.log(y);
+ let copiedWidget = y.replaceAll("x", "");
+ copiedWidget = copiedWidget.replaceAll(/\u200B/g, "");
+ cy.log(copiedWidget);
+ expect(originalWidget).to.be.equal(copiedWidget);
+ });
+ });
+});
+
+Cypress.Commands.add("deleteWidget", (widget) => {
+ // Delete the button widget
+ cy.get(widgetsPage.removeWidget).click({ force: true });
+ cy.get(widgetsPage.deleteToast).should("have.text", "UNDO");
+});
+
+Cypress.Commands.add("UpdateChartType", (typeOfChart) => {
+ // Command to change the chart type if the property pane of the chart widget is opened.
+ cy.get(viewWidgetsPage.chartType)
+ .last()
+ .click({ force: true });
+ cy.get(commonlocators.dropdownmenu)
+ .children()
+ .contains(typeOfChart)
+ .click({ force: true });
+
+ cy.get(viewWidgetsPage.chartType + " span.cs-text").should(
+ "have.text",
+ typeOfChart,
+ );
+});
+
Cypress.Commands.add("createAndFillApi", (url, parameters) => {
cy.NavigateToApiEditor();
+ cy.get(pages.integrationCreateNew)
+ .should("be.visible")
+ .click({ force: true });
cy.testCreateApiButton();
cy.get("@createNewApi").then((response) => {
cy.get(ApiEditor.ApiNameField).should("be.visible");
@@ -2187,6 +2390,8 @@ Cypress.Commands.add("startServerAndRoutes", () => {
cy.route("PUT", "/api/v1/actions/move").as("moveAction");
cy.route("POST", "/api/v1/organizations").as("createOrg");
+ cy.route("POST", "api/v1/applications/import/*").as("importNewApplication");
+ cy.route("GET", "api/v1/applications/export/*").as("exportApplication");
cy.route("GET", "/api/v1/organizations/roles?organizationId=*").as(
"getRoles",
);
@@ -2200,6 +2405,7 @@ Cypress.Commands.add("startServerAndRoutes", () => {
cy.route("POST", "/api/v1/organizations/*/logo").as("updateLogo");
cy.route("DELETE", "/api/v1/organizations/*/logo").as("deleteLogo");
cy.route("POST", "/api/v1/applications/*/fork/*").as("postForkAppOrg");
+ cy.route("PUT", "/api/v1/users/leaveOrganization/*").as("leaveOrgApiCall");
});
Cypress.Commands.add("alertValidate", (text) => {
@@ -2242,8 +2448,6 @@ Cypress.Commands.add("assertEvaluatedValuePopup", (expectedType) => {
cy.get(dynamicInputLocators.evaluatedValue)
.should("be.visible")
.children("p")
- .should("contain.text", "Expected Data Type")
- .should("contain.text", "Evaluated Value")
.siblings("pre")
.should("have.text", expectedType);
});
@@ -2319,15 +2523,33 @@ Cypress.Commands.add("ValidatePaginationInputData", () => {
Cypress.Commands.add("callApi", (apiname) => {
cy.get(commonlocators.callApi)
.first()
- .click();
+ .click({ force: true });
cy.get(commonlocators.singleSelectMenuItem)
- .contains("Call An API")
- .click();
+ .contains("Execute a Query")
+ .click({ force: true });
cy.get(commonlocators.selectMenuItem)
.contains(apiname)
- .click();
+ .click({ force: true });
});
Cypress.Commands.add("assertPageSave", () => {
cy.get(commonlocators.saveStatusSuccess);
});
+
+Cypress.Commands.add("ValidateQueryParams", (param) => {
+ cy.xpath(apiwidget.paramsTab)
+ .should("be.visible")
+ .click({ force: true });
+
+ cy.validateCodeEditorContent(apiwidget.paramKey, param.key);
+ cy.validateCodeEditorContent(apiwidget.paramValue, param.value);
+});
+
+Cypress.Commands.add(
+ "validateCodeEditorContent",
+ (selector, contentToValidate) => {
+ cy.get(selector).within(() => {
+ cy.get(".CodeMirror-code").should("have.text", contentToValidate);
+ });
+ },
+);
diff --git a/app/client/cypress/support/index.js b/app/client/cypress/support/index.js
index 71c1be4d99..bf914f1c91 100644
--- a/app/client/cypress/support/index.js
+++ b/app/client/cypress/support/index.js
@@ -18,6 +18,7 @@ let appId;
// Import commands.js using ES2015 syntax:
import "./commands";
+import { initLocalstorage } from "./commands";
Cypress.on("uncaught:exception", (err, runnable) => {
// returning false here prevents Cypress from
@@ -31,11 +32,13 @@ Cypress.on("fail", (error, runnable) => {
});
before(function() {
+ initLocalstorage();
cy.startServerAndRoutes();
// Clear indexedDB
cy.window().then((window) => {
window.indexedDB.deleteDatabase("Appsmith");
});
+
const username = Cypress.env("USERNAME");
const password = Cypress.env("PASSWORD");
cy.LoginFromAPI(username, password);
@@ -58,6 +61,7 @@ before(function() {
});
beforeEach(function() {
+ initLocalstorage();
Cypress.Cookies.preserveOnce("SESSION", "remember_token");
cy.startServerAndRoutes();
});
diff --git a/app/client/cypress/test.sh b/app/client/cypress/test.sh
index b1a36fb9d4..f8c75f22cc 100755
--- a/app/client/cypress/test.sh
+++ b/app/client/cypress/test.sh
@@ -27,10 +27,10 @@ if [ "$target" == "ci" ]; then
# On the CI server run the tests in parallel
# This requires the projectId and the record_key to be configured in your environment variables. By default this is defined on the CI server
echo "Got the Build ID: $BUILD_ID"
- $(npm bin)/cypress run --headless --browser chrome \
+ $(npm bin)/cypress run --headless \
--record --key "$CYPRESS_RECORD_KEY" --ci-build-id $BUILD_ID \
--parallel --group "Electrons on Gitlab CI" \
--spec "cypress/integration/Smoke_TestSuite/**/*.js"
else
- $(npm bin)/cypress run --headless --browser chrome --spec "cypress/integration/Smoke_TestSuite/**/*.js"
+ $(npm bin)/cypress run --headless --browser chromium --spec "cypress/integration/Smoke_TestSuite/**/*.js"
fi
diff --git a/app/client/docker/templates/nginx-app.conf.template b/app/client/docker/templates/nginx-app.conf.template
index 4b7c5fdf4e..6b8ed93474 100644
--- a/app/client/docker/templates/nginx-app.conf.template
+++ b/app/client/docker/templates/nginx-app.conf.template
@@ -48,6 +48,7 @@ server {
sub_filter __APPSMITH_MAIL_ENABLED__ '${APPSMITH_MAIL_ENABLED}';
sub_filter __APPSMITH_DISABLE_TELEMETRY__ '${APPSMITH_DISABLE_TELEMETRY}';
sub_filter __APPSMITH_CLOUD_SERVICES_BASE_URL__ '${APPSMITH_CLOUD_SERVICES_BASE_URL}';
+ sub_filter __APPSMITH_RECAPTCHA_SITE_KEY__ '${APPSMITH_RECAPTCHA_SITE_KEY}';
}
location /f {
diff --git a/app/client/jest.config.js b/app/client/jest.config.js
index c88bf5f8ec..f390ccddc2 100644
--- a/app/client/jest.config.js
+++ b/app/client/jest.config.js
@@ -27,6 +27,9 @@ module.exports = {
"test/(.*)": "/test/$1",
},
globals: {
+ "ts-jest": {
+ isolatedModules: true,
+ },
APPSMITH_FEATURE_CONFIGS: {
sentry: {
dsn: parseConfig("__APPSMITH_SENTRY_DSN__"),
diff --git a/app/client/netlify.toml b/app/client/netlify.toml
index 435adaafc5..4f0c0edc20 100644
--- a/app/client/netlify.toml
+++ b/app/client/netlify.toml
@@ -8,35 +8,20 @@
REACT_APP_ALGOLIA_SEARCH_INDEX_NAME = "test_appsmith"
REACT_APP_CLIENT_LOG_LEVEL = "debug"
REACT_APP_GOOGLE_MAPS_API_KEY = "AIzaSyBOQFulljufGt3VDhBAwNjZN09KEFufVyg"
+ REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY = ""
REACT_APP_TNC_PP = "true"
REACT_APP_CLOUD_HOSTING = "true"
REACT_APP_INTERCOM_APP_ID = "y10e7138"
REACT_APP_MAIL_ENABLED = "true"
REACT_APP_SENTRY_DSN = "https://abf15a075d1347969df44c746cca7eaa@o296332.ingest.sentry.io/1546547"
REACT_APP_SENTRY_ENVIRONMENT = "Production"
+ REACT_APP_SHOW_ONBOARDING_FORM = "true"
SENTRY_AUTH_TOKEN = "dfdf7fa46c5b483a944b4571554d6466da3c64a6ed8b46e3b8a4285183a6bcc3"
SENTRY_DSN = "https://abf15a075d1347969df44c746cca7eaa@o296332.ingest.sentry.io/1546547"
SENTRY_ORG = "appsmith"
SENTRY_PROJECT = "appsmith"
SENTRY_LOG_LEVEL = "info"
-[context.production]
- [context.production.environment]
- REACT_APP_ENVIRONMENT = "PRODUCTION"
- REACT_APP_SENTRY_ENVIRONMENT = "Production"
- REACT_APP_SMART_LOOK_ID = "c370af0df0edf38360adbefbdc47d2b42ea137c9"
- REACT_APP_SEGMENT_KEY = "9OnZ6LnDztuZZo4zXfoutEEBB2wftHUH"
- REACT_APP_BASE_URL = "https://api.appsmith.com"
- APP_HOST = "app.appsmith.com"
- REACT_APP_CLIENT_LOG_LEVEL = "error"
-
-[context.release]
- [context.release.environment]
- REACT_APP_ENVIRONMENT = "STAGING"
- REACT_APP_SENTRY_ENVIRONMENT = "Staging"
- REACT_APP_BASE_URL = "https://release.app.appsmith.com"
- APP_HOST = "release.app.appsmith.com"
-
[context.deploy-preview]
[context.deploy-preview.environment]
REACT_APP_ENVIRONMENT = "STAGING"
diff --git a/app/client/package.json b/app/client/package.json
index bc24cf83d9..f2ee5e6319 100644
--- a/app/client/package.json
+++ b/app/client/package.json
@@ -16,7 +16,7 @@
"@blueprintjs/timezone": "^3.6.0",
"@craco/craco": "^5.7.0",
"@draft-js-plugins/editor": "^4.1.0",
- "@draft-js-plugins/mention": "^4.3.2",
+ "@draft-js-plugins/mention": "^4.5.1",
"@fusioncharts/powercharts": "^3.16.0",
"@github/g-emoji-element": "^1.1.5",
"@manaflair/redux-batch": "^1.0.0",
@@ -40,10 +40,12 @@
"@types/react-table": "^7.0.13",
"@types/styled-components": "^5.1.3",
"@types/tinycolor2": "^1.4.2",
+ "@types/zipcelx": "^1.5.0",
"@uppy/core": "^1.16.0",
"@uppy/dashboard": "^1.16.0",
"@uppy/file-input": "^1.4.22",
"@uppy/google-drive": "^1.5.22",
+ "@uppy/image-editor": "^0.2.4",
"@uppy/onedrive": "^1.1.22",
"@uppy/react": "^1.11.2",
"@uppy/url": "^1.5.16",
@@ -61,11 +63,11 @@
"deep-diff": "^1.0.2",
"downloadjs": "^1.4.7",
"draft-js": "^0.11.7",
- "emoji-picker-react": "^3.4.2",
+ "emoji-mart": "^3.0.1",
"eslint": "^7.11.0",
"fast-deep-equal": "^3.1.1",
"fast-xml-parser": "^3.17.5",
- "flow-bin": "^0.91.0",
+ "flow-bin": "^0.148.0",
"fuse.js": "^3.4.5",
"fusioncharts": "^3.16.0",
"history": "^4.10.1",
@@ -73,13 +75,14 @@
"immer": "^8.0.1",
"instantsearch.css": "^7.4.2",
"instantsearch.js": "^4.4.1",
- "interweave": "^12.1.1",
- "interweave-autolink": "^4.0.1",
+ "interweave": "^12.7.2",
+ "interweave-autolink": "^4.4.2",
"js-sha256": "^0.9.0",
+ "jshint": "^2.12.0",
"json-fn": "^1.1.1",
"lint-staged": "^9.2.5",
"localforage": "^1.7.3",
- "lodash": "^4.17.19",
+ "lodash": "^4.17.21",
"lodash-es": "4.17.14",
"lodash-move": "^1.1.1",
"loglevel": "^1.6.7",
@@ -88,12 +91,13 @@
"moment": "^2.24.0",
"moment-timezone": "^0.5.27",
"nanoid": "^2.0.4",
- "node-sass": "^4.11.0",
+ "node-forge": "^0.10.0",
+ "node-sass": "^6.0.1",
"normalizr": "^3.3.0",
"path-to-regexp": "^6.2.0",
"popper.js": "^1.15.0",
"prettier": "^1.18.2",
- "prismjs": "^1.23.0",
+ "prismjs": "^1.24.0",
"rc-pagination": "^3.1.3",
"re-reselect": "^3.4.0",
"react": "^16.12.0",
@@ -105,6 +109,7 @@
"react-dnd-touch-backend": "^9.4.0",
"react-dom": "^16.7.0",
"react-google-maps": "^9.4.5",
+ "react-google-recaptcha": "^2.1.0",
"react-helmet": "^5.2.1",
"react-infinite-scroller": "^1.2.4",
"react-instantsearch-dom": "^6.4.0",
@@ -112,6 +117,7 @@
"react-mentions": "^4.1.1",
"react-paginating": "^1.4.0",
"react-player": "^2.3.1",
+ "react-rating": "^2.0.5",
"react-redux": "^7.1.3",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
@@ -123,6 +129,7 @@
"react-toastify": "^5.5.0",
"react-transition-group": "^4.3.0",
"react-use-gesture": "^7.0.4",
+ "react-virtuoso": "^1.9.0",
"react-window": "^1.8.6",
"react-zoom-pan-pinch": "^1.6.1",
"redux": "^4.0.1",
@@ -144,12 +151,12 @@
"typescript": "^4.1.3",
"unescape-js": "^1.1.4",
"url-search-params-polyfill": "^8.0.0",
- "worker-loader": "^3.0.2"
+ "worker-loader": "^3.0.2",
+ "zipcelx": "^1.6.2"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
- "start": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start",
- "start-m1": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=0.0.0.0 craco start",
+ "start": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT REACT_APP_CLIENT_LOG_LEVEL=debug HOST=dev.appsmith.com craco start",
"build": "./build.sh",
"build-local": "craco --max-old-space-size=4096 build --config craco.build.config.js",
"build-staging": "REACT_APP_ENVIRONMENT=STAGING craco --max-old-space-size=4096 build --config craco.build.config.js",
@@ -161,7 +168,8 @@
"test:unit": "$(npm bin)/jest -b --colors --no-cache --coverage --collectCoverage=true --coverageDirectory='../../' --coverageReporters='json-summary'",
"test:jest": "$(npm bin)/jest --watch",
"storybook": "start-storybook -p 9009 -s public",
- "build-storybook": "build-storybook -s public"
+ "build-storybook": "build-storybook -s public",
+ "postinstall": "patch-package"
},
"resolution": {
"jest": "24.8.0"
@@ -195,15 +203,20 @@
"@types/deep-diff": "^1.0.0",
"@types/downloadjs": "^1.4.2",
"@types/draft-js": "^0.11.1",
+ "@types/emoji-mart": "^3.0.4",
"@types/jest": "^24.0.22",
+ "@types/jshint": "^2.12.0",
"@types/marked": "^1.2.2",
+ "@types/node-forge": "^0.10.0",
"@types/react-beautiful-dnd": "^11.0.4",
+ "@types/react-google-recaptcha": "^2.1.1",
"@types/react-select": "^3.0.5",
"@types/react-tabs": "^2.3.1",
"@types/react-test-renderer": "^17.0.1",
"@types/react-window": "^1.8.2",
"@types/redux-form": "^8.1.9",
"@types/redux-mock-store": "^1.0.2",
+ "@types/resize-observer-browser": "^0.1.5",
"@types/styled-system": "^5.1.9",
"@types/tern": "0.22.0",
"@types/toposort": "^2.0.3",
@@ -224,6 +237,7 @@
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.3",
"eslint-plugin-react-hooks": "^2.3.0",
+ "eslint-plugin-sort-destructure-keys": "^1.3.5",
"factory.ts": "^0.5.1",
"jest-canvas-mock": "^2.3.1",
"mocha": "^7.1.0",
@@ -231,6 +245,8 @@
"mochawesome": "^5.0.0",
"mochawesome-report-generator": "^4.1.0",
"msw": "^0.28.0",
+ "patch-package": "^6.4.7",
+ "postinstall-postinstall": "^2.1.0",
"raw-loader": "^4.0.2",
"react-docgen-typescript-loader": "^3.6.0",
"react-is": "^16.12.0",
diff --git a/app/client/patches/@blueprintjs+core+3.36.0.patch b/app/client/patches/@blueprintjs+core+3.36.0.patch
new file mode 100644
index 0000000000..3342cbfefc
--- /dev/null
+++ b/app/client/patches/@blueprintjs+core+3.36.0.patch
@@ -0,0 +1,22 @@
+diff --git a/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js b/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js
+index 84f03fa..5e5488a 100644
+--- a/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js
++++ b/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js
+@@ -188,7 +188,16 @@ var EditableText = /** @class */ (function (_super) {
+ if (this.state.isEditing && !prevState.isEditing) {
+ (_b = (_a = this.props).onEdit) === null || _b === void 0 ? void 0 : _b.call(_a, this.state.value);
+ }
+- this.updateInputDimensions();
++ // updateInputDimensions is an expensive method. Call it only when the props
++ // it depends on change
++ if (this.state.value !== prevState.value ||
++ this.props.alwaysRenderInput !== prevProps.alwaysRenderInput ||
++ this.props.maxLines !== prevProps.maxLines ||
++ this.props.minLines !== prevProps.minLines ||
++ this.props.minWidth !== prevProps.minWidth ||
++ this.props.multiline !== prevProps.multiline) {
++ this.updateInputDimensions();
++ }
+ };
+ EditableText.prototype.renderInput = function (value) {
+ var _a = this.props, disabled = _a.disabled, maxLength = _a.maxLength, multiline = _a.multiline, type = _a.type, placeholder = _a.placeholder;
diff --git a/app/client/public/index.html b/app/client/public/index.html
index 558f9ccca9..a022cc7b1c 100755
--- a/app/client/public/index.html
+++ b/app/client/public/index.html
@@ -33,6 +33,11 @@
You need to enable JavaScript to run this app.
+
+
diff --git a/app/client/src/AppRouter.tsx b/app/client/src/AppRouter.tsx
index 1ba02d5b77..430762a01b 100644
--- a/app/client/src/AppRouter.tsx
+++ b/app/client/src/AppRouter.tsx
@@ -13,6 +13,7 @@ import {
getApplicationViewerPageURL,
ORG_URL,
SIGN_UP_URL,
+ SIGNUP_SUCCESS_URL,
USER_AUTH_URL,
USERS_URL,
PROFILE,
@@ -47,7 +48,8 @@ function changeAppBackground(currentTheme: any) {
if (
trimTrailingSlash(window.location.pathname) === "/applications" ||
window.location.pathname.indexOf("/settings/") !== -1 ||
- trimTrailingSlash(window.location.pathname) === "/profile"
+ trimTrailingSlash(window.location.pathname) === "/profile" ||
+ trimTrailingSlash(window.location.pathname) === "/signup-success"
) {
document.body.style.backgroundColor =
currentTheme.colors.homepageBackground;
@@ -101,6 +103,11 @@ class AppRouter extends React.Component {
exact
path={APPLICATIONS_URL}
/>
+
{
};
};
+export const runActionViaShortcut = () => {
+ return {
+ type: ReduxActionTypes.RUN_ACTION_SHORTCUT_REQUEST,
+ };
+};
+
export const runAction = (id: string, paginationField?: PaginationField) => {
return {
type: ReduxActionTypes.RUN_ACTION_REQUEST,
@@ -117,7 +123,11 @@ export const updateActionSuccess = (payload: { data: Action }) => {
};
};
-export const deleteAction = (payload: { id: string; name: string }) => {
+export const deleteAction = (payload: {
+ id: string;
+ name: string;
+ onSuccess?: () => void;
+}) => {
return {
type: ReduxActionTypes.DELETE_ACTION_INIT,
payload,
@@ -233,6 +243,12 @@ export const updateActionProperty = (
});
};
+export const executePageLoadActionsComplete = () => {
+ return {
+ type: ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS_COMPLETE,
+ };
+};
+
export const setActionsToExecuteOnPageLoad = (
actions: Array<{
executeOnLoad: boolean;
diff --git a/app/client/src/actions/apiPaneActions.ts b/app/client/src/actions/apiPaneActions.ts
index ccb0096922..e4bf0466f7 100644
--- a/app/client/src/actions/apiPaneActions.ts
+++ b/app/client/src/actions/apiPaneActions.ts
@@ -70,3 +70,11 @@ export const updateBodyContentType = (
type: ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE,
payload: { title, apiId },
});
+
+export const redirectToNewIntegrations = (
+ applicationId: string,
+ pageId: string,
+): ReduxAction<{ applicationId: string; pageId: string }> => ({
+ type: ReduxActionTypes.REDIRECT_TO_NEW_INTEGRATIONS,
+ payload: { applicationId, pageId },
+});
diff --git a/app/client/src/actions/applicationActions.ts b/app/client/src/actions/applicationActions.ts
index 4578f5fc11..e4cf360f65 100644
--- a/app/client/src/actions/applicationActions.ts
+++ b/app/client/src/actions/applicationActions.ts
@@ -1,6 +1,9 @@
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
import { APP_MODE } from "../reducers/entityReducers/appReducer";
-import { UpdateApplicationPayload } from "api/ApplicationApi";
+import {
+ UpdateApplicationPayload,
+ ImportApplicationRequest,
+} from "api/ApplicationApi";
export const setDefaultApplicationPageSuccess = (
pageId: string,
@@ -77,6 +80,13 @@ export const duplicateApplication = (applicationId: string) => {
};
};
+export const importApplication = (appDetails: ImportApplicationRequest) => {
+ return {
+ type: ReduxActionTypes.IMPORT_APPLICATION_INIT,
+ payload: appDetails,
+ };
+};
+
export const getAllApplications = () => {
return {
type: ReduxActionTypes.GET_ALL_APPLICATION_INIT,
@@ -88,3 +98,8 @@ export const resetCurrentApplication = () => {
type: ReduxActionTypes.RESET_CURRENT_APPLICATION,
};
};
+
+export const setShowAppInviteUsersDialog = (payload: boolean) => ({
+ type: ReduxActionTypes.SET_SHOW_APP_INVITE_USERS_MODAL,
+ payload,
+});
diff --git a/app/client/src/actions/canvasSelectionActions.ts b/app/client/src/actions/canvasSelectionActions.ts
new file mode 100644
index 0000000000..e9030b14e2
--- /dev/null
+++ b/app/client/src/actions/canvasSelectionActions.ts
@@ -0,0 +1,22 @@
+import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
+import { SelectedArenaDimensions } from "pages/common/CanvasSelectionArena";
+
+export const setCanvasSelectionStateAction = (start: boolean) => {
+ return {
+ type: start
+ ? ReduxActionTypes.START_CANVAS_SELECTION
+ : ReduxActionTypes.STOP_CANVAS_SELECTION,
+ };
+};
+
+export const selectAllWidgetsInAreaAction = (
+ selectionArena: SelectedArenaDimensions,
+ snapToNextColumn: boolean,
+ snapToNextRow: boolean,
+ isMultiSelect: boolean,
+): ReduxAction => {
+ return {
+ type: ReduxActionTypes.SELECT_WIDGETS_IN_AREA,
+ payload: { selectionArena, snapToNextColumn, snapToNextRow, isMultiSelect },
+ };
+};
diff --git a/app/client/src/actions/commentActions.ts b/app/client/src/actions/commentActions.ts
index 4554af5f78..622582fcea 100644
--- a/app/client/src/actions/commentActions.ts
+++ b/app/client/src/actions/commentActions.ts
@@ -1,5 +1,7 @@
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { COMMENT_EVENTS_CHANNEL } from "constants/CommentConstants";
+import { options as filterOptions } from "comments/AppComments/AppCommentsFilterPopover";
+
import {
CommentThread,
CommentEventPayload,
@@ -11,21 +13,12 @@ import {
NewCommentThreadPayload,
} from "entities/Comments/CommentsInterfaces";
+import { RawDraftContentState } from "draft-js";
+
export const setCommentThreadsRequest = () => ({
type: ReduxActionTypes.SET_COMMENT_THREADS_REQUEST,
});
-// todo remove (for dev)
-export const setCommentThreadsSuccess = (payload: any) => ({
- type: ReduxActionTypes.SET_COMMENT_THREADS_SUCCESS,
- payload,
-});
-
-// todo remove (for dev)
-export const initCommentThreads = () => ({
- type: ReduxActionTypes.INIT_COMMENT_THREADS,
-});
-
export const commentEvent = (payload: CommentEventPayload) => ({
type: COMMENT_EVENTS_CHANNEL,
payload,
@@ -81,14 +74,6 @@ export const setCommentMode = (payload: boolean) => ({
payload,
});
-export const setIsCommentThreadVisible = (payload: {
- commentThreadId: string;
- isVisible: boolean;
-}) => ({
- type: ReduxActionTypes.SET_IS_COMMENT_THREAD_VISIBLE,
- payload,
-});
-
export const fetchApplicationCommentsRequest = () => ({
type: ReduxActionTypes.FETCH_APPLICATION_COMMENTS_REQUEST,
});
@@ -130,7 +115,10 @@ export const updateCommentThreadEvent = (payload: Partial) => ({
payload,
});
-export const pinCommentThreadRequest = (payload: { threadId: string }) => ({
+export const pinCommentThreadRequest = (payload: {
+ threadId: string;
+ pin: boolean;
+}) => ({
type: ReduxActionTypes.PIN_COMMENT_THREAD_REQUEST,
payload,
});
@@ -158,3 +146,117 @@ export const deleteCommentSuccess = (payload: {
type: ReduxActionTypes.DELETE_COMMENT_SUCCESS,
payload,
});
+
+export const setShouldShowResolvedComments = (payload: boolean) => ({
+ type: ReduxActionTypes.SET_SHOULD_SHOW_RESOLVED_COMMENTS,
+ payload,
+});
+
+export const setAppCommentsFilter = (
+ payload: typeof filterOptions[number]["value"],
+) => ({
+ type: ReduxActionTypes.SET_APP_COMMENTS_FILTER,
+ payload,
+});
+
+export const resetVisibleThread = (threadId?: string) => ({
+ type: ReduxActionTypes.RESET_VISIBLE_THREAD,
+ payload: threadId,
+});
+
+export const setVisibleThread = (threadId: string) => ({
+ type: ReduxActionTypes.SET_VISIBLE_THREAD,
+ payload: threadId,
+});
+
+export const markThreadAsReadRequest = (threadId: string) => ({
+ type: ReduxActionTypes.MARK_THREAD_AS_READ_REQUEST,
+ payload: { threadId },
+});
+
+export const editCommentRequest = ({
+ body,
+ commentId,
+ commentThreadId,
+}: {
+ commentThreadId: string;
+ commentId: string;
+ body: RawDraftContentState;
+}) => ({
+ type: ReduxActionTypes.EDIT_COMMENT_REQUEST,
+ payload: {
+ body,
+ commentId,
+ commentThreadId,
+ },
+});
+
+export const updateCommentSuccess = (payload: {
+ comment: Comment;
+ commentThreadId: string;
+}) => ({
+ type: ReduxActionTypes.EDIT_COMMENT_SUCCESS,
+ payload,
+});
+
+export const deleteCommentThreadRequest = (commentThreadId: string) => ({
+ type: ReduxActionTypes.DELETE_THREAD_REQUEST,
+ payload: commentThreadId,
+});
+
+export const deleteCommentThreadSuccess = (payload: {
+ commentThreadId: string;
+ appId: string;
+}) => ({
+ type: ReduxActionTypes.DELETE_THREAD_SUCCESS,
+ payload,
+});
+
+export const addCommentReaction = (payload: {
+ emoji: string;
+ commentId: string;
+}) => ({
+ type: ReduxActionTypes.ADD_COMMENT_REACTION,
+ payload,
+});
+
+export const removeCommentReaction = (payload: {
+ emoji: string;
+ commentId: string;
+}) => ({
+ type: ReduxActionTypes.REMOVE_COMMENT_REACTION,
+ payload,
+});
+
+export const updateCommentEvent = (payload: Comment) => ({
+ type: ReduxActionTypes.UPDATE_COMMENT_EVENT,
+ payload,
+});
+
+export const showCommentsIntroCarousel = () => ({
+ type: ReduxActionTypes.SHOW_COMMENTS_INTRO_CAROUSEL,
+ payload: undefined,
+});
+
+export const hideCommentsIntroCarousel = () => ({
+ type: ReduxActionTypes.HIDE_COMMENTS_INTRO_CAROUSEL,
+ payload: undefined,
+});
+
+export const setAreCommentsEnabled = (flag: boolean) => ({
+ type: ReduxActionTypes.SET_ARE_COMMENTS_ENABLED,
+ payload: flag,
+});
+
+export const fetchUnreadCommentThreadsCountSuccess = (count?: number) => ({
+ type: ReduxActionTypes.FETCH_UNREAD_COMMENT_THREADS_COUNT_SUCCESS,
+ payload: count,
+});
+
+export const incrementThreadUnreadCount = () => ({
+ type: ReduxActionTypes.INCREMENT_COMMENT_THREAD_UNREAD_COUNT,
+});
+
+export const decrementThreadUnreadCount = () => ({
+ type: ReduxActionTypes.DECREMENT_COMMENT_THREAD_UNREAD_COUNT,
+});
diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts
index 6fa834fd62..65f08d4582 100644
--- a/app/client/src/actions/datasourceActions.ts
+++ b/app/client/src/actions/datasourceActions.ts
@@ -137,6 +137,29 @@ export const fetchDatasources = () => {
};
};
+export const fetchMockDatasources = () => {
+ return {
+ type: ReduxActionTypes.FETCH_MOCK_DATASOURCES_INIT,
+ };
+};
+
+export const addMockDatasourceToOrg = (
+ name: string,
+ organizationId: string,
+ pluginId: string,
+ packageName: string,
+): ReduxAction<{
+ name: string;
+ organizationId: string;
+ pluginId: string;
+ packageName: string;
+}> => {
+ return {
+ type: ReduxActionTypes.ADD_MOCK_DATASOURCES_INIT,
+ payload: { name, packageName, pluginId, organizationId },
+ };
+};
+
export const initDatasourcePane = (
pluginType: string,
urlId?: string,
diff --git a/app/client/src/actions/debuggerActions.ts b/app/client/src/actions/debuggerActions.ts
index a0ca54e4a7..ce7beb04cd 100644
--- a/app/client/src/actions/debuggerActions.ts
+++ b/app/client/src/actions/debuggerActions.ts
@@ -1,5 +1,15 @@
import { ReduxActionTypes } from "constants/ReduxActionConstants";
-import { Message } from "entities/AppsmithConsole";
+import { Message, ENTITY_TYPE } from "entities/AppsmithConsole";
+import { EventName } from "utils/AnalyticsUtil";
+
+export interface LogDebuggerErrorAnalyticsPayload {
+ entityName: string;
+ entityId: string;
+ entityType: ENTITY_TYPE;
+ eventName: EventName;
+ propertyPath: string;
+ errorMessages: { message: string }[];
+}
export const debuggerLogInit = (payload: Message) => ({
type: ReduxActionTypes.DEBUGGER_LOG_INIT,
@@ -29,3 +39,11 @@ export const updateErrorLog = (payload: Message) => ({
type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOG,
payload,
});
+
+// Only used for analytics
+export const logDebuggerErrorAnalytics = (
+ payload: LogDebuggerErrorAnalyticsPayload,
+) => ({
+ type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
+ payload,
+});
diff --git a/app/client/src/actions/notificationActions.ts b/app/client/src/actions/notificationActions.ts
new file mode 100644
index 0000000000..209c04504a
--- /dev/null
+++ b/app/client/src/actions/notificationActions.ts
@@ -0,0 +1,58 @@
+import { ReduxActionTypes } from "constants/ReduxActionConstants";
+import { AppsmithNotification } from "entities/Notification";
+
+export const fetchNotificationsRequest = (beforeTime?: string) => ({
+ type: ReduxActionTypes.FETCH_NOTIFICATIONS_REQUEST,
+ payload: beforeTime,
+});
+
+export const fetchNotificationsSuccess = (payload: {
+ notifications: Array;
+}) => ({
+ type: ReduxActionTypes.FETCH_NOTIFICATIONS_SUCCESS,
+ payload,
+});
+
+export const newNotificationEvent = (payload: Notification) => ({
+ type: ReduxActionTypes.NEW_NOTIFICATION_EVENT,
+ payload,
+});
+
+export const setIsNotificationsListVisible = (payload: boolean) => ({
+ type: ReduxActionTypes.SET_IS_NOTIFICATIONS_LIST_VISIBLE,
+ payload,
+});
+
+export const markAllNotificationsAsReadRequest = () => ({
+ type: ReduxActionTypes.MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST,
+});
+
+export const markAllNotificationsAsReadSuccess = () => ({
+ type: ReduxActionTypes.MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS,
+});
+
+export const resetNotifications = (payload: {
+ notifications: Array;
+}) => ({
+ type: ReduxActionTypes.RESET_NOTIFICATIONS,
+ payload,
+});
+
+export const fetchUnreadNotificationsCountRequest = () => ({
+ type: ReduxActionTypes.FETCH_UNREAD_NOTIFICATIONS_COUNT_REQUEST,
+});
+
+export const fetchUnreadNotificationsCountSuccess = (payload: number) => ({
+ type: ReduxActionTypes.FETCH_UNREAD_NOTIFICATIONS_COUNT_SUCCESS,
+ payload,
+});
+
+export const markNotificationAsReadRequest = (payload: string) => ({
+ type: ReduxActionTypes.MARK_NOTIFICATION_AS_READ_REQUEST,
+ payload,
+});
+
+export const markNotificationAsReadSuccess = (payload: string) => ({
+ type: ReduxActionTypes.MARK_NOTIFICATION_AS_READ_SUCCESS,
+ payload,
+});
diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx
index f454c0349d..177661dd01 100644
--- a/app/client/src/actions/pageActions.tsx
+++ b/app/client/src/actions/pageActions.tsx
@@ -30,11 +30,15 @@ export const fetchPageList = (
};
};
-export const fetchPage = (pageId: string): ReduxAction => {
+export const fetchPage = (
+ pageId: string,
+ isFirstLoad = false,
+): ReduxAction => {
return {
type: ReduxActionTypes.FETCH_PAGE_INIT,
payload: {
id: pageId,
+ isFirstLoad,
},
};
};
@@ -47,9 +51,13 @@ export const fetchPublishedPage = (pageId: string, bustCache = false) => ({
},
});
-export const fetchPageSuccess = (): ReduxActionWithoutPayload => {
+export const fetchPageSuccess = (
+ postEvalActions: Array | ReduxActionWithoutPayload>,
+): EvaluationReduxAction => {
return {
type: ReduxActionTypes.FETCH_PAGE_SUCCESS,
+ postEvalActions,
+ payload: undefined,
};
};
@@ -75,6 +83,11 @@ export const initCanvasLayout = (
};
};
+export const setLastUpdatedTime = (payload: number): ReduxAction => ({
+ type: ReduxActionTypes.SET_LAST_UPDATED_TIME,
+ payload,
+});
+
export const savePageSuccess = (payload: SavePageResponse) => {
return {
type: ReduxActionTypes.SAVE_PAGE_SUCCESS,
@@ -104,9 +117,10 @@ export const updateAndSaveLayout = (
};
};
-export const saveLayout = () => {
+export const saveLayout = (isRetry?: boolean) => {
return {
type: ReduxActionTypes.SAVE_PAGE_INIT,
+ payload: { isRetry },
};
};
@@ -203,6 +217,12 @@ export type WidgetDelete = {
isShortcut?: boolean;
};
+export type MultipleWidgetDeletePayload = {
+ widgetIds: string[];
+ disallowUndo?: boolean;
+ isShortcut?: boolean;
+};
+
export type WidgetResize = {
widgetId: string;
leftColumn: number;
diff --git a/app/client/src/actions/tourActions.ts b/app/client/src/actions/tourActions.ts
new file mode 100644
index 0000000000..b32c709a4a
--- /dev/null
+++ b/app/client/src/actions/tourActions.ts
@@ -0,0 +1,22 @@
+import { ReduxActionTypes } from "constants/ReduxActionConstants";
+import { TourType } from "entities/Tour";
+
+export const setActiveTour = (tourType: TourType) => ({
+ type: ReduxActionTypes.SET_ACTIVE_TOUR,
+ payload: tourType,
+});
+
+export const resetActiveTour = () => ({
+ type: ReduxActionTypes.RESET_ACTIVE_TOUR,
+ payload: undefined,
+});
+
+export const setActiveTourIndex = (index: number) => ({
+ type: ReduxActionTypes.SET_ACTIVE_TOUR_INDEX,
+ payload: index,
+});
+
+export const proceedToNextTourStep = () => ({
+ type: ReduxActionTypes.PROCEED_TO_NEXT_TOUR_STEP,
+ payload: undefined,
+});
diff --git a/app/client/src/actions/userActions.ts b/app/client/src/actions/userActions.ts
index 85f1fb7b97..d446a5755c 100644
--- a/app/client/src/actions/userActions.ts
+++ b/app/client/src/actions/userActions.ts
@@ -65,3 +65,25 @@ export const updateUserDetails = (payload: UpdateUserRequest) => ({
type: ReduxActionTypes.UPDATE_USER_DETAILS_INIT,
payload,
});
+
+export const updatePhoto = (payload: {
+ file: File;
+ callback?: () => void;
+}) => ({
+ type: ReduxActionTypes.UPLOAD_PROFILE_PHOTO,
+ payload,
+});
+
+export const removePhoto = (callback: () => void) => ({
+ type: ReduxActionTypes.REMOVE_PROFILE_PHOTO,
+ payload: { callback },
+});
+
+export const leaveOrganization = (orgId: string) => {
+ return {
+ type: ReduxActionTypes.LEAVE_ORG_INIT,
+ payload: {
+ orgId,
+ },
+ };
+};
diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx
index 03e1f59548..e45f15e366 100644
--- a/app/client/src/actions/widgetActions.tsx
+++ b/app/client/src/actions/widgetActions.tsx
@@ -63,13 +63,6 @@ export const focusWidget = (
payload: { widgetId },
});
-export const selectWidget = (
- widgetId?: string,
-): ReduxAction<{ widgetId?: string }> => ({
- type: ReduxActionTypes.SELECT_WIDGET,
- payload: { widgetId },
-});
-
export const showModal = (id: string) => {
return {
type: ReduxActionTypes.SHOW_MODAL,
diff --git a/app/client/src/actions/widgetSelectionActions.ts b/app/client/src/actions/widgetSelectionActions.ts
new file mode 100644
index 0000000000..c323a53128
--- /dev/null
+++ b/app/client/src/actions/widgetSelectionActions.ts
@@ -0,0 +1,71 @@
+import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
+import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
+
+export const selectWidgetAction = (
+ widgetId?: string,
+ isMultiSelect?: boolean,
+): ReduxAction<{ widgetId?: string; isMultiSelect?: boolean }> => ({
+ type: ReduxActionTypes.SELECT_WIDGET,
+ payload: { widgetId, isMultiSelect },
+});
+
+export const selectWidgetInitAction = (
+ widgetId?: string,
+ isMultiSelect?: boolean,
+): ReduxAction<{ widgetId?: string; isMultiSelect?: boolean }> => ({
+ type: ReduxActionTypes.SELECT_WIDGET_INIT,
+ payload: { widgetId, isMultiSelect },
+});
+
+export const selectMultipleWidgetsAction = (
+ widgetIds?: string[],
+): ReduxAction<{ widgetIds?: string[] }> => {
+ return {
+ type: ReduxActionTypes.SELECT_MULTIPLE_WIDGETS,
+ payload: { widgetIds },
+ };
+};
+
+export const silentAddSelectionsAction = (
+ widgetIds?: string[],
+): ReduxAction<{ widgetIds?: string[] }> => {
+ return {
+ type: ReduxActionTypes.SELECT_WIDGETS,
+ payload: { widgetIds },
+ };
+};
+
+export const deselectMultipleWidgetsAction = (
+ widgetIds?: string[],
+): ReduxAction<{ widgetIds?: string[] }> => {
+ return {
+ type: ReduxActionTypes.DESELECT_WIDGETS,
+ payload: { widgetIds },
+ };
+};
+
+export const selectAllWidgetsInCanvasInitAction = (
+ canvasId = MAIN_CONTAINER_WIDGET_ID,
+): ReduxAction<{ canvasId: string }> => {
+ return {
+ type: ReduxActionTypes.SELECT_ALL_WIDGETS_IN_CANVAS_INIT,
+ payload: {
+ canvasId,
+ },
+ };
+};
+
+export const selectMultipleWidgetsInitAction = (widgetIds: string[]) => {
+ return {
+ type: ReduxActionTypes.SELECT_MULTIPLE_WIDGETS_INIT,
+ payload: { widgetIds },
+ };
+};
+
+export const shiftSelectWidgetsEntityExplorerInitAction = (
+ widgetId: string,
+ siblingWidgets: string[],
+): ReduxAction<{ widgetId: string; siblingWidgets: string[] }> => ({
+ type: ReduxActionTypes.SHIFT_SELECT_WIDGET_INIT,
+ payload: { widgetId, siblingWidgets },
+});
diff --git a/app/client/src/api/Api.ts b/app/client/src/api/Api.ts
index bc1cdfbd2a..32dfc0f65c 100644
--- a/app/client/src/api/Api.ts
+++ b/app/client/src/api/Api.ts
@@ -68,6 +68,22 @@ class Api {
);
}
+ static patch(
+ url: string,
+ body?: any,
+ queryParams?: any,
+ config: Partial = {},
+ ) {
+ return axiosInstance.patch(
+ url + convertObjectToQueryParams(queryParams),
+ body,
+ {
+ ...apiRequestConfig,
+ ...config,
+ },
+ );
+ }
+
static delete(
url: string,
queryParams?: any,
@@ -80,6 +96,6 @@ class Api {
}
}
-export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
+export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
export default Api;
diff --git a/app/client/src/api/ApiUtils.ts b/app/client/src/api/ApiUtils.ts
index 2da9b22cf9..81f94cca20 100644
--- a/app/client/src/api/ApiUtils.ts
+++ b/app/client/src/api/ApiUtils.ts
@@ -10,10 +10,11 @@ import {
ERROR_CODES,
SERVER_ERROR_CODES,
} from "constants/ApiConstants";
-import history from "utils/history";
-import { AUTH_LOGIN_URL } from "constants/routes";
import log from "loglevel";
import { ActionExecutionResponse } from "api/ActionAPI";
+import store from "store";
+import { logoutUser } from "actions/userActions";
+import { AUTH_LOGIN_URL } from "constants/routes";
const executeActionRegex = /actions\/execute/;
const timeoutErrorRegex = /timeout of (\d+)ms exceeded/;
@@ -100,10 +101,13 @@ export const apiFailureResponseInterceptor = (error: any) => {
const currentUrl = `${window.location.href}`;
if (error.response.status === API_STATUS_CODES.REQUEST_NOT_AUTHORISED) {
// Redirect to login and set a redirect url.
- history.replace({
- pathname: AUTH_LOGIN_URL,
- search: `redirectUrl=${currentUrl}`,
- });
+ store.dispatch(
+ logoutUser({
+ redirectURL: `${AUTH_LOGIN_URL}?redirectUrl=${encodeURIComponent(
+ currentUrl,
+ )}`,
+ }),
+ );
return Promise.reject({
code: ERROR_CODES.REQUEST_NOT_AUTHORISED,
message: "Unauthorized. Redirecting to login page...",
diff --git a/app/client/src/api/ApplicationApi.tsx b/app/client/src/api/ApplicationApi.tsx
index 0347802ff0..21349820f9 100644
--- a/app/client/src/api/ApplicationApi.tsx
+++ b/app/client/src/api/ApplicationApi.tsx
@@ -31,11 +31,12 @@ export interface ApplicationResponsePayload {
pages?: ApplicationPagePayload[];
appIsExample: boolean;
appLayout?: AppLayoutConfig;
+ unreadCommentThreads?: number;
}
-// export interface FetchApplicationResponse extends ApiResponse {
-// data: ApplicationResponsePayload & { pages: ApplicationPagePayload[] };
-// }
+export interface FetchApplicationResponse extends ApiResponse {
+ data: ApplicationResponsePayload & { pages: ApplicationPagePayload[] };
+}
export interface FetchApplicationsResponse extends ApiResponse {
data: Array;
@@ -119,6 +120,13 @@ export interface FetchUsersApplicationsOrgsResponse extends ApiResponse {
};
}
+export interface ImportApplicationRequest {
+ orgId: string;
+ applicationFile?: File;
+ progress?: (progressEvent: ProgressEvent) => void;
+ onSuccessCallback?: () => void;
+}
+
class ApplicationApi extends Api {
static baseURL = "v1/applications/";
static publishURLPath = (applicationId: string) => `publish/${applicationId}`;
@@ -147,13 +155,13 @@ class ApplicationApi extends Api {
static fetchApplication(
applicationId: string,
- ): AxiosPromise {
+ ): AxiosPromise {
return Api.get(ApplicationApi.baseURL + applicationId);
}
static fetchApplicationForViewMode(
applicationId: string,
- ): AxiosPromise {
+ ): AxiosPromise {
return Api.get(ApplicationApi.baseURL + `view/${applicationId}`);
}
@@ -212,6 +220,21 @@ class ApplicationApi extends Api {
request.organizationId,
);
}
+
+ static importApplicationToOrg(
+ request: ImportApplicationRequest,
+ ): AxiosPromise {
+ const formData = new FormData();
+ if (request.applicationFile) {
+ formData.append("file", request.applicationFile);
+ }
+ return Api.post("v1/applications/import/" + request.orgId, formData, null, {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ onUploadProgress: request.progress,
+ });
+ }
}
export default ApplicationApi;
diff --git a/app/client/src/api/CommentsAPI.tsx b/app/client/src/api/CommentsAPI.tsx
index ce9fdf00d3..f4efd9307a 100644
--- a/app/client/src/api/CommentsAPI.tsx
+++ b/app/client/src/api/CommentsAPI.tsx
@@ -10,6 +10,8 @@ class CommentsApi extends Api {
static baseURL = "v1/comments";
static getThreadsAPI = `${CommentsApi.baseURL}/threads`;
static getCommentsAPI = CommentsApi.baseURL;
+ static getReactionsAPI = (commentId: string) =>
+ `${CommentsApi.getCommentsAPI}/${commentId}/reactions`;
static createNewThread(
request: CreateCommentThreadRequest,
@@ -33,23 +35,48 @@ class CommentsApi extends Api {
}
static updateCommentThread(
- updateCommentRequest: Partial,
+ updateCommentThreadRequest: Partial,
threadId: string,
): AxiosPromise {
return Api.put(
`${CommentsApi.getThreadsAPI}/${threadId}`,
- updateCommentRequest,
+ updateCommentThreadRequest,
);
}
- static pinCommentThread(threadId: string) {
- console.log(threadId);
- return Promise.resolve();
+ static updateComment(
+ updateCommentRequest: Partial,
+ commentId: string,
+ ): AxiosPromise {
+ return Api.put(
+ `${CommentsApi.getCommentsAPI}/${commentId}`,
+ updateCommentRequest,
+ );
}
static deleteComment(commentId: string): AxiosPromise {
return Api.delete(`${CommentsApi.getCommentsAPI}/${commentId}`);
}
+
+ static deleteCommentThread(threadId: string): AxiosPromise {
+ return Api.delete(`${CommentsApi.getThreadsAPI}/${threadId}`);
+ }
+
+ static addCommentReaction(
+ commentId: string,
+ request: { emoji: string },
+ ): AxiosPromise {
+ return Api.post(CommentsApi.getReactionsAPI(commentId), request);
+ }
+
+ static removeCommentReaction(
+ commentId: string,
+ request: { emoji: string },
+ ): AxiosPromise {
+ return Api.delete(CommentsApi.getReactionsAPI(commentId), null, {
+ data: request,
+ });
+ }
}
export default CommentsApi;
diff --git a/app/client/src/api/DatasourcesApi.ts b/app/client/src/api/DatasourcesApi.ts
index f879711775..8b15c02be8 100644
--- a/app/client/src/api/DatasourcesApi.ts
+++ b/app/client/src/api/DatasourcesApi.ts
@@ -63,6 +63,26 @@ class DatasourcesApi extends API {
DatasourcesApi.url + `/${id}/structure?ignoreCache=${ignoreCache}`,
);
}
+
+ static fetchMockDatasources(): AxiosPromise<
+ GenericApiResponse
+ > {
+ return API.get(DatasourcesApi.url + "/mocks");
+ }
+
+ static addMockDbToDatasources(
+ name: string,
+ organizationId: string,
+ pluginId: string,
+ packageName: string,
+ ): Promise {
+ return API.post(DatasourcesApi.url + `/mocks`, {
+ name,
+ organizationId,
+ pluginId,
+ packageName,
+ });
+ }
}
export default DatasourcesApi;
diff --git a/app/client/src/api/ImportApi.ts b/app/client/src/api/ImportApi.ts
index 310d4c5515..eac6e0ad76 100644
--- a/app/client/src/api/ImportApi.ts
+++ b/app/client/src/api/ImportApi.ts
@@ -14,7 +14,7 @@ class CurlImportApi extends Api {
static curlImportURL = `v1/import`;
static curlImport(request: CurlImportRequest): AxiosPromise {
- const { pageId, name, curl, organizationId } = request;
+ const { curl, name, organizationId, pageId } = request;
return Api.post(CurlImportApi.curlImportURL, curl, {
type: "CURL",
pageId,
diff --git a/app/client/src/api/NotificationsAPI.tsx b/app/client/src/api/NotificationsAPI.tsx
new file mode 100644
index 0000000000..51abaacab7
--- /dev/null
+++ b/app/client/src/api/NotificationsAPI.tsx
@@ -0,0 +1,33 @@
+import { AxiosPromise } from "axios";
+import Api from "./Api";
+import { ApiResponse } from "./ApiResponses";
+
+class NotificationsApi extends Api {
+ static baseURL = "v1/notifications";
+ static markAsReadURL = `${NotificationsApi.baseURL}/isRead`;
+ static markAllAsReadURL = `${NotificationsApi.markAsReadURL}/all`;
+ static fetchUnreadNotificationsCountURL = `${NotificationsApi.baseURL}/count/unread`;
+
+ static fetchNotifications(beforeDate?: string): AxiosPromise {
+ return Api.get(NotificationsApi.baseURL, beforeDate ? { beforeDate } : {});
+ }
+
+ static markAllNotificationsAsRead(): AxiosPromise {
+ return Api.patch(NotificationsApi.markAllAsReadURL, { isRead: true });
+ }
+
+ static fetchUnreadNotificationsCount(): AxiosPromise {
+ return Api.get(NotificationsApi.fetchUnreadNotificationsCountURL);
+ }
+
+ static markNotificationsAsRead(
+ ids: Array,
+ ): AxiosPromise {
+ return Api.patch(NotificationsApi.markAsReadURL, {
+ isRead: true,
+ idList: ids,
+ });
+ }
+}
+
+export default NotificationsApi;
diff --git a/app/client/src/api/PageApi.tsx b/app/client/src/api/PageApi.tsx
index fa229d750c..fdc931d00a 100644
--- a/app/client/src/api/PageApi.tsx
+++ b/app/client/src/api/PageApi.tsx
@@ -7,6 +7,7 @@ import { PageAction } from "constants/AppsmithActionConstants/ActionConstants";
export interface FetchPageRequest {
id: string;
+ isFirstLoad?: boolean;
}
export interface FetchPublishedPageRequest {
diff --git a/app/client/src/api/UserApi.tsx b/app/client/src/api/UserApi.tsx
index 72bdb2b5a8..827dc2f977 100644
--- a/app/client/src/api/UserApi.tsx
+++ b/app/client/src/api/UserApi.tsx
@@ -40,6 +40,10 @@ export interface FetchUserRequest {
id: string;
}
+export interface LeaveOrgRequest {
+ orgId: string;
+}
+
export interface InviteUserRequest {
email: string;
groupIds: string[];
@@ -47,7 +51,8 @@ export interface InviteUserRequest {
}
export interface UpdateUserRequest {
- name: string;
+ name?: string;
+ email?: string;
}
class UserApi extends Api {
@@ -59,8 +64,10 @@ class UserApi extends Api {
static verifyInviteTokenURL = `${UserApi.inviteUserURL}/verify`;
static confirmUserInviteURL = `${UserApi.inviteUserURL}/confirm`;
static addOrgURL = `${UserApi.usersURL}/addOrganization`;
+ static leaveOrgURL = `${UserApi.usersURL}/leaveOrganization`;
static logoutURL = "v1/logout";
static currentUserURL = "v1/users/me";
+ static photoURL = "v1/users/photo";
static createUser(
request: CreateUserRequest,
@@ -117,6 +124,27 @@ class UserApi extends Api {
static logoutUser(): AxiosPromise {
return Api.post(UserApi.logoutURL);
}
+
+ static uploadPhoto(request: { file: File }): AxiosPromise {
+ const formData = new FormData();
+ if (request.file) {
+ formData.append("file", request.file);
+ }
+
+ return Api.post(UserApi.photoURL, formData, null, {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ });
+ }
+
+ static deletePhoto(): AxiosPromise {
+ return Api.delete(UserApi.photoURL);
+ }
+
+ static leaveOrg(request: LeaveOrgRequest): AxiosPromise {
+ return Api.put(UserApi.leaveOrgURL + "/" + request.orgId);
+ }
}
export default UserApi;
diff --git a/app/client/src/assets/icons/ads/app-icons/draghandler.svg b/app/client/src/assets/icons/ads/app-icons/draghandler.svg
index 133b2b6bdb..2b3f4cc694 100644
--- a/app/client/src/assets/icons/ads/app-icons/draghandler.svg
+++ b/app/client/src/assets/icons/ads/app-icons/draghandler.svg
@@ -1,8 +1 @@
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/bell.svg b/app/client/src/assets/icons/ads/bell.svg
new file mode 100644
index 0000000000..e4a36d411c
--- /dev/null
+++ b/app/client/src/assets/icons/ads/bell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/bug.svg b/app/client/src/assets/icons/ads/bug.svg
index 28f60f75af..43864e8a92 100644
--- a/app/client/src/assets/icons/ads/bug.svg
+++ b/app/client/src/assets/icons/ads/bug.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/cancel.svg b/app/client/src/assets/icons/ads/cancel.svg
index 254210469e..d397aa2f65 100644
--- a/app/client/src/assets/icons/ads/cancel.svg
+++ b/app/client/src/assets/icons/ads/cancel.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/card-context-menu.svg b/app/client/src/assets/icons/ads/card-context-menu.svg
index eadc7a9738..e3fe0a28e8 100644
--- a/app/client/src/assets/icons/ads/card-context-menu.svg
+++ b/app/client/src/assets/icons/ads/card-context-menu.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/commentIcon.png b/app/client/src/assets/icons/ads/commentIcon.png
index 2775b7b58d..e6d22c7d5e 100644
Binary files a/app/client/src/assets/icons/ads/commentIcon.png and b/app/client/src/assets/icons/ads/commentIcon.png differ
diff --git a/app/client/src/assets/icons/ads/cross.svg b/app/client/src/assets/icons/ads/cross.svg
index 9589ed4d54..ac7eaaca70 100644
--- a/app/client/src/assets/icons/ads/cross.svg
+++ b/app/client/src/assets/icons/ads/cross.svg
@@ -1,4 +1 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/download.svg b/app/client/src/assets/icons/ads/download.svg
new file mode 100644
index 0000000000..230d2ec50d
--- /dev/null
+++ b/app/client/src/assets/icons/ads/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/expand-more.svg b/app/client/src/assets/icons/ads/expand-more.svg
new file mode 100644
index 0000000000..41a5c0133c
--- /dev/null
+++ b/app/client/src/assets/icons/ads/expand-more.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/client/src/assets/icons/ads/open.svg b/app/client/src/assets/icons/ads/open.svg
index 2b43521e49..1ca1dd170b 100644
--- a/app/client/src/assets/icons/ads/open.svg
+++ b/app/client/src/assets/icons/ads/open.svg
@@ -1,4 +1 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/trending-flat.svg b/app/client/src/assets/icons/ads/trending-flat.svg
new file mode 100644
index 0000000000..01b641efcd
--- /dev/null
+++ b/app/client/src/assets/icons/ads/trending-flat.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/client/src/assets/icons/ads/upload_success.svg b/app/client/src/assets/icons/ads/upload_success.svg
new file mode 100644
index 0000000000..20a260c740
--- /dev/null
+++ b/app/client/src/assets/icons/ads/upload_success.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/wand.svg b/app/client/src/assets/icons/ads/wand.svg
index 5a51055344..824e0a3545 100644
--- a/app/client/src/assets/icons/ads/wand.svg
+++ b/app/client/src/assets/icons/ads/wand.svg
@@ -1,10 +1 @@
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/ads/warning-triangle.svg b/app/client/src/assets/icons/ads/warning-triangle.svg
index 350e33a327..c4807dff5c 100644
--- a/app/client/src/assets/icons/ads/warning-triangle.svg
+++ b/app/client/src/assets/icons/ads/warning-triangle.svg
@@ -1,4 +1 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/chat.svg b/app/client/src/assets/icons/comments/chat.svg
new file mode 100644
index 0000000000..7bb3a13ea4
--- /dev/null
+++ b/app/client/src/assets/icons/comments/chat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/check-oval.svg b/app/client/src/assets/icons/comments/check-oval.svg
index 13fcb0032a..8abc88b134 100644
--- a/app/client/src/assets/icons/comments/check-oval.svg
+++ b/app/client/src/assets/icons/comments/check-oval.svg
@@ -1,4 +1 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/comment-mode-unread-indicator.svg b/app/client/src/assets/icons/comments/comment-mode-unread-indicator.svg
new file mode 100644
index 0000000000..6462f9cb80
--- /dev/null
+++ b/app/client/src/assets/icons/comments/comment-mode-unread-indicator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/commentCursor.png b/app/client/src/assets/icons/comments/commentCursor.png
new file mode 100644
index 0000000000..25adce859f
Binary files /dev/null and b/app/client/src/assets/icons/comments/commentCursor.png differ
diff --git a/app/client/src/assets/icons/comments/context-menu.svg b/app/client/src/assets/icons/comments/context-menu.svg
new file mode 100644
index 0000000000..646ae34044
--- /dev/null
+++ b/app/client/src/assets/icons/comments/context-menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/down-arrow.svg b/app/client/src/assets/icons/comments/down-arrow.svg
new file mode 100644
index 0000000000..6274f608f9
--- /dev/null
+++ b/app/client/src/assets/icons/comments/down-arrow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/edit-mode.svg b/app/client/src/assets/icons/comments/edit-mode.svg
new file mode 100644
index 0000000000..5aea2a20fa
--- /dev/null
+++ b/app/client/src/assets/icons/comments/edit-mode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/emoji.svg b/app/client/src/assets/icons/comments/emoji.svg
index bc123ca1a0..9337c2dede 100644
--- a/app/client/src/assets/icons/comments/emoji.svg
+++ b/app/client/src/assets/icons/comments/emoji.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/eye.svg b/app/client/src/assets/icons/comments/eye.svg
new file mode 100644
index 0000000000..987bf4e7c2
--- /dev/null
+++ b/app/client/src/assets/icons/comments/eye.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/filter.svg b/app/client/src/assets/icons/comments/filter.svg
new file mode 100644
index 0000000000..5d4f05f9de
--- /dev/null
+++ b/app/client/src/assets/icons/comments/filter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/link.svg b/app/client/src/assets/icons/comments/link.svg
index b0f4250ef4..72f68c513b 100644
--- a/app/client/src/assets/icons/comments/link.svg
+++ b/app/client/src/assets/icons/comments/link.svg
@@ -1,4 +1 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/notifications-empty-state.svg b/app/client/src/assets/icons/comments/notifications-empty-state.svg
new file mode 100644
index 0000000000..cbce95b8ec
--- /dev/null
+++ b/app/client/src/assets/icons/comments/notifications-empty-state.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/pen.svg b/app/client/src/assets/icons/comments/pen.svg
new file mode 100644
index 0000000000..80d650514f
--- /dev/null
+++ b/app/client/src/assets/icons/comments/pen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/pin.svg b/app/client/src/assets/icons/comments/pin.svg
index 5ba851dbdc..60301bb220 100644
--- a/app/client/src/assets/icons/comments/pin.svg
+++ b/app/client/src/assets/icons/comments/pin.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/pin_2.svg b/app/client/src/assets/icons/comments/pin_2.svg
index d5ca8f9991..d7d1825d40 100644
--- a/app/client/src/assets/icons/comments/pin_2.svg
+++ b/app/client/src/assets/icons/comments/pin_2.svg
@@ -1,4 +1 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/pin_3.svg b/app/client/src/assets/icons/comments/pin_3.svg
new file mode 100644
index 0000000000..59101bf219
--- /dev/null
+++ b/app/client/src/assets/icons/comments/pin_3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/reaction-2.svg b/app/client/src/assets/icons/comments/reaction-2.svg
new file mode 100644
index 0000000000..91b9cce1af
--- /dev/null
+++ b/app/client/src/assets/icons/comments/reaction-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/reaction.svg b/app/client/src/assets/icons/comments/reaction.svg
new file mode 100644
index 0000000000..433c705a35
--- /dev/null
+++ b/app/client/src/assets/icons/comments/reaction.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/read-pin.svg b/app/client/src/assets/icons/comments/read-pin.svg
new file mode 100644
index 0000000000..24d0fa35eb
--- /dev/null
+++ b/app/client/src/assets/icons/comments/read-pin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/send-button.svg b/app/client/src/assets/icons/comments/send-button.svg
index c524e029bd..246664a879 100644
--- a/app/client/src/assets/icons/comments/send-button.svg
+++ b/app/client/src/assets/icons/comments/send-button.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/toggle-comment-mode.svg b/app/client/src/assets/icons/comments/toggle-comment-mode.svg
index 9df15abe13..7e6bcd4849 100644
--- a/app/client/src/assets/icons/comments/toggle-comment-mode.svg
+++ b/app/client/src/assets/icons/comments/toggle-comment-mode.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/trash.svg b/app/client/src/assets/icons/comments/trash.svg
index b67db540e0..0d0cd882fc 100644
--- a/app/client/src/assets/icons/comments/trash.svg
+++ b/app/client/src/assets/icons/comments/trash.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/unpin.svg b/app/client/src/assets/icons/comments/unpin.svg
new file mode 100644
index 0000000000..0f1c9866c7
--- /dev/null
+++ b/app/client/src/assets/icons/comments/unpin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/comments/unread-pin.svg b/app/client/src/assets/icons/comments/unread-pin.svg
new file mode 100644
index 0000000000..5b731a24e0
--- /dev/null
+++ b/app/client/src/assets/icons/comments/unread-pin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/arrow_forward.svg b/app/client/src/assets/icons/control/arrow_forward.svg
new file mode 100644
index 0000000000..70344d66f5
--- /dev/null
+++ b/app/client/src/assets/icons/control/arrow_forward.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/back.svg b/app/client/src/assets/icons/control/back.svg
index 3c600a22a9..b34278d7d9 100644
--- a/app/client/src/assets/icons/control/back.svg
+++ b/app/client/src/assets/icons/control/back.svg
@@ -1,4 +1 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/cap_dot.svg b/app/client/src/assets/icons/control/cap_dot.svg
new file mode 100644
index 0000000000..9dc534c5ee
--- /dev/null
+++ b/app/client/src/assets/icons/control/cap_dot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/cap_solid.svg b/app/client/src/assets/icons/control/cap_solid.svg
new file mode 100644
index 0000000000..b3c4759250
--- /dev/null
+++ b/app/client/src/assets/icons/control/cap_solid.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/copy2.svg b/app/client/src/assets/icons/control/copy2.svg
new file mode 100644
index 0000000000..f07eade443
--- /dev/null
+++ b/app/client/src/assets/icons/control/copy2.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/client/src/assets/icons/control/cut.svg b/app/client/src/assets/icons/control/cut.svg
new file mode 100644
index 0000000000..24d44a1945
--- /dev/null
+++ b/app/client/src/assets/icons/control/cut.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/client/src/assets/icons/control/divider_cap_all.svg b/app/client/src/assets/icons/control/divider_cap_all.svg
new file mode 100644
index 0000000000..e854c51bab
--- /dev/null
+++ b/app/client/src/assets/icons/control/divider_cap_all.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/divider_cap_left.svg b/app/client/src/assets/icons/control/divider_cap_left.svg
new file mode 100644
index 0000000000..820d7fc5cb
--- /dev/null
+++ b/app/client/src/assets/icons/control/divider_cap_left.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/divider_cap_right.svg b/app/client/src/assets/icons/control/divider_cap_right.svg
new file mode 100644
index 0000000000..ffa6105c39
--- /dev/null
+++ b/app/client/src/assets/icons/control/divider_cap_right.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/download-data-icon.svg b/app/client/src/assets/icons/control/download-data-icon.svg
new file mode 100644
index 0000000000..eb400b1f56
--- /dev/null
+++ b/app/client/src/assets/icons/control/download-data-icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/download-table.svg b/app/client/src/assets/icons/control/download-table.svg
deleted file mode 100755
index 129ee6b5ad..0000000000
--- a/app/client/src/assets/icons/control/download-table.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/filter-icon.svg b/app/client/src/assets/icons/control/filter-icon.svg
old mode 100755
new mode 100644
index 1723d3efe8..4d92889575
--- a/app/client/src/assets/icons/control/filter-icon.svg
+++ b/app/client/src/assets/icons/control/filter-icon.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/group.svg b/app/client/src/assets/icons/control/group.svg
new file mode 100644
index 0000000000..03b7eea423
--- /dev/null
+++ b/app/client/src/assets/icons/control/group.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/client/src/assets/icons/control/line_dashed.svg b/app/client/src/assets/icons/control/line_dashed.svg
new file mode 100644
index 0000000000..34e76b522a
--- /dev/null
+++ b/app/client/src/assets/icons/control/line_dashed.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/line_dotted.svg b/app/client/src/assets/icons/control/line_dotted.svg
new file mode 100644
index 0000000000..99693fd967
--- /dev/null
+++ b/app/client/src/assets/icons/control/line_dotted.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/control/open-new-tab.svg b/app/client/src/assets/icons/control/open-new-tab.svg
index 05d3815c3c..2cf241d021 100644
--- a/app/client/src/assets/icons/control/open-new-tab.svg
+++ b/app/client/src/assets/icons/control/open-new-tab.svg
@@ -1,11 +1 @@
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/menu/binding.svg b/app/client/src/assets/icons/menu/binding.svg
new file mode 100644
index 0000000000..22692a0acd
--- /dev/null
+++ b/app/client/src/assets/icons/menu/binding.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/client/src/assets/icons/menu/new-plus.svg b/app/client/src/assets/icons/menu/new-plus.svg
new file mode 100644
index 0000000000..5016558969
--- /dev/null
+++ b/app/client/src/assets/icons/menu/new-plus.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/client/src/assets/icons/widget/divider.svg b/app/client/src/assets/icons/widget/divider.svg
new file mode 100644
index 0000000000..e3d413a99b
--- /dev/null
+++ b/app/client/src/assets/icons/widget/divider.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/widget/embed.svg b/app/client/src/assets/icons/widget/embed.svg
new file mode 100755
index 0000000000..f56a358be2
--- /dev/null
+++ b/app/client/src/assets/icons/widget/embed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/widget/list.svg b/app/client/src/assets/icons/widget/list.svg
index 87348a0e9e..4e26df872f 100644
--- a/app/client/src/assets/icons/widget/list.svg
+++ b/app/client/src/assets/icons/widget/list.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/client/src/assets/icons/widget/rating.svg b/app/client/src/assets/icons/widget/rating.svg
new file mode 100644
index 0000000000..4b3e80c619
--- /dev/null
+++ b/app/client/src/assets/icons/widget/rating.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/client/src/assets/images/Curl-logo.svg b/app/client/src/assets/images/Curl-logo.svg
index 08f86ba19f..fa01f7bca6 100644
--- a/app/client/src/assets/images/Curl-logo.svg
+++ b/app/client/src/assets/images/Curl-logo.svg
@@ -1 +1,4 @@
-
\ No newline at end of file
+
+
+
+
diff --git a/app/client/src/assets/images/InspectElement.svg b/app/client/src/assets/images/InspectElement.svg
new file mode 100644
index 0000000000..7c74deff90
--- /dev/null
+++ b/app/client/src/assets/images/InspectElement.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/images/Plus-logo.svg b/app/client/src/assets/images/Plus-logo.svg
new file mode 100644
index 0000000000..2116267a8f
--- /dev/null
+++ b/app/client/src/assets/images/Plus-logo.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/client/src/assets/images/comments-onboarding/step-1.png b/app/client/src/assets/images/comments-onboarding/step-1.png
new file mode 100644
index 0000000000..961bea1a83
Binary files /dev/null and b/app/client/src/assets/images/comments-onboarding/step-1.png differ
diff --git a/app/client/src/assets/images/comments-onboarding/step-2.png b/app/client/src/assets/images/comments-onboarding/step-2.png
new file mode 100644
index 0000000000..47bbf9a563
Binary files /dev/null and b/app/client/src/assets/images/comments-onboarding/step-2.png differ
diff --git a/app/client/src/assets/images/comments-onboarding/step-3.png b/app/client/src/assets/images/comments-onboarding/step-3.png
new file mode 100644
index 0000000000..3f1db4eea7
Binary files /dev/null and b/app/client/src/assets/images/comments-onboarding/step-3.png differ
diff --git a/app/client/src/assets/images/comments-onboarding/step-4.png b/app/client/src/assets/images/comments-onboarding/step-4.png
new file mode 100644
index 0000000000..6afb39482c
Binary files /dev/null and b/app/client/src/assets/images/comments-onboarding/step-4.png differ
diff --git a/app/client/src/assets/images/comments-onboarding/step-5.png b/app/client/src/assets/images/comments-onboarding/step-5.png
new file mode 100644
index 0000000000..3b14415efa
Binary files /dev/null and b/app/client/src/assets/images/comments-onboarding/step-5.png differ
diff --git a/app/client/src/assets/images/profile-placeholder.svg b/app/client/src/assets/images/profile-placeholder.svg
new file mode 100644
index 0000000000..b8ef8acc18
--- /dev/null
+++ b/app/client/src/assets/images/profile-placeholder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/assets/lottie/pulse-dot.json b/app/client/src/assets/lottie/pulse-dot.json
new file mode 100644
index 0000000000..a4901959e1
--- /dev/null
+++ b/app/client/src/assets/lottie/pulse-dot.json
@@ -0,0 +1 @@
+{"v":"5.5.0","fr":29.9700012207031,"ip":0,"op":70.0000028511585,"w":250,"h":250,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":90,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[118.549,117.468,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,2.889]},"t":0,"s":[83.62,83.62,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,-0.549]},"t":27,"s":[105.62,105.62,100]},{"t":52.0000021180034,"s":[83.62,83.62,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[106.672,106.672],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9490196078431372,0.16862745098039217,0.16862745098039217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[7.715,9.008],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":72.0000029326201,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"w1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":25.0000010182709,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[125.156,124.494,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[83.6,83.6,100]},{"t":25.0000010182709,"s":[234.048,234.048,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[69.508,69.508],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9490196078431372,0.16862745098039217,0.16862745098039217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.187,0.605],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[139.443,139.443],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"w2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":30.0000012219251,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[125.326,124.285,0],"ix":2},"a":{"a":0,"k":[-44.248,-66.371,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[92.661,92.661,100]},{"t":30.0000012219251,"s":[229.892,229.892,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[86.801,86.801],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9490196078431372,0.16862745098039217,0.16862745098039217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-44.6,-65.6],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/app/client/src/comments/AppComments/AppCommentThreads.tsx b/app/client/src/comments/AppComments/AppCommentThreads.tsx
index d15727fe6b..e3fc2d4036 100644
--- a/app/client/src/comments/AppComments/AppCommentThreads.tsx
+++ b/app/client/src/comments/AppComments/AppCommentThreads.tsx
@@ -1,14 +1,29 @@
import React, { useMemo } from "react";
+import { useSelector } from "react-redux";
+import styled from "styled-components";
+
import {
- getSortedAppCommentThreadIds,
+ getSortedAndFilteredAppCommentThreadIds,
applicationCommentsSelector,
allCommentThreadsMap,
getAppCommentThreads,
+ shouldShowResolved as shouldShowResolvedSelector,
+ appCommentsFilter as appCommentsFilterSelector,
} from "selectors/commentsSelectors";
import { getCurrentApplicationId } from "selectors/editorSelectors";
-import { useSelector } from "react-redux";
import CommentThread from "comments/CommentThread/connectedCommentThread";
+import AppCommentsPlaceholder from "./AppCommentsPlaceholder";
+import { getCurrentUser } from "selectors/usersSelectors";
+
+import { Virtuoso } from "react-virtuoso";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow: auto;
+`;
function AppCommentThreads() {
const applicationId = useSelector(getCurrentApplicationId) as string;
@@ -17,23 +32,53 @@ function AppCommentThreads() {
);
const appCommentThreadIds = getAppCommentThreads(appCommentThreadsByRefMap);
const commentThreadsMap = useSelector(allCommentThreadsMap);
+ const shouldShowResolved = useSelector(shouldShowResolvedSelector);
+ const appCommentsFilter = useSelector(appCommentsFilterSelector);
+
+ const currentUser = useSelector(getCurrentUser);
+ const currentUsername = currentUser?.username;
const commentThreadIds = useMemo(
- () => getSortedAppCommentThreadIds(appCommentThreadIds, commentThreadsMap),
- [appCommentThreadIds, commentThreadsMap],
+ () =>
+ getSortedAndFilteredAppCommentThreadIds(
+ appCommentThreadIds,
+ commentThreadsMap,
+ shouldShowResolved,
+ appCommentsFilter,
+ currentUsername,
+ ),
+ [
+ appCommentThreadIds,
+ commentThreadsMap,
+ shouldShowResolved,
+ appCommentsFilter,
+ currentUsername,
+ ],
);
return (
- // eslint-disable-next-line react/jsx-no-useless-fragment
- <>
- {commentThreadIds?.map((commentThreadId: string) => (
-
+ {commentThreadIds.length > 0 && (
+ (
+ /** Keeping this as a fail safe: since zero
+ * height elements throw an error
+ * */
+
+
+
+ )}
/>
- ))}
- >
+ )}
+ {commentThreadIds.length === 0 && }
+
);
}
diff --git a/app/client/src/comments/AppComments/AppComments.tsx b/app/client/src/comments/AppComments/AppComments.tsx
index 5791a54a62..c78e9df639 100644
--- a/app/client/src/comments/AppComments/AppComments.tsx
+++ b/app/client/src/comments/AppComments/AppComments.tsx
@@ -1,28 +1,19 @@
-import React, { useState } from "react";
+import React from "react";
import { useSelector } from "react-redux";
import { commentModeSelector } from "selectors/commentsSelectors";
import AppCommentsHeader from "./AppCommentsHeader";
-import AppCommentThreads from "./AppCommentThreadsContainer";
+import AppCommentThreads from "./AppCommentThreads";
import Container from "./Container";
-import { useCallback } from "react";
-function AppComments() {
- const [isOpen, setIsOpen] = useState(false);
+function AppComments(props: { isInline?: boolean }) {
const isCommentMode = useSelector(commentModeSelector);
- const onClose = useCallback(() => {
- setIsOpen(!isOpen);
- }, [isOpen]);
if (!isCommentMode) return null;
return (
-
-
-
+
+
+
);
}
diff --git a/app/client/src/comments/AppComments/AppCommentsFilterPopover.tsx b/app/client/src/comments/AppComments/AppCommentsFilterPopover.tsx
new file mode 100644
index 0000000000..f3a4fe2466
--- /dev/null
+++ b/app/client/src/comments/AppComments/AppCommentsFilterPopover.tsx
@@ -0,0 +1,114 @@
+import React, { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import styled, { withTheme } from "styled-components";
+import Icon, { IconSize } from "components/ads/Icon";
+import { Popover2 } from "@blueprintjs/popover2";
+import RadioGroup, { Radio } from "components/ads/Radio";
+import { Theme } from "constants/DefaultTheme";
+import Checkbox from "components/ads/Checkbox";
+
+import {
+ shouldShowResolved as shouldShowResolvedSelector,
+ appCommentsFilter as appCommentsFilterSelector,
+} from "selectors/commentsSelectors";
+import {
+ setShouldShowResolvedComments,
+ setAppCommentsFilter,
+} from "actions/commentActions";
+
+import "@blueprintjs/popover2/lib/css/blueprint-popover2.css";
+
+export const options = [
+ { label: "Show all comments", value: "show-all" },
+ { label: "Show only pinned", value: "show-only-pinned" },
+ { label: "Show only yours", value: "show-only-yours" },
+];
+
+const checkboxes = [
+ { label: "Show resolved comments", value: "show-resolved" },
+];
+
+const Container = styled.div`
+ width: 180px;
+ padding: ${(props) => props.theme.spaces[5]}px;
+ && ${Radio} {
+ margin-bottom: ${(props) => props.theme.spaces[6]}px;
+ }
+`;
+
+const Row = styled.div`
+ margin-bottom: ${(props) => props.theme.spaces[6]}px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+`;
+
+const useSetResolvedFilterFromQuery = () => {
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ const url = new URL(window.location.href);
+ const searchParams = url.searchParams;
+ console.log(window.location.href, "window.location.href");
+ if (searchParams.get("isResolved")) {
+ dispatch(setShouldShowResolvedComments(true));
+ }
+ }, []);
+};
+
+const AppCommentsFilter = withTheme(({ theme }: { theme: Theme }) => {
+ const dispatch = useDispatch();
+ const shouldShowResolved = useSelector(shouldShowResolvedSelector);
+ const appCommentsFilter = useSelector(appCommentsFilterSelector);
+
+ return (
+
+ dispatch(setAppCommentsFilter(value))}
+ options={options}
+ rows={3}
+ />
+ {checkboxes.map(({ label }) => (
+
+
+ dispatch(setShouldShowResolvedComments(isChecked))
+ }
+ />
+
+ ))}
+
+ );
+});
+
+function AppCommentsFilterPopover() {
+ useSetResolvedFilterFromQuery();
+
+ return (
+ }
+ modifiers={{
+ offset: {
+ enabled: true,
+ options: {
+ offset: [7, 10],
+ },
+ },
+ preventOverflow: {
+ enabled: true,
+ },
+ }}
+ placement={"bottom-end"}
+ portalClassName="comment-context-menu"
+ >
+
+
+ );
+}
+
+export default AppCommentsFilterPopover;
diff --git a/app/client/src/comments/AppComments/AppCommentsHeader.tsx b/app/client/src/comments/AppComments/AppCommentsHeader.tsx
index 5fb37c55a6..d6b12c76b6 100644
--- a/app/client/src/comments/AppComments/AppCommentsHeader.tsx
+++ b/app/client/src/comments/AppComments/AppCommentsHeader.tsx
@@ -1,49 +1,32 @@
-import React, { useCallback } from "react";
-import styled, { withTheme } from "styled-components";
-import { getTypographyByKey, Theme } from "constants/DefaultTheme";
+import React from "react";
+import styled from "styled-components";
+import { getTypographyByKey } from "constants/DefaultTheme";
import { COMMENTS, createMessage } from "constants/messages";
-import Icon from "components/ads/Icon";
+
+import AppCommentsFilterPopover from "./AppCommentsFilterPopover";
const AppCommentHeaderTitle = styled.div`
color: ${(props) => props.theme.colors.comments.appCommentsHeaderTitle};
- ${(props) => getTypographyByKey(props, "h2")}
+ ${(props) => getTypographyByKey(props, "h5")}
`;
-const Header = styled.div<{ isOpen: boolean }>`
+const Header = styled.div`
display: flex;
- padding: ${(props) =>
- `${props.theme.spaces[6]}px ${props.theme.spaces[8]}px`};
+ padding: ${(props) => props.theme.spaces[6]}px;
width: 100%;
justify-content: space-between;
- cursor: ${(props) => (!props.isOpen ? "pointer" : "auto")};
+ align-items: center;
+ border-bottom: 1px solid
+ ${(props) => props.theme.colors.comments.appCommentsHeaderBorder};
`;
-type Props = {
- isOpen: boolean;
- setIsOpen: (isOpen: boolean) => void;
- onClose: () => void;
- theme: Theme;
-};
-
-const AppCommentsHeader = withTheme(
- ({ onClose, isOpen, setIsOpen, theme }: Props) => {
- const showCommentThreads = useCallback(() => {
- if (!isOpen) setIsOpen(true);
- }, [isOpen]);
-
- return (
-
- {createMessage(COMMENTS)}
- {isOpen && (
-
- )}
-
- );
- },
-);
+function AppCommentsHeader() {
+ return (
+
+ {createMessage(COMMENTS)}
+
+
+ );
+}
export default AppCommentsHeader;
diff --git a/app/client/src/comments/AppComments/AppCommentsPlaceholder.tsx b/app/client/src/comments/AppComments/AppCommentsPlaceholder.tsx
new file mode 100644
index 0000000000..589dec8247
--- /dev/null
+++ b/app/client/src/comments/AppComments/AppCommentsPlaceholder.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import styled from "styled-components";
+import {
+ NO_COMMENTS_CLICK_ON_CANVAS_TO_ADD,
+ createMessage,
+} from "constants/messages";
+import Icon, { IconSize } from "components/ads/Icon";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+ white-space: pre-line;
+ & .message {
+ text-align: center;
+ }
+ color: ${(props) => props.theme.colors.comments.appCommentsPlaceholderText};
+`;
+
+function AppCommentsPlaceholder() {
+ return (
+
+
+
+ {createMessage(NO_COMMENTS_CLICK_ON_CANVAS_TO_ADD)}
+
+
+ );
+}
+
+export default AppCommentsPlaceholder;
diff --git a/app/client/src/comments/AppComments/Container.tsx b/app/client/src/comments/AppComments/Container.tsx
index 22b0b75bae..396f221d86 100644
--- a/app/client/src/comments/AppComments/Container.tsx
+++ b/app/client/src/comments/AppComments/Container.tsx
@@ -1,13 +1,24 @@
import styled from "styled-components";
import { Colors } from "constants/Colors";
+import { Layers } from "constants/Layers";
-const Container = styled.div`
+const Container = styled.div<{ isInline?: boolean }>`
background: ${Colors.WHITE};
- width: 280px;
- position: fixed;
- right: 30px;
- bottom: 0;
- z-index: 11;
+ width: 250px;
+ ${(props) =>
+ !props.isInline
+ ? `
+ position: fixed;
+ left: 0;
+ top: ${props.theme.smallHeaderHeight};
+ height: calc(100% - ${props.theme.smallHeaderHeight});
+ `
+ : `
+ position: relative;
+ `}
+ z-index: ${Layers.appComments};
+ display: flex;
+ flex-direction: column;
`;
export default Container;
diff --git a/app/client/src/comments/CommentCard/CommentCard.tsx b/app/client/src/comments/CommentCard/CommentCard.tsx
index ed09cb9ccd..876330e3b6 100644
--- a/app/client/src/comments/CommentCard/CommentCard.tsx
+++ b/app/client/src/comments/CommentCard/CommentCard.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from "react";
+import React, { useCallback, useEffect, useState } from "react";
import Editor from "@draft-js-plugins/editor";
import {
CompositeDecorator,
@@ -8,54 +8,71 @@ import {
RawDraftContentState,
} from "draft-js";
import styled from "styled-components";
-import ProfileImage from "pages/common/ProfileImage";
-import { Comment } from "entities/Comments/CommentsInterfaces";
+import ProfileImage, { Profile } from "pages/common/ProfileImage";
+import { Comment, Reaction } from "entities/Comments/CommentsInterfaces";
import { getTypographyByKey } from "constants/DefaultTheme";
import CommentContextMenu from "./CommentContextMenu";
import ResolveCommentButton from "comments/CommentCard/ResolveCommentButton";
+import { MentionComponent } from "components/ads/MentionsInput";
+import Icon, { IconSize } from "components/ads/Icon";
+import EmojiReactions, {
+ Reaction as ComponentReaction,
+ Reactions,
+ ReactionOperation,
+} from "components/ads/EmojiReactions";
+import { Toaster } from "components/ads/Toast";
+import AddCommentInput from "comments/inlineComments/AddCommentInput";
import createMentionPlugin from "@draft-js-plugins/mention";
import { flattenDeep, noop } from "lodash";
import copy from "copy-to-clipboard";
+import moment from "moment";
+import history from "utils/history";
+
+import { getAppMode } from "selectors/applicationSelectors";
+
+import { USER_PHOTO_URL } from "constants/userConstants";
+
+import { getCommentThreadURL } from "../utils";
import {
deleteCommentRequest,
+ markThreadAsReadRequest,
pinCommentThreadRequest,
+ editCommentRequest,
+ deleteCommentThreadRequest,
+ addCommentReaction,
+ removeCommentReaction,
} from "actions/commentActions";
-import { useDispatch } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
+import { commentThreadsSelector } from "selectors/commentsSelectors";
+import { getCurrentUser } from "selectors/usersSelectors";
+import { createMessage, LINK_COPIED_SUCCESSFULLY } from "constants/messages";
+import { Variant } from "components/ads/common";
+import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper";
+import { TourType } from "entities/Tour";
+import { getCurrentApplicationId } from "selectors/editorSelectors";
const StyledContainer = styled.div`
width: 100%;
padding: ${(props) =>
- `${props.theme.spaces[5]}px ${props.theme.spaces[7]}px`};
+ `${props.theme.spaces[6]}px ${props.theme.spaces[5]}px`};
border-radius: 0;
+ &:hover {
+ background-color: ${(props) =>
+ props.theme.colors.comments.cardHoverBackground};
+ }
`;
-const Separator = styled.div`
- background-color: ${(props) =>
- props.theme.colors.comments.childCommentsIndent};
- height: 1px;
- width: calc(100% - ${(props) => props.theme.spaces[7] * 2}px);
- margin-left: ${(props) => props.theme.spaces[7]}px;
-`;
-
-// ${(props) => getTypographyByKey(props, "p1")};
-// line-height: 24px;
-// color: ${(props) => props.theme.colors.comments.commentBody};
-// margin-top: ${(props) => props.theme.spaces[3]}px;
-
const CommentBodyContainer = styled.div`
- background-color: ${(props) => props.theme.colors.comments.commentBackground};
- border-radius: ${(props) => props.theme.spaces[3]}px;
- padding: ${(props) =>
- `${props.theme.spaces[4]}px ${props.theme.spaces[5]}px`};
+ padding-bottom: ${(props) => props.theme.spaces[4]}px;
`;
const CommentHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
- padding-bottom: ${(props) => props.theme.spaces[5]}px;
+ padding-bottom: ${(props) => props.theme.spaces[4]}px;
`;
const UserName = styled.span`
@@ -64,105 +81,409 @@ const UserName = styled.span`
margin-left: ${(props) => props.theme.spaces[4]}px;
display: flex;
align-items: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2; /* number of lines to show */
+ -webkit-box-orient: vertical;
`;
const HeaderSection = styled.div`
display: flex;
align-items: center;
+
+ & ${Profile} {
+ flex-shrink: 0;
+ }
`;
-const mentionPlugin = createMentionPlugin();
+const CommentTime = styled.div`
+ color: ${(props) => props.theme.colors.comments.commentTime};
+ ${(props) => getTypographyByKey(props, "p3")}
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: ${(props) => props.theme.spaces[4]}px;
+`;
+
+const CommentSubheader = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: ${(props) => props.theme.spaces[4]}px;
+ white-space: nowrap;
+
+ ${(props) => getTypographyByKey(props, "p3")}
+
+ color: ${(props) => props.theme.colors.comments.pinnedByText};
+
+ & .thread-id {
+ flex-shrink: 0;
+ max-width: 50px;
+ }
+
+ & .pin {
+ margin: 0 ${(props) => props.theme.spaces[3]}px;
+ }
+
+ strong {
+ white-space: pre;
+ margin-left: ${(props) => props.theme.spaces[0]}px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+`;
+
+const CommentThreadId = styled.div`
+ color: ${(props) => props.theme.colors.comments.commentTime};
+ ${(props) => getTypographyByKey(props, "p3")}
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+const Section = styled.div`
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+const UnreadIndicator = styled.div`
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: ${(props) =>
+ props.theme.colors.comments.unreadIndicatorCommentCard};
+ margin-right: ${(props) => props.theme.spaces[2]}px;
+ flex-shrink: 0;
+`;
+
+const ReactionsRow = styled.div`
+ display: flex;
+`;
+
+const EmojiReactionsBtnContainer = styled.div``;
+
+const mentionPlugin = createMentionPlugin({
+ mentionComponent: MentionComponent,
+});
const plugins = [mentionPlugin];
const decorators = flattenDeep(plugins.map((plugin) => plugin.decorators));
const decorator = new CompositeDecorator(
decorators.filter((_decorator, index) => index !== 1) as DraftDecorator[],
);
-const useSelectCommentUsingQuery = (commentId: string) => {
- useEffect(() => {
- const searchParams = new URL(window.location.href).searchParams;
- const commentIdInUrl = searchParams.get("commentId");
- if (commentIdInUrl && commentIdInUrl === commentId) {
- const commentCard = document.getElementById(`comment-card-${commentId}`);
- commentCard?.scrollIntoView();
- }
- }, []);
+function StopClickPropagation({ children }: { children: React.ReactNode }) {
+ return (
+ e.stopPropagation()}
+ style={{ display: "flex" }}
+ >
+ {children}
+
+ );
+}
+
+const replyText = (replies?: number) => {
+ if (!replies) return "";
+ return replies > 1 ? `${replies} Replies` : `1 Reply`;
};
+enum CommentCardModes {
+ EDIT = "EDIT",
+ VIEW = "VIEW",
+}
+
+const reduceReactions = (
+ reactions: Array | undefined,
+ username?: string,
+) => {
+ return (
+ (Array.isArray(reactions) &&
+ reactions.reduce(
+ (res: Record, reaction: Reaction) => {
+ const { byUsername, emoji } = reaction;
+ const sameAsCurrent = byUsername === username;
+ if (res[reaction.emoji]) {
+ res[reaction.emoji].count++;
+ if (!sameAsCurrent) {
+ res[reaction.emoji].users = [
+ ...(res[reaction.emoji].users || []),
+ byUsername,
+ ];
+ }
+ } else {
+ const users = !sameAsCurrent ? [byUsername] : [];
+ res[emoji] = {
+ count: 1,
+ reactionEmoji: emoji,
+ users,
+ } as ComponentReaction;
+ }
+
+ if (sameAsCurrent) {
+ res[reaction.emoji].active = true;
+ }
+
+ return res;
+ },
+ {},
+ )) ||
+ undefined
+ );
+};
+
+const ResolveButtonContainer = styled.div`
+ margin-left: ${(props) => props.theme.spaces[2]}px;
+`;
+
function CommentCard({
comment,
- isParentComment,
- toggleResolved,
- resolved,
commentThreadId,
+ inline,
+ isParentComment,
+ numberOfReplies,
+ resolved,
+ showReplies,
+ showSubheader,
+ toggleResolved,
+ unread = true,
+ visible,
}: {
comment: Comment;
+ isEditMode?: boolean;
isParentComment?: boolean;
resolved?: boolean;
toggleResolved?: () => void;
commentThreadId: string;
+ numberOfReplies?: number;
+ showReplies?: boolean;
+ showSubheader?: boolean;
+ unread?: boolean;
+ inline?: boolean;
+ visible?: boolean;
}) {
+ const [isHovered, setIsHovered] = useState(false);
+ const [cardMode, setCardMode] = useState(CommentCardModes.VIEW);
const dispatch = useDispatch();
- const { authorName, body, id: commentId } = comment;
+ const { authorName, authorUsername, body, id: commentId } = comment;
const contentState = convertFromRaw(body as RawDraftContentState);
const editorState = EditorState.createWithContent(contentState, decorator);
+ const commentThread = useSelector(commentThreadsSelector(commentThreadId));
+ const [reactions, setReactions] = useState();
+ const currentUser = useSelector(getCurrentUser);
+ const currentUserUsername = currentUser?.username;
- const copyCommentLink = useCallback(() => {
- const url = new URL(window.location.href);
- url.searchParams.set("commentId", commentId);
- url.searchParams.set("commentThreadId", commentThreadId);
- url.searchParams.set("isCommentMode", "true");
- copy(url.toString());
- }, []);
+ const isPinned = commentThread.pinnedState?.active;
+ const pinnedByUsername = commentThread.pinnedState?.authorUsername;
+ let pinnedBy = commentThread.pinnedState?.authorName;
+
+ const appMode = useSelector(getAppMode);
+
+ if (currentUserUsername === pinnedByUsername) {
+ pinnedBy = "You";
+ }
+
+ const applicationId = useSelector(getCurrentApplicationId);
+
+ const commentThreadURL = getCommentThreadURL({
+ applicationId,
+ commentThreadId,
+ isResolved: !!commentThread?.resolvedState?.active,
+ pageId: commentThread?.pageId,
+ mode: appMode,
+ });
+
+ const copyCommentLink = () => {
+ copy(commentThreadURL.toString());
+ Toaster.show({
+ text: createMessage(LINK_COPIED_SUCCESSFULLY),
+ variant: Variant.success,
+ });
+ };
const pin = useCallback(() => {
- dispatch(pinCommentThreadRequest({ threadId: commentThreadId }));
- }, []);
+ dispatch(
+ pinCommentThreadRequest({ threadId: commentThreadId, pin: !isPinned }),
+ );
+ }, [isPinned]);
const deleteComment = useCallback(() => {
dispatch(deleteCommentRequest({ threadId: commentThreadId, commentId }));
}, []);
+ const deleteThread = () => {
+ dispatch(deleteCommentThreadRequest(commentThreadId));
+ };
+
+ const isCreatedByMe = currentUserUsername === comment.authorUsername;
+
+ const switchToEditCommentMode = () => setCardMode(CommentCardModes.EDIT);
+ const switchToViewCommentMode = () => setCardMode(CommentCardModes.VIEW);
+
+ const onSaveComment = (body: RawDraftContentState) => {
+ dispatch(editCommentRequest({ commentId, commentThreadId, body }));
+ setCardMode(CommentCardModes.VIEW);
+ };
+
const contextMenuProps = {
+ switchToEditCommentMode,
pin,
copyCommentLink,
deleteComment,
+ deleteThread,
+ isParentComment,
+ isCreatedByMe,
+ isPinned,
};
- useSelectCommentUsingQuery(comment.id);
+ // TODO enable when comments links are enabled
+ // useSelectCommentUsingQuery(comment.id);
+
+ // Dont make inline cards clickable
+ const handleCardClick = () => {
+ if (inline) return;
+ history.push(
+ `${commentThreadURL.pathname}${commentThreadURL.search}${commentThreadURL.hash}`,
+ );
+ if (!commentThread.isViewed) {
+ dispatch(markThreadAsReadRequest(commentThreadId));
+ }
+ };
+
+ useEffect(() => {
+ setReactions(reduceReactions(comment.reactions, currentUserUsername));
+ }, [comment.reactions]);
+
+ const handleReaction = (
+ _event: React.MouseEvent,
+ emojiData: string,
+ updatedReactions: Reactions,
+ addOrRemove: ReactionOperation,
+ ) => {
+ setReactions(updatedReactions);
+ if (addOrRemove == ReactionOperation.ADD) {
+ dispatch(addCommentReaction({ emoji: emojiData, commentId }));
+ } else {
+ dispatch(removeCommentReaction({ emoji: emojiData, commentId }));
+ }
+ };
+
+ const showOptions = visible || isHovered;
+
+ const showResolveBtn =
+ (showOptions || !!resolved) && isParentComment && toggleResolved;
+
+ const hasReactions = !!reactions && Object.keys(reactions).length > 0;
return (
- <>
-
- {!isParentComment && }
- >
+ )}
+
+
+ {moment(comment.creationTime).fromNow()}
+ {showReplies && replyText(numberOfReplies)}
+
+ {hasReactions && (
+
+
+
+
+
+ )}
+
);
}
diff --git a/app/client/src/comments/CommentCard/CommentContextMenu.tsx b/app/client/src/comments/CommentCard/CommentContextMenu.tsx
index bef783b01c..17cd05dc5d 100644
--- a/app/client/src/comments/CommentCard/CommentContextMenu.tsx
+++ b/app/client/src/comments/CommentCard/CommentContextMenu.tsx
@@ -5,7 +5,10 @@ import {
PIN_COMMENT,
COPY_LINK,
DELETE_COMMENT,
+ DELETE_THREAD,
+ UNPIN_COMMENT,
createMessage,
+ EDIT_COMMENT,
} from "constants/messages";
import { noop } from "lodash";
@@ -14,9 +17,7 @@ import "@blueprintjs/popover2/lib/css/blueprint-popover2.css";
import { Popover2 } from "@blueprintjs/popover2";
// render over popover portals
-const Container = styled.div`
- z-index: 11;
-`;
+const Container = styled.div``;
const MenuItem = styled.div`
display: flex;
@@ -29,14 +30,7 @@ const MenuItem = styled.div`
`;
const StyledIcon = styled(Icon)`
- && path {
- stroke: ${(props) => props.theme.colors.comments.contextMenuIcon};
- fill: ${(props) => props.theme.colors.comments.contextMenuIcon};
- }
- ${MenuItem}:hover & path {
- stroke: ${(props) =>
- props.theme.colors.comments.contextMenuIconStrokeHover};
- }
+ margin-left: ${(props) => props.theme.spaces[2]}px;
`;
const MenuIcon = styled.div`
@@ -56,31 +50,68 @@ type Props = {
pin: typeof noop;
copyCommentLink: typeof noop;
deleteComment: typeof noop;
+ deleteThread: typeof noop;
+ switchToEditCommentMode: typeof noop;
+ isParentComment?: boolean;
+ isCreatedByMe?: boolean;
+ isPinned?: boolean;
};
-function CommentContextMenu({ pin, copyCommentLink, deleteComment }: Props) {
+function CommentContextMenu({
+ copyCommentLink,
+ deleteComment,
+ deleteThread,
+ isCreatedByMe,
+ isParentComment,
+ isPinned,
+ pin,
+ switchToEditCommentMode,
+}: Props) {
const [isOpen, setIsOpen] = useState(false);
- const options = useMemo(
- () => [
- {
- icon: "pin-2",
- display: createMessage(PIN_COMMENT),
- onClick: pin,
- },
- {
- icon: "link-2",
- display: createMessage(COPY_LINK),
- onClick: copyCommentLink,
- },
- {
- icon: "trash",
- display: createMessage(DELETE_COMMENT),
- onClick: deleteComment,
- },
- ],
- [],
- );
+ const options = useMemo(() => {
+ const options = [];
+ if (isParentComment) {
+ options.push(
+ {
+ icon: isPinned ? "unpin" : "pin-3",
+ display: isPinned
+ ? createMessage(UNPIN_COMMENT)
+ : createMessage(PIN_COMMENT),
+ onClick: pin,
+ },
+ {
+ icon: "link-2",
+ display: createMessage(COPY_LINK),
+ onClick: copyCommentLink,
+ },
+ );
+ }
+
+ if (isCreatedByMe) {
+ if (!isParentComment) {
+ options.push({
+ icon: "trash",
+ display: createMessage(DELETE_COMMENT),
+ onClick: deleteComment,
+ });
+ } else {
+ options.push({
+ icon: "trash",
+ display: createMessage(DELETE_THREAD),
+ onClick: deleteThread,
+ });
+ }
+
+ options.push({
+ icon: "edit",
+ display: createMessage(EDIT_COMMENT),
+ onClick: switchToEditCommentMode,
+ });
+ }
+
+ return options;
+ }, [isPinned]);
const handleInteraction = useCallback((isOpen) => {
setIsOpen(isOpen);
@@ -91,6 +122,8 @@ function CommentContextMenu({ pin, copyCommentLink, deleteComment }: Props) {
option.onClick();
}, []);
+ if (!options.length) return null;
+
return (
(
handleClick(option)}>
-
+
{option.display}
@@ -107,11 +144,12 @@ function CommentContextMenu({ pin, copyCommentLink, deleteComment }: Props) {
}
isOpen={isOpen}
minimal
+ modifiers={{ offset: { enabled: true, options: { offset: [7, 15] } } }}
onInteraction={handleInteraction}
placement={"bottom-end"}
portalClassName="comment-context-menu"
>
-
+
);
}
diff --git a/app/client/src/comments/CommentCard/ResolveCommentButton.tsx b/app/client/src/comments/CommentCard/ResolveCommentButton.tsx
index a5e2e5081c..2c3062ba0e 100644
--- a/app/client/src/comments/CommentCard/ResolveCommentButton.tsx
+++ b/app/client/src/comments/CommentCard/ResolveCommentButton.tsx
@@ -6,7 +6,6 @@ import { Theme } from "constants/DefaultTheme";
const Container = styled.div`
display: flex;
cursor: pointer;
- margin-right: ${(props) => props.theme.spaces[4]}px;
`;
type Props = {
@@ -15,30 +14,51 @@ type Props = {
theme: Theme;
};
-const StyledResolveIcon = styled(Icon)<{ strokeColor: string }>`
- & circle,
- & path {
- stroke: ${(props) => props.strokeColor};
+const StyledResolveIcon = styled(Icon)<{
+ strokeColorCircle: string;
+ strokeColorPath: string;
+ fillColor: string;
+}>`
+ & circle {
+ stroke: ${(props) => props.strokeColorCircle};
}
&& path {
+ stroke: ${(props) => props.strokeColorPath};
fill: transparent;
}
+ && svg {
+ fill: ${(props) => props.fillColor};
+ }
`;
const ResolveCommentButton = withTheme(
({ handleClick, resolved, theme }: Props) => {
const {
resolved: resolvedColor,
+ resolvedFill: resolvedFillColor,
+ resolvedPath: resolvedPathColor,
unresolved: unresolvedColor,
+ unresolvedFill: unresolvedFillColor,
} = theme.colors.comments;
- const strokeColor = resolved ? resolvedColor : unresolvedColor;
+
+ const strokeColorCircle = resolved ? resolvedColor : unresolvedColor;
+ const strokeColorPath = resolved ? resolvedPathColor : unresolvedColor;
+ const fillColor = resolved ? resolvedFillColor : unresolvedFillColor;
+
+ const _handleClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ handleClick();
+ };
+
return (
-
+
);
diff --git a/app/client/src/comments/CommentThread/CommentThread.tsx b/app/client/src/comments/CommentThread/CommentThread.tsx
index c6f4fb9b86..9071fcb4df 100644
--- a/app/client/src/comments/CommentThread/CommentThread.tsx
+++ b/app/client/src/comments/CommentThread/CommentThread.tsx
@@ -1,5 +1,5 @@
import React, { useRef } from "react";
-import { useDispatch } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import CommentCard from "comments/CommentCard/CommentCard";
import AddCommentInput from "comments/inlineComments/AddCommentInput";
@@ -7,38 +7,46 @@ import ScrollToLatest from "./ScrollToLatest";
import {
addCommentToThreadRequest,
+ markThreadAsReadRequest,
+ resetVisibleThread,
setCommentResolutionRequest,
} from "actions/commentActions";
+import { shouldShowResolved as shouldShowResolvedSelector } from "selectors/commentsSelectors";
+
import useIsScrolledToBottom from "utils/hooks/useIsScrolledToBottom";
import { CommentThread } from "entities/Comments/CommentsInterfaces";
import { RawDraftContentState } from "draft-js";
import styled from "styled-components";
+import { animated } from "react-spring";
+import { AppState } from "reducers";
+import { useEffect } from "react";
-const ThreadContainer = styled.div`
- width: 400px;
+const ThreadContainer = styled(animated.div)<{
+ visible?: boolean;
+ inline?: boolean;
+ pinned?: boolean;
+}>`
+ width: 280px;
max-width: 100%;
+ background-color: ${(props) =>
+ props.inline
+ ? "transparent"
+ : props.pinned
+ ? props.theme.colors.comments.pinnedThreadBackground
+ : props.visible
+ ? props.theme.colors.comments.visibleThreadBackground
+ : "transparent"};
+ max-height: ${(props) =>
+ props.inline ? `calc(100vh - ${props.theme.smallHeaderHeight})` : "unset"};
+ /* overflow: auto collapses the comment threads in the sidebar */
+ overflow: ${(props) => (props.inline ? "auto" : "unset")};
`;
const CommentsContainer = styled.div<{ inline?: boolean }>`
position: relative;
- max-height: ${(props) => (!props.inline ? "unset" : "285px")};
- overflow: auto;
-`;
-
-const ChildCommentsContainer = styled.div`
- display: flex;
-`;
-
-const ChildCommentIndent = styled.div`
- width: 1px;
- background-color: ${(props) =>
- props.theme.colors.comments.childCommentsIndent};
- margin-left: ${(props) => props.theme.spaces[11]}px;
- margin-bottom: ${(props) => props.theme.spaces[7]}px;
- margin-top: ${(props) => props.theme.spaces[5]}px;
`;
const ChildComments = styled.div`
@@ -47,19 +55,38 @@ const ChildComments = styled.div`
function CommentThreadContainer({
commentThread,
+ hideChildren,
hideInput,
inline,
+ showSubheader,
}: {
commentThread: CommentThread;
isOpen?: boolean;
hideInput?: boolean;
inline?: boolean;
+ hideChildren?: boolean;
+ showSubheader?: boolean;
}) {
const dispatch = useDispatch();
- const { comments, id: commentThreadId } = commentThread;
+ const { comments, id: commentThreadId } = commentThread || {};
const messagesBottomRef = useRef(null);
const commentsContainerRef = useRef(null);
+ const shouldShowResolved = useSelector(shouldShowResolvedSelector);
+ const isThreadVisible =
+ shouldShowResolved || !commentThread?.resolvedState?.active;
+
+ const isVisible = useSelector(
+ (state: AppState) =>
+ state.ui.comments.visibleCommentThreadId === commentThreadId,
+ );
+
+ useEffect(() => {
+ if (isVisible && !commentThread?.isViewed) {
+ dispatch(markThreadAsReadRequest(commentThreadId));
+ }
+ }, [isVisible, commentThread?.isViewed]);
+
// Check if the comments window is scrolled to the bottom
// We don't autoscroll for the user receiving the updates
// for better UX, instead we'd show a helper message to indicate
@@ -90,42 +117,58 @@ function CommentThreadContainer({
const resolveCommentThread = () => {
dispatch(
setCommentResolutionRequest({
- threadId: commentThread.id,
- resolved: !commentThread.resolved,
+ threadId: commentThread?.id,
+ resolved: !commentThread?.resolvedState?.active,
}),
);
};
const parentComment = Array.isArray(comments) && comments[0];
const childComments = Array.isArray(comments) && comments.slice(1);
+ const numberOfReplies =
+ (Array.isArray(childComments) && childComments.length) || 0;
- return (
-
+ const handleCancel = () => dispatch(resetVisibleThread(commentThreadId));
+
+ if (!commentThread) return null;
+
+ return isThreadVisible ? (
+
{parentComment && (
)}
- {childComments && childComments.length > 0 && (
-
-
-
- {childComments.map((comment) => (
-
- ))}
-
-
+ {!hideChildren && childComments && childComments.length > 0 && (
+
+ {childComments.map((comment) => (
+
+ ))}
+
)}
@@ -133,9 +176,11 @@ function CommentThreadContainer({
)}
- {!hideInput && }
+ {!hideInput && (
+
+ )}
- );
+ ) : null;
}
export default CommentThreadContainer;
diff --git a/app/client/src/comments/CommentThread/ScrollToLatest.tsx b/app/client/src/comments/CommentThread/ScrollToLatest.tsx
index 0f442099a6..8727e18b05 100644
--- a/app/client/src/comments/CommentThread/ScrollToLatest.tsx
+++ b/app/client/src/comments/CommentThread/ScrollToLatest.tsx
@@ -1,15 +1,37 @@
+import Icon from "components/ads/Icon";
+import { Theme } from "constants/DefaultTheme";
import React from "react";
-import styled from "styled-components";
+import styled, { withTheme } from "styled-components";
+import { getTypographyByKey } from "constants/DefaultTheme";
+import { VIEW_LATEST, createMessage } from "constants/messages";
const Container = styled.div`
position: absolute;
right: 10px;
bottom: 10px;
cursor: pointer;
+ color: ${(props) => props.theme.colors.comments.viewLatest};
+ text-transform: uppercase;
+ display: flex;
+ ${(props) => getTypographyByKey(props, "btnMedium")}
+ & .view-latest {
+ margin-right: ${(props) => props.theme.spaces[1]}px;
+ }
`;
-function ScrollToLatest({ scrollToBottom }: { scrollToBottom: () => void }) {
- return View latest ;
-}
+const ScrollToLatest = withTheme(
+ ({ scrollToBottom, theme }: { scrollToBottom: () => void; theme: Theme }) => {
+ return (
+
+ {createMessage(VIEW_LATEST)}
+
+
+ );
+ },
+);
export default ScrollToLatest;
diff --git a/app/client/src/comments/CommentsShowcaseCarousel/CommentsCarouselModal.tsx b/app/client/src/comments/CommentsShowcaseCarousel/CommentsCarouselModal.tsx
new file mode 100644
index 0000000000..066d2418fc
--- /dev/null
+++ b/app/client/src/comments/CommentsShowcaseCarousel/CommentsCarouselModal.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import ModalComponent from "components/designSystems/blueprint/ModalComponent";
+import { Layers } from "constants/Layers";
+
+function ShowcaseCarouselModal({ children }: { children: React.ReactNode }) {
+ return (
+ {
+ // TODO (rishabh) handle close
+ }}
+ overlayClassName="comments-onboarding-carousel"
+ portalClassName="comments-onboarding-carousel-portal"
+ scrollContents
+ width={325}
+ zIndex={Layers.appComments}
+ >
+ {children}
+
+ );
+}
+
+export default ShowcaseCarouselModal;
diff --git a/app/client/src/comments/CommentsShowcaseCarousel/ProfileForm.tsx b/app/client/src/comments/CommentsShowcaseCarousel/ProfileForm.tsx
new file mode 100644
index 0000000000..03f749f8b1
--- /dev/null
+++ b/app/client/src/comments/CommentsShowcaseCarousel/ProfileForm.tsx
@@ -0,0 +1,81 @@
+import React from "react";
+
+import { reduxForm } from "redux-form";
+
+import FormGroup from "components/ads/formFields/FormGroup";
+import FormTextField from "components/ads/formFields/TextField";
+
+import UserProfileImagePicker from "components/ads/UserProfileImagePicker";
+
+import { createMessage, DISPLAY_NAME, EMAIL_ADDRESS } from "constants/messages";
+import styled from "styled-components";
+
+import { isEmail, isEmptyString } from "utils/formhelpers";
+
+export type FormValues = {
+ fullName?: string;
+ displayName?: string;
+ emailAddress?: string;
+};
+
+const Container = styled.div`
+ padding: ${(props) => props.theme.spaces[5]}px;
+`;
+
+export const PROFILE_FORM = "PROFILE_FORM";
+
+const fieldNames = {
+ displayName: "displayName",
+ emailAddress: "emailAddress",
+};
+
+const validate = (values: any) => {
+ const errors: any = {};
+ const displayName = values[fieldNames.displayName] || "";
+ const emailAddress = values[fieldNames.emailAddress] || "";
+
+ if (!displayName || isEmptyString(displayName)) {
+ errors[fieldNames.displayName] = "Required";
+ }
+ if (!emailAddress || isEmptyString(emailAddress) || !isEmail(emailAddress)) {
+ errors[fieldNames.emailAddress] = "Required";
+ }
+
+ return errors;
+};
+
+function ProfileForm(props: any) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default reduxForm({
+ // Currently while using this feature,
+ // a destroy action is dispatched
+ // so the initial values don't get set
+ // TODO: triage this issue
+ destroyOnUnmount: false,
+ form: PROFILE_FORM,
+ validate,
+})(ProfileForm);
diff --git a/app/client/src/comments/CommentsShowcaseCarousel/index.tsx b/app/client/src/comments/CommentsShowcaseCarousel/index.tsx
new file mode 100644
index 0000000000..76d725e080
--- /dev/null
+++ b/app/client/src/comments/CommentsShowcaseCarousel/index.tsx
@@ -0,0 +1,176 @@
+import React from "react";
+
+import Text, { TextType } from "components/ads/Text";
+import ShowcaseCarousel, { Steps } from "components/ads/ShowcaseCarousel";
+import ProfileForm, { PROFILE_FORM } from "./ProfileForm";
+import CommentsCarouselModal from "./CommentsCarouselModal";
+
+import styled, { withTheme } from "styled-components";
+import { Theme } from "constants/DefaultTheme";
+import { useDispatch, useSelector } from "react-redux";
+import { getFormSyncErrors } from "redux-form";
+import { getFormValues } from "redux-form";
+
+import { isIntroCarouselVisibleSelector } from "selectors/commentsSelectors";
+import { getCurrentUser } from "selectors/usersSelectors";
+
+import { setActiveTour } from "actions/tourActions";
+import { TourType } from "entities/Tour";
+import { hideCommentsIntroCarousel } from "actions/commentActions";
+import { setCommentsIntroSeen } from "utils/storage";
+
+import { updateUserDetails } from "actions/userActions";
+
+import { S3_BUCKET_URL } from "constants/ThirdPartyConstants";
+
+const getBanner = (step: number) =>
+ `${S3_BUCKET_URL}/comments/step-${step}.png`;
+
+const introSteps = [
+ {
+ title: "Introducing Live Comments",
+ content:
+ "We are introducing live comments. From now on you will be able to comment on your apps, tag other people and exchange thoughts in threads. Click βNextβ to learn more about comments and start commenting.",
+ banner: getBanner(1),
+ hideBackBtn: true,
+ },
+ {
+ title: "Give feedback",
+ content:
+ "Comment on your co-workerβs work and share your thoughts on what works and what needs change.",
+ banner: getBanner(2),
+ },
+ {
+ title: "Invite other people to your conversations",
+ content:
+ "When leaving a comment you can tag other people by writing β@β and their name. This way the person you tagged will get a notification and an e-mail that you tagged them in a comment.",
+ banner: getBanner(3),
+ },
+ {
+ title: "Tag a comment to a widget",
+ content:
+ "If you click on a component while in a comment mode you will tag that comment to that widget. This way if the widget is moved the comment will be moved as well. You can disconnect the comment and widget y simply moving the the comment away from the widget.",
+ banner: getBanner(4),
+ },
+ {
+ title: "You are all set!",
+ content:
+ "By clicking on the comments icon in the top right corner you will activate the βcollaboration modeβ and will be able to start a thread or answer to someone elseβs comment.",
+ banner: getBanner(5),
+ },
+];
+
+const IntroContentContainer = styled.div`
+ padding: ${(props) => props.theme.spaces[5]}px;
+`;
+
+const StyledImg = styled.img`
+ width: 100%;
+ object-fit: contain;
+`;
+
+function IntroStep(props: {
+ title: string;
+ content: string;
+ banner: string;
+ theme: Theme;
+}) {
+ return (
+ <>
+
+
+
+
+ {props.title}
+
+
+
+ {props.content}
+
+
+ >
+ );
+}
+
+const IntroStepThemed = withTheme(IntroStep);
+
+const getSteps = (
+ onSubmitProfileForm: any,
+ isSubmitProfileFormDisabled: boolean,
+ startTutorial: () => void,
+ initialProfileFormValues: { emailAddress?: string; displayName?: string },
+ emailDisabled: boolean,
+) => [
+ ...introSteps.slice(0, 4).map((stepConfig: any) => ({
+ props: stepConfig,
+ component: IntroStepThemed,
+ })),
+ {
+ component: ProfileForm,
+ props: {
+ isSubmitDisabled: isSubmitProfileFormDisabled,
+ onSubmit: onSubmitProfileForm,
+ initialValues: initialProfileFormValues,
+ emailDisabled,
+ },
+ },
+ {
+ component: IntroStepThemed,
+ props: {
+ ...introSteps[4],
+ hideBackBtn: true,
+ nextBtnText: "Start Tutorial",
+ onSubmit: startTutorial,
+ },
+ },
+];
+
+export default function CommentsShowcaseCarousel() {
+ const dispatch = useDispatch();
+ const isIntroCarouselVisible = useSelector(isIntroCarouselVisibleSelector);
+ const profileFormValues = useSelector(getFormValues(PROFILE_FORM));
+ const profileFormErrors = useSelector(getFormSyncErrors("PROFILE_FORM"));
+ const isSubmitDisabled = Object.keys(profileFormErrors).length !== 0;
+
+ const currentUser = useSelector(getCurrentUser);
+ const { email, name } = currentUser || {};
+
+ const initialProfileFormValues = { emailAddress: email, displayName: name };
+ const onSubmitProfileForm = () => {
+ const { displayName: name, emailAddress: email } = profileFormValues as {
+ displayName: string;
+ emailAddress: string;
+ };
+ dispatch(updateUserDetails({ name, email }));
+ };
+
+ const startTutorial = () => {
+ dispatch(setActiveTour(TourType.COMMENTS_TOUR));
+ dispatch(hideCommentsIntroCarousel());
+ setCommentsIntroSeen(true);
+ };
+
+ const steps = getSteps(
+ onSubmitProfileForm,
+ isSubmitDisabled,
+ startTutorial,
+ initialProfileFormValues,
+ !!email,
+ );
+
+ if (!isIntroCarouselVisible) return null;
+
+ return (
+
+
+
+ );
+}
diff --git a/app/client/src/comments/ToggleCommentModeButton.tsx b/app/client/src/comments/ToggleCommentModeButton.tsx
deleted file mode 100644
index 0c69ec17b4..0000000000
--- a/app/client/src/comments/ToggleCommentModeButton.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, { useCallback, useEffect } from "react";
-import styled from "styled-components";
-import { useDispatch, useSelector } from "react-redux";
-import { ReactComponent as ToggleCommmentMode } from "assets/icons/comments/toggle-comment-mode.svg";
-import {
- setCommentMode as setCommentModeAction,
- fetchApplicationCommentsRequest,
-} from "actions/commentActions";
-import {
- commentModeSelector,
- areCommentsEnabledForUser as areCommentsEnabledForUserSelector,
-} from "../selectors/commentsSelectors";
-import { useLocation } from "react-router";
-import history from "utils/history";
-
-const StyledToggleCommentMode = styled.div<{ isCommentMode: boolean }>`
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
-
- background: ${(props) =>
- !props.isCommentMode
- ? props.theme.colors.comments.commentModeButtonBackground
- : props.theme.colors.comments.commentModeButtonIcon};
- svg path {
- fill: ${(props) =>
- props.isCommentMode
- ? "#fff"
- : props.theme.colors.comments.commentModeButtonIcon};
- }
-
- height: ${(props) => props.theme.smallHeaderHeight};
- width: ${(props) => props.theme.smallHeaderHeight};
-`;
-
-// update isCommentMode in the store based on the query search param
-const useUpdateCommentModeInStore = () => {
- const location = useLocation();
- const dispatch = useDispatch();
-
- const setCommentModeInStore = useCallback(
- (updatedIsCommentMode) =>
- dispatch(setCommentModeAction(updatedIsCommentMode)),
- [],
- );
-
- useEffect(() => {
- if (window.location.href) {
- const searchParams = new URL(window.location.href).searchParams;
- const isCommentMode = searchParams.get("isCommentMode");
- if (isCommentMode) {
- const updatedIsCommentMode = isCommentMode === "true" ? true : false;
- setCommentModeInStore(updatedIsCommentMode);
- }
- }
- }, [location]);
-};
-
-/**
- * Toggle comment mode:
- * This component is also responsible for fetching
- * application comments
- */
-function ToggleCommentModeButton() {
- const dispatch = useDispatch();
-
- const commentsEnabled = useSelector(areCommentsEnabledForUserSelector);
- const isCommentMode = useSelector(commentModeSelector);
-
- const setCommentModeInUrl = useCallback(() => {
- const currentURL = new URL(window.location.href);
- const searchParams = currentURL.searchParams;
- searchParams.set("isCommentMode", `${!isCommentMode}`);
- // remove comment link params so that they don't get retriggered
- // on toggling comment mode
- searchParams.delete("commentId");
- searchParams.delete("commentThreadId");
- history.replace({
- pathname: currentURL.pathname,
- search: searchParams.toString(),
- hash: currentURL.hash,
- });
- }, [isCommentMode]);
-
- // fetch applications comments when comment mode is turned on
- useEffect(() => {
- if (isCommentMode) {
- dispatch(fetchApplicationCommentsRequest());
- }
- }, [isCommentMode]);
-
- useUpdateCommentModeInStore();
-
- return commentsEnabled ? (
-
-
-
- ) : null;
-}
-
-export default ToggleCommentModeButton;
diff --git a/app/client/src/comments/init.ts b/app/client/src/comments/init.ts
deleted file mode 100644
index f3db7d4a4b..0000000000
--- a/app/client/src/comments/init.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { updateAndSaveLayout } from "actions/pageActions";
-import { uniqueId } from "lodash";
-
-const dsl = require("./dsl.json");
-
-export const updateLayout = () => updateAndSaveLayout(dsl.widgets as any);
-
-export const getTestComments = () => {
- const commentThreads = Object.entries(dsl.widgets).map(([widgetId]) => {
- return {
- refId: widgetId,
- position: { top: 10, left: 15 },
- id: uniqueId(),
- comments: [{ body: widgetId, authorName: uniqueId() }],
- };
- });
-
- // const [widgetId] = Object.entries(dsl.widgets)[0];
- // const commentThreads = [
- // {
- // refId: widgetId,
- // meta: {
- // position: { top: 10, left: 15 },
- // },
- // id: `${1}`,
- // comments: [{ body: widgetId }],
- // },
- // ];
-
- return commentThreads;
-};
diff --git a/app/client/src/comments/inlineComments/AddCommentInput.tsx b/app/client/src/comments/inlineComments/AddCommentInput.tsx
index 5d140e9147..e8b7d44aff 100644
--- a/app/client/src/comments/inlineComments/AddCommentInput.tsx
+++ b/app/client/src/comments/inlineComments/AddCommentInput.tsx
@@ -6,35 +6,38 @@ import React, {
useMemo,
useState,
} from "react";
-import Icon, { IconSize } from "components/ads/Icon";
-import styled, { withTheme } from "styled-components";
-import { EditorState, convertToRaw, Modifier, SelectionState } from "draft-js";
import EmojiPicker from "components/ads/EmojiPicker";
-import MentionsInput from "components/ads/MentionsInput";
-import useOrgUsers from "./useOrgUsers";
-import { MentionData } from "@draft-js-plugins/mention";
-import { OrgUser } from "constants/orgConstants";
-import { IEmojiData } from "emoji-picker-react";
+import MentionsInput, { Trigger } from "components/ads/MentionsInput";
+import Button, { Category } from "components/ads/Button";
-import { createMessage, ADD_COMMENT_PLACEHOLDER } from "constants/messages";
+import { BaseEmoji } from "emoji-mart";
+import styled from "styled-components";
+import { EditorState, convertToRaw, Modifier, SelectionState } from "draft-js";
+import { MentionData } from "@draft-js-plugins/mention";
+
+import { OrgUser } from "constants/orgConstants";
+import useOrgUsers from "./useOrgUsers";
+
+import { RawDraftContentState } from "draft-js";
+
+import {
+ createMessage,
+ ADD_COMMENT_PLACEHOLDER,
+ CANCEL,
+ POST,
+} from "constants/messages";
+
+import { setShowAppInviteUsersDialog } from "actions/applicationActions";
+import { useDispatch } from "react-redux";
+import { change } from "redux-form";
+
+import { INVITE_USERS_TO_ORG_FORM } from "constants/forms";
+
+import { isEmail } from "utils/formhelpers";
const StyledInputContainer = styled.div`
- display: flex;
- align-items: center;
width: 100%;
- padding: ${(props) =>
- `${props.theme.spaces[3]}px ${props.theme.spaces[4]}px`};
- background: ${(props) =>
- props.theme.colors.comments.addCommentInputBackground};
-`;
-
-const StyledSendButton = styled.button`
- display: inline-flex;
- background: transparent;
- border: none;
- align-items: center;
- position: relative;
- top: -1px;
+ margin-bottom: ${(props) => props.theme.spaces[7]}px;
`;
const StyledEmojiTrigger = styled.div`
@@ -43,11 +46,24 @@ const StyledEmojiTrigger = styled.div`
margin-right: ${(props) => props.theme.spaces[4]}px;
`;
-const PaddingContainer = styled.div`
+const PaddingContainer = styled.div<{ removePadding?: boolean }>`
padding: ${(props) =>
- `${props.theme.spaces[4]}px ${props.theme.spaces[6]}px`};
+ !props.removePadding
+ ? `${props.theme.spaces[7]}px ${props.theme.spaces[5]}px`
+ : 0};
+
+ & .cancel-button {
+ margin-right: ${(props) => props.theme.spaces[5]}px;
+ }
`;
+const Row = styled.div`
+ display: flex;
+ justify-content: space-between;
+`;
+
+// Trigger tests
+
const insertCharacter = (
characterToInsert: string,
editorState: EditorState,
@@ -95,88 +111,150 @@ const resetEditorState = (editorState: EditorState) => {
return editorState;
};
+const otherSuggestions = [{ name: "appsmith" }];
+
const useUserSuggestions = (
users: Array,
setSuggestions: Dispatch>>,
) => {
useEffect(() => {
- setSuggestions(users.map((user) => ({ name: user.username })));
+ setSuggestions([
+ ...otherSuggestions,
+ ...users.map((user) => ({ name: user.name || user.username, user })),
+ ]);
}, [users]);
};
-const AddCommentInput = withTheme(({ onSave, theme }: any) => {
+function AddCommentInput({
+ initialEditorState,
+ onCancel,
+ onSave,
+ removePadding,
+}: {
+ removePadding?: boolean;
+ initialEditorState?: EditorState;
+ onSave: (state: RawDraftContentState) => void;
+ onCancel?: () => void;
+}) {
+ const dispatch = useDispatch();
const users = useOrgUsers();
const [suggestions, setSuggestions] = useState>([]);
+ const [trigger, setTrigger] = useState();
useUserSuggestions(users, setSuggestions);
- const [editorState, setEditorState] = useState(EditorState.createEmpty());
+ const [editorState, setEditorState] = useState(
+ initialEditorState || EditorState.createEmpty(),
+ );
const [suggestionsQuery, setSuggestionsQuery] = useState("");
+ const clearEditor = useCallback(() => {
+ setEditorState(resetEditorState(editorState));
+ }, [editorState]);
+
+ const _onCancel = () => {
+ clearEditor();
+ if (onCancel) onCancel();
+ };
+
const onSaveComment = useCallback(
(editorStateArg?: EditorState) => {
const latestEditorState = editorStateArg || editorState;
+ const plainText = latestEditorState.getCurrentContent().getPlainText();
- if (!latestEditorState.getCurrentContent().hasText()) return;
+ if (!plainText || plainText.trim().length === 0) return;
const contentState = latestEditorState.getCurrentContent();
const rawContent = convertToRaw(contentState);
+ clearEditor();
onSave(rawContent);
- setEditorState(resetEditorState(latestEditorState));
},
[editorState],
);
const handleSubmit = useCallback(() => onSaveComment(), [editorState]);
const handleEmojiClick = useCallback(
- (e: React.MouseEvent, emojiObject: IEmojiData) => {
- const newEditorState = insertCharacter(emojiObject.emoji, editorState);
+ (e: React.MouseEvent, emojiObject: BaseEmoji) => {
+ const newEditorState = insertCharacter(emojiObject.native, editorState);
setEditorState(newEditorState);
},
[editorState],
);
const onSearchChange = useCallback(
- ({ value }: { value: string }) => {
+ ({ trigger, value }: { trigger: string; value: string }) => {
setSuggestionsQuery(value);
+ setTrigger(trigger as Trigger);
},
[suggestions],
);
const filteredSuggestions = useMemo(() => {
- if (!suggestionsQuery) return suggestions;
+ let suggestionResults = suggestions;
+ if (!suggestionsQuery) return suggestionResults;
else {
- return suggestions.filter((suggestion) => {
- const str = suggestion.name.toLowerCase();
+ suggestionResults = suggestions.filter((suggestion) => {
+ const name = suggestion.name.toLowerCase();
+ const username = suggestion.user?.username.toLowerCase() || "";
const filter = suggestionsQuery.toLowerCase();
- return str.indexOf(filter) !== -1;
+ return name.indexOf(filter) !== -1 || username.indexOf(filter) !== -1;
});
}
- }, [suggestionsQuery, suggestions]);
+
+ if (suggestionResults.length !== 0) return suggestionResults;
+
+ if (isEmail(suggestionsQuery)) {
+ return [{ name: suggestionsQuery, isInviteTrigger: true }];
+ }
+
+ return [];
+ }, [suggestionsQuery, suggestions, trigger]);
+
+ const onAddMention = (mention: MentionData) => {
+ if (isEmail(mention.name) && mention.isInviteTrigger) {
+ dispatch(setShowAppInviteUsersDialog(true));
+ dispatch(change(INVITE_USERS_TO_ORG_FORM, "users", mention.name));
+ }
+ };
return (
-
-
-
+
+
+
+
+
+
+
-
-
+
-
-
+
+
+
);
-});
+}
export default AddCommentInput;
diff --git a/app/client/src/comments/inlineComments/Comments.tsx b/app/client/src/comments/inlineComments/Comments.tsx
index 822c45e7b9..5533ffa719 100644
--- a/app/client/src/comments/inlineComments/Comments.tsx
+++ b/app/client/src/comments/inlineComments/Comments.tsx
@@ -1,12 +1,53 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import UnpublishedCommentThread from "./UnpublishedCommentThread";
import InlineCommentPin from "./InlineCommentPin";
import {
+ commentThreadsSelector,
refCommentThreadsSelector,
unpublishedCommentThreadSelector,
} from "../../selectors/commentsSelectors";
-import { getCurrentApplicationId } from "selectors/editorSelectors";
+import {
+ getCurrentApplicationId,
+ getCurrentPageId,
+} from "selectors/editorSelectors";
+import { useLocation } from "react-router";
+
+// TODO refactor application comment threads by page id to optimise
+// if lists turn out to be expensive
+function InlinePageCommentPin({
+ commentThreadId,
+ focused,
+}: {
+ commentThreadId: string;
+ focused: boolean;
+}) {
+ const commentThread = useSelector(commentThreadsSelector(commentThreadId));
+ const currentPageId = useSelector(getCurrentPageId);
+
+ if (commentThread && commentThread.pageId !== currentPageId) return null;
+
+ return (
+
+ );
+}
+
+const MemoisedInlinePageCommentPin = React.memo(InlinePageCommentPin);
+
+const useSelectCommentThreadUsingQuery = () => {
+ const location = useLocation();
+ const [commentThreadIdInUrl, setCommentThreadIdInUrl] = useState<
+ string | null
+ >();
+
+ useEffect(() => {
+ const searchParams = new URL(window.location.href).searchParams;
+ const commentThreadIdInUrl = searchParams.get("commentThreadId");
+ setCommentThreadIdInUrl(commentThreadIdInUrl);
+ }, [location]);
+
+ return commentThreadIdInUrl;
+};
/**
* Renders comment threads associated with a refId (for example widgetId)
@@ -21,13 +62,15 @@ function Comments({ refId }: { refId: string }) {
const unpublishedCommentThread = useSelector(
unpublishedCommentThreadSelector(refId),
);
+ const commentThreadIdInUrl = useSelectCommentThreadUsingQuery();
return (
<>
{Array.isArray(commentsThreadIds) &&
commentsThreadIds.map((commentsThreadId: any) => (
-
))}
diff --git a/app/client/src/comments/inlineComments/InlineCommentPin.tsx b/app/client/src/comments/inlineComments/InlineCommentPin.tsx
index 6a265d8a40..1bbd94e97a 100644
--- a/app/client/src/comments/inlineComments/InlineCommentPin.tsx
+++ b/app/client/src/comments/inlineComments/InlineCommentPin.tsx
@@ -1,107 +1,213 @@
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
-import styled, { withTheme } from "styled-components";
+import styled from "styled-components";
import CommentThread from "comments/CommentThread/CommentThread";
import Icon, { IconSize } from "components/ads/Icon";
-import { Popover } from "@blueprintjs/core";
import { get } from "lodash";
-import { commentThreadsSelector } from "selectors/commentsSelectors";
-import { Theme } from "constants/DefaultTheme";
-import { setIsCommentThreadVisible as setIsCommentThreadVisibleAction } from "actions/commentActions";
+import {
+ commentThreadsSelector,
+ shouldShowResolved as shouldShowResolvedSelector,
+} from "selectors/commentsSelectors";
+import { getTypographyByKey } from "constants/DefaultTheme";
+import {
+ setVisibleThread,
+ resetVisibleThread,
+ markThreadAsReadRequest,
+} from "actions/commentActions";
+import scrollIntoView from "scroll-into-view-if-needed";
+import { AppState } from "reducers";
+import "@blueprintjs/popover2/lib/css/blueprint-popover2.css";
+import { Popover2 } from "@blueprintjs/popover2";
+
+/**
+ * The relavent pixel position is bottom right for the comment cursor
+ * instead of the top left for the default arrow cursor
+ */
const CommentTriggerContainer = styled.div<{ top: number; left: number }>`
position: absolute;
- top: ${(props) => props.top}%;
- left: ${(props) => props.left}%;
+ bottom: calc(${(props) => 100 - props.top}% - 2px);
+ right: calc(${(props) => 100 - props.left}% - 2px);
z-index: 1;
`;
-const useSelectCommentThreadUsingQuery = (commentThreadId: string) => {
- const dispatch = useDispatch();
- useEffect(() => {
- const searchParams = new URL(window.location.href).searchParams;
- const commentThreadIdInUrl = searchParams.get("commentThreadId");
- if (commentThreadIdInUrl && commentThreadIdInUrl === commentThreadId) {
- const elements = document.getElementsByClassName(
- `comment-thread-pin-${commentThreadId}`,
- );
- const commentPin = elements && elements[0];
- commentPin?.scrollIntoView();
- // set comment thread visible after scrollIntoView is complete
- setTimeout(() => {
- dispatch(
- setIsCommentThreadVisibleAction({
- commentThreadId,
- isVisible: true,
- }),
- );
+const StyledPinContainer = styled.div<{ unread?: boolean }>`
+ position: relative;
+ & .pin-id {
+ position: absolute;
+ top: 15%;
+ left: 50%;
+ transform: translate(-50%);
+ color: ${(props) =>
+ props.unread ? "#fff" : props.theme.colors.comments.pinId};
+ ${(props) => getTypographyByKey(props, "p1")}
+ max-width: 25px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ & svg {
+ width: 30px;
+ height: 30px;
+ box-shadow: 0px 8px 10px rgb(0 0 0 / 15%);
+ border-radius: 15px;
+ overflow: visible;
+ }
+ cursor: pointer;
+`;
+
+function Pin({
+ commentThreadId,
+ onClick,
+ sequenceId = "",
+ unread,
+}: {
+ commentThreadId: string;
+ sequenceId?: string;
+ unread?: boolean;
+ onClick: () => void;
+}) {
+ return (
+
+
+ {sequenceId.slice(1)}
+
+ );
+}
+
+const Container = document.getElementById("root");
+
+const modifiers = {
+ preventOverflow: { enabled: true },
+ offset: {
+ enabled: true,
+ options: {
+ offset: [-8, 10] as [
+ number | null | undefined,
+ number | null | undefined,
+ ],
+ },
+ },
+};
+
+const focusThread = (commentThreadId: string) => {
+ if (commentThreadId) {
+ const elements = document.getElementsByClassName(
+ `comment-thread-pin-${commentThreadId}`,
+ );
+ const commentPin = elements && elements[0];
+ if (commentPin) {
+ scrollIntoView(commentPin, {
+ scrollMode: "if-needed",
+ block: "nearest",
+ inline: "nearest",
});
}
- }, []);
+ }
};
/**
* Comment pins that toggle comment thread popover visibility on click
* They position themselves using position absolute based on top and left values (in percent)
*/
-const InlineCommentPin = withTheme(
- ({ commentThreadId, theme }: { commentThreadId: string; theme: Theme }) => {
- const commentThread = useSelector(commentThreadsSelector(commentThreadId));
- const { top, left } = get(commentThread, "position", {
- top: 0,
- left: 0,
- });
+function InlineCommentPin({
+ commentThreadId,
+ focused,
+}: {
+ commentThreadId: string;
+ focused: boolean;
+}) {
+ const commentThread = useSelector(commentThreadsSelector(commentThreadId));
+ const { left, top } = get(commentThread, "position", {
+ top: 0,
+ left: 0,
+ });
- const dispatch = useDispatch();
- const setIsCommentThreadVisible = (isVisible: boolean) =>
- dispatch(
- setIsCommentThreadVisibleAction({
- commentThreadId,
- isVisible,
- }),
- );
+ const dispatch = useDispatch();
- useSelectCommentThreadUsingQuery(commentThreadId);
+ const isPinVisible = useSelector(
+ (state: AppState) =>
+ shouldShowResolvedSelector(state) ||
+ !commentThread?.resolvedState?.active,
+ );
- return (
- {
- // capture clicks so that create new thread is not triggered
- e.preventDefault();
- e.stopPropagation();
- }}
- top={top}
- >
- {
- setIsCommentThreadVisible(nextOpenState);
- }}
- popoverClassName="comment-thread"
- >
-
+ const isCommentThreadVisible = useSelector(
+ (state: AppState) =>
+ state.ui.comments.visibleCommentThreadId === commentThreadId,
+ );
+
+ const handlePinClick = () => {
+ if (!commentThread?.isViewed) {
+ dispatch(markThreadAsReadRequest(commentThreadId));
+ }
+ };
+
+ useEffect(() => {
+ if (focused) {
+ focusThread(commentThreadId);
+ // set comment thread visible after scrollIntoView is complete
+ setTimeout(() => {
+ dispatch(setVisibleThread(commentThreadId));
+ });
+ }
+ }, [focused]);
+
+ if (!commentThread) return null;
+
+ return isPinVisible ? (
+ {
+ // capture clicks so that create new thread is not triggered
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ top={top}
+ >
+
-
-
- );
- },
-);
+ }
+ enforceFocus={false}
+ hasBackdrop
+ // isOpen is controlled so that newly created threads are set to be visible
+ isOpen={!!isCommentThreadVisible}
+ minimal
+ modifiers={modifiers}
+ onInteraction={(nextOpenState: boolean) => {
+ if (nextOpenState) {
+ dispatch(setVisibleThread(commentThreadId));
+ } else {
+ dispatch(resetVisibleThread(commentThreadId));
+ }
+ }}
+ placement={"right-start"}
+ popoverClassName="comment-thread"
+ portalClassName="inline-comment-thread"
+ >
+
+
+
+ ) : null;
+}
export default InlineCommentPin;
diff --git a/app/client/src/comments/inlineComments/OverlayCommentsWrapper.tsx b/app/client/src/comments/inlineComments/OverlayCommentsWrapper.tsx
index 77527580b9..1abbbcd639 100644
--- a/app/client/src/comments/inlineComments/OverlayCommentsWrapper.tsx
+++ b/app/client/src/comments/inlineComments/OverlayCommentsWrapper.tsx
@@ -4,54 +4,21 @@ import { useSelector, useDispatch } from "react-redux";
import Comments from "./Comments";
import { commentModeSelector } from "selectors/commentsSelectors";
import { createUnpublishedCommentThreadRequest } from "actions/commentActions";
-import commentIcon from "assets/icons/ads/commentIcon.png";
+import commentIcon from "assets/icons/comments/commentCursor.png";
+import { getOffsetPos } from "comments/utils";
+import useProceedToNextTourStep from "utils/hooks/useProceedToNextTourStep";
+import { TourType } from "entities/Tour";
type Props = {
children: React.ReactNode;
refId: string;
};
-/**
- * Returns the offset position relative to the container
- * using the coordinates from the click event
- * @param clickEvent
- * @param containerRef
- */
-const getOffsetPos = (
- clickEvent: React.MouseEvent,
- containerRef: HTMLDivElement,
-) => {
- const boundingClientRect = containerRef.getBoundingClientRect();
- const containerPosition = {
- left: boundingClientRect.left,
- top: boundingClientRect.top,
- };
- const clickPosition = {
- left: clickEvent.clientX,
- top: clickEvent.clientY,
- };
-
- const offsetLeft = clickPosition.left - containerPosition.left;
- const offsetTop = clickPosition.top - containerPosition.top;
-
- const offsetLeftPercent = parseInt(
- `${(offsetLeft / boundingClientRect.width) * 100}`,
- );
- const offsetTopPercent = parseInt(
- `${(offsetTop / boundingClientRect.height) * 100}`,
- );
-
- return {
- left: offsetLeftPercent,
- top: offsetTopPercent,
- };
-};
-
const Container = styled.div`
width: 100%;
height: 100%;
position: relative;
- cursor: -webkit-image-set(url("${commentIcon}") 2x) 11 17, auto;
+ cursor: url("${commentIcon}") 25 20 , auto;
`;
/**
@@ -62,8 +29,15 @@ function OverlayCommentsWrapper({ children, refId }: Props) {
const containerRef = useRef(null);
const isCommentMode = useSelector(commentModeSelector);
const dispatch = useDispatch();
+
+ const proceedToNextTourStep = useProceedToNextTourStep(
+ TourType.COMMENTS_TOUR,
+ 1,
+ );
+
// create new unpublished thread
const clickHandler = (e: any) => {
+ proceedToNextTourStep();
e.persist();
if (containerRef.current) {
const position = getOffsetPos(e, containerRef.current);
diff --git a/app/client/src/comments/inlineComments/StyledComponents.tsx b/app/client/src/comments/inlineComments/StyledComponents.tsx
index bcb4d26b1e..6846d03527 100644
--- a/app/client/src/comments/inlineComments/StyledComponents.tsx
+++ b/app/client/src/comments/inlineComments/StyledComponents.tsx
@@ -2,9 +2,7 @@ import styled from "styled-components";
import { getTypographyByKey } from "constants/DefaultTheme";
export const ThreadContainer = styled.div`
- border: 1px solid
- ${(props) => props.theme.colors.comments.threadContainerBorder};
- width: 400px;
+ width: 280px;
max-width: 100%;
`;
@@ -21,6 +19,4 @@ export const ThreadHeaderTitle = styled.div`
export const CommentsContainer = styled.div`
position: relative;
- max-height: 285px;
- overflow: auto;
`;
diff --git a/app/client/src/comments/inlineComments/UnpublishedCommentThread.tsx b/app/client/src/comments/inlineComments/UnpublishedCommentThread.tsx
index 5861dbd56c..087d396127 100644
--- a/app/client/src/comments/inlineComments/UnpublishedCommentThread.tsx
+++ b/app/client/src/comments/inlineComments/UnpublishedCommentThread.tsx
@@ -1,93 +1,94 @@
import React from "react";
import { Popover, Position } from "@blueprintjs/core";
import AddCommentInput from "./AddCommentInput";
-import {
- ThreadContainer,
- ThreadHeaderTitle,
- ThreadHeader,
-} from "./StyledComponents";
+import { ThreadContainer } from "./StyledComponents";
import { useDispatch } from "react-redux";
-import styled, { withTheme } from "styled-components";
+import styled from "styled-components";
import { get } from "lodash";
import {
createCommentThreadRequest as createCommentThreadAction,
removeUnpublishedCommentThreads,
} from "actions/commentActions";
-import Icon, { IconSize } from "components/ads/Icon";
-import { Theme } from "constants/DefaultTheme";
+import Icon from "components/ads/Icon";
import { RawDraftContentState } from "draft-js";
import { CommentThread } from "entities/Comments/CommentsInterfaces";
const CommentTriggerContainer = styled.div<{ top: number; left: number }>`
position: absolute;
- top: ${(props) => props.top}%;
- left: ${(props) => props.left}%;
+ bottom: calc(${(props) => 100 - props.top}% - 2px);
+ right: calc(${(props) => 100 - props.left}% - 2px);
+
+ & svg {
+ width: 30px;
+ height: 30px;
+ box-shadow: 0px 8px 10px rgb(0 0 0 / 15%);
+ border-radius: 15px;
+ overflow: visible;
+ }
`;
// TODO look into drying this up using comment thread component
-const UnpublishedCommentThread = withTheme(
- ({
- theme,
- commentThread,
- }: {
- commentThread: CommentThread;
- theme: Theme;
- }) => {
- const { top, left } = get(commentThread, "position", {
- top: 0,
- left: 0,
- });
- const dispatch = useDispatch();
- const onClosing = () => {
- dispatch(removeUnpublishedCommentThreads());
- };
+function UnpublishedCommentThread({
+ commentThread,
+}: {
+ commentThread: CommentThread;
+}) {
+ const { left, top } = get(commentThread, "position", {
+ top: 0,
+ left: 0,
+ });
+ const dispatch = useDispatch();
+ const onClosing = () => {
+ dispatch(removeUnpublishedCommentThreads());
+ };
- const createCommentThread = (text: RawDraftContentState) => {
- dispatch(createCommentThreadAction({ commentBody: text, commentThread }));
- };
+ const createCommentThread = (text: RawDraftContentState) => {
+ dispatch(createCommentThreadAction({ commentBody: text, commentThread }));
+ };
- return (
- {
- // capture clicks so that create new thread is not triggered
- e.preventDefault();
- e.stopPropagation();
- }}
- >
-
- {
- if (!nextOpenState) {
- onClosing();
- }
- }}
- popoverClassName="comment-thread"
- position={Position.BOTTOM_RIGHT}
- >
- {
+ // capture clicks so that create new thread is not triggered
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+
+ {
+ if (!nextOpenState) {
+ onClosing();
+ }
+ }}
+ popoverClassName="comment-thread"
+ position={Position.RIGHT_TOP}
+ >
+
+
+
-
-
- Add a Comment
-
-
-
-
-
-
- );
- },
-);
+
+
+
+
+ );
+}
export default UnpublishedCommentThread;
diff --git a/app/client/src/comments/inlineComments/useOrgUsers.tsx b/app/client/src/comments/inlineComments/useOrgUsers.tsx
index 642a49c28b..bcf055d5f1 100644
--- a/app/client/src/comments/inlineComments/useOrgUsers.tsx
+++ b/app/client/src/comments/inlineComments/useOrgUsers.tsx
@@ -8,12 +8,14 @@ const useOrgUsers = () => {
const orgId = useSelector(getCurrentOrgId);
const orgUsers = useSelector(getAllUsers);
useEffect(() => {
- dispatch({
- type: ReduxActionTypes.FETCH_ALL_USERS_INIT,
- payload: {
- orgId,
- },
- });
+ if (!orgUsers || !orgUsers.length) {
+ dispatch({
+ type: ReduxActionTypes.FETCH_ALL_USERS_INIT,
+ payload: {
+ orgId,
+ },
+ });
+ }
}, [orgId]);
return orgUsers;
};
diff --git a/app/client/src/comments/tests/Comments.test.tsx b/app/client/src/comments/tests/Comments.test.tsx
index 07ad0806b6..dd3cb0fb05 100644
--- a/app/client/src/comments/tests/Comments.test.tsx
+++ b/app/client/src/comments/tests/Comments.test.tsx
@@ -49,7 +49,7 @@ describe("Comment threads", () => {
});
it("can be created", async (done) => {
- const { getByDataCy, getAllByDataCy, findByDataCy, findByText } = render(
+ const { findByDataCy, findByText, getAllByDataCy, getByDataCy } = render(
,
@@ -87,7 +87,7 @@ describe("Comment threads", () => {
done();
});
it("accept replies", async (done) => {
- const { getByDataCy, findByText, findByDataCy } = render(
+ const { findByDataCy, findByText, getByDataCy } = render(
,
diff --git a/app/client/src/comments/tour/AddCommentTourComponent.tsx b/app/client/src/comments/tour/AddCommentTourComponent.tsx
new file mode 100644
index 0000000000..c8bfe95bca
--- /dev/null
+++ b/app/client/src/comments/tour/AddCommentTourComponent.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import styled from "styled-components";
+import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper";
+import { TourType } from "entities/Tour";
+
+const Dot = styled.div`
+ position: fixed;
+ top: 50%;
+ left: calc(125px + 50%);
+ width: 0px;
+ height: 0px;
+`;
+
+export default function AddCommentTourComponent() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/app/client/src/comments/tour/commentsTourSteps.ts b/app/client/src/comments/tour/commentsTourSteps.ts
new file mode 100644
index 0000000000..fdf40b8bfd
--- /dev/null
+++ b/app/client/src/comments/tour/commentsTourSteps.ts
@@ -0,0 +1,11 @@
+const steps = [
+ {
+ id: "ENTER_COMMENTS_MODE",
+ data: { message: "Click on the comment icon \n to enter comments mode" },
+ },
+ {
+ id: "CREATE_UNPUBLISHED_COMMENT",
+ data: { message: "Click anywhere on the canvas \n and leave a comment." },
+ },
+];
+export default steps;
diff --git a/app/client/src/comments/utils.ts b/app/client/src/comments/utils.ts
index 2beb5e03ee..fbd25f8116 100644
--- a/app/client/src/comments/utils.ts
+++ b/app/client/src/comments/utils.ts
@@ -1,4 +1,9 @@
import { CommentThread } from "entities/Comments/CommentsInterfaces";
+import {
+ BUILDER_PAGE_URL,
+ getApplicationViewerPageURL,
+} from "constants/routes";
+import { APP_MODE } from "reducers/entityReducers/appReducer";
// used for dev
export const reduceCommentsByRef = (comments: any[]) => {
@@ -42,3 +47,77 @@ export const transformUnpublishCommentThreadToCreateNew = (payload: any) => {
comments: [{ body: commentBody }],
};
};
+
+/**
+ * Returns the offset position relative to the container
+ * using the coordinates from the click event
+ * @param clickEvent
+ * @param containerRef
+ */
+export const getOffsetPos = (
+ clickEvent: React.MouseEvent,
+ containerRef: HTMLDivElement,
+) => {
+ const boundingClientRect = containerRef.getBoundingClientRect();
+ const containerPosition = {
+ left: boundingClientRect.left,
+ top: boundingClientRect.top,
+ };
+ const clickPosition = {
+ left: clickEvent.clientX,
+ top: clickEvent.clientY,
+ };
+
+ const offsetLeft = clickPosition.left - containerPosition.left;
+ const offsetTop = clickPosition.top - containerPosition.top;
+
+ const offsetLeftPercent = parseFloat(
+ `${(offsetLeft / boundingClientRect.width) * 100}`,
+ );
+ const offsetTopPercent = parseFloat(
+ `${(offsetTop / boundingClientRect.height) * 100}`,
+ );
+
+ return {
+ left: offsetLeftPercent,
+ top: offsetTopPercent,
+ };
+};
+
+export const getCommentThreadURL = ({
+ applicationId,
+ commentThreadId,
+ isResolved,
+ pageId,
+ mode = APP_MODE.PUBLISHED,
+}: {
+ applicationId?: string;
+ commentThreadId: string;
+ isResolved?: boolean;
+ pageId?: string;
+ mode?: APP_MODE;
+}) => {
+ const queryParams: Record = {
+ commentThreadId,
+ isCommentMode: true,
+ };
+
+ if (isResolved) {
+ queryParams.isResolved = true;
+ }
+
+ const urlBuilder =
+ mode === APP_MODE.PUBLISHED
+ ? getApplicationViewerPageURL
+ : BUILDER_PAGE_URL;
+
+ const url = new URL(
+ `${window.location.origin}${urlBuilder(
+ applicationId,
+ pageId,
+ queryParams,
+ )}`,
+ );
+
+ return url;
+};
diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx
index ea97229575..f1379ff564 100644
--- a/app/client/src/components/ads/Button.tsx
+++ b/app/client/src/components/ads/Button.tsx
@@ -70,7 +70,7 @@ const defaultProps = {
size: Size.small,
isLoading: false,
disabled: false,
- fill: false,
+ fill: undefined,
tag: "a",
};
@@ -380,7 +380,7 @@ function TextLoadingState({ text }: { text?: string }) {
return {text} ;
}
-function IconLoadingState({ size, icon }: { size?: Size; icon?: IconName }) {
+function IconLoadingState({ icon, size }: { size?: Size; icon?: IconName }) {
return ;
}
diff --git a/app/client/src/components/ads/Callout.tsx b/app/client/src/components/ads/Callout.tsx
index 2df99e39a2..9e57b0e257 100644
--- a/app/client/src/components/ads/Callout.tsx
+++ b/app/client/src/components/ads/Callout.tsx
@@ -73,7 +73,11 @@ Callout.defaultProps = {
function Callout(props: CalloutProps) {
return (
-
+
{props.text && props.variant !== Variant.info ? (
) : null}
diff --git a/app/client/src/components/ads/Checkbox.tsx b/app/client/src/components/ads/Checkbox.tsx
index a8ab84d1ae..c9041b0aef 100644
--- a/app/client/src/components/ads/Checkbox.tsx
+++ b/app/client/src/components/ads/Checkbox.tsx
@@ -9,12 +9,14 @@ export type CheckboxProps = CommonComponentProps & {
isDefaultChecked?: boolean;
onCheckChange?: (isChecked: boolean) => void;
info?: string;
+ backgroundColor?: string;
};
const Checkmark = styled.span<{
disabled?: boolean;
isChecked?: boolean;
info?: string;
+ backgroundColor?: string;
}>`
position: absolute;
top: ${(props) => (props.info ? "6px" : "1px")};
@@ -25,14 +27,14 @@ const Checkmark = styled.span<{
props.isChecked
? props.disabled
? props.theme.colors.checkbox.disabled
- : props.theme.colors.info.main
+ : props.backgroundColor || props.theme.colors.info.main
: "transparent"};
border: 2px solid
${(props) =>
props.isChecked
? props.disabled
? props.theme.colors.checkbox.disabled
- : props.theme.colors.info.main
+ : props.backgroundColor || props.theme.colors.info.main
: props.theme.colors.checkbox.unchecked};
&::after {
@@ -129,6 +131,7 @@ function Checkbox(props: CheckboxProps) {
type="checkbox"
/>
`
`;
const defaultColors: string[] = [
- "rgb(3, 179, 101)",
+ "#03b365",
"#FFC13D",
"#38AFF4",
"#DD4B34",
"#3366FF",
"#2E3D49",
"#F6F7F8",
+ "#FFFFFF",
"#231F20",
];
diff --git a/app/client/src/components/ads/DisplayImageUpload.tsx b/app/client/src/components/ads/DisplayImageUpload.tsx
new file mode 100644
index 0000000000..8bd949d98f
--- /dev/null
+++ b/app/client/src/components/ads/DisplayImageUpload.tsx
@@ -0,0 +1,198 @@
+import React, { useEffect, useState } from "react";
+import { ReactComponent as ProfileImagePlaceholder } from "assets/images/profile-placeholder.svg";
+import Uppy from "@uppy/core";
+import Dialog from "components/ads/DialogComponent";
+
+import { Dashboard, useUppy } from "@uppy/react";
+import { getTypographyByKey } from "constants/DefaultTheme";
+
+import styled from "styled-components";
+import ImageEditor from "@uppy/image-editor";
+import { REMOVE, createMessage } from "constants/messages";
+
+import "@uppy/core/dist/style.css";
+import "@uppy/dashboard/dist/style.css";
+import "@uppy/image-editor/dist/style.css";
+import "@blueprintjs/popover2/lib/css/blueprint-popover2.css";
+
+type Props = {
+ onChange: (file: File) => void;
+ onRemove?: () => void;
+ submit: (uppy: Uppy.Uppy) => void;
+ value: string;
+ label?: string;
+};
+
+const Container = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ & .input-component {
+ display: none;
+ }
+
+ & .view {
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .image-view {
+ width: 80px;
+ height: 80px;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: ${(props) =>
+ props.theme.colors.displayImageUpload.background};
+ border-radius: 50%;
+ margin-bottom: ${(props) => props.theme.spaces[7]}px;
+
+ img {
+ height: 100%;
+ width: 100%;
+ border-radius: 50%;
+ object-fit: cover;
+ }
+ }
+
+ .label {
+ ${(props) => getTypographyByKey(props, "h6")}
+ color: ${(props) => props.theme.colors.displayImageUpload.label};
+ }
+ }
+`;
+
+const defaultLabel = "Upload Display Picture";
+
+export default function DisplayImageUpload({
+ onChange,
+ onRemove,
+ submit,
+ value,
+}: Props) {
+ const [loadError, setLoadError] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const uppy = useUppy(() => {
+ const uppy = Uppy({
+ id: "uppy",
+ autoProceed: false,
+ allowMultipleUploads: false,
+ restrictions: {
+ maxNumberOfFiles: 1,
+ maxFileSize: 250000,
+ allowedFileTypes: [".jpg", ".jpeg", ".png"],
+ },
+ infoTimeout: 5000,
+ locale: {
+ strings: {},
+ },
+ });
+
+ uppy.setOptions({
+ locale: {
+ strings: {
+ cancel: "Cancel",
+ done: "Cancel",
+ },
+ },
+ });
+
+ uppy.use(ImageEditor, {
+ id: "ImageEditor",
+ quality: 0.8,
+ cropperOptions: {
+ viewMode: 1,
+ background: false,
+ autoCropArea: 1,
+ responsive: true,
+ },
+ actions: {
+ revert: false,
+ rotate: false,
+ flip: false,
+ zoomIn: false,
+ zoomOut: false,
+ cropSquare: false,
+ cropWidescreen: false,
+ cropWidescreenVertical: false,
+ },
+ });
+
+ uppy.on("file-added", (file: File) => {
+ onChange(file);
+ // TO trigger edit modal
+ const dashboard = uppy.getPlugin("uppy-img-upload-dashboard");
+ setTimeout(() => {
+ (dashboard as any).openFileEditor(file);
+ });
+ });
+
+ uppy.on("upload", () => {
+ submit(uppy);
+ setIsModalOpen(false);
+ });
+
+ uppy.on("file-editor:complete", (updatedFile) => {
+ onChange(updatedFile);
+ });
+
+ return uppy;
+ });
+
+ useEffect(() => {
+ if (value) setLoadError(false);
+ }, [value]);
+
+ return (
+ setIsModalOpen(true)}>
+
+
+ {!value || loadError ? (
+
+ ) : (
+
{
+ setLoadError(true);
+ }}
+ onLoad={() => setLoadError(false)}
+ src={value}
+ />
+ )}
+
+ {(!value || loadError) && (
+ {defaultLabel}
+ )}
+ {value && !loadError && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ if (onRemove) onRemove();
+ }}
+ >
+ {createMessage(REMOVE)}
+
+ )}
+
+ }
+ >
+
+
+
+ );
+}
diff --git a/app/client/src/components/ads/DraggableList.tsx b/app/client/src/components/ads/DraggableList.tsx
index 79a03ed66d..a14d9bc176 100644
--- a/app/client/src/components/ads/DraggableList.tsx
+++ b/app/client/src/components/ads/DraggableList.tsx
@@ -32,7 +32,7 @@ const updateSpringStyles = (
// Styles when items are dragged/idle
const dragIdleSpringStyles = (
order: Array,
- { down, originalIndex, curIndex, y, itemHeight }: SpringStyleProps,
+ { curIndex, down, itemHeight, originalIndex, y }: SpringStyleProps,
) => (index: number) => {
// picked/dragged item style
if (down && index === originalIndex) {
@@ -59,7 +59,7 @@ const DraggableListWrapper = styled.div`
}
`;
-function DraggableList({ items, ItemRenderer, onUpdate, itemHeight }: any) {
+function DraggableList({ itemHeight, ItemRenderer, items, onUpdate }: any) {
// order of items in the list
const order = useRef(items.map((_: any, index: any) => index));
@@ -119,7 +119,7 @@ function DraggableList({ items, ItemRenderer, onUpdate, itemHeight }: any) {
}}
style={{ height: items.length * itemHeight }}
>
- {springs.map(({ zIndex, y, scale }, i) => (
+ {springs.map(({ scale, y, zIndex }, i) => (
+ ItemRenderer={({ index, item }: any) =>
renderComponent({
deleteOption,
updateOption,
diff --git a/app/client/src/components/ads/Dropdown.tsx b/app/client/src/components/ads/Dropdown.tsx
index 77e7ce9191..8741a6a8da 100644
--- a/app/client/src/components/ads/Dropdown.tsx
+++ b/app/client/src/components/ads/Dropdown.tsx
@@ -1,9 +1,10 @@
-import React, { useState, useEffect, useCallback } from "react";
+import React, { ReactNode, useState, useEffect, useCallback } from "react";
import Icon, { IconName, IconSize } from "./Icon";
import { CommonComponentProps, Classes } from "./common";
import Text, { TextType } from "./Text";
import { Popover, Position } from "@blueprintjs/core";
import styled from "constants/DefaultTheme";
+import { Colors } from "constants/Colors";
export type DropdownOption = {
label?: string;
@@ -16,6 +17,22 @@ export type DropdownOption = {
onSelect?: (value?: string) => void;
};
+export interface DefaultDropDownValueNodeProps {
+ selected: DropdownOption;
+ showLabelOnly?: boolean;
+ isOpen?: boolean;
+}
+
+export interface RenderDropdownOptionType {
+ option: DropdownOption;
+ optionClickHandler?: (dropdownOption: DropdownOption) => void;
+}
+
+type RenderOption = ({
+ option,
+ optionClickHandler,
+}: RenderDropdownOptionType) => ReactNode;
+
export type DropdownProps = CommonComponentProps & {
options: DropdownOption[];
selected: DropdownOption;
@@ -25,7 +42,10 @@ export type DropdownProps = CommonComponentProps & {
showLabelOnly?: boolean;
optionWidth?: string;
showDropIcon?: boolean;
+ headerLabel?: string;
SelectedValueNode?: typeof DefaultDropDownValueNode;
+ bgColor?: string;
+ renderOption?: RenderOption;
};
export const DropdownContainer = styled.div<{ width: string; height: string }>`
@@ -38,12 +58,15 @@ const Selected = styled.div<{
isOpen: boolean;
disabled?: boolean;
height: string;
+ bgColor?: string;
}>`
padding: ${(props) => props.theme.spaces[2]}px
${(props) => props.theme.spaces[3]}px;
background: ${(props) =>
props.disabled
? props.theme.colors.dropdown.header.disabledBg
+ : !!props.bgColor
+ ? props.bgColor
: props.theme.colors.dropdown.header.bg};
display: flex;
align-items: center;
@@ -53,21 +76,31 @@ const Selected = styled.div<{
cursor: pointer;
${(props) =>
props.isOpen
- ? `border: 1px solid ${props.theme.colors.info.main}`
+ ? `border: 1px solid ${
+ !!props.bgColor ? props.bgColor : props.theme.colors.info.main
+ }`
: props.disabled
? `border: 1px solid ${props.theme.colors.dropdown.header.disabledBg}`
- : `border: 1px solid ${props.theme.colors.dropdown.header.bg}`};
+ : `border: 1px solid ${
+ !!props.bgColor
+ ? props.bgColor
+ : props.theme.colors.dropdown.header.bg
+ }`};
${(props) =>
props.isOpen && !props.disabled ? "box-sizing: border-box" : null};
${(props) =>
- props.isOpen && !props.disabled
+ props.isOpen && !props.disabled && !props.bgColor
? "box-shadow: 0px 0px 4px 4px rgba(203, 72, 16, 0.18)"
: null};
.${Classes.TEXT} {
${(props) =>
props.disabled
? `color: ${props.theme.colors.dropdown.header.disabledText}`
- : `color: ${props.theme.colors.dropdown.header.text}`};
+ : `color: ${
+ !!props.bgColor
+ ? Colors.WHITE
+ : props.theme.colors.dropdown.header.text
+ }`};
}
`;
@@ -159,6 +192,12 @@ const StyledSubText = styled(Text)`
}
`;
+const HeaderWrapper = styled.div`
+ color: #6d6d6d;
+ font-size: 10px;
+ padding: 0px 7px 7px 7px;
+`;
+
const SelectedDropDownHolder = styled.div`
display: flex;
align-items: center;
@@ -185,17 +224,17 @@ const SelectedIcon = styled(Icon)`
}
`;
-function DefaultDropDownValueNode({
+export function DefaultDropDownValueNode({
selected,
showLabelOnly,
-}: {
- selected: DropdownOption;
- showLabelOnly?: boolean;
-}) {
+}: DefaultDropDownValueNodeProps) {
return (
{selected.icon ? (
-
+
) : null}
{showLabelOnly ? selected.label : selected.value}
@@ -209,6 +248,7 @@ export default function Dropdown(props: DropdownProps) {
onSelect,
showDropIcon = true,
SelectedValueNode = DefaultDropDownValueNode,
+ renderOption,
} = { ...props };
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState(props.selected);
@@ -242,6 +282,7 @@ export default function Dropdown(props: DropdownProps) {
position={Position.BOTTOM_LEFT}
>
setIsOpen(!isOpen)}
>
{showDropIcon && }
+ {props.headerLabel && (
+ {props.headerLabel}
+ )}
{props.options.map((option: DropdownOption, index: number) => {
+ if (renderOption) {
+ return renderOption({
+ option,
+ optionClickHandler,
+ });
+ }
return (
{
+ if (isError) {
+ // if there is any error occurs while saving appname.
+ // last saved app name will be shown to user.
+ setValue(defaultValue);
+ }
+ }, [isError]);
+
useEffect(() => {
setSavingState(props.savingState);
}, [props.savingState]);
diff --git a/app/client/src/components/ads/EmojiPicker.tsx b/app/client/src/components/ads/EmojiPicker.tsx
index 7829e7ec0e..09f1937f86 100644
--- a/app/client/src/components/ads/EmojiPicker.tsx
+++ b/app/client/src/components/ads/EmojiPicker.tsx
@@ -1,46 +1,62 @@
import React, { useCallback, useState } from "react";
-import Picker, { IEmojiData } from "emoji-picker-react";
-import { withTheme } from "styled-components";
-import Icon, { IconSize } from "components/ads/Icon";
-import { Popover, Position } from "@blueprintjs/core";
-import { Theme } from "constants/DefaultTheme";
+import { Picker, BaseEmoji } from "emoji-mart";
+import { Popover2 } from "@blueprintjs/popover2";
+import Icon, { IconName, IconSize } from "components/ads/Icon";
+
+import { withTheme } from "styled-components";
+import { Theme } from "constants/DefaultTheme";
+import "@blueprintjs/popover2/lib/css/blueprint-popover2.css";
+import "emoji-mart/css/emoji-mart.css";
-// TODO remove: (trigger tests)
const EmojiPicker = withTheme(
({
- theme,
+ iconName,
+ iconSize,
onSelectEmoji,
+ theme,
}: {
+ iconName?: IconName;
theme: Theme;
- onSelectEmoji: (e: React.MouseEvent, emojiObject: IEmojiData) => void;
+ onSelectEmoji: (e: React.MouseEvent, emojiObject: BaseEmoji) => void;
+ iconSize?: IconSize;
}) => {
const [isOpen, setIsOpen] = useState(false);
const handleSelectEmoji = useCallback(
- (e: React.MouseEvent, emojiObject: IEmojiData) => {
- onSelectEmoji(e, emojiObject);
+ (emoji, event) => {
+ onSelectEmoji(event, emoji);
setIsOpen(false);
},
[onSelectEmoji],
);
return (
-
+ }
isOpen={isOpen}
minimal
onInteraction={(nextOpenState) => {
setIsOpen(nextOpenState);
}}
- position={Position.BOTTOM_RIGHT}
+ portalClassName="emoji-picker-portal"
>
-
-
+
);
},
);
diff --git a/app/client/src/components/ads/EmojiReactions.tsx b/app/client/src/components/ads/EmojiReactions.tsx
new file mode 100644
index 0000000000..cd57d6f64e
--- /dev/null
+++ b/app/client/src/components/ads/EmojiReactions.tsx
@@ -0,0 +1,185 @@
+import React from "react";
+import styled from "styled-components";
+import EmojiPicker from "./EmojiPicker";
+import { IconSize } from "./Icon";
+import TooltipComponent from "./Tooltip";
+
+const Container = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+`;
+
+const Bubble = styled.div<{ active?: boolean }>`
+ font-size: 16px; // emoji
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ padding: ${(props) => `2px ${props.theme.spaces[2]}px`};
+
+ background-color: ${(props) =>
+ props.active
+ ? props.theme.colors.reactionsComponent.reactionBackgroundActive
+ : props.theme.colors.reactionsComponent.reactionBackground};
+
+ border: 1px solid
+ ${(props) =>
+ props.active
+ ? props.theme.colors.reactionsComponent.borderActive
+ : "transparent"};
+
+ border-radius: ${(props) => `${props.theme.radii[4]}px`};
+ margin-left: ${(props) => `${props.theme.radii[1]}px`};
+ &:first-child {
+ margin-left: 0;
+ }
+`;
+
+const Count = styled.div<{ active?: boolean }>`
+ display: inline;
+ color: ${(props) =>
+ props.active
+ ? props.theme.colors.reactionsComponent.textActive
+ : props.theme.colors.reactionsComponent.text};
+ margin-left: ${(props) => props.theme.spaces[1]}px;
+ max-width: 30px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+`;
+
+const ReactionsByContainer = styled.span`
+ max-width: 200px;
+ display: inline-block;
+`;
+
+function ReactionsBy(props: { reaction: Reaction }) {
+ const { reaction } = props;
+
+ if (!reaction?.users) return null;
+ const isSliced = reaction?.users.length > 5;
+ const users = reaction?.users.slice(0, 5);
+
+ if (isSliced) users.push("...");
+ if (reaction.active) users.unshift("You");
+
+ return {users.join(", ")} ;
+}
+
+export type Reaction = {
+ count: number;
+ reactionEmoji: string;
+ active?: boolean;
+ users?: Array;
+};
+
+export type Reactions = Record;
+
+const transformReactions = (reactions: Reactions): Array => {
+ return Object.keys(reactions).map((emojiReaction: string) => ({
+ ...reactions[emojiReaction],
+ emojiReaction,
+ }));
+};
+
+export enum ReactionOperation {
+ ADD = "ADD",
+ REMOVE = "REMOVE",
+}
+
+function EmojiReactions({
+ onSelectReaction,
+ reactions = {},
+ hideReactions = false,
+ iconSize = IconSize.XXL,
+}: {
+ onSelectReaction: (
+ event: React.MouseEvent,
+ emojiData: string,
+ updatedReactions: Reactions,
+ addOrRemove: ReactionOperation,
+ ) => void;
+ reactions?: Reactions;
+ hideReactions?: boolean;
+ iconSize?: IconSize;
+}) {
+ const handleSelectReaction = (
+ _event: React.MouseEvent,
+ emojiData: string,
+ ) => {
+ const reactionsObject = reactions[emojiData];
+ let addOrRemove;
+ if (reactionsObject) {
+ if (reactionsObject.active) {
+ addOrRemove = ReactionOperation.REMOVE;
+ reactions[emojiData] = {
+ active: false,
+ reactionEmoji: emojiData,
+ count: reactionsObject.count - 1,
+ };
+ if (reactions[emojiData].count === 0) delete reactions[emojiData];
+ } else {
+ addOrRemove = ReactionOperation.ADD;
+ reactions[emojiData] = {
+ active: true,
+ reactionEmoji: emojiData,
+ count: reactionsObject.count + 1,
+ };
+ }
+ } else {
+ addOrRemove = ReactionOperation.ADD;
+ reactions[emojiData] = {
+ active: true,
+ reactionEmoji: emojiData,
+ count: 1,
+ };
+ }
+ onSelectReaction(
+ _event,
+ emojiData,
+ { ...reactions },
+ addOrRemove as ReactionOperation,
+ );
+ };
+
+ return (
+
+ {!hideReactions &&
+ transformReactions(reactions).map((reaction: Reaction) => (
+ }
+ disabled={!reaction.users || reaction.users.length === 0}
+ key={reaction.reactionEmoji}
+ modifiers={{ preventOverflow: { enabled: true } }}
+ >
+ handleSelectReaction(e, reaction.reactionEmoji)}
+ >
+ {reaction.reactionEmoji}
+ {reaction.count > 1 && (
+ {reaction.count}
+ )}
+
+
+ ))}
+ {!hideReactions ? (
+
+ handleSelectReaction(e, emoji.native)}
+ />
+
+ ) : (
+ handleSelectReaction(e, emoji.native)}
+ />
+ )}
+
+ );
+}
+
+export default EmojiReactions;
diff --git a/app/client/src/components/ads/FilePicker.tsx b/app/client/src/components/ads/FilePicker.tsx
index 1d5d6936b4..4ccffaa125 100644
--- a/app/client/src/components/ads/FilePicker.tsx
+++ b/app/client/src/components/ads/FilePicker.tsx
@@ -3,28 +3,52 @@ 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 { ReactComponent as UploadSuccessIcon } from "../../assets/icons/ads/upload_success.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";
-import { createMessage, ERROR_FILE_TOO_LARGE } from "constants/messages";
-
+import {
+ createMessage,
+ ERROR_FILE_TOO_LARGE,
+ REMOVE_FILE_TOOL_TIP,
+} from "constants/messages";
+import TooltipComponent from "components/ads/Tooltip";
+import { Position } from "@blueprintjs/core/lib/esm/common/position";
+import Icon, { IconSize } from "./Icon";
const CLOUDINARY_PRESETS_NAME = "";
const CLOUDINARY_CLOUD_NAME = "";
+const FileEndings = {
+ IMAGE: ".jpeg,.png,.svg",
+ JSON: ".json",
+ TEXT: ".txt",
+ ANY: "*",
+};
+
+export enum FileType {
+ IMAGE = "IMAGE",
+ JSON = "JSON",
+ TEXT = "TEXT",
+ ANY = "ANY",
+}
+
type FilePickerProps = {
onFileUploaded?: (fileUrl: string) => void;
onFileRemoved?: () => void;
fileUploader?: FileUploader;
url?: string;
logoUploadError?: string;
+ fileType: FileType;
+ delayedUpload?: boolean;
};
const ContainerDiv = styled.div<{
isUploaded: boolean;
isActive: boolean;
canDrop: boolean;
+ fileType: FileType;
}>`
width: 320px;
height: 190px;
@@ -41,7 +65,7 @@ const ContainerDiv = styled.div<{
color: ${(props) => props.theme.colors.filePicker.color};
}
- .bg-image {
+ .upload-form-container {
width: 100%;
height: 100%;
display: grid;
@@ -51,15 +75,36 @@ const ContainerDiv = styled.div<{
background-size: contain;
}
+ .centered {
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+
+ .success-container {
+ display: flex;
+ align-items: center;
+ .success-icon {
+ margin-right: ${(props) => props.theme.spaces[4]}px;
+ }
+
+ .success-text {
+ color: #03b365;
+ margin-right: ${(props) => props.theme.spaces[4]}px;
+ }
+ }
+ }
+
.file-description {
width: 95%;
- margin-top: auto;
+ margin: 0 auto;
+ margin-top: ${(props) =>
+ props.fileType === FileType.IMAGE ? "auto" : "0px"};
margin-bottom: ${(props) => props.theme.spaces[6] + 1}px;
display: none;
}
.file-spec {
- margin-bottom: ${(props) => props.theme.spaces[2]}px;
+ margin-bottom: ${(props) => props.theme.spaces[3]}px;
span {
margin-right: ${(props) => props.theme.spaces[4]}px;
}
@@ -116,6 +161,11 @@ const ContainerDiv = styled.div<{
}
`;
+const IconWrapper = styled.div`
+ width: ${(props) => props.theme.spaces[9]}px;
+ padding-left: ${(props) => props.theme.spaces[2]}px;
+`;
+
export type SetProgress = (percentage: number) => void;
export type UploadCallback = (url: string) => void;
export type FileUploader = (
@@ -159,7 +209,7 @@ export function CloudinaryUploader(
}
function FilePickerComponent(props: FilePickerProps) {
- const { logoUploadError } = props;
+ const { fileType, logoUploadError } = props;
const [fileInfo, setFileInfo] = useState<{ name: string; size: number }>({
name: "",
size: 0,
@@ -207,7 +257,7 @@ function FilePickerComponent(props: FilePickerProps) {
}
if (uploadPercentage === 100) {
setIsUploaded(true);
- if (fileDescRef.current && bgRef.current) {
+ if (fileDescRef.current && bgRef.current && fileType === FileType.IMAGE) {
fileDescRef.current.style.display = "none";
bgRef.current.style.opacity = "1";
}
@@ -219,6 +269,35 @@ function FilePickerComponent(props: FilePickerProps) {
}
function handleFileUpload(files: FileList | null) {
+ if (fileType === FileType.IMAGE) {
+ handleImageFileUpload(files);
+ } else {
+ handleOtherFileUpload(files);
+ }
+ }
+
+ function handleOtherFileUpload(files: FileList | null) {
+ const file = files && files[0];
+ let fileSize = 0;
+ if (!file) {
+ return;
+ }
+ fileSize = Math.floor(file.size / 1024);
+ setFileInfo({ name: file.name, size: fileSize });
+ if (props.delayedUpload) {
+ setIsUploaded(true);
+ setProgress(100);
+ }
+ if (fileDescRef.current) {
+ fileDescRef.current.style.display = "flex";
+ }
+ if (fileContainerRef.current) {
+ fileContainerRef.current.style.display = "none";
+ }
+ props.fileUploader && props.fileUploader(file, setProgress, onUpload);
+ }
+
+ function handleImageFileUpload(files: FileList | null) {
const file = files && files[0];
let fileSize = 0;
@@ -253,10 +332,15 @@ function FilePickerComponent(props: FilePickerProps) {
}
function removeFile() {
- if (fileContainerRef.current && bgRef.current) {
+ if (fileContainerRef.current) {
setFileUrl("");
+ if (fileDescRef.current) {
+ fileDescRef.current.style.display = "none";
+ }
fileContainerRef.current.style.display = "flex";
- bgRef.current.style.backgroundImage = "url('')";
+ if (bgRef.current) {
+ bgRef.current.style.backgroundImage = "url('')";
+ }
setIsUploaded(false);
props.onFileRemoved && props.onFileRemoved();
}
@@ -275,8 +359,9 @@ function FilePickerComponent(props: FilePickerProps) {
}
}, [props.url]);
+ // Following hook should be used only if file type is image.
useEffect(() => {
- if (fileUrl && !isUploaded) {
+ if (fileUrl && !isUploaded && fileType === FileType.IMAGE) {
setIsUploaded(true);
if (bgRef.current) {
bgRef.current.style.backgroundImage = `url(${fileUrl})`;
@@ -291,42 +376,47 @@ function FilePickerComponent(props: FilePickerProps) {
}
}, [fileUrl, logoUploadError]);
- return (
-
-
-
-
-
- Drag & Drop files to upload or
-
-
-
+ //
+
+ const uploadFileForm = (
+
+
+
+ Drag & Drop files to upload or
+
+
+
+ );
+
+ const uploadStatus = (
+
+ {fileInfo.name}
+ {fileInfo.size}KB
+
+ );
+
+ const imageUploadComponent = (
+ <>
+
+ {uploadFileForm}
-
- {fileInfo.name}
- {fileInfo.size}KB
-
+ {uploadStatus}
@@ -341,6 +431,45 @@ function FilePickerComponent(props: FilePickerProps) {
text="remove"
/>
+ >
+ );
+
+ const uploadComponent = (
+
+ {uploadFileForm}
+
+ {uploadStatus}
+
+
+
+ Successfully Uploaded!
+
+
+ removeFile()}>
+
+
+
+
+
+
+ );
+
+ return (
+
+ {fileType === FileType.IMAGE ? imageUploadComponent : uploadComponent}
);
}
diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx
index 17c778094a..109f5a704e 100644
--- a/app/client/src/components/ads/Icon.tsx
+++ b/app/client/src/components/ads/Icon.tsx
@@ -3,6 +3,7 @@ import { ReactComponent as DeleteIcon } from "assets/icons/ads/delete.svg";
import { ReactComponent as BookIcon } from "assets/icons/ads/book.svg";
import { ReactComponent as BugIcon } from "assets/icons/ads/bug.svg";
import { ReactComponent as CancelIcon } from "assets/icons/ads/cancel.svg";
+import { ReactComponent as ExpandMore } from "assets/icons/ads/expand-more.svg";
import { ReactComponent as CrossIcon } from "assets/icons/ads/cross.svg";
import { ReactComponent as OpenIcon } from "assets/icons/ads/open.svg";
import { ReactComponent as UserIcon } from "assets/icons/ads/user.svg";
@@ -39,6 +40,7 @@ import { ReactComponent as NoResponseIcon } from "assets/icons/ads/no-response.s
import { ReactComponent as LightningIcon } from "assets/icons/ads/lightning.svg";
import { ReactComponent as AddMoreIcon } from "assets/icons/ads/add-more.svg";
import { ReactComponent as RightArrowIcon } from "assets/icons/ads/right-arrow.svg";
+import { ReactComponent as TrendingFlat } from "assets/icons/ads/trending-flat.svg";
import { ReactComponent as DatasourceIcon } from "assets/icons/ads/datasource.svg";
import { ReactComponent as PlayIcon } from "assets/icons/ads/play.svg";
import { ReactComponent as DesktopIcon } from "assets/icons/ads/desktop.svg";
@@ -53,8 +55,24 @@ import { ReactComponent as Pin } from "assets/icons/comments/pin.svg";
import { ReactComponent as OvalCheck } from "assets/icons/comments/check-oval.svg";
import { ReactComponent as ContextMenu } from "assets/icons/ads/context-menu.svg";
import { ReactComponent as Trash } from "assets/icons/comments/trash.svg";
-import { ReactComponent as Pin2 } from "assets/icons/comments/pin_2.svg";
+import { ReactComponent as ReadPin } from "assets/icons/comments/read-pin.svg";
+import { ReactComponent as UnreadPin } from "assets/icons/comments/unread-pin.svg";
import { ReactComponent as Link2 } from "assets/icons/comments/link.svg";
+import { ReactComponent as CommentContextMenu } from "assets/icons/comments/context-menu.svg";
+import { ReactComponent as DownArrow2 } from "assets/icons/comments/down-arrow.svg";
+import { ReactComponent as Filter } from "assets/icons/comments/filter.svg";
+import { ReactComponent as Chat } from "assets/icons/comments/chat.svg";
+import { ReactComponent as Pin3 } from "assets/icons/comments/pin_3.svg";
+import { ReactComponent as Unpin } from "assets/icons/comments/unpin.svg";
+import { ReactComponent as Reaction } from "assets/icons/comments/reaction.svg";
+import { ReactComponent as Reaction2 } from "assets/icons/comments/reaction-2.svg";
+import { ReactComponent as Upload } from "assets/icons/ads/upload.svg";
+import { ReactComponent as Download } from "assets/icons/ads/download.svg";
+import { ReactComponent as ArrowForwardIcon } from "assets/icons/control/arrow_forward.svg";
+import { ReactComponent as CapSolidIcon } from "assets/icons/control/cap_solid.svg";
+import { ReactComponent as CapDotIcon } from "assets/icons/control/cap_dot.svg";
+import { ReactComponent as LineDottedIcon } from "assets/icons/control/line_dotted.svg";
+import { ReactComponent as LineDashedIcon } from "assets/icons/control/line_dashed.svg";
import styled from "styled-components";
import { CommonComponentProps, Classes } from "./common";
import { noop } from "lodash";
@@ -108,11 +126,14 @@ export const sizeHandler = (size?: IconSize) => {
};
export const IconCollection = [
+ "upload",
+ "download",
"book",
"bug",
"cancel",
"cross",
"delete",
+ "expand-more",
"open",
"user",
"general",
@@ -149,6 +170,7 @@ export const IconCollection = [
"lightning",
"add-more",
"right-arrow",
+ "trending-flat",
"datasource",
"play",
"desktop",
@@ -168,9 +190,23 @@ export const IconCollection = [
"PARAGRAPH_TWO",
"context-menu",
"trash",
- "pin-2",
"link-2",
"close-x",
+ "comment-context-menu",
+ "down-arrow-2",
+ "read-pin",
+ "unread-pin",
+ "filter",
+ "chat",
+ "pin-3",
+ "unpin",
+ "reaction",
+ "reaction-2",
+ "arrow-forward",
+ "cap-solid",
+ "cap-dot",
+ "line-dotted",
+ "line-dashed",
] as const;
export type IconName = typeof IconCollection[number];
@@ -203,17 +239,22 @@ export const IconWrapper = styled.span
`
!props.keepColors
? `
path {
- fill: ${props.theme.colors.icon.hover};
+ fill: ${props.hoverFillColor || props.theme.colors.icon.hover};
}
`
: ""}
}
- &:active {
+ &:hover {
cursor: pointer;
+ ${(props) =>
+ !props.keepColors
+ ? `
path {
- fill: ${(props) => props.theme.colors.icon.active};
+ fill: ${props.hoverFillColor || props.theme.colors.icon.hover};
}
+ `
+ : ""}
}
`;
@@ -224,6 +265,7 @@ export type IconProps = {
className?: string;
onClick?: (e: React.MouseEvent) => void;
fillColor?: string;
+ hoverFillColor?: string;
keepColors?: boolean;
};
@@ -246,6 +288,9 @@ const Icon = forwardRef(
case "delete":
returnIcon = ;
break;
+ case "expand-more":
+ returnIcon = ;
+ break;
case "open":
returnIcon = ;
break;
@@ -358,6 +403,9 @@ const Icon = forwardRef(
case "right-arrow":
returnIcon = ;
break;
+ case "trending-flat":
+ returnIcon = ;
+ break;
case "datasource":
returnIcon = ;
break;
@@ -405,8 +453,12 @@ const Icon = forwardRef(
returnIcon = ;
break;
- case "pin-2":
- returnIcon = ;
+ case "read-pin":
+ returnIcon = ;
+ break;
+
+ case "unread-pin":
+ returnIcon = ;
break;
case "link-2":
@@ -417,6 +469,66 @@ const Icon = forwardRef(
returnIcon = ;
break;
+ case "comment-context-menu":
+ returnIcon = ;
+ break;
+
+ case "down-arrow-2":
+ returnIcon = ;
+ break;
+
+ case "filter":
+ returnIcon = ;
+ break;
+
+ case "chat":
+ returnIcon = ;
+ break;
+
+ case "pin-3":
+ returnIcon = ;
+ break;
+
+ case "unpin":
+ returnIcon = ;
+ break;
+
+ case "reaction":
+ returnIcon = ;
+ break;
+
+ case "reaction-2":
+ returnIcon = ;
+ break;
+
+ case "upload":
+ returnIcon = ;
+ break;
+
+ case "download":
+ returnIcon = ;
+ break;
+
+ case "arrow-forward":
+ returnIcon = ;
+ break;
+
+ case "cap-solid":
+ returnIcon = ;
+ break;
+
+ case "cap-dot":
+ returnIcon = ;
+ break;
+
+ case "line-dotted":
+ returnIcon = ;
+ break;
+
+ case "line-dashed":
+ returnIcon = ;
+ break;
+
default:
returnIcon = null;
break;
diff --git a/app/client/src/components/ads/IconTabsComponent.tsx b/app/client/src/components/ads/IconTabsComponent.tsx
index 0ea7d8e293..a4c7909255 100644
--- a/app/client/src/components/ads/IconTabsComponent.tsx
+++ b/app/client/src/components/ads/IconTabsComponent.tsx
@@ -3,7 +3,8 @@ import styled from "styled-components";
import { ControlIcons, ControlIconName } from "icons/ControlIcons";
const ItemWrapper = styled.div<{ selected: boolean }>`
- width: 32px;
+ width: auto;
+ padding: 0 5px;
height: 32px;
display: flex;
align-items: center;
@@ -30,6 +31,7 @@ const FlexWrapper = styled.div`
export interface IconTabOption {
icon: string;
value: string;
+ width?: number;
}
interface IconTabsComponentProps {
@@ -41,21 +43,23 @@ interface IconTabsComponentProps {
function IconTabsComponent(props: IconTabsComponentProps) {
return (
- {props.options.map((option: IconTabOption, index: number) => {
- const controlIconName: ControlIconName = option.icon;
- const ControlIcon = ControlIcons[controlIconName];
- const isSelected = props.value === option.value;
- return (
- props.selectOption(option.value)}
- selected={isSelected}
- >
-
-
- );
- })}
+ {props.options.map(
+ ({ icon, value, width = 24 }: IconTabOption, index: number) => {
+ const controlIconName: ControlIconName = icon;
+ const ControlIcon = ControlIcons[controlIconName];
+ const isSelected = props.value === value;
+ return (
+ props.selectOption(value)}
+ selected={isSelected}
+ >
+
+
+ );
+ },
+ )}
);
}
diff --git a/app/client/src/components/ads/MentionsInput.tsx b/app/client/src/components/ads/MentionsInput.tsx
index 94525cb16f..18640a24db 100644
--- a/app/client/src/components/ads/MentionsInput.tsx
+++ b/app/client/src/components/ads/MentionsInput.tsx
@@ -1,16 +1,126 @@
import React, { useCallback, useMemo, useRef, useState } from "react";
import { DraftHandleValue, EditorState } from "draft-js";
import Editor from "@draft-js-plugins/editor";
+import ProfileImage, { Profile } from "pages/common/ProfileImage";
+
import createMentionPlugin, { MentionData } from "@draft-js-plugins/mention";
import styled from "styled-components";
import "@draft-js-plugins/mention/lib/plugin.css";
import "draft-js/dist/Draft.css";
+import { getTypographyByKey } from "constants/DefaultTheme";
+import { EntryComponentProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry";
+
+import Icon from "components/ads/Icon";
+
+import { INVITE_A_NEW_USER, createMessage } from "constants/messages";
+import { USER_PHOTO_URL } from "constants/userConstants";
+
+const StyledMention = styled.span`
+ color: ${(props) => props.theme.colors.comments.mention};
+`;
+
+export function MentionComponent(props: {
+ children: React.ReactNode;
+ entityKey: string;
+}) {
+ const { children } = props;
+ return @{children} ;
+}
+
+export enum Trigger {
+ "@" = "@",
+ "+" = "+",
+}
+
+const StyledSuggestionsComponent = styled.div<{ isFocused?: boolean }>`
+ display: flex;
+ padding: ${(props) =>
+ `${props.theme.spaces[4]}px ${props.theme.spaces[6]}px`};
+
+ & ${Profile} {
+ margin-right: ${(props) => props.theme.spaces[4]}px;
+ }
+ align-items: center;
+
+ &:hover {
+ background-color: ${(props) => props.theme.colors.mentionSuggestion.hover};
+ }
+
+ ${(props) =>
+ props.isFocused
+ ? `
+ background-color: ${props.theme.colors.mentionSuggestion.hover};
+ `
+ : ""}
+`;
+
+const Name = styled.div`
+ ${(props) => getTypographyByKey(props, "h5")}
+ color: ${(props) => props.theme.colors.mentionSuggestion.nameText};
+`;
+
+const Username = styled.div`
+ ${(props) => getTypographyByKey(props, "p3")}
+ color: ${(props) => props.theme.colors.mentionSuggestion.usernameText};
+`;
+
+const PlusCircle = styled.div`
+ width: 25px;
+ height: 25px;
+ display: flex;
+ border-radius: 50%;
+ align-items: center;
+ justify-content: center;
+ background-color: ${(props) =>
+ props.theme.colors.mentionsInput.mentionsInviteBtnPlusIcon};
+ & svg path {
+ stroke: #fff;
+ }
+ margin-right: ${(props) => props.theme.spaces[4]}px;
+`;
+
+function SuggestionComponent(props: EntryComponentProps) {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { theme, ...parentProps } = props;
+ const { user } = props.mention;
+
+ if (props.mention?.isInviteTrigger) {
+ return (
+
+
+
+
+
+ {createMessage(INVITE_A_NEW_USER)}
+ {props.mention.name}
+
+
+ );
+ }
+
+ return (
+
+
+
+ {props.mention.name}
+ {user?.username}
+
+
+ );
+}
const StyledContainer = styled.div`
- max-height: 60px;
overflow: auto;
flex: 1;
+
+ & [id^="mentions-list"] {
+ padding: 0;
+ }
`;
type Props = {
@@ -19,24 +129,30 @@ type Props = {
editorState: EditorState;
setEditorState: (editorState: EditorState) => void;
readOnly?: boolean;
- onSearchSuggestions: ({ value }: { value: string }) => void;
+ onSearchSuggestions: (props: { value: string; trigger: string }) => void;
autoFocus: boolean;
placeholder?: string;
+ onAddMention?: (mention: MentionData) => void;
};
function MentionsInput({
- onSubmit,
- suggestions,
- onSearchSuggestions,
- editorState,
- setEditorState,
autoFocus,
+ editorState,
+ onAddMention,
+ onSearchSuggestions,
+ onSubmit,
placeholder,
+ setEditorState,
+ suggestions,
}: Props) {
const ref = useRef(null);
const [open, setOpen] = useState(false);
const { MentionSuggestions, plugins } = useMemo(() => {
- const mentionPlugin = createMentionPlugin({ mentionTrigger: "@" });
+ const mentionPlugin = createMentionPlugin({
+ mentionRegExp: "[\\w@\\._]",
+ mentionTrigger: ["@", "+"],
+ mentionComponent: MentionComponent,
+ });
const { MentionSuggestions } = mentionPlugin;
return { plugins: [mentionPlugin], MentionSuggestions };
}, []);
@@ -79,13 +195,16 @@ function MentionsInput({
ref={setRef}
/>
{
+ if (onAddMention) {
+ onAddMention(mention);
+ }
+ }}
onOpenChange={onOpenChange}
onSearchChange={onSearchSuggestions}
open={open}
suggestions={suggestions}
- // onAddMention={() => {
- // // get the mention object selected
- // }}
/>
);
diff --git a/app/client/src/components/ads/Menu.tsx b/app/client/src/components/ads/Menu.tsx
index dc3437cba9..b235273c38 100644
--- a/app/client/src/components/ads/Menu.tsx
+++ b/app/client/src/components/ads/Menu.tsx
@@ -12,6 +12,8 @@ type MenuProps = CommonComponentProps & {
onOpening?: (node: HTMLElement) => void;
onClosing?: (node: HTMLElement) => void;
modifiers?: PopperModifiers;
+ isOpen?: boolean;
+ onClose?: () => void;
};
const MenuWrapper = styled.div`
@@ -30,8 +32,10 @@ function Menu(props: MenuProps) {
className={props.className}
data-cy={props.cypressSelector}
disabled={props.disabled}
+ isOpen={props.isOpen}
minimal
modifiers={props.modifiers}
+ onClose={props.onClose}
onClosing={props.onClosing}
onOpening={props.onOpening}
portalClassName={props.className}
diff --git a/app/client/src/components/ads/Radio.tsx b/app/client/src/components/ads/Radio.tsx
index 2d89c7e2d3..cbb38db562 100644
--- a/app/client/src/components/ads/Radio.tsx
+++ b/app/client/src/components/ads/Radio.tsx
@@ -15,6 +15,7 @@ export type RadioProps = CommonComponentProps & {
defaultValue: string;
onSelect?: (value: string) => void;
options: OptionProps[];
+ backgroundColor?: string;
};
const RadioGroup = styled.div<{
@@ -35,10 +36,11 @@ const RadioGroup = styled.div<{
: null};
`;
-const Radio = styled.label<{
+export const Radio = styled.label<{
disabled?: boolean;
columns?: number;
rows?: number;
+ backgroundColor?: string;
}>`
display: block;
position: relative;
@@ -100,7 +102,8 @@ const Radio = styled.label<{
${(props) =>
props.disabled
? `background-color: ${props.theme.colors.radio.disabled}`
- : `background-color: ${props.theme.colors.info.main};`};
+ : `background-color: ${props.backgroundColor ||
+ props.theme.colors.info.main};`};
top: ${(props) => props.theme.spaces[1] - 2}px;
left: ${(props) => props.theme.spaces[1] - 2}px;
border-radius: 50%;
@@ -137,6 +140,7 @@ export default function RadioComponent(props: RadioProps) {
>
{props.options.map((option: OptionProps, index: number) => (
props.theme.spaces[7]}px;
+ justify-content: space-between;
+ display: flex;
+`;
+
+const Dot = styled.div<{ active: boolean }>`
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ margin-right: ${(props) => props.theme.spaces[1]}px;
+ background-color: ${(props) =>
+ props.active
+ ? props.theme.colors.showcaseCarousel.activeStepDot
+ : props.theme.colors.showcaseCarousel.inactiveStepDot};
+`;
+
+const Row = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+const Buttons = styled.div`
+ display: flex;
+ & button:last-child {
+ margin-left: ${(props) => props.theme.spaces[1]}px;
+`;
+
+type Step = {
+ component: any;
+ props: any;
+};
+
+export type Steps = Array;
+
+type Props = {
+ steps: Steps;
+};
+
+type DotsProps = {
+ count: number;
+ activeIndex: number;
+};
+
+function Dots(props: DotsProps) {
+ return (
+
+ {Array.from(new Array(props.count)).map((_a, index) => (
+
+ ))}
+
+ );
+}
+
+export default function ShowcaseCarousel(props: Props) {
+ const { steps } = props;
+ const [activeIndex, setCurrentIdx] = useState(0);
+ const currentStep = steps[activeIndex];
+ const { component: ContentComponent, props: componentProps } = currentStep;
+ const length = steps.length;
+
+ const transition = useTransition("key", null, {
+ from: { transform: "translateY(+2%)" },
+ enter: { transform: "translateY(0%)" },
+ leave: { transform: "translateY(0%)" },
+ config: { duration: 300 },
+ });
+
+ return (
+
+ {transition.map(
+ ({ item, props: springProps }: { item: string; props: any }) => (
+
+
+
+ ),
+ )}
+
+
+
+ {!componentProps.hideBackBtn && (
+ setCurrentIdx(Math.max(0, activeIndex - 1))}
+ size={Size.large}
+ tag="button"
+ text={createMessage(BACK)}
+ />
+ )}
+ {
+ setCurrentIdx(Math.min(length - 1, activeIndex + 1));
+ if (typeof componentProps.onSubmit === "function") {
+ componentProps.onSubmit();
+ }
+ }}
+ size={Size.large}
+ tag="button"
+ text={componentProps.nextBtnText || createMessage(NEXT)}
+ />
+
+
+
+ );
+}
diff --git a/app/client/src/components/ads/Table.tsx b/app/client/src/components/ads/Table.tsx
index fdfd1501be..2c1f3d8e5c 100644
--- a/app/client/src/components/ads/Table.tsx
+++ b/app/client/src/components/ads/Table.tsx
@@ -93,14 +93,14 @@ interface TableProps {
}
function Table(props: TableProps) {
- const { data, columns } = props;
+ const { columns, data } = props;
const {
- getTableProps,
getTableBodyProps,
+ getTableProps,
headerGroups,
- rows,
prepareRow,
+ rows,
} = useTable({ columns, data }, useSortBy);
return (
diff --git a/app/client/src/components/ads/Tabs.tsx b/app/client/src/components/ads/Tabs.tsx
index a70206d531..2a52390282 100644
--- a/app/client/src/components/ads/Tabs.tsx
+++ b/app/client/src/components/ads/Tabs.tsx
@@ -11,10 +11,13 @@ export type TabProp = {
count?: number;
panelComponent: JSX.Element;
icon?: IconName;
+ iconSize?: IconSize;
};
-const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
- user-select: none;
+const TabsWrapper = styled.div<{
+ shouldOverflow?: boolean;
+ vertical?: boolean;
+}>`
border-radius: 0px;
height: 100%;
.react-tabs {
@@ -27,9 +30,9 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
.react-tabs__tab-list {
margin: 0px;
display: flex;
- align-items: center;
- border-bottom: ${(props) => props.theme.spaces[1] - 2}px solid
- ${(props) => props.theme.colors.tabs.border};
+ flex-direction: ${(props) => (!!props.vertical ? "column" : "row")};
+ align-items: ${(props) => (!!props.vertical ? "flex-start" : "center")};
+ border-bottom: none;
color: ${(props) => props.theme.colors.tabs.normal};
path {
fill: ${(props) => props.theme.colors.tabs.normal};
@@ -46,8 +49,10 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
display: flex;
align-items: center;
justify-content: flex-start;
- padding: ${(props) => props.theme.spaces[3] - 1}px 0
- ${(props) => props.theme.spaces[4]}px 0;
+ padding: ${(props) => props.theme.spaces[3] - 1}px
+ ${(props) => (props.vertical ? `${props.theme.spaces[4] - 1}px` : 0)}
+ ${(props) => props.theme.spaces[4]}px
+ ${(props) => (props.vertical ? `${props.theme.spaces[4] - 1}px` : 0)};
margin-right: ${(props) => props.theme.spaces[12] - 3}px;
text-align: center;
display: inline-flex;
@@ -73,10 +78,15 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
&::after {
content: "";
position: absolute;
- width: 100%;
- bottom: ${(props) => props.theme.spaces[0] - 1}px;
+ width: ${(props) =>
+ props.vertical ? `${props.theme.spaces[1] - 2}px` : "100%"};
+ bottom: ${(props) =>
+ props.vertical ? "0%" : `${props.theme.spaces[0] - 1}px`};
+ top: ${(props) =>
+ props.vertical ? `${props.theme.spaces[0] - 1}px` : "100%"};
left: ${(props) => props.theme.spaces[0]}px;
- height: ${(props) => props.theme.spaces[1] - 2}px;
+ height: ${(props) =>
+ props.vertical ? "100%" : `${props.theme.spaces[1] - 2}px`};
background-color: ${(props) => props.theme.colors.info.main};
}
}
@@ -84,10 +94,15 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
&::after {
content: "";
position: absolute;
- width: 100%;
- bottom: ${(props) => props.theme.spaces[0] - 1}px;
+ width: ${(props) =>
+ props.vertical ? `${props.theme.spaces[1] - 2}px` : "100%"};
+ bottom: ${(props) =>
+ props.vertical ? "0%" : `${props.theme.spaces[0] - 1}px`};
+ top: ${(props) =>
+ props.vertical ? `${props.theme.spaces[0] - 1}px` : "100%"};
left: ${(props) => props.theme.spaces[0]}px;
- height: ${(props) => props.theme.spaces[1] - 2}px;
+ height: ${(props) =>
+ props.vertical ? "100%" : `${props.theme.spaces[1] - 2}px`};
background-color: ${(props) => props.theme.colors.info.main};
}
box-shadow: none;
@@ -102,19 +117,19 @@ const TabTitleWrapper = styled.div`
display: flex;
align-items: center;
.${Classes.ICON} {
- margin-right: ${(props) => props.theme.spaces[3]}px;
+ margin-right: ${(props) => props.theme.spaces[2]}px;
}
`;
-const TabTitle = styled.span`
+export const TabTitle = styled.span`
font-size: ${(props) => props.theme.typography.h5.fontSize}px;
font-weight: ${(props) => props.theme.typography.h5.fontWeight};
line-height: ${(props) => props.theme.typography.h5.lineHeight - 3}px;
letter-spacing: ${(props) => props.theme.typography.h5.letterSpacing}px;
- margin: 0 5px;
+ margin: 0;
`;
-const TabCount = styled.div`
+export const TabCount = styled.div`
background-color: ${(props) => props.theme.colors.tabs.countBg};
border-radius: 8px;
width: 17px;
@@ -128,6 +143,7 @@ type TabbedViewComponentType = CommonComponentProps & {
selectedIndex?: number;
onSelect?: (tabIndex: number) => void;
overflow?: boolean;
+ vertical?: boolean;
};
export function TabComponent(props: TabbedViewComponentType) {
@@ -135,6 +151,7 @@ export function TabComponent(props: TabbedViewComponentType) {
{
@@ -144,10 +161,13 @@ export function TabComponent(props: TabbedViewComponentType) {
>
{props.tabs.map((tab) => (
-
+
{tab.icon ? (
-
+
) : null}
{tab.title}
{tab.count && tab.count > 0 ? (
diff --git a/app/client/src/components/ads/Text.tsx b/app/client/src/components/ads/Text.tsx
index c2a58a9a06..e2a3a526bb 100644
--- a/app/client/src/components/ads/Text.tsx
+++ b/app/client/src/components/ads/Text.tsx
@@ -32,6 +32,7 @@ export type TextProps = CommonComponentProps & {
className?: string;
weight?: FontWeight;
highlight?: boolean;
+ textAlign?: string;
};
const typeSelector = (props: TextProps & ThemeProp): string => {
@@ -72,6 +73,7 @@ const Text = styled.span.attrs((props: TextProps) => ({
color: ${(props) =>
props.highlight ? props.theme.colors.text.highlight : typeSelector(props)};
text-transform: ${(props) => (props.case ? props.case : "none")};
+ text-align: ${(props) => (props.textAlign ? props.textAlign : "normal")};
`;
export default Text;
diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx
index 633528236b..4fc64505d0 100644
--- a/app/client/src/components/ads/TextInput.tsx
+++ b/app/client/src/components/ads/TextInput.tsx
@@ -84,7 +84,7 @@ const boxStyles = (
const StyledInput = styled((props) => {
// we are removing non input related props before passing them in the components
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { inputStyle, inputRef, dataType, theme, ...inputProps } = props;
+ const { dataType, inputRef, inputStyle, theme, ...inputProps } = props;
return props.asyncControl ? (
void;
dispatchableAction?: { type: ReduxActionType; payload: any };
+ showDebugButton?: boolean;
hideProgressBar?: boolean;
};
@@ -36,7 +37,7 @@ const WrappedToastContainer = styled.div`
margin-bottom: ${(props) => props.theme.spaces[4]}px;
}
.Toastify__toast-container--top-right {
- top: 4em;
+ top: 8em;
}
`;
export function StyledToastContainer(props: ToastOptions) {
@@ -96,6 +97,7 @@ const ToastBody = styled.div<{
color: ${props.theme.colors.toast.undo};
line-height: 18px;
font-weight: 600;
+ white-space: nowrap
}
`
: null}
@@ -131,7 +133,7 @@ function ToastComponent(props: ToastProps & { undoAction?: () => void }) {
) : null}
{props.text}
- {props.variant === Variant.danger ? (
+ {props.variant === Variant.danger && props.showDebugButton ? (
) : null}
diff --git a/app/client/src/components/ads/Tooltip.tsx b/app/client/src/components/ads/Tooltip.tsx
index da2b2d7b23..ce1c37a1b8 100644
--- a/app/client/src/components/ads/Tooltip.tsx
+++ b/app/client/src/components/ads/Tooltip.tsx
@@ -2,13 +2,15 @@ import React from "react";
import { CommonComponentProps } from "./common";
import { Position, Tooltip, PopperBoundary } from "@blueprintjs/core";
import { GLOBAL_STYLE_TOOLTIP_CLASSNAME } from "globalStyles/tooltip";
+import { Modifiers } from "popper.js";
type Variant = "dark" | "light";
type TooltipProps = CommonComponentProps & {
content: JSX.Element | string;
+ disabled?: boolean;
position?: Position;
- children: JSX.Element;
+ children: JSX.Element | React.ReactNode;
variant?: Variant;
maxWidth?: string;
boundary?: PopperBoundary;
@@ -17,21 +19,29 @@ type TooltipProps = CommonComponentProps & {
autoFocus?: boolean;
hoverOpenDelay?: number;
minimal?: boolean;
+ modifiers?: Modifiers;
+ isOpen?: boolean;
};
+const portalContainer = document.getElementById("tooltip-root");
+
function TooltipComponent(props: TooltipProps) {
return (
diff --git a/app/client/src/components/ads/TreeDropdown.tsx b/app/client/src/components/ads/TreeDropdown.tsx
index bede261783..be5315306c 100644
--- a/app/client/src/components/ads/TreeDropdown.tsx
+++ b/app/client/src/components/ads/TreeDropdown.tsx
@@ -19,6 +19,7 @@ export type TreeDropdownOption = DropdownOption & {
children?: TreeDropdownOption[];
className?: string;
type?: string;
+ icon?: React.ReactNode;
};
type Setter = (value: TreeDropdownOption, defaultVal?: string) => void;
@@ -137,13 +138,13 @@ function getSelectedOption(
export default function TreeDropdown(props: TreeDropdownProps) {
const {
- selectedValue,
defaultText,
- optionTree,
- onSelect,
- getDefaults,
- selectedLabelModifier,
displayValue,
+ getDefaults,
+ onSelect,
+ optionTree,
+ selectedLabelModifier,
+ selectedValue,
toggle,
} = props;
const selectedOption = getSelectedOption(
diff --git a/app/client/src/components/ads/UserProfileImagePicker.tsx b/app/client/src/components/ads/UserProfileImagePicker.tsx
new file mode 100644
index 0000000000..72a50becb8
--- /dev/null
+++ b/app/client/src/components/ads/UserProfileImagePicker.tsx
@@ -0,0 +1,54 @@
+import React, { useState, useRef, useEffect } from "react";
+import { updatePhoto, removePhoto } from "actions/userActions";
+import { useDispatch } from "react-redux";
+
+import DisplayImageUpload from "components/ads/DisplayImageUpload";
+
+import UserApi from "api/UserApi";
+import Uppy from "@uppy/core";
+
+function FormDisplayImage() {
+ const [file, setFile] = useState();
+ const [imageURL, setImageURL] = useState(`/api/${UserApi.photoURL}`);
+ const dispatch = useDispatch();
+ const dispatchActionRef = useRef<(uppy: Uppy.Uppy) => void | null>();
+
+ const onUploadComplete = (uppy: Uppy.Uppy) => {
+ uppy.reset();
+ setImageURL(`/api/${UserApi.photoURL}?${new Date().getTime()}`);
+ };
+
+ const onSelectFile = (file: any) => {
+ setFile(file.data);
+ };
+
+ useEffect(() => {
+ dispatchActionRef.current = (uppy: Uppy.Uppy) => {
+ dispatch(updatePhoto({ file, callback: () => onUploadComplete(uppy) }));
+ };
+ }, [file]);
+
+ const upload = (uppy: Uppy.Uppy) => {
+ if (typeof dispatchActionRef.current === "function")
+ dispatchActionRef.current(uppy);
+ };
+
+ const removeProfileImage = () => {
+ dispatch(
+ removePhoto(() => {
+ setImageURL(`/api/${UserApi.photoURL}?${new Date().getTime()}`);
+ }),
+ );
+ };
+
+ return (
+
+ );
+}
+
+export default FormDisplayImage;
diff --git a/app/client/src/components/ads/formFields/TextField.tsx b/app/client/src/components/ads/formFields/TextField.tsx
index 38df047f1d..1e1b62ab36 100644
--- a/app/client/src/components/ads/formFields/TextField.tsx
+++ b/app/client/src/components/ads/formFields/TextField.tsx
@@ -19,7 +19,9 @@ const renderComponent = (
return (
<>
-
+ {!componentProps.hideErrorMessage && (
+
+ )}
>
);
};
@@ -32,6 +34,7 @@ type FormTextFieldProps = {
intent?: Intent;
disabled?: boolean;
autoFocus?: boolean;
+ hideErrorMessage?: boolean;
};
function FormTextField(props: FormTextFieldProps) {
diff --git a/app/client/src/components/ads/tour/TourTooltipWrapper.tsx b/app/client/src/components/ads/tour/TourTooltipWrapper.tsx
new file mode 100644
index 0000000000..abd14b7a81
--- /dev/null
+++ b/app/client/src/components/ads/tour/TourTooltipWrapper.tsx
@@ -0,0 +1,110 @@
+import React, { useEffect, useRef } from "react";
+import TooltipComponent from "components/ads/Tooltip";
+import { useSelector } from "react-redux";
+import Text, { TextType } from "../Text";
+import { Position } from "@blueprintjs/core";
+import { getActiveTourIndex, getActiveTourType } from "selectors/tourSelectors";
+import { TourType } from "entities/Tour";
+import TourStepsByType from "constants/TourSteps";
+import { AppState } from "reducers";
+import { noop } from "lodash";
+import styled, { CSSProperties } from "styled-components";
+import { Modifiers } from "popper.js";
+import lottie from "lottie-web";
+import pulsatingDot from "assets/lottie/pulse-dot.json";
+import { Indices } from "constants/Layers";
+
+type Props = {
+ children: React.ReactNode;
+ hasOverlay?: boolean;
+ tourType: TourType;
+ tourIndex: number;
+ modifiers?: Modifiers;
+ onClick?: () => void;
+ pulseStyles?: CSSProperties;
+ showPulse?: boolean;
+};
+
+const Overlay = styled.div`
+ background-color: ${(props) => props.theme.colors.overlayColor};
+ height: 100%;
+ width: 100%;
+ left: 0;
+ top: 0;
+ position: fixed;
+ z-index: ${Indices.Layer1};
+`;
+
+const PulseDot = styled.div`
+ position: absolute;
+ height: 50px;
+ width: 50px;
+`;
+
+const Container = styled.div`
+ position: relative;
+ z-index: ${Indices.Layer1};
+`;
+
+function TourTooltipWrapper(props: Props) {
+ const { children, tourIndex, tourType } = props;
+ const isCurrentStepActive = useSelector(
+ (state: AppState) => getActiveTourIndex(state) === tourIndex,
+ );
+ const isCurrentTourActive = useSelector(
+ (state: AppState) => getActiveTourType(state) === tourType,
+ );
+ const tourStepsConfig = TourStepsByType[tourType as TourType];
+ const tourStepConfig = tourStepsConfig[tourIndex];
+ const isOpen = isCurrentStepActive && isCurrentTourActive;
+ const dotRef = useRef(null);
+
+ useEffect(() => {
+ const anim = lottie.loadAnimation({
+ animationData: pulsatingDot,
+ autoplay: true,
+ container: dotRef?.current as HTMLDivElement,
+ renderer: "svg",
+ loop: true,
+ });
+
+ return () => {
+ anim?.destroy();
+ };
+ }, [isOpen, dotRef?.current]);
+
+ return (
+ <>
+ {/* A crude overlay which won't work with containers having overflow hidden */}
+ {isOpen && props.hasOverlay && }
+
+ {isOpen && props.showPulse && (
+
+ )}
+
+ {tourStepConfig?.data.message}
+
+ }
+ isOpen={!!isOpen}
+ modifiers={props.modifiers}
+ position={Position.BOTTOM}
+ >
+ {children}
+
+
+ >
+ );
+}
+
+export default TourTooltipWrapper;
diff --git a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx
index 43b3bf3b7b..c9b30a9583 100644
--- a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx
@@ -72,7 +72,7 @@ class ChartComponent extends React.Component {
chartInstance = new FusionCharts();
getChartType = () => {
- const { chartType, allowHorizontalScroll, chartData } = this.props;
+ const { allowHorizontalScroll, chartData, chartType } = this.props;
const dataLength = Object.keys(chartData).length;
const isMSChart = dataLength > 1;
switch (chartType) {
@@ -307,6 +307,15 @@ class ChartComponent extends React.Component {
renderAt: this.props.widgetId + "chart-container",
width: "100%",
height: "100%",
+ events: {
+ dataPlotClick: (evt: any) => {
+ const data = evt.data;
+ this.props.onDataPointClick({
+ x: data.categoryLabel,
+ y: data.dataValue,
+ });
+ },
+ },
...this.getCustomFusionChartDataSource(),
};
this.chartInstance = new FusionCharts(chartConfig);
diff --git a/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx b/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx
index 16cc613961..edda15651f 100644
--- a/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx
@@ -1,9 +1,11 @@
import React, { ReactNode, useRef, useEffect, RefObject } from "react";
import styled, { css } from "styled-components";
+import tinycolor from "tinycolor2";
import { ComponentProps } from "./BaseComponent";
-import { getBorderCSSShorthand, invisible } from "constants/DefaultTheme";
+import { invisible } from "constants/DefaultTheme";
import { Color } from "constants/Colors";
import { generateClassName, getCanvasClassName } from "utils/generators";
+import { useCanvasMinHeightUpdateHook } from "utils/hooks/useCanvasMinHeightUpdateHook";
const scrollContents = css`
overflow-y: auto;
@@ -17,24 +19,44 @@ const StyledContainerComponent = styled.div<
${(props) =>
props.containerStyle !== "none"
? `
- border: ${getBorderCSSShorthand(props.theme.borders[2])};
+ box-shadow: 0px 0px 0px 1px #E7E7E7;
border-radius: 0;`
: ""}
height: 100%;
width: 100%;
background: ${(props) => props.backgroundColor};
-
+ opacity: ${(props) => (props.resizeDisabled ? "0.8" : "1")};
+ position: relative;
${(props) => (!props.isVisible ? invisible : "")};
- opacity: ${(props) => (props.resizeDisabled ? "0.5" : "1")};
- pointer-events: ${(props) => (props.resizeDisabled ? "none" : "inherit")};
- overflow: hidden;
- ${(props) => (props.shouldScrollContents ? scrollContents : "")}
+ box-shadow: ${(props) =>
+ props.selected ? "0px 0px 0px 3px rgba(59,130,246,0.5)" : "none"};
+ z-index: ${(props) => (props.focused ? "3" : props.selected ? "2" : "1")};
+ ${(props) =>
+ props.shouldScrollContents === true
+ ? scrollContents
+ : props.shouldScrollContents === false
+ ? css`
+ overflow: hidden;
+ `
+ : ""}
+
+ &:hover {
+ z-index: ${(props) => (props.onClickCapture ? "2" : "1")};
+ cursor: ${(props) => (props.onClickCapture ? "pointer" : "inherit")};
+ background: ${(props) => {
+ return props.onClickCapture && props.backgroundColor
+ ? tinycolor(props.backgroundColor)
+ .darken(5)
+ .toString()
+ : props.backgroundColor;
+ }};
+ }
}`;
function ContainerComponent(props: ContainerComponentProps) {
const containerStyle = props.containerStyle || "card";
const containerRef: RefObject = useRef(null);
-
+ useCanvasMinHeightUpdateHook(props.widgetId, props.minHeight);
useEffect(() => {
if (!props.shouldScrollContents) {
const supportsNativeSmoothScroll =
@@ -73,6 +95,9 @@ export interface ContainerComponentProps extends ComponentProps {
backgroundColor?: Color;
shouldScrollContents?: boolean;
resizeDisabled?: boolean;
+ selected?: boolean;
+ focused?: boolean;
+ minHeight?: number;
}
export default ContainerComponent;
diff --git a/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx b/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx
index d6465d62a9..7220d1656c 100644
--- a/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx
+++ b/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx
@@ -77,14 +77,14 @@ const selectStyles = {
class CreatableDropdown extends React.Component {
render() {
const {
- placeholder,
- options,
- isLoading,
- input,
- noOptionsMessage,
components,
+ input,
inputValue,
+ isLoading,
+ noOptionsMessage,
onInputChange,
+ options,
+ placeholder,
} = this.props;
const optionalProps: Partial = {};
if (noOptionsMessage) optionalProps.noOptionsMessage = noOptionsMessage;
diff --git a/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx b/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx
index ca1fb37219..967ef2a981 100644
--- a/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx
@@ -81,16 +81,16 @@ export class DroppableComponent extends React.Component<
render() {
const {
- renderComponent,
deleteOption,
- updateOption,
- toggleVisibility,
onEdit,
+ renderComponent,
+ toggleVisibility,
+ updateOption,
} = this.props;
return (
- {({ innerRef, droppableProps, placeholder }) => (
+ {({ droppableProps, innerRef, placeholder }) => (
}
{...droppableProps}
@@ -103,7 +103,7 @@ export class DroppableComponent extends React.Component<
index={index}
key={item.id}
>
- {({ innerRef, draggableProps, dragHandleProps }) => (
+ {({ draggableProps, dragHandleProps, innerRef }) => (
}
{...draggableProps}
diff --git a/app/client/src/components/designSystems/appsmith/Dropdown.tsx b/app/client/src/components/designSystems/appsmith/Dropdown.tsx
index ecade3fff9..29359e7c23 100644
--- a/app/client/src/components/designSystems/appsmith/Dropdown.tsx
+++ b/app/client/src/components/designSystems/appsmith/Dropdown.tsx
@@ -4,6 +4,7 @@ import Select from "react-select";
import { WrappedFieldInputProps } from "redux-form";
import { theme } from "constants/DefaultTheme";
import { SelectComponentsConfig } from "react-select/src/components";
+import { LayersContext } from "../../../constants/Layers";
export type DropdownProps = {
options: Array<{
@@ -50,13 +51,21 @@ const selectStyles = {
padding: "5px",
}),
indicatorSeparator: () => ({}),
+ menu: (provided: any) => ({ ...provided, zIndex: 2 }),
+ menuPortal: (base: any) => ({ ...base, zIndex: 2 }),
};
export function BaseDropdown(props: DropdownProps) {
- const { input, customSelectStyles } = props;
+ const layer = React.useContext(LayersContext);
+ const { customSelectStyles, input } = props;
+ const menuPortalStyle = {
+ menuPortal: (styles: any) => ({ ...styles, zIndex: layer.max }),
+ };
+
return (
) => void;
}
@@ -17,16 +20,16 @@ export const StyledImage = styled.div<
}
>`
position: relative;
- display: flex;
+ display: flex;
flex-direction: "row";
+ background-size: ${(props) => props.objectFit ?? "cover"};
cursor: ${(props) =>
props.showHoverPointer && props.onClick ? "pointer" : "inherit"};
background: ${(props) => props.backgroundColor};
- background-image: url("${(props) =>
- props.imageError ? props.defaultImageUrl : props.imageUrl}");
+ background-image: ${(props) =>
+ `url(${props.imageError ? props.defaultImageUrl : props.imageUrl})`};
background-position: center;
background-repeat: no-repeat;
- background-size: contain;
height: 100%;
width: 100%;
`;
@@ -41,6 +44,46 @@ const Wrapper = styled.div`
}
`;
+const ControlBtnWrapper = styled.div`
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ padding: 5px 0px;
+ z-index: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: white;
+`;
+
+const ControlBtn = styled.div`
+ height: 25px;
+ width: 45px;
+ color: white;
+ padding: 0px 10px;
+ display: inline-block;
+
+ &.separator {
+ border-right: 1px solid ${Colors.ALTO2};
+ }
+
+ & > div {
+ cursor: pointer;
+ height: 100%;
+ width: 100%;
+ padding: 4px;
+ transition: background 0.2s linear;
+
+ & > svg {
+ height: 16px;
+ width: 17px;
+ }
+ &: hover {
+ background: #ebebeb;
+ }
+ }
+`;
+
enum ZoomingState {
MAX_ZOOMED_OUT = "MAX_ZOOMED_OUT",
MAX_ZOOMED_IN = "MAX_ZOOMED_IN",
@@ -49,6 +92,8 @@ class ImageComponent extends React.Component<
ImageComponentProps,
{
imageError: boolean;
+ showImageControl: boolean;
+ imageRotation: number;
zoomingState: ZoomingState;
}
> {
@@ -58,11 +103,14 @@ class ImageComponent extends React.Component<
this.isPanning = false;
this.state = {
imageError: false,
+ showImageControl: false,
+ imageRotation: 0,
zoomingState: ZoomingState.MAX_ZOOMED_OUT,
};
}
render() {
const { maxZoomLevel } = this.props;
+ const { imageRotation } = this.state;
const zoomActive =
maxZoomLevel !== undefined && maxZoomLevel > 1 && !this.isPanning;
const isZoomingIn = this.state.zoomingState === ZoomingState.MAX_ZOOMED_OUT;
@@ -71,7 +119,10 @@ class ImageComponent extends React.Component<
cursor = isZoomingIn ? "zoom-in" : "zoom-out";
}
return (
-
+
{({ zoomIn, zoomOut }: any) => (
-
- ) => {
- if (!this.isPanning) {
- if (isZoomingIn) {
- zoomIn(event);
- } else {
- zoomOut(event);
+ <>
+ {this.renderImageControl()}
+
+ ) => {
+ if (!this.isPanning) {
+ if (isZoomingIn) {
+ zoomIn(event);
+ } else {
+ zoomOut(event);
+ }
+ this.props.onClick && this.props.onClick(event);
}
- this.props.onClick && this.props.onClick(event);
- }
- this.isPanning = false;
- }}
- style={{
- cursor,
- }}
- >
-
-
-
+ style={{
+ cursor,
+ transform: `rotate(${imageRotation}deg)`,
+ }}
+ >
+ {/* Used for running onImageError and onImageLoad Functions since Background Image doesn't have the functionality */}
+
+
+
+ >
)}
);
}
+ renderImageControl = () => {
+ const { enableDownload, enableRotation } = this.props;
+ const { showImageControl } = this.state;
+
+ if (showImageControl && (enableRotation || enableDownload)) {
+ return (
+
+ {enableRotation && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {enableDownload && (
+
+
+
+ )}
+
+ );
+ }
+ };
+
+ handleImageRotate = (rotateRight: boolean) => (e: any) => {
+ const { imageRotation } = this.state;
+
+ const nextRotation = rotateRight ? imageRotation + 90 : imageRotation - 90;
+
+ this.setState({ imageRotation: nextRotation % 360 });
+
+ if (!!e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ };
+
+ handleImageDownload = (e: any) => {
+ const { imageUrl, widgetId } = this.props;
+ const fileName = `${widgetId}-download`;
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", imageUrl, true);
+ xhr.responseType = "blob";
+
+ xhr.onload = function() {
+ const urlCreator = window.URL || window.webkitURL;
+ const imageUrl = urlCreator.createObjectURL(this.response);
+ const tag = document.createElement("a");
+ tag.href = imageUrl;
+ tag.download = fileName;
+ document.body.appendChild(tag);
+ tag.click();
+ document.body.removeChild(tag);
+ window.URL.revokeObjectURL(imageUrl);
+ };
+ xhr.send();
+
+ if (!!e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ };
+
+ onMouseEnter = () => this.setState({ showImageControl: true });
+
+ onMouseLeave = () => this.setState({ showImageControl: false });
+
onImageError = () => {
this.setState({
imageError: true,
@@ -178,6 +347,9 @@ export interface ImageComponentProps extends ComponentProps {
isLoading: boolean;
showHoverPointer?: boolean;
maxZoomLevel: number;
+ enableRotation?: boolean;
+ enableDownload?: boolean;
+ objectFit: string;
disableDrag: (disabled: boolean) => void;
onClick?: (event: React.MouseEvent) => void;
}
diff --git a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx
index db47d63a73..5e5bed6124 100644
--- a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx
+++ b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx
@@ -1,13 +1,15 @@
-import React, { CSSProperties, ReactNode, useMemo } from "react";
+import React, { CSSProperties, ReactNode, useCallback, useMemo } from "react";
import { BaseStyle } from "widgets/BaseWidget";
import { WIDGET_PADDING } from "constants/WidgetConstants";
import { generateClassName } from "utils/generators";
import styled from "styled-components";
import { useClickOpenPropPane } from "utils/hooks/useClickOpenPropPane";
+import { stopEventPropagation } from "utils/AppsmithUtils";
+import { Layers } from "constants/Layers";
const PositionedWidget = styled.div`
&:hover {
- z-index: 1;
+ z-index: ${Layers.positionedWidget + 1} !important;
}
`;
type PositionedContainerProps = {
@@ -15,6 +17,9 @@ type PositionedContainerProps = {
children: ReactNode;
widgetId: string;
widgetType: string;
+ selected?: boolean;
+ focused?: boolean;
+ resizeDisabled?: boolean;
};
export function PositionedContainer(props: PositionedContainerProps) {
@@ -22,7 +27,6 @@ export function PositionedContainer(props: PositionedContainerProps) {
const y = props.style.yPosition + (props.style.yPositionUnit || "px");
const padding = WIDGET_PADDING;
const openPropertyPane = useClickOpenPropPane();
-
// memoized classname
const containerClassName = useMemo(() => {
return (
@@ -42,14 +46,28 @@ export function PositionedContainer(props: PositionedContainerProps) {
height: props.style.componentHeight + (props.style.heightUnit || "px"),
width: props.style.componentWidth + (props.style.widthUnit || "px"),
padding: padding + "px",
+ zIndex:
+ props.selected || props.focused
+ ? Layers.selectedWidget
+ : Layers.positionedWidget,
+ backgroundColor: "inherit",
};
}, [props.style]);
+ const openPropPane = useCallback((e) => openPropertyPane(e, props.widgetId), [
+ props.widgetId,
+ openPropertyPane,
+ ]);
+
return (
diff --git a/app/client/src/components/designSystems/appsmith/SearchComponent.tsx b/app/client/src/components/designSystems/appsmith/SearchComponent.tsx
index 46bfac9a87..5795ba92fa 100644
--- a/app/client/src/components/designSystems/appsmith/SearchComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/SearchComponent.tsx
@@ -2,6 +2,7 @@ import React from "react";
import styled from "styled-components";
import { InputGroup } from "@blueprintjs/core";
import { debounce } from "lodash";
+import { Colors } from "constants/Colors";
interface SearchProps {
onSearch: (value: any) => void;
@@ -13,9 +14,12 @@ const SearchInputWrapper = styled(InputGroup)`
&&& input {
box-shadow: none;
font-size: 12px;
+ color: ${Colors.SILVER_CHALICE};
}
&&& svg {
- opacity: 0.6;
+ path {
+ fill: ${Colors.SILVER_CHALICE};
+ }
}
margin: 5px 16px;
width: 250px;
@@ -52,6 +56,7 @@ class SearchComponent extends React.Component<
render() {
return (
;
}
return (
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx
index f0925d75e3..bbf90c2653 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx
@@ -433,7 +433,7 @@ function CascadeField(props: CascadeFieldProps) {
}
function Fields(props: CascadeFieldProps & { state: CascadeFieldState }) {
- const { index, removeFilter, applyFilter, hasAnyFilters } = props;
+ const { applyFilter, hasAnyFilters, index, removeFilter } = props;
const [state, dispatch] = React.useReducer(CaseCaseFieldReducer, props.state);
const handleRemoveFilter = () => {
dispatch({ type: CascadeFieldActionTypes.DELETE_FILTER });
@@ -470,16 +470,16 @@ function Fields(props: CascadeFieldProps & { state: CascadeFieldState }) {
};
const {
- operator,
column,
condition,
- showConditions,
- value,
- showInput,
- showDateInput,
conditions,
isDeleted,
isUpdate,
+ operator,
+ showConditions,
+ showDateInput,
+ showInput,
+ value,
} = state;
useEffect(() => {
if (!isDeleted && isUpdate) {
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/CommonUtilities.test.ts b/app/client/src/components/designSystems/appsmith/TableComponent/CommonUtilities.test.ts
index 026291ff16..bf008539d3 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/CommonUtilities.test.ts
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/CommonUtilities.test.ts
@@ -31,7 +31,6 @@ describe("TableUtilities", () => {
"url",
true,
);
- // console.log(JSON.stringify(sortedTableData));
expect(sortedTableData).toStrictEqual(expected);
});
});
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts b/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts
index a8dbb7d807..88c74bac35 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts
@@ -94,6 +94,7 @@ export interface CellLayoutProperties {
buttonStyle?: string;
buttonLabelColor?: string;
buttonLabel?: string;
+ displayText?: string;
}
export interface TableColumnMetaProps {
@@ -144,6 +145,7 @@ export interface ColumnProperties {
inputFormat?: string;
dropdownOptions?: string;
onOptionChange?: string;
+ displayText?: string;
}
export const ConditionFunctions: {
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx
index aceab79e54..45e16fcd1e 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx
@@ -1,10 +1,12 @@
import React, { useRef } from "react";
+import { reduce } from "lodash";
import {
useTable,
usePagination,
useBlockLayout,
useResizeColumns,
useRowSelect,
+ Row,
} from "react-table";
import {
TableWrapper,
@@ -12,7 +14,12 @@ import {
TableHeaderInnerWrapper,
} from "./TableStyledWrappers";
import { ReactTableFilter } from "components/designSystems/appsmith/TableComponent/TableFilters";
-import { TableHeaderCell, renderEmptyRows } from "./TableUtilities";
+import {
+ TableHeaderCell,
+ renderEmptyRows,
+ renderCheckBoxCell,
+ renderCheckBoxHeaderCell,
+} from "./TableUtilities";
import TableHeader from "./TableHeader";
import { Classes } from "@blueprintjs/core";
import {
@@ -47,6 +54,7 @@ interface TableProps {
}) => void;
pageNo: number;
updatePageNo: (pageNo: number, event?: EventType) => void;
+ multiRowSelection?: boolean;
nextPageClick: () => void;
prevPageClick: () => void;
serverSidePaginationEnabled: boolean;
@@ -54,12 +62,21 @@ interface TableProps {
selectedRowIndices: number[];
disableDrag: () => void;
enableDrag: () => void;
+ toggleAllRowSelect: (
+ isSelect: boolean,
+ pageData: Row>[],
+ ) => void;
triggerRowSelection: boolean;
searchTableData: (searchKey: any) => void;
filters?: ReactTableFilter[];
applyFilter: (filters: ReactTableFilter[]) => void;
compactMode?: CompactMode;
updateCompactMode: (compactMode: CompactMode) => void;
+ isVisibleCompactMode?: boolean;
+ isVisibleDownload?: boolean;
+ isVisibleFilters?: boolean;
+ isVisiblePagination?: boolean;
+ isVisibleSearch?: boolean;
}
const defaultColumn = {
@@ -111,12 +128,12 @@ export function Table(props: TableProps) {
const pageCount = Math.ceil(props.data.length / props.pageSize);
const currentPageIndex = props.pageNo < pageCount ? props.pageNo : 0;
const {
- getTableProps,
getTableBodyProps,
+ getTableProps,
headerGroups,
- prepareRow,
page,
pageOptions,
+ prepareRow,
state,
} = useTable(
{
@@ -161,65 +178,106 @@ export function Table(props: TableProps) {
const tableWrapperRef = useRef(null);
const tableBodyRef = useRef(null);
const tableHeaderWrapperRef = React.createRef();
+ const rowSelectionState = React.useMemo(() => {
+ // return : 0; no row selected | 1; all row selected | 2: some rows selected
+ if (!props.multiRowSelection) return null;
+ const selectedRowCount = reduce(
+ subPage,
+ (count, row) => {
+ return selectedRowIndices.includes(row.index) ? count + 1 : count;
+ },
+ 0,
+ );
+ const result =
+ selectedRowCount === 0 ? 0 : selectedRowCount === subPage.length ? 1 : 2;
+ return result;
+ }, [selectedRowIndices, subPage]);
+ const handleAllRowSelectClick = (
+ e: React.MouseEvent,
+ ) => {
+ // if all / some rows are selected we remove selection on click
+ // else select all rows
+ props.toggleAllRowSelect(!Boolean(rowSelectionState), subPage);
+ // loop over subPage rows and toggleRowSelected if required
+ e.stopPropagation();
+ };
+ const isHeaderVisible =
+ props.isVisibleSearch ||
+ props.isVisibleFilters ||
+ props.isVisibleDownload ||
+ props.isVisibleCompactMode ||
+ props.isVisiblePagination;
+
return (
-
-
-
-
-
-
-
+ width={props.width}
+ >
+
+
+
+
+ )}
- {headerGroups.map((headerGroup: any, index: number) => (
-
- {headerGroup.headers.map(
- (column: any, columnIndex: number) => {
- return (
-
- );
- },
- )}
-
- ))}
+ {headerGroups.map((headerGroup: any, index: number) => {
+ const headerRowProps = {
+ ...headerGroup.getHeaderGroupProps(),
+ style: { display: "flex" },
+ };
+ return (
+
+ {props.multiRowSelection &&
+ renderCheckBoxHeaderCell(
+ handleAllRowSelectClick,
+ rowSelectionState,
+ )}
+ {headerGroup.headers.map(
+ (column: any, columnIndex: number) => {
+ return (
+
+ );
+ },
+ )}
+
+ );
+ })}
{headerGroups.length === 0 &&
renderEmptyRows(
1,
@@ -258,6 +323,7 @@ export function Table(props: TableProps) {
props.width,
subPage,
prepareRow,
+ props.multiRowSelection,
)}
{subPage.map((row, rowIndex) => {
prepareRow(row);
+ const rowProps = {
+ ...row.getRowProps(),
+ style: { display: "flex" },
+ };
+ const isRowSelected =
+ row.index === selectedRowIndex ||
+ selectedRowIndices.includes(row.index);
return (
{
row.toggleRowSelected();
@@ -288,6 +353,8 @@ export function Table(props: TableProps) {
e.stopPropagation();
}}
>
+ {props.multiRowSelection &&
+ renderCheckBoxCell(isRowSelected)}
{row.cells.map((cell, cellIndex) => {
return (
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx
new file mode 100644
index 0000000000..54d4f4c443
--- /dev/null
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx
@@ -0,0 +1,68 @@
+import React, { useCallback } from "react";
+import { IconWrapper } from "constants/IconConstants";
+import { Colors } from "constants/Colors";
+import styled from "styled-components";
+
+interface TableActionProps {
+ selected: boolean;
+ selectMenu: (selected: boolean) => void;
+ className: string;
+ title: string;
+ children: React.ReactNode;
+ icon?: React.ReactNode;
+ titleColor?: string;
+}
+
+export const TableIconWrapper = styled.div<{
+ selected?: boolean;
+ disabled?: boolean;
+ titleColor?: string;
+}>`
+ height: 38px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: ${(props) => (props.disabled ? 0.6 : 1)};
+ cursor: ${(props) => !props.disabled && "pointer"};
+ color: ${(props) => (props.selected ? Colors.CODE_GRAY : Colors.GRAY)};
+ .action-title {
+ margin-left: 4px;
+ white-space: nowrap;
+ color: ${(props) => props.titleColor || Colors.GRAY};
+ }
+ position: relative;
+ margin-left: 5px;
+ padding: 0 5px;
+ &:hover {
+ background: ${Colors.ATHENS_GRAY};
+ }
+`;
+
+function TableAction(props: TableActionProps) {
+ const handleIconClick = useCallback(
+ (e: React.MouseEvent
) => {
+ props.selectMenu(!props.selected);
+ e.stopPropagation();
+ },
+ [props.selected],
+ );
+ return (
+
+
+ {props.children}
+
+ {props.title}
+
+ );
+}
+
+export default TableAction;
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx
index d59cdacf23..37b1dac601 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx
@@ -34,13 +34,12 @@ function TableActionIcon(props: TableActionIconProps) {
selected={props.selected}
>
{props.children}
- {props.icon ? props.icon : null}
);
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx
index 1a41a1d277..bd195b1c2f 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx
@@ -1,18 +1,84 @@
import React from "react";
+import {
+ Popover,
+ Classes,
+ PopoverInteractionKind,
+ Position,
+} from "@blueprintjs/core";
import { IconWrapper } from "constants/IconConstants";
import { Colors } from "constants/Colors";
-import { ReactComponent as DownloadIcon } from "assets/icons/control/download-table.svg";
+import { ReactComponent as DownloadIcon } from "assets/icons/control/download-data-icon.svg";
import { ReactTableColumnProps } from "components/designSystems/appsmith/TableComponent/Constants";
-import { TableIconWrapper } from "components/designSystems/appsmith/TableComponent/TableStyledWrappers";
-import TableActionIcon from "components/designSystems/appsmith/TableComponent/TableActionIcon";
+import TableAction, {
+ TableIconWrapper,
+} from "components/designSystems/appsmith/TableComponent/TableAction";
+import styled from "styled-components";
import { transformTableDataIntoCsv } from "./CommonUtilities";
+import zipcelx from "zipcelx";
+const DropDownWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ background: white;
+ z-index: 1;
+ border-radius: 4px;
+ box-shadow: 0px 12px 28px -8px rgba(0, 0, 0, 0.1);
+ padding: 0;
+`;
+
+const OptionWrapper = styled.div`
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+ height: 32px;
+ box-sizing: border-box;
+ padding: 6px 12px;
+ color: ${Colors.CHARCOAL};
+ min-width: 200px;
+ cursor: pointer;
+ background: ${Colors.WHITE};
+ border-left: none;
+ border-radius: none;
+ .option-title {
+ font-weight: 500;
+ font-size: 13px;
+ line-height: 20px;
+ }
+ &:hover {
+ background: ${Colors.SEA_SHELL};
+ color: ${Colors.CODE_GRAY};
+ }
+`;
interface TableDataDownloadProps {
data: Array>;
columns: ReactTableColumnProps[];
widgetName: string;
}
+type FileDownloadType = "CSV" | "EXCEL";
+
+type DataCellProps = {
+ value: string | number;
+ type: "string" | "number";
+};
+
+interface DownloadOptionProps {
+ label: string;
+ value: FileDownloadType;
+}
+
+const dowloadOptions: DownloadOptionProps[] = [
+ {
+ label: "Download as CSV",
+ value: "CSV",
+ },
+ {
+ label: "Download as Excel",
+ value: "EXCEL",
+ },
+];
+
const downloadDataAsCSV = (props: {
csvData: Array>;
fileName: string;
@@ -45,9 +111,57 @@ const downloadDataAsCSV = (props: {
};
function TableDataDownload(props: TableDataDownloadProps) {
- const [selected, toggleButtonClick] = React.useState(false);
- const downloadTableData = () => {
- toggleButtonClick(true);
+ const [selected, selectMenu] = React.useState(false);
+ const downloadFile = (type: string) => {
+ if (type === "CSV") {
+ downloadTableDataAsCsv();
+ } else if (type === "EXCEL") {
+ downloadTableDataAsExcel();
+ }
+ };
+ const downloadTableDataAsExcel = () => {
+ const tableData: Array> = [];
+ const tableHeaders: Array = props.columns
+ .filter((column: ReactTableColumnProps) => {
+ return column.metaProperties && !column.metaProperties.isHidden;
+ })
+ .map((column: ReactTableColumnProps) => {
+ return {
+ value: column.Header,
+ type:
+ column.columnProperties?.columnType === "number"
+ ? "number"
+ : "string",
+ };
+ });
+ tableData.push(tableHeaders);
+ for (let row = 0; row < props.data.length; row++) {
+ const data: { [key: string]: any } = props.data[row];
+ const tableRow: Array = [];
+ for (let colIndex = 0; colIndex < props.columns.length; colIndex++) {
+ const column = props.columns[colIndex];
+ const type =
+ column.columnProperties?.columnType === "number"
+ ? "number"
+ : "string";
+ if (column.metaProperties && !column.metaProperties.isHidden) {
+ tableRow.push({
+ value: data[column.accessor],
+ type: type,
+ });
+ }
+ }
+ tableData.push(tableRow);
+ }
+ zipcelx({
+ filename: props.widgetName,
+ sheet: {
+ data: tableData,
+ },
+ });
+ };
+ const downloadTableDataAsCsv = () => {
+ selectMenu(true);
const csvData = transformTableDataIntoCsv({
columns: props.columns,
data: props.data,
@@ -56,7 +170,11 @@ function TableDataDownload(props: TableDataDownloadProps) {
csvData: csvData,
fileName: `${props.widgetName}.csv`,
});
- toggleButtonClick(false);
+ selectMenu(false);
+ };
+
+ const handleCloseMenu = () => {
+ selectMenu(false);
};
if (props.columns.length === 0) {
@@ -65,20 +183,43 @@ function TableDataDownload(props: TableDataDownloadProps) {
+ Download
);
}
return (
- {
- downloadTableData();
- }}
- selected={selected}
- tooltip="Download"
+
-
-
+
+
+
+
+ {dowloadOptions.map((item: DownloadOptionProps, index: number) => {
+ return (
+ {
+ downloadFile(item.value);
+ }}
+ >
+ {item.label}
+
+ );
+ })}
+
+
);
}
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableFilters.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableFilters.tsx
index 145ace9fe5..d05dfbb753 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/TableFilters.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableFilters.tsx
@@ -12,7 +12,7 @@ import { ReactComponent as FilterIcon } from "assets/icons/control/filter-icon.s
import { TableIconWrapper } from "components/designSystems/appsmith/TableComponent/TableStyledWrappers";
import Button from "components/editorComponents/Button";
import CascadeFields from "components/designSystems/appsmith/TableComponent/CascadeFields";
-import TableActionIcon from "components/designSystems/appsmith/TableComponent/TableActionIcon";
+import TableAction from "components/designSystems/appsmith/TableComponent/TableAction";
import {
ReactTableColumnProps,
Condition,
@@ -173,7 +173,7 @@ function TableFilters(props: TableFilterProps) {
position={Position.BOTTOM}
usePortal
>
-
-
+
e.stopPropagation()}>
{filters.map((filter: ReactTableFilter, index: number) => {
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableHeader.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableHeader.tsx
index 8c5ae271c1..01fb8b0188 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/TableHeader.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableHeader.tsx
@@ -25,11 +25,11 @@ import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
const PageNumberInputWrapper = styled(NumericInput)`
&&& input {
box-shadow: none;
+ border: 1px solid ${Colors.DANUBE};
background: linear-gradient(0deg, ${Colors.WHITE}, ${Colors.WHITE}),
${Colors.POLAR};
- border: 1px solid ${Colors.GREEN};
+ border-radius: none;
box-sizing: border-box;
- border-radius: 4px;
width: 24px;
height: 24px;
line-height: 24px;
@@ -37,6 +37,9 @@ const PageNumberInputWrapper = styled(NumericInput)`
text-align: center;
font-size: 12px;
}
+ &&&.bp3-control-group > :only-child {
+ border-radius: 0;
+ }
margin: 0 8px;
`;
@@ -107,34 +110,54 @@ interface TableHeaderProps {
compactMode?: CompactMode;
updateCompactMode: (compactMode: CompactMode) => void;
tableSizes: TableSizes;
+ isVisibleCompactMode?: boolean;
+ isVisibleDownload?: boolean;
+ isVisibleFilters?: boolean;
+ isVisiblePagination?: boolean;
+ isVisibleSearch?: boolean;
}
function TableHeader(props: TableHeaderProps) {
return (
<>
-
-
-
-
-
-
- {props.serverSidePaginationEnabled && (
+ )}
+ {(props.isVisibleFilters ||
+ props.isVisibleDownload ||
+ props.isVisibleCompactMode) && (
+
+ {props.isVisibleFilters && (
+
+ )}
+
+ {props.isVisibleDownload && (
+
+ )}
+
+ {props.isVisibleCompactMode && (
+
+ )}
+
+ )}
+
+ {props.isVisiblePagination && props.serverSidePaginationEnabled && (
)}
- {!props.serverSidePaginationEnabled && (
+ {props.isVisiblePagination && !props.serverSidePaginationEnabled && (
{props.tableData?.length} Records
@@ -173,7 +196,7 @@ function TableHeader(props: TableHeaderProps) {
props.updatePageNo(pageNo + 1, EventType.ON_PREV_PAGE);
}}
>
-
+
Page{" "}
@@ -196,7 +219,7 @@ function TableHeader(props: TableHeaderProps) {
props.updatePageNo(pageNo + 1, EventType.ON_NEXT_PAGE);
}}
>
-
+
)}
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx
index e516d2d432..a25ca36fd3 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx
@@ -14,6 +14,7 @@ export const TableWrapper = styled.div<{
tableSizes: TableSizes;
backgroundColor?: Color;
triggerRowSelection: boolean;
+ isHeaderVisible?: boolean;
}>`
width: 100%;
height: 100%;
@@ -48,34 +49,25 @@ export const TableWrapper = styled.div<{
display: table;
width: 100%;
${hideScrollbar};
- .tr {
- width: 100%;
- }
.thead,
.tbody {
overflow: hidden;
}
.tbody {
- height: ${(props) => props.height - 80}px;
+ height: ${(props) =>
+ props.isHeaderVisible ? props.height - 80 : props.height - 40}px;
width: 100%;
overflow-y: auto;
${hideScrollbar};
- .tr {
- width: 100%;
- }
}
.tr {
- width: calc(100% - 8px);
overflow: hidden;
cursor: ${(props) => props.triggerRowSelection && "pointer"};
background: ${Colors.WHITE};
&.selected-row {
- & > .td > div {
- background: ${Colors.POLAR}!important;
- }
- background: ${Colors.POLAR}!important;
+ background: ${Colors.OPAQ_BLUE}!important;
&:hover {
- background: ${Colors.POLAR};
+ background: ${Colors.OPAQ_BLUE};
}
}
&:hover {
@@ -111,8 +103,10 @@ export const TableWrapper = styled.div<{
}
.th {
padding: 0 10px 0 0;
- height: ${(props) => props.tableSizes.COLUMN_HEADER_HEIGHT}px;
- line-height: ${(props) => props.tableSizes.COLUMN_HEADER_HEIGHT}px;
+ height: ${(props) =>
+ props.isHeaderVisible ? props.tableSizes.COLUMN_HEADER_HEIGHT : 40}px;
+ line-height: ${(props) =>
+ props.isHeaderVisible ? props.tableSizes.COLUMN_HEADER_HEIGHT : 40}px;
background: ${Colors.ATHENS_GRAY_DARKER};
}
.td {
@@ -232,18 +226,16 @@ export const PaginationWrapper = styled.div`
justify-content: flex-end;
align-items: center;
padding: 8px 20px;
+ color: ${Colors.GRAY};
`;
export const PaginationItemWrapper = styled.div<{
disabled?: boolean;
selected?: boolean;
}>`
- background: ${(props) =>
- props.disabled ? Colors.ATHENS_GRAY : Colors.WHITE};
- border: 1px solid
- ${(props) => (props.selected ? Colors.GREEN : Colors.GEYSER_LIGHT)};
+ background: ${(props) => (props.disabled ? Colors.MERCURY : Colors.WHITE)};
+ border: 1px solid ${Colors.ALTO2};
box-sizing: border-box;
- border-radius: 4px;
width: 24px;
height: 24px;
display: flex;
@@ -413,6 +405,9 @@ export const CellWrapper = styled.div<{
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ text-align: ${(props) =>
+ props?.cellProperties?.horizontalAlignment &&
+ TEXT_ALIGN[props?.cellProperties?.horizontalAlignment]};
}
.hidden-icon {
display: none;
@@ -424,6 +419,46 @@ export const CellWrapper = styled.div<{
}
`;
+export const CellCheckboxWrapper = styled(CellWrapper)<{ isChecked?: boolean }>`
+ justify-content: center;
+ width: 40px;
+ background: ${Colors.WHITE};
+ & > div {
+ background: ${(props) => (props.isChecked ? Colors.DANUBE : "")};
+ }
+
+ ${(props) =>
+ props.isChecked
+ ? `
+ background: #FAFAFA;
+ &:hover {
+ background: ${Colors.OPAQ_BLUE};
+ }
+ `
+ : `
+ &:hover {
+ & > div {
+ background: ${Colors.Gallery};
+ }
+ }
+ `}
+`;
+
+export const CellCheckbox = styled.div`
+ height: 15px;
+ width: 15px;
+ border: 0.5px solid ${Colors.GEYSER_LIGHT};
+ position: relative;
+ .th-svg {
+ display: block;
+ position: absolute;
+ left: -1px;
+ top: -1px;
+ height: 15px;
+ width: 15px;
+ }
+`;
+
export const TableHeaderWrapper = styled.div<{
serverSidePaginationEnabled: boolean;
width: number;
@@ -477,7 +512,7 @@ export const RowWrapper = styled.div`
justify-content: center;
font-size: 12px;
line-height: 20px;
- color: ${Colors.THUNDER};
+ color: ${Colors.GRAY};
margin: 0 4px;
white-space: nowrap;
`;
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.test.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.test.tsx
new file mode 100644
index 0000000000..6939bcc694
--- /dev/null
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.test.tsx
@@ -0,0 +1,113 @@
+import { renderCell } from "./TableUtilities";
+import { ColumnTypes } from "components/designSystems/appsmith/TableComponent/Constants";
+
+describe("Test table columnType Image render", () => {
+ it("columnType Image accepts single image url and renders image correctly", () => {
+ const value =
+ "http://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/back02.jpg";
+
+ const expected = [
+ "http://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/back02.jpg",
+ ];
+ const ImageCellComponent = renderCell(
+ value,
+ ColumnTypes.IMAGE,
+ false,
+ {},
+ 930,
+ );
+ const result = ImageCellComponent.props.children.map((imageDiv: any) => {
+ return imageDiv.props.href;
+ });
+
+ expect(expected).toEqual(result);
+ });
+
+ it("columnType Image accepts single base64 url and renders image correctly", () => {
+ const value =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+ const expected = [
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",
+ ];
+ const ImageCellComponent = renderCell(
+ value,
+ ColumnTypes.IMAGE,
+ false,
+ {},
+ 930,
+ );
+ const result = ImageCellComponent.props.children.map((imageDiv: any) => {
+ return imageDiv.props.href;
+ });
+
+ expect(expected).toEqual(result);
+ });
+
+ it("columnType Image accepts comma separeted image urls and renders all images correctly", () => {
+ const value =
+ "http://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/back02.jpg,http://commondatastorage.googleapis.com/codeskulptor-assets/gutenberg.jpg,http://commondatastorage.googleapis.com/codeskulptor-assets/gutenberg.jpg";
+
+ const expected = [
+ "http://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/back02.jpg",
+ "http://commondatastorage.googleapis.com/codeskulptor-assets/gutenberg.jpg",
+ "http://commondatastorage.googleapis.com/codeskulptor-assets/gutenberg.jpg",
+ ];
+ const ImageCellComponent = renderCell(
+ value,
+ ColumnTypes.IMAGE,
+ false,
+ {},
+ 930,
+ );
+ const result = ImageCellComponent.props.children.map((imageDiv: any) => {
+ return imageDiv.props.href;
+ });
+
+ expect(expected).toEqual(result);
+ });
+
+ it("columnType Image accepts comma separeted image urls that has base64 image and renders images correctly", () => {
+ const value =
+ "http://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/back02.jpg,data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+ const expected = [
+ "http://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/back02.jpg",
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",
+ ];
+ const ImageCellComponent = renderCell(
+ value,
+ ColumnTypes.IMAGE,
+ false,
+ {},
+ 930,
+ );
+ const result = ImageCellComponent.props.children.map((imageDiv: any) => {
+ return imageDiv.props.href;
+ });
+
+ expect(expected).toEqual(result);
+ });
+
+ it("columnType Image accepts image url that may have incorrect use of comma and renders image correctly", () => {
+ const value =
+ "http://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/back02.jpg, ";
+
+ const expected = [
+ "http://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/back02.jpg",
+ undefined,
+ ];
+ const ImageCellComponent = renderCell(
+ value,
+ ColumnTypes.IMAGE,
+ false,
+ {},
+ 930,
+ );
+ const result = ImageCellComponent.props.children.map((imageDiv: any) => {
+ return imageDiv.props.href;
+ });
+
+ expect(expected).toEqual(result);
+ });
+});
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.tsx
index e639867e84..ce2129653e 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.tsx
@@ -2,6 +2,8 @@ import React, { useState } from "react";
import { MenuItem, Classes, Button as BButton } from "@blueprintjs/core";
import {
CellWrapper,
+ CellCheckboxWrapper,
+ CellCheckbox,
ActionWrapper,
SortIconWrapper,
DraggableHeaderWrapper,
@@ -49,13 +51,18 @@ export const renderCell = (
);
}
- const imageSplitRegex = /[^(base64)],/;
+ // better regex: /(?
{value
.toString()
+ // imageSplitRegex matched "," and char before it, so add space before ","
+ .replace(imageSplitRegex, (match) =>
+ match.length > 1 ? `${match.charAt(0)} ,` : " ,",
+ )
.split(imageSplitRegex)
.map((item: string, index: number) => {
if (imageUrlRegex.test(item) || base64ImageRegex.test(item)) {
@@ -75,7 +82,7 @@ export const renderCell = (
);
} else {
- return Invalid Image
;
+ return Invalid Image
;
}
})}
@@ -112,7 +119,9 @@ export const renderCell = (
tableWidth={tableWidth}
title={value.toString()}
>
- {value.toString()}
+ {value && columnType === ColumnTypes.URL && cellProperties.displayText
+ ? cellProperties.displayText
+ : value.toString()}
);
}
@@ -189,20 +198,86 @@ function TableAction(props: {
);
}
+function CheckBoxLineIcon() {
+ return (
+
+
+
+ );
+}
+
+function CheckBoxCheckIcon() {
+ return (
+
+
+
+ );
+}
+
+export const renderCheckBoxCell = (isChecked: boolean) => (
+
+ {isChecked && }
+
+);
+
+export const renderCheckBoxHeaderCell = (
+ onClick: (e: React.MouseEvent) => void,
+ checkState: number | null,
+) => (
+
+
+ {checkState === 1 && }
+ {checkState === 2 && }
+
+
+);
+
export const renderEmptyRows = (
rowCount: number,
columns: any,
tableWidth: number,
page: any,
prepareRow: any,
+ multiRowSelection = false,
) => {
const rows: string[] = new Array(rowCount).fill("");
if (page.length) {
const row = page[0];
return rows.map((item: string, index: number) => {
prepareRow(row);
+ const rowProps = {
+ ...row.getRowProps(),
+ style: { display: "flex" },
+ };
return (
-
+
+ {multiRowSelection && renderCheckBoxCell(false)}
{row.cells.map((cell: any, cellIndex: number) => {
return (
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx
index 0e6c769657..73de795c33 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx
@@ -1,12 +1,14 @@
-import React, { useEffect, useMemo } from "react";
-import Table from "components/designSystems/appsmith/TableComponent/Table";
import {
ColumnTypes,
CompactMode,
ReactTableColumnProps,
ReactTableFilter,
} from "components/designSystems/appsmith/TableComponent/Constants";
+import { Row } from "react-table";
+import Table from "components/designSystems/appsmith/TableComponent/Table";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
+import { isEqual } from "lodash";
+import React, { useEffect, useMemo } from "react";
export interface ColumnMenuOptionProps {
content: string | JSX.Element;
@@ -45,6 +47,8 @@ interface ReactTableComponentProps {
disableDrag: (disable: boolean) => void;
onRowClick: (rowData: Record
, rowIndex: number) => void;
onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void;
+ selectAllRow: (pageData: Row>[]) => void;
+ unSelectAllRow: (pageData: Row>[]) => void;
updatePageNo: (pageNo: number, event?: EventType) => void;
sortTableColumn: (column: string, asc: boolean) => void;
nextPageClick: () => void;
@@ -65,13 +69,58 @@ interface ReactTableComponentProps {
columns: ReactTableColumnProps[];
compactMode?: CompactMode;
updateCompactMode: (compactMode: CompactMode) => void;
+ isVisibleSearch?: boolean;
+ isVisibleFilters?: boolean;
+ isVisibleDownload?: boolean;
+ isVisibleCompactMode?: boolean;
+ isVisiblePagination?: boolean;
}
function ReactTableComponent(props: ReactTableComponentProps) {
+ const {
+ applyFilter,
+ columns,
+ columnSizeMap,
+ compactMode,
+ disableDrag,
+ editMode,
+ filters,
+ handleReorderColumn,
+ handleResizeColumn,
+ height,
+ isLoading,
+ isVisibleCompactMode,
+ isVisibleDownload,
+ isVisibleFilters,
+ isVisiblePagination,
+ isVisibleSearch,
+ multiRowSelection,
+ nextPageClick,
+ onRowClick,
+ pageNo,
+ pageSize,
+ prevPageClick,
+ searchKey,
+ searchTableData,
+ selectAllRow,
+ selectedRowIndex,
+ selectedRowIndices,
+ serverSidePaginationEnabled,
+ sortTableColumn: _sortTableColumn,
+ tableData,
+ triggerRowSelection,
+ unSelectAllRow,
+ updateCompactMode,
+ updatePageNo,
+ widgetId,
+ widgetName,
+ width,
+ } = props;
+
const { columnOrder, hiddenColumns } = useMemo(() => {
const order: string[] = [];
const hidden: string[] = [];
- props.columns.forEach((item) => {
+ columns.forEach((item) => {
if (item.isHidden) {
hidden.push(item.accessor);
} else {
@@ -79,12 +128,12 @@ function ReactTableComponent(props: ReactTableComponentProps) {
}
});
return { columnOrder: order, hiddenColumns: hidden };
- }, [props.columns]);
+ }, [columns]);
useEffect(() => {
let dragged = -1;
const headers = Array.prototype.slice.call(
- document.querySelectorAll(`#table${props.widgetId} .draggable-header`),
+ document.querySelectorAll(`#table${widgetId} .draggable-header`),
);
headers.forEach((header, i) => {
header.setAttribute("draggable", true);
@@ -149,7 +198,7 @@ function ReactTableComponent(props: ReactTableComponentProps) {
if (movedColumnName && movedColumnName.length === 1) {
newColumnOrder.splice(i, 0, movedColumnName[0]);
}
- props.handleReorderColumn([...newColumnOrder, ...hiddenColumns]);
+ handleReorderColumn([...newColumnOrder, ...hiddenColumns]);
} else {
dragged = -1;
}
@@ -159,15 +208,15 @@ function ReactTableComponent(props: ReactTableComponentProps) {
const sortTableColumn = (columnIndex: number, asc: boolean) => {
if (columnIndex === -1) {
- props.sortTableColumn("", asc);
+ _sortTableColumn("", asc);
} else {
- const column = props.columns[columnIndex];
+ const column = columns[columnIndex];
const columnType = column.metaProperties?.type || ColumnTypes.TEXT;
if (
columnType !== ColumnTypes.IMAGE &&
columnType !== ColumnTypes.VIDEO
) {
- props.sortTableColumn(column.accessor, asc);
+ _sortTableColumn(column.accessor, asc);
}
}
};
@@ -176,50 +225,103 @@ function ReactTableComponent(props: ReactTableComponentProps) {
original: Record;
index: number;
}) => {
- props.onRowClick(row.original, row.index);
+ onRowClick(row.original, row.index);
+ };
+
+ const toggleAllRowSelect = (
+ isSelect: boolean,
+ pageData: Row>[],
+ ) => {
+ if (isSelect) {
+ selectAllRow(pageData);
+ } else {
+ unSelectAllRow(pageData);
+ }
};
return (
{
- props.disableDrag(true);
+ disableDrag(true);
}}
- editMode={props.editMode}
+ editMode={editMode}
enableDrag={() => {
- props.disableDrag(false);
+ disableDrag(false);
}}
- filters={props.filters}
- handleResizeColumn={props.handleResizeColumn}
- height={props.height}
- isLoading={props.isLoading}
- nextPageClick={() => {
- props.nextPageClick();
- }}
- pageNo={props.pageNo - 1}
- pageSize={props.pageSize || 1}
- prevPageClick={() => {
- props.prevPageClick();
- }}
- searchKey={props.searchKey}
- searchTableData={props.searchTableData}
+ filters={filters}
+ handleResizeColumn={handleResizeColumn}
+ height={height}
+ isLoading={isLoading}
+ isVisibleCompactMode={isVisibleCompactMode}
+ isVisibleDownload={isVisibleDownload}
+ isVisibleFilters={isVisibleFilters}
+ isVisiblePagination={isVisiblePagination}
+ isVisibleSearch={isVisibleSearch}
+ multiRowSelection={multiRowSelection}
+ nextPageClick={nextPageClick}
+ pageNo={pageNo - 1}
+ pageSize={pageSize || 1}
+ prevPageClick={prevPageClick}
+ searchKey={searchKey}
+ searchTableData={searchTableData}
selectTableRow={selectTableRow}
- selectedRowIndex={props.selectedRowIndex}
- selectedRowIndices={props.selectedRowIndices}
- serverSidePaginationEnabled={props.serverSidePaginationEnabled}
+ selectedRowIndex={selectedRowIndex}
+ selectedRowIndices={selectedRowIndices}
+ serverSidePaginationEnabled={serverSidePaginationEnabled}
sortTableColumn={sortTableColumn}
- triggerRowSelection={props.triggerRowSelection}
- updateCompactMode={props.updateCompactMode}
- updatePageNo={props.updatePageNo}
- widgetId={props.widgetId}
- widgetName={props.widgetName}
- width={props.width}
+ toggleAllRowSelect={toggleAllRowSelect}
+ triggerRowSelection={triggerRowSelection}
+ updateCompactMode={updateCompactMode}
+ updatePageNo={updatePageNo}
+ widgetId={widgetId}
+ widgetName={widgetName}
+ width={width}
/>
);
}
-export default ReactTableComponent;
+export default React.memo(ReactTableComponent, (prev, next) => {
+ return (
+ prev.applyFilter === next.applyFilter &&
+ prev.compactMode === next.compactMode &&
+ prev.disableDrag === next.disableDrag &&
+ prev.editMode === next.editMode &&
+ prev.filters === next.filters &&
+ prev.handleReorderColumn === next.handleReorderColumn &&
+ prev.handleResizeColumn === next.handleResizeColumn &&
+ prev.height === next.height &&
+ prev.isLoading === next.isLoading &&
+ prev.isVisibleCompactMode === next.isVisibleCompactMode &&
+ prev.isVisibleDownload === next.isVisibleDownload &&
+ prev.isVisibleFilters === next.isVisibleFilters &&
+ prev.isVisiblePagination === next.isVisiblePagination &&
+ prev.isVisibleSearch === next.isVisibleSearch &&
+ prev.nextPageClick === next.nextPageClick &&
+ prev.onRowClick === next.onRowClick &&
+ prev.pageNo === next.pageNo &&
+ prev.pageSize === next.pageSize &&
+ prev.prevPageClick === next.prevPageClick &&
+ prev.searchKey === next.searchKey &&
+ prev.searchTableData === next.searchTableData &&
+ prev.selectedRowIndex === next.selectedRowIndex &&
+ prev.selectedRowIndices === next.selectedRowIndices &&
+ prev.serverSidePaginationEnabled === next.serverSidePaginationEnabled &&
+ prev.sortTableColumn === next.sortTableColumn &&
+ prev.triggerRowSelection === next.triggerRowSelection &&
+ prev.updateCompactMode === next.updateCompactMode &&
+ prev.updatePageNo === next.updatePageNo &&
+ prev.widgetId === next.widgetId &&
+ prev.widgetName === next.widgetName &&
+ prev.width === next.width &&
+ isEqual(prev.columnSizeMap, next.columnSizeMap) &&
+ isEqual(prev.tableData, next.tableData) &&
+ // Using JSON stringify becuase isEqual doesnt work with functions,
+ // and we are not changing the columns manually.
+ JSON.stringify(prev.columns) === JSON.stringify(next.columns)
+ );
+});
diff --git a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx
index 75a541f905..06c79f905b 100644
--- a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx
@@ -6,7 +6,6 @@ import {
TabContainerWidgetProps,
} from "widgets/Tabs/TabsWidget";
import { generateClassName, getCanvasClassName } from "utils/generators";
-import { getBorderCSSShorthand } from "constants/DefaultTheme";
import ScrollIndicator from "components/ads/ScrollIndicator";
interface TabsComponentProps extends ComponentProps {
@@ -37,14 +36,13 @@ const TabsContainerWrapper = styled.div<{
width: 100%;
justify-content: center;
align-items: center;
- border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])};
+ box-shadow: 0px 0px 0px 1px #e7e7e7;
border-radius: 0;
- box-shadow: none;
overflow: hidden;
`;
const ChildrenWrapper = styled.div`
- height: 100%;
+ height: calc(100% - 40px);
width: 100%;
position: relative;
background: ${(props) => props.theme.colors.builderBodyBG};
diff --git a/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx b/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx
index b6464432aa..4703969f05 100644
--- a/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx
@@ -109,11 +109,11 @@ export class BaseTextInput extends Component {
};
render() {
const {
+ className,
input,
meta,
- showError,
- className,
refHandler,
+ showError,
...rest
} = this.props;
const hasError = !!(
diff --git a/app/client/src/components/designSystems/appsmith/VideoComponent.tsx b/app/client/src/components/designSystems/appsmith/VideoComponent.tsx
index b24fda9851..ab6faffd7c 100644
--- a/app/client/src/components/designSystems/appsmith/VideoComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/VideoComponent.tsx
@@ -29,18 +29,18 @@ const Error = styled.span``;
export default function VideoComponent(props: VideoComponentProps) {
const {
- url,
autoplay,
controls,
- onStart,
- onPlay,
- onPause,
onEnded,
- onReady,
- onProgress,
- onSeek,
onError,
+ onPause,
+ onPlay,
+ onProgress,
+ onReady,
+ onSeek,
+ onStart,
player,
+ url,
} = props;
return url ? (
{
if (props.filled) return props.theme.colors.textOnDarkBG;
@@ -35,6 +36,14 @@ const ButtonColorStyles = css`
fill: ${getButtonColorStyles};
}
`;
+
+const RecaptchaWrapper = styled.div`
+ position: relative;
+ .grecaptcha-badge {
+ visibility: hidden;
+ }
+`;
+
const AccentColorMap: Record = {
primary: "primaryOld",
secondary: "secondaryOld",
@@ -42,7 +51,7 @@ const AccentColorMap: Record = {
};
const ButtonWrapper = styled((props: ButtonStyleProps & IButtonProps) => (
-
+
))`
&&&& {
${ButtonColorStyles};
@@ -122,7 +131,10 @@ type ButtonStyleProps = {
// To be used in any other part of the app
export function BaseButton(props: IButtonProps & ButtonStyleProps) {
- return ;
+ const className = props.disabled
+ ? `${props.className} bp3-disabled`
+ : props.className;
+ return ;
}
BaseButton.defaultProps = {
@@ -141,6 +153,7 @@ export enum ButtonType {
interface RecaptchaProps {
googleRecaptchaKey?: string;
clickWithRecaptcha: (token: string) => void;
+ recaptchaV2?: boolean;
}
interface ButtonContainerProps extends ComponentProps {
@@ -167,50 +180,107 @@ const mapButtonStyleToStyleName = (buttonStyle?: ButtonStyle) => {
}
};
-function RecaptchaComponent(
+function RecaptchaV2Component(
props: {
children: any;
onClick?: (event: React.MouseEvent) => void;
+ recaptchaV2?: boolean;
+ handleError: (event: React.MouseEvent, error: string) => void;
} & RecaptchaProps,
) {
- function handleError(event: React.MouseEvent, error: string) {
- Toaster.show({
- text: error,
- variant: Variant.danger,
- });
- props.onClick && props.onClick(event);
- }
- const status = useScript(
- `https://www.google.com/recaptcha/api.js?render=${props.googleRecaptchaKey}`,
- );
- return (
- ) => {
- if (status === ScriptStatus.READY) {
- (window as any).grecaptcha.ready(() => {
- try {
- (window as any).grecaptcha
- .execute(props.googleRecaptchaKey, { action: "submit" })
- .then((token: any) => {
- props.clickWithRecaptcha(token);
- })
- .catch(() => {
- // Handle corrent key with wrong
- handleError(event, createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
- });
- } catch (ex) {
- // Handle wrong key
- handleError(event, createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
- }
- });
+ const recaptchaRef = useRef(null);
+ const [isInvalidKey, setInvalidKey] = useState(false);
+ const handleBtnClick = async (event: React.MouseEvent) => {
+ if (isInvalidKey) {
+ // Handle incorrent google recaptcha site key
+ props.handleError(event, createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
+ } else {
+ try {
+ const token = await recaptchaRef?.current?.executeAsync();
+ if (token) {
+ props.clickWithRecaptcha(token);
+ } else {
+ // Handle incorrent google recaptcha site key
+ props.handleError(event, createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
}
- }}
- >
+ } catch (err) {
+ // Handle error due to google recaptcha key of different domain
+ props.handleError(event, createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
+ }
+ }
+ };
+ return (
+
{props.children}
-
+ setInvalidKey(true)}
+ ref={recaptchaRef}
+ sitekey={props.googleRecaptchaKey || ""}
+ size="invisible"
+ />
+
);
}
+function RecaptchaV3Component(
+ props: {
+ children: any;
+ onClick?: (event: React.MouseEvent) => void;
+ recaptchaV2?: boolean;
+ handleError: (event: React.MouseEvent, error: string) => void;
+ } & RecaptchaProps,
+) {
+ // Check if a string is a valid JSON string
+ const checkValidJson = (inputString: string): boolean => {
+ try {
+ JSON.parse(inputString);
+ return true;
+ } catch (err) {
+ return false;
+ }
+ };
+
+ const handleBtnClick = (event: React.MouseEvent) => {
+ if (status === ScriptStatus.READY) {
+ (window as any).grecaptcha.ready(() => {
+ try {
+ (window as any).grecaptcha
+ .execute(props.googleRecaptchaKey, {
+ action: "submit",
+ })
+ .then((token: any) => {
+ props.clickWithRecaptcha(token);
+ })
+ .catch(() => {
+ // Handle incorrent google recaptcha site key
+ props.handleError(
+ event,
+ createMessage(GOOGLE_RECAPTCHA_KEY_ERROR),
+ );
+ });
+ } catch (err) {
+ // Handle error due to google recaptcha key of different domain
+ props.handleError(
+ event,
+ createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR),
+ );
+ }
+ });
+ }
+ };
+
+ let validGoogleRecaptchaKey = props.googleRecaptchaKey;
+
+ if (validGoogleRecaptchaKey && checkValidJson(validGoogleRecaptchaKey)) {
+ validGoogleRecaptchaKey = undefined;
+ }
+
+ const status = useScript(
+ `https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`,
+ );
+ return {props.children}
;
+}
+
function BtnWrapper(
props: {
children: any;
@@ -219,7 +289,23 @@ function BtnWrapper(
) {
if (!props.googleRecaptchaKey)
return {props.children}
;
- return ;
+ else {
+ const handleError = (
+ event: React.MouseEvent,
+ error: string,
+ ) => {
+ Toaster.show({
+ text: error,
+ variant: Variant.danger,
+ });
+ props.onClick && props.onClick(event);
+ };
+ if (props.recaptchaV2) {
+ return ;
+ } else {
+ return ;
+ }
+ }
}
// To be used with the canvas
@@ -231,6 +317,7 @@ function ButtonContainer(
clickWithRecaptcha={props.clickWithRecaptcha}
googleRecaptchaKey={props.googleRecaptchaKey}
onClick={props.onClick}
+ recaptchaV2={props.recaptchaV2}
>
{
const date = moment(dateStr);
-
- if (date.isValid()) return moment(dateStr).toDate();
+ const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT;
+ if (date.isValid()) return moment(dateStr, dateFormat).toDate();
else return moment().toDate();
};
diff --git a/app/client/src/components/designSystems/blueprint/DividerComponent.tsx b/app/client/src/components/designSystems/blueprint/DividerComponent.tsx
new file mode 100644
index 0000000000..e3c8330a66
--- /dev/null
+++ b/app/client/src/components/designSystems/blueprint/DividerComponent.tsx
@@ -0,0 +1,191 @@
+import React from "react";
+import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
+import styled from "styled-components";
+import { isUndefined } from "lodash";
+
+const DividerWrapper = styled.div<{
+ isHorizontal: boolean;
+ thickness: number;
+ showStartCap: boolean;
+ showEndCap: boolean;
+}>`
+ height: 100%;
+ width: 100%;
+ position: relative;
+ display: flex;
+ overflow: hidden;
+ justify-content: center;
+ align-items: center;
+ ${(props) => {
+ const paddingVal = props.thickness / 2;
+ let padStyle: string;
+ if (props.isHorizontal) {
+ padStyle = `padding: 0px ${
+ props.showEndCap ? paddingVal + "px" : "0px"
+ } 0px ${props.showStartCap ? paddingVal + "px" : "0px"};`;
+ } else {
+ padStyle = `padding: ${
+ props.showStartCap ? paddingVal + "px" : "0px"
+ } 0px ${props.showEndCap ? paddingVal + "px" : "0px"} 0px;`;
+ }
+ return padStyle;
+ }}
+`;
+const HorizontalDivider = styled.div>`
+ height: 0px;
+ width: 100%;
+ border-top: ${(props) =>
+ `${props.thickness || 1}px ${props.strokeStyle ||
+ "solid"} ${props.dividerColor || "black"};`};
+`;
+const VerticalDivider = styled.div>`
+ width: 0px;
+ height: 100%;
+ border-right: ${(props) =>
+ `${props.thickness || 1}px ${props.strokeStyle ||
+ "solid"} ${props.dividerColor || "black"};`};
+`;
+
+const CapWrapper = styled.div<{
+ isHorizontal: boolean;
+ isStartCap: boolean;
+ size: number;
+}>`
+ position: absolute;
+ height: ${(props) => props.size}px;
+ width: ${(props) => props.size}px;
+ ${(props) =>
+ props.isStartCap
+ ? props.isHorizontal
+ ? "left: 0px;"
+ : "top: 0px;"
+ : props.isHorizontal
+ ? "right: 0px;"
+ : "bottom: 0px;"}
+ ${(props) => (props.isHorizontal ? "top" : "left")}: 50%;
+ ${(props) =>
+ props.isHorizontal
+ ? "transform: translateY(-50%);"
+ : "transform: translateX(-50%);"};
+
+ svg {
+ display: block;
+ height: 100%;
+ width: 100%;
+
+ &.arrow {
+ ${(
+ props, // rotate icon according to its pos
+ ) =>
+ props.isStartCap
+ ? props.isHorizontal
+ ? "transform: rotate(0deg);"
+ : "transform: rotate(90deg);"
+ : props.isHorizontal
+ ? "transform: rotate(180deg);"
+ : "transform: rotate(270deg);"}
+
+ path {
+ transform: translateX(-3px);
+ }
+ }
+ }
+`;
+
+class DividerComponent extends React.Component {
+ render() {
+ const {
+ capSide,
+ capType,
+ dividerColor,
+ orientation,
+ strokeStyle,
+ thickness,
+ } = this.props;
+ const showStartCap =
+ capType !== "nc" && (isUndefined(capSide) ? false : capSide <= 0);
+ const showEndCap =
+ capType !== "nc" && (isUndefined(capSide) ? false : capSide >= 0);
+
+ return (
+
+ {orientation === "horizontal" ? (
+
+ ) : (
+
+ )}
+
+ {showStartCap && this.renderCap(true)}
+ {showEndCap && this.renderCap(false)}
+
+ );
+ }
+
+ renderCap = (isStartCap: boolean) => {
+ const { capType, dividerColor, orientation, thickness } = this.props;
+ const isHorizontal = orientation === "horizontal";
+ // size calculations
+ const strokeSize = thickness || 1;
+ const sizeMultiplier = capType === "dot" ? 3.5 : 5;
+ const capSize = strokeSize * sizeMultiplier;
+ const halfCapSize = capSize / 2;
+
+ return (
+
+ {capType === "dot" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+ };
+}
+
+export interface DividerComponentProps extends ComponentProps {
+ capType: string;
+ capSide?: number;
+ orientation: string;
+ strokeStyle?: "solid" | "dashed" | "dotted";
+ dividerColor?: string;
+ thickness?: number;
+ className?: string;
+}
+
+export default DividerComponent;
diff --git a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx
index 3487aab4b8..67405695a5 100644
--- a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx
+++ b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx
@@ -188,6 +188,7 @@ const StyledMultiDropDown = styled(MultiDropDown)<{
overflow: hidden;
display: flex;
height: ${(props) => props.height - WIDGET_PADDING * 2 - 2}px;
+ align-content: flex-start;
}
.${Classes.TAG} {
@@ -228,17 +229,35 @@ const StyledMultiDropDown = styled(MultiDropDown)<{
}
}
`;
+interface DropDownComponentState {
+ portalContainer: HTMLElement;
+}
+
+class DropDownComponent extends React.Component<
+ DropDownComponentProps,
+ DropDownComponentState
+> {
+ private _menu = React.createRef();
+ constructor(props: DropDownComponentProps) {
+ super(props);
+ this.state = { portalContainer: this._menu.current as HTMLElement };
+ }
+ componentDidMount() {
+ this.setState({
+ portalContainer: this.props.getDropdownPosition(this._menu.current),
+ });
+ }
-class DropDownComponent extends React.Component {
render() {
- const { selectedIndexArr, options } = this.props;
+ const { options, selectedIndexArr } = this.props;
+ const { portalContainer } = this.state;
const selectedItems = selectedIndexArr
? _.map(selectedIndexArr, (index) => options[index])
: [];
const hideCloseButtonIndex = -1;
return (
-
+ }>
{
{
popoverProps={{
minimal: true,
usePortal: true,
+ modifiers: {
+ preventOverflow: {
+ enabled: false,
+ },
+ },
popoverClassName: "select-popover-wrapper",
}}
>
@@ -294,6 +318,12 @@ class DropDownComponent extends React.Component {
popoverProps={{
minimal: true,
usePortal: true,
+ portalContainer,
+ modifiers: {
+ preventOverflow: {
+ enabled: false,
+ },
+ },
popoverClassName: "select-popover-wrapper",
}}
resetOnSelect
@@ -332,7 +362,7 @@ class DropDownComponent extends React.Component {
this.props.onOptionSelected(item);
};
- onItemRemoved = (_tag: string, index: number) => {
+ onItemRemoved = (_tag: ReactNode, index: number) => {
this.props.onOptionRemoved(this.props.selectedIndexArr[index]);
};
@@ -412,8 +442,10 @@ export interface DropDownComponentProps extends ComponentProps {
selectedIndexArr: number[];
options: DropdownOption[];
isLoading: boolean;
+ isFilterable: boolean;
width: number;
height: number;
+ getDropdownPosition: (node: HTMLDivElement | null) => HTMLElement;
}
export default DropDownComponent;
diff --git a/app/client/src/components/designSystems/blueprint/IframeComponent.tsx b/app/client/src/components/designSystems/blueprint/IframeComponent.tsx
new file mode 100644
index 0000000000..9ad03cc783
--- /dev/null
+++ b/app/client/src/components/designSystems/blueprint/IframeComponent.tsx
@@ -0,0 +1,75 @@
+import React, { useEffect } from "react";
+import styled from "styled-components";
+
+import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
+import { hexToRgba } from "components/ads/common";
+
+interface IframeContainerProps {
+ borderColor?: string;
+ borderOpacity?: number;
+ borderWidth?: number;
+}
+
+export const IframeContainer = styled.div`
+ height: 100%;
+ iframe {
+ width: 100%;
+ height: 100%;
+ border-style: solid;
+ border-color: ${(props) =>
+ hexToRgba(
+ props.borderColor || props.theme.colors.border,
+ (props.borderOpacity && !isNaN(props.borderOpacity)
+ ? props.borderOpacity
+ : 100) / 100,
+ )};
+ border-width: ${(props) =>
+ props.borderWidth ? Number(props.borderWidth) : 0}px;
+ }
+`;
+
+export interface IframeComponentProps extends ComponentProps {
+ source: string;
+ title?: string;
+ onURLChanged: (url: string) => void;
+ onMessageReceived: (message: MessageEvent) => void;
+ borderColor?: string;
+ borderOpacity?: number;
+ borderWidth?: number;
+}
+
+function IframeComponent(props: IframeComponentProps) {
+ const {
+ borderColor,
+ borderOpacity,
+ borderWidth,
+ onMessageReceived,
+ onURLChanged,
+ source,
+ title,
+ } = props;
+
+ useEffect(() => {
+ // add a listener
+ window.addEventListener("message", onMessageReceived, false);
+ // clean up
+ return () =>
+ window.removeEventListener("message", onMessageReceived, false);
+ }, []);
+
+ useEffect(() => {
+ onURLChanged(source);
+ }, [source]);
+
+ return (
+
+
+
+ );
+}
+
+export default IframeComponent;
diff --git a/app/client/src/components/designSystems/blueprint/InputComponent.test.tsx b/app/client/src/components/designSystems/blueprint/InputComponent.test.tsx
new file mode 100644
index 0000000000..ad89728f32
--- /dev/null
+++ b/app/client/src/components/designSystems/blueprint/InputComponent.test.tsx
@@ -0,0 +1,52 @@
+import store from "store";
+import React from "react";
+import { ThemeProvider, theme } from "constants/DefaultTheme";
+import InputComponent from "components/designSystems/blueprint/InputComponent";
+import { Provider } from "react-redux";
+import ReactDOM from "react-dom";
+import { act } from "react-dom/test-utils";
+
+let container: HTMLDivElement | null;
+
+beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+});
+
+afterEach(() => {
+ document.body.removeChild(container);
+ container = null;
+});
+
+describe(" ", () => {
+ it("contains textarea with resize disabled", () => {
+ act(() => {
+ ReactDOM.render(
+
+
+ {
+ console.log(state);
+ }}
+ onValueChange={(valueAsString: string) => {
+ console.log(valueAsString);
+ }}
+ showError={false}
+ value="something"
+ widgetId="24234r35"
+ />
+
+ ,
+ container,
+ );
+ });
+ const textarea = container?.querySelector("textarea");
+ const styles = textarea ? getComputedStyle(textarea) : { resize: "" };
+ expect(styles.resize).toEqual("none");
+ });
+});
diff --git a/app/client/src/components/designSystems/blueprint/InputComponent.tsx b/app/client/src/components/designSystems/blueprint/InputComponent.tsx
index 24d684b7fc..6595466ebe 100644
--- a/app/client/src/components/designSystems/blueprint/InputComponent.tsx
+++ b/app/client/src/components/designSystems/blueprint/InputComponent.tsx
@@ -201,6 +201,7 @@ class InputComponent extends React.Component<
onFocus={() => this.setFocusState(true)}
onKeyDown={this.onKeyDownTextArea}
placeholder={this.props.placeholder}
+ style={{ resize: "none" }}
value={this.props.value}
/>
);
diff --git a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx
index e287d0662e..77b62fca83 100644
--- a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx
+++ b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx
@@ -2,12 +2,15 @@ import React, { ReactNode, RefObject, useRef, useEffect } from "react";
import { Overlay, Classes } from "@blueprintjs/core";
import styled from "styled-components";
import { getCanvasClassName } from "utils/generators";
+import { Layers } from "constants/Layers";
const Container = styled.div<{
- width: number;
- height: number;
+ width?: number;
+ height?: number;
top?: number;
left?: number;
+ bottom?: number;
+ right?: number;
zIndex?: number;
}>`
&&& {
@@ -27,41 +30,48 @@ const Container = styled.div<{
align-items: center;
& .${Classes.OVERLAY_CONTENT} {
max-width: 95%;
- width: ${(props) => props.width}px;
- min-height: ${(props) => props.height}px;
+ width: ${(props) => (props.width ? `${props.width}px` : "auto")};
+ min-height: ${(props) => (props.height ? `${props.height}px` : "auto")};
background: white;
border-radius: ${(props) => props.theme.radii[0]}px;
top: ${(props) => props.top}px;
left: ${(props) => props.left}px;
+ bottom: ${(props) => props.bottom}px;
+ right: ${(props) => props.right}px;
}
}
}
`;
const Content = styled.div<{
- height: number;
+ height?: number;
scroll: boolean;
ref: RefObject;
}>`
overflow-y: ${(props) => (props.scroll ? "visible" : "hidden")};
overflow-x: hidden;
width: 100%;
- height: ${(props) => props.height}px;
+ height: ${(props) => (props.height ? `${props.height}px` : "auto")};
`;
export type ModalComponentProps = {
isOpen: boolean;
onClose: (e: any) => void;
+ onModalClose?: () => void;
children: ReactNode;
- width: number;
+ width?: number;
className?: string;
canOutsideClickClose: boolean;
canEscapeKeyClose: boolean;
+ overlayClassName?: string;
scrollContents: boolean;
- height: number;
+ height?: number;
top?: number;
left?: number;
+ bottom?: number;
+ right?: number;
hasBackDrop?: boolean;
zIndex?: number;
+ portalClassName?: string;
};
/* eslint-disable react/display-name */
@@ -69,42 +79,64 @@ export function ModalComponent(props: ModalComponentProps) {
const modalContentRef: RefObject = useRef(
null,
);
+ useEffect(() => {
+ return () => {
+ // handle modal close events when this component unmounts
+ // will be called in all cases :-
+ // escape key press, click out side, close click from other btn widget
+ if (props.onModalClose) props.onModalClose();
+ };
+ }, []);
useEffect(() => {
if (!props.scrollContents) {
modalContentRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}
}, [props.scrollContents]);
return (
-
-
-
-
- {props.children}
-
-
-
-
+
+
+
+ {props.children}
+
+
+
+
+
);
}
diff --git a/app/client/src/components/designSystems/blueprint/RateComponent.tsx b/app/client/src/components/designSystems/blueprint/RateComponent.tsx
new file mode 100644
index 0000000000..207829a063
--- /dev/null
+++ b/app/client/src/components/designSystems/blueprint/RateComponent.tsx
@@ -0,0 +1,145 @@
+import React, { useState, useEffect } from "react";
+import { Icon, Position } from "@blueprintjs/core";
+import { IconNames } from "@blueprintjs/icons";
+import styled from "styled-components";
+import Rating from "react-rating";
+import _ from "lodash";
+
+import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
+import { RateSize, RATE_SIZES } from "constants/WidgetConstants";
+import TooltipComponent from "components/ads/Tooltip";
+
+/*
+ Note:
+ -webkit-line-clamp may seem like a wierd way to doing this
+ however, it is getting more and more useful with more browser support.
+ It suffices for our target browsers
+ More info: https://css-tricks.com/line-clampin/
+*/
+
+interface RateContainerProps {
+ scrollable: boolean;
+}
+
+export const RateContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ align-content: flex-start;
+ overflow: auto;
+
+ > span {
+ align-self: ${(props) => (props.scrollable ? "flex-start" : "center")};
+ }
+`;
+
+export const Star = styled(Icon)`
+ padding: ${(props) =>
+ props.iconSize === 12 ? 2.92 : props.iconSize === 16 ? 4.37 : 4.93}px;
+`;
+
+export interface RateComponentProps extends ComponentProps {
+ value: number;
+ isLoading: boolean;
+ maxCount: number;
+ size: RateSize;
+ onValueChanged: (value: number) => void;
+ tooltips?: Array;
+ activeColor?: string;
+ inactiveColor?: string;
+ isAllowHalf?: boolean;
+ readonly?: boolean;
+ leftColumn?: number;
+ rightColumn?: number;
+ topRow?: number;
+ bottomRow?: number;
+}
+
+function renderStarsWithTooltip(props: RateComponentProps) {
+ const rateTooltips = props.tooltips || [];
+ const rateTooltipsCount = rateTooltips.length;
+ const deltaCount = props.maxCount - rateTooltipsCount;
+ if (rateTooltipsCount === 0) {
+ return (
+
+ );
+ }
+ const starWithTooltip = rateTooltips.map((tooltip) => (
+
+
+
+ ));
+ const starWithoutTooltip = _.times(deltaCount, (num: number) => (
+
+ ));
+
+ return _.concat(starWithTooltip, starWithoutTooltip);
+}
+
+function RateComponent(props: RateComponentProps) {
+ const rateContainerRef = React.createRef();
+
+ const {
+ bottomRow,
+ inactiveColor,
+ isAllowHalf,
+ leftColumn,
+ maxCount,
+ onValueChanged,
+ readonly,
+ rightColumn,
+ size,
+ topRow,
+ value,
+ } = props;
+
+ const [scrollable, setScrollable] = useState(false);
+
+ useEffect(() => {
+ const rateContainerElement = rateContainerRef.current;
+ if (
+ rateContainerElement &&
+ rateContainerElement.scrollHeight > rateContainerElement.clientHeight
+ ) {
+ setScrollable(true);
+ } else {
+ setScrollable(false);
+ }
+ }, [leftColumn, rightColumn, topRow, bottomRow, maxCount, size]);
+
+ return (
+
+
+ }
+ fractions={isAllowHalf ? 2 : 1}
+ fullSymbol={renderStarsWithTooltip(props)}
+ initialRating={value}
+ onChange={onValueChanged}
+ readonly={readonly}
+ stop={maxCount}
+ />
+
+ );
+}
+
+export default RateComponent;
diff --git a/app/client/src/components/designSystems/blueprint/SwitchComponent.tsx b/app/client/src/components/designSystems/blueprint/SwitchComponent.tsx
index b0b2475a58..d086e7f62e 100644
--- a/app/client/src/components/designSystems/blueprint/SwitchComponent.tsx
+++ b/app/client/src/components/designSystems/blueprint/SwitchComponent.tsx
@@ -27,12 +27,12 @@ const SwitchComponentContainer = styled.div`
`;
export function SwitchComponent({
- label,
- isSwitchedOn,
alignWidget,
- onChange,
isDisabled,
isLoading,
+ isSwitchedOn,
+ label,
+ onChange,
}: SwitchComponentProps) {
const switchAlignClass =
alignWidget === "RIGHT" ? Alignment.RIGHT : Alignment.LEFT;
diff --git a/app/client/src/components/designSystems/blueprint/TextComponent.tsx b/app/client/src/components/designSystems/blueprint/TextComponent.tsx
index 2efd60d384..f6aef1368d 100644
--- a/app/client/src/components/designSystems/blueprint/TextComponent.tsx
+++ b/app/client/src/components/designSystems/blueprint/TextComponent.tsx
@@ -16,20 +16,13 @@ type TextStyleProps = {
export const BaseText = styled(Text)``;
-/*
- Note:
- -webkit-line-clamp may seem like a wierd way to doing this
- however, it is getting more and more useful with more browser support.
- It suffices for our target browsers
- More info: https://css-tricks.com/line-clampin/
-*/
-
export const TextContainer = styled.div`
&& {
height: 100%;
width: 100%;
}
`;
+
export const StyledText = styled(Text)<{
scroll: boolean;
textAlign: string;
@@ -76,13 +69,13 @@ export interface TextComponentProps extends ComponentProps {
class TextComponent extends React.Component {
render() {
const {
- text,
- ellipsize,
- textAlign,
- fontStyle,
- fontSize,
- textColor,
backgroundColor,
+ ellipsize,
+ fontSize,
+ fontStyle,
+ text,
+ textAlign,
+ textColor,
} = this.props;
return (
@@ -98,7 +91,7 @@ class TextComponent extends React.Component {
>
diff --git a/app/client/src/components/editorComponents/ActionCreator.tsx b/app/client/src/components/editorComponents/ActionCreator/Fields.tsx
similarity index 66%
rename from app/client/src/components/editorComponents/ActionCreator.tsx
rename to app/client/src/components/editorComponents/ActionCreator/Fields.tsx
index 9b15a4ff96..29884f65fb 100644
--- a/app/client/src/components/editorComponents/ActionCreator.tsx
+++ b/app/client/src/components/editorComponents/ActionCreator/Fields.tsx
@@ -1,14 +1,5 @@
import React from "react";
-import { AppState } from "reducers";
-import { getActionsForCurrentPage } from "selectors/entitiesSelector";
-import {
- getModalDropdownList,
- getNextModalName,
-} from "selectors/widgetSelectors";
-import { getCurrentPageId } from "selectors/editorSelectors";
-import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { DropdownOption } from "widgets/DropdownWidget";
-import { useDispatch, useSelector } from "react-redux";
import TreeDropdown, { TreeDropdownOption } from "components/ads/TreeDropdown";
import {
ControlWrapper,
@@ -16,20 +7,9 @@ import {
} from "components/propertyControls/StyledControls";
import { KeyValueComponent } from "components/propertyControls/KeyValueComponent";
import { InputText } from "components/propertyControls/InputTextControl";
-import { createModalAction } from "actions/widgetActions";
-import { createNewApiName, createNewQueryName } from "utils/AppsmithUtils";
import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils";
import HightlightedCode from "components/editorComponents/HighlightedCode";
-import TreeStructure from "components/utils/TreeStructure";
-import {
- createNewApiAction,
- createNewQueryAction,
-} from "actions/apiPaneActions";
import { NavigationTargetType } from "sagas/ActionExecutionSagas";
-import { checkCurrentStep } from "sagas/OnboardingSagas";
-import { OnboardingStep } from "constants/OnboardingConstants";
-import { getWidgets } from "sagas/selectors";
-import { PluginType } from "entities/Action";
import { Skin } from "constants/DefaultTheme";
/* eslint-disable @typescript-eslint/ban-types */
@@ -71,11 +51,11 @@ const NAVIGATION_TARGET_FIELD_OPTIONS = [
},
];
-const FUNC_ARGS_REGEX = /((["][^"]*["])|([\[].*[\]])|([\{].*[\}])|(['][^']*['])|([\(].*[\)[=][>][{].*[}])|([^'",][^,"+]*[^'",]*))*/gi;
-const ACTION_TRIGGER_REGEX = /^{{([\s\S]*?)\(([\s\S]*?)\)}}$/g;
+export const FUNC_ARGS_REGEX = /((["][^"]*["])|([\[].*[\]])|([\{].*[\}])|(['][^']*['])|([\(].*[\)[=][>][{].*[}])|([^'",][^,"+]*[^'",]*))*/gi;
+export 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;
-const IS_URL_OR_MODAL = /^'.*'$/;
+export const ACTION_ANONYMOUS_FUNC_REGEX = /\(\) => (({[\s\S]*?})|([\s\S]*?)(\([\s\S]*?\)))/g;
+export const IS_URL_OR_MODAL = /^'.*'$/;
const modalSetter = (changeValue: any, currentValue: string) => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
let args: string[] = [];
@@ -108,7 +88,7 @@ export const modalGetter = (value: string) => {
};
export const stringToJS = (string: string): string => {
- const { stringSegments, jsSnippets } = getDynamicBindings(string);
+ const { jsSnippets, stringSegments } = getDynamicBindings(string);
const js = stringSegments
.map((segment, index) => {
if (jsSnippets[index] && jsSnippets[index].length > 0) {
@@ -213,16 +193,9 @@ const enumTypeGetter = (
return defaultValue;
};
-type ActionCreatorProps = {
- value: string;
- isValid: boolean;
- validationMessage?: string;
- onValueChange: (newValue: string) => void;
- additionalAutoComplete?: Record>;
-};
-
-const ActionType = {
+export const ActionType = {
none: "none",
+ integration: "integration",
api: "api",
query: "query",
showModal: "showModal",
@@ -263,8 +236,6 @@ type SelectorViewProps = ViewProps & {
type KeyValueViewProps = ViewProps;
type TextViewProps = ViewProps & {
- isValid: boolean;
- validationMessage?: string;
additionalAutoComplete?: Record>;
};
@@ -307,10 +278,8 @@ const views = {
{props.label && {props.label} }
{
if (event.target) {
@@ -327,7 +296,7 @@ const views = {
},
};
-const FieldType = {
+export const FieldType = {
ACTION_SELECTOR_FIELD: "ACTION_SELECTOR_FIELD",
ON_SUCCESS_FIELD: "ON_SUCCESS_FIELD",
ON_ERROR_FIELD: "ON_ERROR_FIELD",
@@ -382,8 +351,7 @@ const fieldConfigs: FieldConfigs = {
const type: ActionType = option.type || option.value;
let value = option.value;
switch (type) {
- case ActionType.api:
- case ActionType.query:
+ case ActionType.integration:
value = `${value}.run`;
break;
default:
@@ -547,235 +515,15 @@ const fieldConfigs: FieldConfigs = {
},
};
-const baseOptions: any = [
- {
- label: "No Action",
- value: ActionType.none,
- },
- {
- label: "Call An API",
- value: ActionType.api,
- },
- {
- label: "Execute a DB Query",
- value: ActionType.query,
- },
- {
- label: "Navigate To",
- value: ActionType.navigateTo,
- },
- {
- label: "Show Message",
- value: ActionType.showAlert,
- },
- {
- label: "Open Modal",
- value: ActionType.showModal,
- },
- {
- label: "Close Modal",
- value: ActionType.closeModal,
- },
- {
- label: "Store Value",
- value: ActionType.storeValue,
- },
- {
- label: "Download",
- value: ActionType.download,
- },
- {
- label: "Copy to Clipboard",
- value: ActionType.copyToClipboard,
- },
- {
- label: "Reset Widget",
- value: ActionType.resetWidget,
- },
-];
-function getOptionsWithChildren(
- options: TreeDropdownOption[],
- actions: ActionDataState,
- createActionOption: TreeDropdownOption,
-) {
- const option = options.find((option) => option.value === ActionType.api);
- if (option) {
- option.children = [createActionOption];
- actions.forEach((action) => {
- (option.children as TreeDropdownOption[]).push({
- label: action.config.name,
- id: action.config.id,
- value: action.config.name,
- type: option.value,
- } as TreeDropdownOption);
- });
- }
- return options;
-}
-
-function getFieldFromValue(
- value: string | undefined,
- getParentValue?: Function,
-): any[] {
- const fields: any[] = [
- {
- field: FieldType.ACTION_SELECTOR_FIELD,
- getParentValue,
- value,
- },
- ];
- if (!value) {
- return fields;
- }
- if (value.indexOf("run") !== -1) {
- const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
- if (matches.length) {
- const funcArgs = matches[0][2];
- const args = [...funcArgs.matchAll(ACTION_ANONYMOUS_FUNC_REGEX)];
- const successArg = args[0];
- const errorArg = args[1];
- let sucesssValue;
- if (successArg && successArg.length > 0) {
- sucesssValue = successArg[1] !== "{}" ? `{{${successArg[1]}}}` : ""; //successArg[1] + successArg[2];
- }
- const successFields = getFieldFromValue(
- sucesssValue,
- (changeValue: string) => {
- const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
- const args = [...matches[0][2].matchAll(ACTION_ANONYMOUS_FUNC_REGEX)];
- let successArg = args[0] ? args[0][0] : "() => {}";
- const errorArg = args[1] ? args[1][0] : "() => {}";
- successArg = changeValue.endsWith(")")
- ? `() => ${changeValue}`
- : `() => ${changeValue}()`;
-
- return value.replace(
- ACTION_TRIGGER_REGEX,
- `{{$1(${successArg}, ${errorArg})}}`,
- );
- },
- );
- successFields[0].label = "onSuccess";
- fields.push(successFields);
-
- let errorValue;
- if (errorArg && errorArg.length > 0) {
- errorValue = errorArg[1] !== "{}" ? `{{${errorArg[1]}}}` : ""; //errorArg[1] + errorArg[2];
- }
- const errorFields = getFieldFromValue(
- errorValue,
- (changeValue: string) => {
- const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
- const args = [...matches[0][2].matchAll(ACTION_ANONYMOUS_FUNC_REGEX)];
- const successArg = args[0] ? args[0][0] : "() => {}";
- let errorArg = args[1] ? args[1][0] : "() => {}";
- errorArg = changeValue.endsWith(")")
- ? `() => ${changeValue}`
- : `() => ${changeValue}()`;
- return value.replace(
- ACTION_TRIGGER_REGEX,
- `{{$1(${successArg}, ${errorArg})}}`,
- );
- },
- );
- errorFields[0].label = "onError";
- fields.push(errorFields);
- }
- return fields;
- }
- if (value.indexOf("navigateTo") !== -1) {
- fields.push({
- field: FieldType.URL_FIELD,
- });
- fields.push({
- field: FieldType.QUERY_PARAMS_FIELD,
- });
- fields.push({
- field: FieldType.NAVIGATION_TARGET_FIELD,
- });
- }
-
- if (value.indexOf("showModal") !== -1) {
- fields.push({
- field: FieldType.SHOW_MODAL_FIELD,
- });
- }
- if (value.indexOf("closeModal") !== -1) {
- fields.push({
- field: FieldType.CLOSE_MODAL_FIELD,
- });
- }
- if (value.indexOf("showAlert") !== -1) {
- fields.push(
- {
- field: FieldType.ALERT_TEXT_FIELD,
- },
- {
- field: FieldType.ALERT_TYPE_SELECTOR_FIELD,
- },
- );
- }
- if (value.indexOf("storeValue") !== -1) {
- fields.push(
- {
- field: FieldType.KEY_TEXT_FIELD,
- },
- {
- field: FieldType.VALUE_TEXT_FIELD,
- },
- );
- }
- if (value.indexOf("resetWidget") !== -1) {
- fields.push(
- {
- field: FieldType.WIDGET_NAME_FIELD,
- },
- {
- field: FieldType.RESET_CHILDREN_FIELD,
- },
- );
- }
- if (value.indexOf("download") !== -1) {
- fields.push(
- {
- field: FieldType.DOWNLOAD_DATA_FIELD,
- },
- {
- field: FieldType.DOWNLOAD_FILE_NAME_FIELD,
- },
- {
- field: FieldType.DOWNLOAD_FILE_TYPE_FIELD,
- },
- );
- }
- if (value.indexOf("copyToClipboard") !== -1) {
- fields.push({
- field: FieldType.COPY_TEXT_FIELD,
- });
- }
- return fields;
-}
-
-function getPageDropdownOptions(state: AppState) {
- return state.entities.pageList.pages.map((page) => ({
- label: page.pageName,
- id: page.pageId,
- value: `'${page.pageName}'`,
- }));
-}
-
function renderField(props: {
onValueChange: Function;
value: string;
field: any;
label?: string;
- isValid: boolean;
- validationMessage?: string;
- apiOptionTree: TreeDropdownOption[];
widgetOptionTree: TreeDropdownOption[];
- queryOptionTree: TreeDropdownOption[];
modalDropdownList: TreeDropdownOption[];
pageDropdownOptions: TreeDropdownOption[];
+ integrationOptionTree: TreeDropdownOption[];
depth: number;
maxDepth: number;
additionalAutoComplete?: Record>;
@@ -800,7 +548,7 @@ function renderField(props: {
case FieldType.WIDGET_NAME_FIELD:
let label = "";
let defaultText = "Select Action";
- let options = props.apiOptionTree;
+ let options = props.integrationOptionTree;
let selectedLabelModifier = undefined;
let displayValue = undefined;
let getDefaults = undefined;
@@ -817,10 +565,7 @@ function renderField(props: {
option: TreeDropdownOption,
displayValue?: string,
) {
- if (
- option.type === ActionType.api ||
- option.type === ActionType.query
- ) {
+ if (option.type === ActionType.integration) {
return (
JSX.Element)({
- options: props.apiOptionTree,
+ options: props.integrationOptionTree,
label: "",
get: fieldConfig.getter,
set: (value: string | DropdownOption) => {
@@ -944,8 +689,6 @@ function renderField(props: {
props.onValueChange(finalValueToSet);
},
value: props.value,
- isValid: props.isValid,
- validationMessage: props.validationMessage,
additionalAutoComplete: props.additionalAutoComplete,
});
break;
@@ -961,11 +704,10 @@ function Fields(props: {
value: string;
fields: any;
label?: string;
- isValid: boolean;
- validationMessage?: string;
- apiOptionTree: TreeDropdownOption[];
+ // apiOptionTree: TreeDropdownOption[];
+ integrationOptionTree: TreeDropdownOption[];
widgetOptionTree: TreeDropdownOption[];
- queryOptionTree: TreeDropdownOption[];
+ // queryOptionTree: TreeDropdownOption[];
modalDropdownList: TreeDropdownOption[];
pageDropdownOptions: TreeDropdownOption[];
depth: number;
@@ -992,10 +734,9 @@ function Fields(props: {
@@ -1036,10 +775,9 @@ function Fields(props: {
const selectorField = field[0];
return (
@@ -1068,160 +804,4 @@ function Fields(props: {
}
}
-function useModalDropdownList() {
- const dispatch = useDispatch();
- const nextModalName = useSelector(getNextModalName);
-
- let finalList: TreeDropdownOption[] = [
- {
- label: "New Modal",
- value: "Modal",
- id: "create",
- icon: "plus",
- className: "t--create-modal-btn",
- onSelect: (option: TreeDropdownOption, setter?: Function) => {
- const modalName = nextModalName;
- if (setter) {
- setter({
- value: `${modalName}`,
- });
- dispatch(createModalAction(nextModalName));
- }
- },
- },
- ];
-
- finalList = finalList.concat(
- (useSelector(getModalDropdownList) || []) as TreeDropdownOption[],
- );
-
- return finalList;
-}
-
-function useApiOptionTree() {
- const dispatch = useDispatch();
- const pageId = useSelector(getCurrentPageId) || "";
-
- const actions = useSelector(getActionsForCurrentPage).filter(
- (action) =>
- action.config.pluginType === PluginType.API ||
- action.config.pluginType === PluginType.SAAS,
- );
- let filteredBaseOptions = baseOptions;
-
- // For onboarding
- const filterOptions = useSelector((state: AppState) =>
- checkCurrentStep(state, OnboardingStep.ADD_INPUT_WIDGET),
- );
- if (filterOptions) {
- filteredBaseOptions = baseOptions.filter(
- (item: any) => item.value === ActionType.query,
- );
- }
-
- const apiOptionTree = getOptionsWithChildren(filteredBaseOptions, actions, {
- label: "Create API",
- value: "api",
- id: "create",
- className: "t--create-api-btn",
- icon: "plus",
- onSelect: (option: TreeDropdownOption, setter?: Function) => {
- const apiName = createNewApiName(actions, pageId);
- if (setter) {
- setter({
- value: `${apiName}`,
- type: ActionType.api,
- });
- dispatch(createNewApiAction(pageId, "API_PANE"));
- }
- },
- });
- return apiOptionTree;
-}
-
-function useWidgetOptionTree() {
- const widgets = useSelector(getWidgets) || {};
- return Object.values(widgets)
- .filter((w) => w.type !== "CANVAS_WIDGET" && w.type !== "BUTTON_WIDGET")
- .map((w) => {
- return {
- label: w.widgetName,
- id: w.widgetName,
- value: `"${w.widgetName}"`,
- };
- });
-}
-function getQueryOptionsWithChildren(
- options: TreeDropdownOption[],
- queries: ActionDataState,
- createQueryOption: TreeDropdownOption,
-) {
- const option = options.find((option) => option.value === ActionType.query);
- if (option) {
- option.children = [createQueryOption];
- queries.forEach((query) => {
- (option.children as TreeDropdownOption[]).push({
- label: query.config.name,
- id: query.config.id,
- value: query.config.name,
- type: option.value,
- } as TreeDropdownOption);
- });
- }
- return options;
-}
-
-function useQueryOptionTree() {
- const dispatch = useDispatch();
- const pageId = useSelector(getCurrentPageId) || "";
-
- const queries = useSelector(getActionsForCurrentPage).filter(
- (action) => action.config.pluginType === PluginType.DB,
- );
- const queryOptionTree = getQueryOptionsWithChildren(baseOptions, queries, {
- label: "Create Query",
- value: "query",
- id: "create",
- icon: "plus",
- className: "t--create-query-btn",
- onSelect: (option: TreeDropdownOption, setter?: Function) => {
- const queryName = createNewQueryName(queries, pageId);
- if (setter) {
- setter({
- value: `${queryName}`,
- type: ActionType.query,
- });
- dispatch(createNewQueryAction(pageId, "QUERY_PANE"));
- }
- },
- });
- return queryOptionTree;
-}
-
-export function ActionCreator(props: ActionCreatorProps) {
- const apiOptionTree = useApiOptionTree();
- const widgetOptionTree = useWidgetOptionTree();
- const queryOptionTree = useQueryOptionTree();
- const modalDropdownList = useModalDropdownList();
- const pageDropdownOptions = useSelector(getPageDropdownOptions);
- const fields = getFieldFromValue(props.value);
- return (
-
-
-
- );
-}
+export default Fields;
diff --git a/app/client/src/components/editorComponents/ActionCreator/index.tsx b/app/client/src/components/editorComponents/ActionCreator/index.tsx
new file mode 100644
index 0000000000..3e8303b47e
--- /dev/null
+++ b/app/client/src/components/editorComponents/ActionCreator/index.tsx
@@ -0,0 +1,420 @@
+import React, { useMemo } from "react";
+import { AppState } from "reducers";
+import {
+ getActionsForCurrentPage,
+ getDBDatasources,
+} from "selectors/entitiesSelector";
+import { createActionRequest } from "actions/actionActions";
+import {
+ getModalDropdownList,
+ getNextModalName,
+} from "selectors/widgetSelectors";
+import {
+ getCurrentApplicationId,
+ getCurrentPageId,
+} from "selectors/editorSelectors";
+import { Datasource } from "entities/Datasource";
+import Fields, {
+ ACTION_TRIGGER_REGEX,
+ ACTION_ANONYMOUS_FUNC_REGEX,
+ ActionType,
+ FieldType,
+} from "./Fields";
+import { TreeDropdownOption } from "components/ads/TreeDropdown";
+import { useDispatch, useSelector } from "react-redux";
+import { createModalAction } from "actions/widgetActions";
+import { createNewQueryName } from "utils/AppsmithUtils";
+import TreeStructure from "components/utils/TreeStructure";
+import { getWidgets } from "sagas/selectors";
+import { PluginType } from "entities/Action";
+import { INTEGRATION_EDITOR_URL, INTEGRATION_TABS } from "constants/routes";
+import history from "utils/history";
+import { keyBy } from "lodash";
+import { getPluginIcon, apiIcon } from "pages/Editor/Explorer/ExplorerIcons";
+import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
+
+/* eslint-disable @typescript-eslint/ban-types */
+/* TODO: Function and object types need to be updated to enable the lint rule */
+
+const baseOptions: any = [
+ {
+ label: "No Action",
+ value: ActionType.none,
+ },
+ {
+ label: "Execute a Query",
+ value: ActionType.integration,
+ },
+
+ {
+ label: "Navigate To",
+ value: ActionType.navigateTo,
+ },
+ {
+ label: "Show Message",
+ value: ActionType.showAlert,
+ },
+ {
+ label: "Open Modal",
+ value: ActionType.showModal,
+ },
+ {
+ label: "Close Modal",
+ value: ActionType.closeModal,
+ },
+ {
+ label: "Store Value",
+ value: ActionType.storeValue,
+ },
+ {
+ label: "Download",
+ value: ActionType.download,
+ },
+ {
+ label: "Copy to Clipboard",
+ value: ActionType.copyToClipboard,
+ },
+ {
+ label: "Reset Widget",
+ value: ActionType.resetWidget,
+ },
+];
+
+function getFieldFromValue(
+ value: string | undefined,
+ getParentValue?: Function,
+): any[] {
+ const fields: any[] = [
+ {
+ field: FieldType.ACTION_SELECTOR_FIELD,
+ getParentValue,
+ value,
+ },
+ ];
+ if (!value) {
+ return fields;
+ }
+ if (value.indexOf("run") !== -1) {
+ const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
+ if (matches.length) {
+ const funcArgs = matches[0][2];
+ const args = [...funcArgs.matchAll(ACTION_ANONYMOUS_FUNC_REGEX)];
+ const successArg = args[0];
+ const errorArg = args[1];
+ let sucesssValue;
+ if (successArg && successArg.length > 0) {
+ sucesssValue = successArg[1] !== "{}" ? `{{${successArg[1]}}}` : ""; //successArg[1] + successArg[2];
+ }
+ const successFields = getFieldFromValue(
+ sucesssValue,
+ (changeValue: string) => {
+ const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
+ const args = [...matches[0][2].matchAll(ACTION_ANONYMOUS_FUNC_REGEX)];
+ let successArg = args[0] ? args[0][0] : "() => {}";
+ const errorArg = args[1] ? args[1][0] : "() => {}";
+ successArg = changeValue.endsWith(")")
+ ? `() => ${changeValue}`
+ : `() => ${changeValue}()`;
+
+ return value.replace(
+ ACTION_TRIGGER_REGEX,
+ `{{$1(${successArg}, ${errorArg})}}`,
+ );
+ },
+ );
+ successFields[0].label = "onSuccess";
+ fields.push(successFields);
+
+ let errorValue;
+ if (errorArg && errorArg.length > 0) {
+ errorValue = errorArg[1] !== "{}" ? `{{${errorArg[1]}}}` : ""; //errorArg[1] + errorArg[2];
+ }
+ const errorFields = getFieldFromValue(
+ errorValue,
+ (changeValue: string) => {
+ const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
+ const args = [...matches[0][2].matchAll(ACTION_ANONYMOUS_FUNC_REGEX)];
+ const successArg = args[0] ? args[0][0] : "() => {}";
+ let errorArg = args[1] ? args[1][0] : "() => {}";
+ errorArg = changeValue.endsWith(")")
+ ? `() => ${changeValue}`
+ : `() => ${changeValue}()`;
+ return value.replace(
+ ACTION_TRIGGER_REGEX,
+ `{{$1(${successArg}, ${errorArg})}}`,
+ );
+ },
+ );
+ errorFields[0].label = "onError";
+ fields.push(errorFields);
+ }
+ return fields;
+ }
+ if (value.indexOf("navigateTo") !== -1) {
+ fields.push({
+ field: FieldType.URL_FIELD,
+ });
+ fields.push({
+ field: FieldType.QUERY_PARAMS_FIELD,
+ });
+ fields.push({
+ field: FieldType.NAVIGATION_TARGET_FIELD,
+ });
+ }
+
+ if (value.indexOf("showModal") !== -1) {
+ fields.push({
+ field: FieldType.SHOW_MODAL_FIELD,
+ });
+ }
+ if (value.indexOf("closeModal") !== -1) {
+ fields.push({
+ field: FieldType.CLOSE_MODAL_FIELD,
+ });
+ }
+ if (value.indexOf("showAlert") !== -1) {
+ fields.push(
+ {
+ field: FieldType.ALERT_TEXT_FIELD,
+ },
+ {
+ field: FieldType.ALERT_TYPE_SELECTOR_FIELD,
+ },
+ );
+ }
+ if (value.indexOf("storeValue") !== -1) {
+ fields.push(
+ {
+ field: FieldType.KEY_TEXT_FIELD,
+ },
+ {
+ field: FieldType.VALUE_TEXT_FIELD,
+ },
+ );
+ }
+ if (value.indexOf("resetWidget") !== -1) {
+ fields.push(
+ {
+ field: FieldType.WIDGET_NAME_FIELD,
+ },
+ {
+ field: FieldType.RESET_CHILDREN_FIELD,
+ },
+ );
+ }
+ if (value.indexOf("download") !== -1) {
+ fields.push(
+ {
+ field: FieldType.DOWNLOAD_DATA_FIELD,
+ },
+ {
+ field: FieldType.DOWNLOAD_FILE_NAME_FIELD,
+ },
+ {
+ field: FieldType.DOWNLOAD_FILE_TYPE_FIELD,
+ },
+ );
+ }
+ if (value.indexOf("copyToClipboard") !== -1) {
+ fields.push({
+ field: FieldType.COPY_TEXT_FIELD,
+ });
+ }
+ return fields;
+}
+
+function getPageDropdownOptions(state: AppState) {
+ return state.entities.pageList.pages.map((page) => ({
+ label: page.pageName,
+ id: page.pageId,
+ value: `'${page.pageName}'`,
+ }));
+}
+
+function useModalDropdownList() {
+ const dispatch = useDispatch();
+ const nextModalName = useSelector(getNextModalName);
+
+ let finalList: TreeDropdownOption[] = [
+ {
+ label: "New Modal",
+ value: "Modal",
+ id: "create",
+ icon: "plus",
+ className: "t--create-modal-btn",
+ onSelect: (option: TreeDropdownOption, setter?: Function) => {
+ const modalName = nextModalName;
+ if (setter) {
+ setter({
+ value: `${modalName}`,
+ });
+ dispatch(createModalAction(nextModalName));
+ }
+ },
+ },
+ ];
+
+ finalList = finalList.concat(
+ (useSelector(getModalDropdownList) || []) as TreeDropdownOption[],
+ );
+
+ return finalList;
+}
+
+function useWidgetOptionTree() {
+ const widgets = useSelector(getWidgets) || {};
+ return Object.values(widgets)
+ .filter((w) => w.type !== "CANVAS_WIDGET" && w.type !== "BUTTON_WIDGET")
+ .map((w) => {
+ return {
+ label: w.widgetName,
+ id: w.widgetName,
+ value: `"${w.widgetName}"`,
+ };
+ });
+}
+
+function getIntegrationOptionsWithChildren(
+ pageId: string,
+ plugins: any,
+ options: TreeDropdownOption[],
+ actions: any[],
+ datasources: Datasource[],
+ createIntegrationOption: TreeDropdownOption,
+ dispatch: any,
+) {
+ const queries = actions.filter(
+ (action) => action.config.pluginType === PluginType.DB,
+ );
+ const apis = actions.filter(
+ (action) =>
+ action.config.pluginType === PluginType.API ||
+ action.config.pluginType === PluginType.SAAS,
+ );
+ const option = options.find(
+ (option) => option.value === ActionType.integration,
+ );
+ if (option) {
+ option.children = [createIntegrationOption];
+ apis.forEach((api) => {
+ (option.children as TreeDropdownOption[]).push({
+ label: api.config.name,
+ id: api.config.id,
+ value: api.config.name,
+ type: option.value,
+ icon:
+ api.config.pluginType === PluginType.API
+ ? apiIcon
+ : getActionConfig(api.config.pluginType)?.getIcon(
+ api.config,
+ plugins[(api as any).config.datasource.pluginId],
+ ),
+ } as TreeDropdownOption);
+ });
+ queries.forEach((query) => {
+ (option.children as TreeDropdownOption[]).push({
+ label: query.config.name,
+ id: query.config.id,
+ value: query.config.name,
+ type: option.value,
+ icon: getActionConfig(query.config.pluginType)?.getIcon(
+ query.config,
+ plugins[(query as any).config.datasource.pluginId],
+ ),
+ } as TreeDropdownOption);
+ });
+ datasources.forEach((dataSource: Datasource) => {
+ (option.children as TreeDropdownOption[]).push({
+ label: dataSource.name,
+ id: dataSource.id,
+ value: dataSource.name,
+ type: option.value,
+ icon: getPluginIcon(plugins[dataSource.pluginId]) as React.ReactNode,
+ onSelect: () => {
+ const newQueryName = createNewQueryName(actions, pageId);
+ dispatch(
+ createActionRequest({
+ name: newQueryName,
+ pageId,
+ datasource: {
+ id: dataSource.id,
+ },
+ eventData: {
+ actionType: "Query",
+ from: "home-screen",
+ dataSource: dataSource.name,
+ },
+ pluginId: dataSource.pluginId,
+ actionConfiguration: {},
+ }),
+ );
+ },
+ } as TreeDropdownOption);
+ });
+ }
+ return options;
+}
+
+function useIntegrationsOptionTree() {
+ const pageId = useSelector(getCurrentPageId) || "";
+ const applicationId = useSelector(getCurrentApplicationId);
+ const datasources: Datasource[] = useSelector(getDBDatasources);
+ const dispatch = useDispatch();
+ const plugins = useSelector((state: AppState) => {
+ return state.entities.plugins.list;
+ });
+ const pluginGroups: any = useMemo(() => keyBy(plugins, "id"), [plugins]);
+ const actions = useSelector(getActionsForCurrentPage);
+
+ const integrationOptionTree = getIntegrationOptionsWithChildren(
+ pageId,
+ pluginGroups,
+ baseOptions,
+ actions,
+ datasources,
+ {
+ label: "Create New Query",
+ value: "datasources",
+ id: "create",
+ icon: "plus",
+ className: "t--create-datasources-query-btn",
+ onSelect: () => {
+ history.push(
+ INTEGRATION_EDITOR_URL(applicationId, pageId, INTEGRATION_TABS.NEW),
+ );
+ },
+ },
+ dispatch,
+ );
+ return integrationOptionTree;
+}
+
+type ActionCreatorProps = {
+ value: string;
+ onValueChange: (newValue: string) => void;
+ additionalAutoComplete?: Record>;
+};
+
+export function ActionCreator(props: ActionCreatorProps) {
+ const integrationOptionTree = useIntegrationsOptionTree();
+ const widgetOptionTree = useWidgetOptionTree();
+ const modalDropdownList = useModalDropdownList();
+ const pageDropdownOptions = useSelector(getPageDropdownOptions);
+ const fields = getFieldFromValue(props.value);
+ return (
+
+
+
+ );
+}
diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx
index 09dc38510e..5354403571 100644
--- a/app/client/src/components/editorComponents/ApiResponseView.tsx
+++ b/app/client/src/components/editorComponents/ApiResponseView.tsx
@@ -12,8 +12,15 @@ import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
import { getActionResponses } from "selectors/entitiesSelector";
import { Colors } from "constants/Colors";
import _ from "lodash";
-import { useLocalStorage } from "utils/hooks/localstorage";
-import { CHECK_REQUEST_BODY, createMessage } from "constants/messages";
+import {
+ CHECK_REQUEST_BODY,
+ createMessage,
+ DEBUGGER_ERRORS,
+ DEBUGGER_LOGS,
+ EMPTY_RESPONSE_FIRST_HALF,
+ EMPTY_RESPONSE_LAST_HALF,
+ INSPECT_ENTITY,
+} from "constants/messages";
import { TabComponent } from "components/ads/Tabs";
import Text, { TextType } from "components/ads/Text";
import Icon from "components/ads/Icon";
@@ -25,11 +32,14 @@ import ErrorLogs from "./Debugger/Errors";
import Resizer, { ResizerCSS } from "./Debugger/Resizer";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { DebugButton } from "./Debugger/DebugCTA";
+import EntityDeps from "./Debugger/EntityDependecies";
+import Button, { Size } from "components/ads/Button";
const ResponseContainer = styled.div`
${ResizerCSS}
// Initial height of bottom tabs
height: 60%;
+ width: 100%;
// Minimum height of bottom tabs as it can be resized
min-height: 36px;
background-color: ${(props) => props.theme.colors.apiPane.responseBody.bg};
@@ -124,6 +134,21 @@ const FailedMessage = styled.div`
display: flex;
align-items: center;
margin-left: 5px;
+
+ .api-debugcta {
+ margin-top: 0px;
+ }
+`;
+
+const StyledCallout = styled(Callout)`
+ .${Classes.TEXT} {
+ line-height: normal;
+ }
+`;
+
+const InlineButton = styled(Button)`
+ display: inline-flex;
+ margin: 0 4px;
`;
interface ReduxStateProps {
@@ -135,6 +160,7 @@ type Props = ReduxStateProps &
RouteComponentProps & {
theme?: EditorTheme;
apiName: string;
+ onRunClick: () => void;
};
export const EMPTY_RESPONSE: ActionResponse = {
@@ -173,11 +199,6 @@ function ApiResponseView(props: Props) {
}
const panelRef: RefObject = useRef(null);
- const [requestDebugVisible, setRequestDebugVisible] = useLocalStorage(
- "requestDebugVisible",
- "true",
- );
-
const onDebugClick = useCallback(() => {
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
source: "API",
@@ -192,16 +213,17 @@ function ApiResponseView(props: Props) {
title: "Response Body",
panelComponent: (
- {hasFailed && !isRunning && requestDebugVisible && (
-
-
+
}
- onClose={() => setRequestDebugVisible(false)}
text={createMessage(CHECK_REQUEST_BODY)}
variant={Variant.danger}
/>
@@ -209,7 +231,18 @@ function ApiResponseView(props: Props) {
{_.isEmpty(response.statusCode) ? (
- Hit Run to get a Response
+
+ {EMPTY_RESPONSE_FIRST_HALF()}
+
+ {EMPTY_RESPONSE_LAST_HALF()}
+
) : (
,
},
{
key: "LOGS",
- title: "Logs",
+ title: createMessage(DEBUGGER_LOGS),
panelComponent: ,
},
+ {
+ key: "ENTITY_DEPENDENCIES",
+ title: createMessage(INSPECT_ENTITY),
+ panelComponent: ,
+ },
];
const onTabSelect = (index: number) => {
diff --git a/app/client/src/components/editorComponents/CloseEditor.tsx b/app/client/src/components/editorComponents/CloseEditor.tsx
index 7b71b40e8e..c5cc2eeb51 100644
--- a/app/client/src/components/editorComponents/CloseEditor.tsx
+++ b/app/client/src/components/editorComponents/CloseEditor.tsx
@@ -1,73 +1,58 @@
-import TooltipComponent from "components/ads/Tooltip";
-import { BUILDER_PAGE_URL } from "constants/routes";
import React from "react";
-import { useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
-import { Position } from "@blueprintjs/core";
import Text, { TextType } from "components/ads/Text";
-import {
- getCurrentApplicationId,
- getCurrentPageId,
-} from "selectors/editorSelectors";
-import Icon, { IconSize } from "components/ads/Icon";
+import { Icon } from "@blueprintjs/core";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
+import {
+ BUILDER_PAGE_URL,
+ INTEGRATION_EDITOR_URL,
+ INTEGRATION_TABS,
+} from "../../constants/routes";
+import { useSelector } from "react-redux";
+import {
+ getCurrentApplicationId,
+ getCurrentPageId,
+} from "../../selectors/editorSelectors";
const IconContainer = styled.div`
- width: 22px;
- height: 22px;
+ //width: 100%;
+ height: 30px;
display: flex;
- margin-right: 16px;
- justify-content: center;
align-items: center;
cursor: pointer;
- svg {
- width: 12px;
- height: 12px;
- path {
- fill: ${(props) => props.theme.colors.apiPane.closeIcon};
- }
- }
- &:hover {
- background-color: ${(props) => props.theme.colors.apiPane.iconHoverBg};
- }
+ padding-left: 16px;
+ /* background-color: ${(props) => props.theme.colors.apiPane.iconHoverBg}; */
`;
function CloseEditor() {
+ const history = useHistory();
const applicationId = useSelector(getCurrentApplicationId);
const pageId = useSelector(getCurrentPageId);
-
- const history = useHistory();
+ const params: string = location.search;
+ const redirectTo = new URLSearchParams(params).get("from");
const handleClose = (e: React.MouseEvent) => {
PerformanceTracker.startTracking(
PerformanceTransactionName.CLOSE_SIDE_PANE,
{ path: location.pathname },
);
e.stopPropagation();
- history.push(BUILDER_PAGE_URL(applicationId, pageId));
+ history.push(
+ redirectTo === "datasources"
+ ? INTEGRATION_EDITOR_URL(applicationId, pageId, INTEGRATION_TABS.ACTIVE)
+ : BUILDER_PAGE_URL(applicationId, pageId),
+ );
};
return (
-
- Close
-
- }
- minWidth="auto !important"
- minimal
- position={Position.BOTTOM_LEFT}
- >
-
-
-
-
+
+
+
+ Back
+
+
);
}
diff --git a/app/client/src/components/editorComponents/CodeEditor/BindingPrompt.tsx b/app/client/src/components/editorComponents/CodeEditor/BindingPrompt.tsx
index 22471c3910..e4906dfc80 100644
--- a/app/client/src/components/editorComponents/CodeEditor/BindingPrompt.tsx
+++ b/app/client/src/components/editorComponents/CodeEditor/BindingPrompt.tsx
@@ -16,11 +16,12 @@ const Wrapper = styled.span<{
background-color: ${(props) =>
props.theme.colors.codeMirror.background.hoverState};
position: absolute;
- bottom: ${(props) => -props.bottomOffset}px;
+ bottom: ${(props) => props.bottomOffset}px;
+ transform: translateY(100%);
width: 100%;
line-height: 13px;
visibility: ${(props) => (props.visible ? "visible" : "hidden")};
- z-index: 1;
+ z-index: 3;
`;
const CurlyBraces = styled.span`
@@ -35,17 +36,12 @@ function BindingPrompt(props: {
promptMessage?: React.ReactNode | string;
isOpen: boolean;
editorTheme?: EditorTheme;
+ showLightningMenu?: boolean;
}): JSX.Element {
const promptRef = useRef(null);
- let bottomOffset = 30;
const customMessage = !!props.promptMessage;
- if (promptRef.current) {
- const boundingRect = promptRef.current.getBoundingClientRect();
- bottomOffset = boundingRect.height;
- }
- if (customMessage) {
- bottomOffset = 36;
- }
+ const bottomOffset = customMessage ? 6 : 0;
+
return (
- Type {"{{"} to see a list of variables
+ Type{" "}
+
+ {props.showLightningMenu === false ? "{{" : "/"}
+ {" "}
+ {props.showLightningMenu === false
+ ? "to see a list of variables"
+ : "to access quick commands"}
>
)}
diff --git a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts
index dbf3cf2707..df7a3f5594 100644
--- a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts
@@ -46,7 +46,12 @@ export type HintHelper = (
additionalData?: Record>,
) => Hinter;
export type Hinter = {
- showHint: (editor: CodeMirror.Editor) => void;
+ showHint: (
+ editor: CodeMirror.Editor,
+ expected: string,
+ entityName: string,
+ additionalData?: any,
+ ) => any;
update?: (data: DataTree) => void;
trigger?: (editor: CodeMirror.Editor) => void;
};
diff --git a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx
index 4285ca25ee..68edfbae49 100644
--- a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx
+++ b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, useState } from "react";
+import React, { memo, useMemo, useRef, useState } from "react";
import styled from "styled-components";
import _ from "lodash";
import Popper from "pages/Editor/Popper";
@@ -11,6 +11,12 @@ import DebugButton from "components/editorComponents/Debugger/DebugCTA";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import Tooltip from "components/ads/Tooltip";
import { Classes } from "@blueprintjs/core";
+import {
+ EvaluationError,
+ PropertyEvaluationErrorType,
+} from "utils/DynamicBindingUtils";
+import * as Sentry from "@sentry/react";
+import { Severity } from "@sentry/react";
const Wrapper = styled.div`
position: relative;
@@ -112,7 +118,7 @@ interface Props {
expected?: string;
evaluatedValue?: any;
children: JSX.Element;
- error?: string;
+ errors: EvaluationError[];
useValidationMessage?: boolean;
hideEvaluatedValue?: boolean;
evaluationSubstitutionType?: EvaluationSubstitutionType;
@@ -121,7 +127,7 @@ interface Props {
interface PopoverContentProps {
hasError: boolean;
expected?: string;
- error?: string;
+ errors: EvaluationError[];
useValidationMessage?: boolean;
evaluatedValue: any;
theme: EditorTheme;
@@ -150,7 +156,14 @@ type PreparedStatementValue = {
export function PreparedStatementViewer(props: {
evaluatedValue: PreparedStatementValue;
}) {
- const { value, parameters } = props.evaluatedValue;
+ const { parameters, value } = props.evaluatedValue;
+ if (!value) {
+ Sentry.captureException("Prepared Statement got no value", {
+ level: Severity.Debug,
+ extra: { props },
+ });
+ return
;
+ }
const stringSegments = value.split(/\$\d/);
const $params = [...value.matchAll(/\$\d/g)].map((matches) => matches[0]);
const paramsWithTooltips = $params.map((param) => (
@@ -173,95 +186,126 @@ export function PreparedStatementViewer(props: {
);
}
-export function CurrentValueViewer(props: {
- theme: EditorTheme;
- evaluatedValue: any;
- hideLabel?: boolean;
- preparedStatementViewer?: boolean;
-}) {
- const currentValueWrapperRef = React.createRef();
- const codeWrapperRef = React.createRef();
+export const CurrentValueViewer = memo(
+ function CurrentValueViewer(props: {
+ theme: EditorTheme;
+ evaluatedValue: any;
+ hideLabel?: boolean;
+ preparedStatementViewer?: boolean;
+ }) {
+ const currentValueWrapperRef = React.createRef();
+ const codeWrapperRef = React.createRef();
- let content = (
-
- {"undefined"}
-
-
- );
- if (props.evaluatedValue !== undefined) {
- if (
- _.isObject(props.evaluatedValue) ||
- Array.isArray(props.evaluatedValue)
- ) {
- if (props.preparedStatementViewer) {
+ let content = (
+
+ {"undefined"}
+
+
+ );
+ if (props.evaluatedValue !== undefined) {
+ if (
+ _.isObject(props.evaluatedValue) ||
+ Array.isArray(props.evaluatedValue)
+ ) {
+ if (props.preparedStatementViewer) {
+ content = (
+
+
+
+
+ );
+ } else {
+ const reactJsonProps = {
+ theme:
+ props.theme === EditorTheme.DARK ? "summerfruit" : "rjv-default",
+ name: null,
+ enableClipboard: false,
+ displayObjectSize: false,
+ displayDataTypes: false,
+ style: {
+ fontSize: "12px",
+ },
+ collapsed: 2,
+ collapseStringsAfterLength: 20,
+ shouldCollapse: (field: any) => {
+ const index = field.name * 1;
+ return index >= 2;
+ },
+ };
+ content = (
+
+ );
+ }
+ } else {
content = (
-
+ {props.evaluatedValue === null
+ ? "null"
+ : props.evaluatedValue.toString()}
);
- } else {
- const reactJsonProps = {
- theme:
- props.theme === EditorTheme.DARK ? "summerfruit" : "rjv-default",
- name: null,
- enableClipboard: false,
- displayObjectSize: false,
- displayDataTypes: false,
- style: {
- fontSize: "12px",
- },
- collapsed: 2,
- collapseStringsAfterLength: 20,
- };
- content = ;
}
- } else {
- content = (
-
- {props.evaluatedValue === null
- ? "null"
- : props.evaluatedValue.toString()}
-
-
- );
}
- }
- return (
- <>
- {!props.hideLabel && (
-
- Evaluated Value
-
- )}
-
- <>
- {content}
-
- >
-
- >
- );
-}
+ return (
+ <>
+ {!props.hideLabel && (
+
+ Evaluated Value
+
+ )}
+
+ <>
+ {content}
+
+ >
+
+ >
+ );
+ },
+ (prevProps, nextProps) => {
+ return (
+ prevProps.theme === nextProps.theme &&
+ prevProps.hideLabel === nextProps.hideLabel &&
+ // Deep-compare evaluated values to ensure we only rerender
+ // when the array actually changes
+ _.isEqual(prevProps.evaluatedValue, nextProps.evaluatedValue)
+ );
+ },
+);
function PopoverContent(props: PopoverContentProps) {
const typeTextRef = React.createRef();
+ const {
+ errors,
+ expected,
+ hasError,
+ onMouseEnter,
+ onMouseLeave,
+ theme,
+ useValidationMessage,
+ } = props;
+ let error;
+ if (hasError) {
+ error = errors[0];
+ }
return (
- {props.hasError && (
+ {hasError && error && (
- {props.useValidationMessage && props.error
- ? props.error
- : `This value does not evaluate to type "${props.expected}". Transform it using JS inside '{{ }}'`}
+ {error.errorType === PropertyEvaluationErrorType.VALIDATION &&
+ !useValidationMessage
+ ? `This value does not evaluate to type "${expected}".`
+ : error.errorMessage}
)}
- {!props.hasError && props.expected && (
+ {!hasError && props.expected && (
<>
Expected Data Type
@@ -293,13 +337,15 @@ function EvaluatedValuePopup(props: Props) {
const [contentHovered, setContentHovered] = useState(false);
const wrapperRef = useRef(null);
- let placement: Placement = "left-start";
- if (wrapperRef.current) {
- const boundingRect = wrapperRef.current.getBoundingClientRect();
- if (boundingRect.left < theme.evaluatedValuePopup.width) {
- placement = "right-start";
+ const placement: Placement = useMemo(() => {
+ if (wrapperRef.current) {
+ const boundingRect = wrapperRef.current.getBoundingClientRect();
+ if (boundingRect.left < theme.evaluatedValuePopup.width) {
+ return "right-start";
+ }
}
- }
+ return "left-start";
+ }, [wrapperRef.current]);
return (
@@ -317,7 +363,7 @@ function EvaluatedValuePopup(props: Props) {
zIndex={5}
>
{
+ let entitiesForSuggestions = Object.values(data).filter(
+ (entity: any) => entity.ENTITY_TYPE && entity.ENTITY_TYPE !== "APPSMITH",
+ );
+ return {
+ showHint: (
+ editor: CodeMirror.Editor,
+ _: string,
+ entityName: string,
+ {
+ datasources,
+ executeCommand,
+ pluginIdToImageLocation,
+ recentEntities,
+ update,
+ }: {
+ datasources: Datasource[];
+ executeCommand: (payload: { actionType: string; args?: any }) => void;
+ pluginIdToImageLocation: Record;
+ recentEntities: string[];
+ update: (value: string) => void;
+ },
+ ): boolean => {
+ const currentEntityType = data[entityName]?.ENTITY_TYPE || "ACTION";
+ entitiesForSuggestions = entitiesForSuggestions.filter((entity: any) => {
+ return currentEntityType === "WIDGET"
+ ? entity.ENTITY_TYPE !== "WIDGET"
+ : entity.ENTITY_TYPE !== "ACTION";
+ });
+ const cursorBetweenBinding = checkIfCursorInsideBinding(editor);
+ const value = editor.getValue();
+ const slashIndex = value.lastIndexOf("/");
+ const shouldShowBinding =
+ slashIndex > -1 || (!value && currentEntityType === ENTITY_TYPE.WIDGET);
+ if (!cursorBetweenBinding && shouldShowBinding) {
+ const searchText = value.substring(slashIndex + 1);
+ const list = generateQuickCommands(
+ entitiesForSuggestions,
+ currentEntityType,
+ searchText,
+ {
+ datasources,
+ executeCommand,
+ pluginIdToImageLocation,
+ recentEntities,
+ },
+ );
+ let currentSelection: CommandsCompletion = {
+ origin: "",
+ type: "UNKNOWN",
+ data: {
+ doc: "",
+ },
+ text: "",
+ shortcut: "",
+ };
+ const cursor = editor.getCursor();
+ editor.showHint({
+ hint: () => {
+ const hints = {
+ list,
+ from: {
+ ch: cursor.ch - searchText.length - 1,
+ line: cursor.line,
+ },
+ to: editor.getCursor(),
+ selectedHint: 1,
+ };
+ CodeMirror.on(hints, "pick", (selected: CommandsCompletion) => {
+ update(value.slice(0, slashIndex) + selected.text);
+ setTimeout(() => {
+ editor.focus();
+ editor.setCursor({
+ line: editor.lineCount() - 1,
+ ch: editor.getLine(editor.lineCount() - 1).length - 2,
+ });
+ if (selected.action && typeof selected.action === "function") {
+ selected.action();
+ } else {
+ CodeMirror.signal(editor, "postPick");
+ }
+ });
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { data, render, ...rest } = selected;
+ const { ENTITY_TYPE, name, pluginType } = data as any;
+ AnalyticsUtil.logEvent("SLASH_COMMAND", {
+ ...rest,
+ ENTITY_TYPE,
+ name,
+ pluginType,
+ });
+ } catch (e) {
+ log.debug(e, "Error logging slash command");
+ }
+ });
+ CodeMirror.on(hints, "select", (selected: CommandsCompletion) => {
+ currentSelection = selected;
+ });
+ return hints;
+ },
+ extraKeys: {
+ Up: (cm: CodeMirror.Editor, handle: any) => {
+ handle.moveFocus(-1);
+ if (currentSelection.isHeader === true) {
+ handle.moveFocus(-1);
+ }
+ },
+ Down: (cm: CodeMirror.Editor, handle: any) => {
+ handle.moveFocus(1);
+ if (currentSelection.isHeader === true) {
+ handle.moveFocus(1);
+ }
+ },
+ },
+ completeSingle: false,
+ });
+ return true;
+ }
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore: No types available
+ editor.closeHint();
+ return false;
+ },
+ };
+};
diff --git a/app/client/src/components/editorComponents/CodeEditor/generateQuickCommands.tsx b/app/client/src/components/editorComponents/CodeEditor/generateQuickCommands.tsx
new file mode 100644
index 0000000000..e5c36a1deb
--- /dev/null
+++ b/app/client/src/components/editorComponents/CodeEditor/generateQuickCommands.tsx
@@ -0,0 +1,213 @@
+import { Datasource } from "entities/Datasource";
+import React from "react";
+import { CommandsCompletion } from "utils/autocomplete/TernServer";
+import ReactDOM from "react-dom";
+import sortBy from "lodash/sortBy";
+import { PluginType } from "entities/Action";
+import { ReactComponent as ApisIcon } from "assets/icons/menu/api-colored.svg";
+import { ReactComponent as DataSourcesColoredIcon } from "assets/icons/menu/datasource-colored.svg";
+import { ReactComponent as NewPlus } from "assets/icons/menu/new-plus.svg";
+import { ReactComponent as Binding } from "assets/icons/menu/binding.svg";
+
+enum Shortcuts {
+ PLUS = "PLUS",
+ BINDING = "BINDING",
+}
+export const generateQuickCommands = (
+ entitiesForSuggestions: any[],
+ currentEntityType: string,
+ searchText: string,
+ {
+ datasources,
+ executeCommand,
+ pluginIdToImageLocation,
+ recentEntities,
+ }: {
+ datasources: Datasource[];
+ executeCommand: (payload: { actionType: string; args?: any }) => void;
+ pluginIdToImageLocation: Record;
+ recentEntities: string[];
+ },
+) => {
+ const suggestionsHeader: CommandsCompletion = commandsHeader("Bind Data");
+ const createNewHeader: CommandsCompletion = commandsHeader("Create New");
+ recentEntities.reverse();
+ const newBinding: CommandsCompletion = generateCreateNewCommand({
+ text: "{{}}",
+ displayText: "New Binding",
+ shortcut: Shortcuts.BINDING,
+ });
+ const newIntegration: CommandsCompletion = generateCreateNewCommand({
+ text: "",
+ displayText: "New Datasource",
+ action: () =>
+ executeCommand({
+ actionType: "NEW_INTEGRATION",
+ }),
+ shortcut: Shortcuts.PLUS,
+ });
+ const suggestions = entitiesForSuggestions.map((suggestion: any) => {
+ const name = suggestion.name || suggestion.widgetName;
+ return {
+ text: currentEntityType === "WIDGET" ? `{{${name}.data}}` : `{{${name}}}`,
+ displayText: `${name}`,
+ className: "CodeMirror-commands",
+ data: suggestion,
+ render: (element: HTMLElement, self: any, data: any) => {
+ const pluginType = data.data.pluginType as PluginType;
+ ReactDOM.render(
+ ,
+ element,
+ );
+ },
+ };
+ });
+ const datasourceCommands = datasources.map((action: any) => {
+ return {
+ text: "",
+ displayText: `${action.name}`,
+ className: "CodeMirror-commands",
+ data: action,
+ action: () =>
+ executeCommand({
+ actionType: "NEW_QUERY",
+ args: { datasource: action },
+ }),
+ render: (element: HTMLElement, self: any, data: any) => {
+ ReactDOM.render(
+ ,
+ element,
+ );
+ },
+ };
+ });
+ const suggestionsMatchingSearchText = matchingCommands(
+ suggestions,
+ searchText,
+ recentEntities,
+ 5,
+ );
+ suggestionsMatchingSearchText.push(
+ ...matchingCommands([newBinding], searchText, []),
+ );
+ let createNewCommands: any = [];
+ if (currentEntityType === "WIDGET") {
+ createNewCommands = [...datasourceCommands];
+ }
+ const createNewCommandsMatchingSearchText = matchingCommands(
+ createNewCommands,
+ searchText,
+ [],
+ 3,
+ );
+ if (currentEntityType === "WIDGET") {
+ createNewCommandsMatchingSearchText.push(
+ ...matchingCommands([newIntegration], searchText, []),
+ );
+ }
+ let list: CommandsCompletion[] = [];
+ if (suggestionsMatchingSearchText.length) {
+ list = [suggestionsHeader, ...suggestionsMatchingSearchText];
+ }
+
+ if (createNewCommandsMatchingSearchText.length) {
+ list = [...list, createNewHeader, ...createNewCommandsMatchingSearchText];
+ }
+ return list;
+};
+
+const matchingCommands = (
+ list: any,
+ searchText: string,
+ recentEntities: string[] = [],
+ limit = 5,
+) => {
+ list = list.filter((action: any) => {
+ return action.displayText
+ .toLowerCase()
+ .startsWith(searchText.toLowerCase());
+ });
+ list = sortBy(list, (a: any) => {
+ return (
+ (a.data.ENTITY_TYPE === "WIDGET"
+ ? recentEntities.indexOf(a.data.widgetId)
+ : recentEntities.indexOf(a.data.actionId)) * -1
+ );
+ });
+ return list.slice(0, limit);
+};
+
+const commandsHeader = (
+ displayText: string,
+ text = "",
+): CommandsCompletion => ({
+ text: text,
+ displayText: displayText,
+ className: "CodeMirror-command-header",
+ data: { doc: "" },
+ origin: "",
+ type: "UNKNOWN",
+ isHeader: true,
+ shortcut: "",
+});
+
+const generateCreateNewCommand = ({
+ action,
+ displayText,
+ shortcut,
+ text,
+}: any): CommandsCompletion => ({
+ text: text,
+ displayText: displayText,
+ data: { doc: "" },
+ origin: "",
+ type: "UNKNOWN",
+ className: "CodeMirror-commands",
+ shortcut: shortcut,
+ action: action,
+ render: (element: HTMLElement, self: any, data: any) => {
+ ReactDOM.render(
+ ,
+ element,
+ );
+ },
+});
+
+function Command(props: {
+ pluginType?: PluginType;
+ imgSrc?: string;
+ name: string;
+ shortcut: Shortcuts;
+ customText?: string;
+}) {
+ return (
+
+
+ {props.pluginType &&
+ {
+ DB:
,
+ API:
,
+ SAAS:
,
+ }[props.pluginType]}
+ {props.imgSrc &&
}
+ {props.shortcut &&
+ { [Shortcuts.BINDING]:
, [Shortcuts.PLUS]:
}[
+ props.shortcut
+ ]}
+
{props.name}
+
+
+ );
+}
diff --git a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.test.ts b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.test.ts
index 296d1660d3..a0f3ccc3ff 100644
--- a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.test.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.test.ts
@@ -9,7 +9,6 @@ describe("hint helpers", () => {
const helper = bindingHint(MockCodemirrorEditor, {});
expect(MockCodemirrorEditor.setOption).toBeCalled();
expect(helper).toHaveProperty("showHint");
- expect(helper).toHaveProperty("update");
});
it("opens hint correctly", () => {
diff --git a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts
index 0c6ae6ba9c..ef71d1c293 100644
--- a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts
@@ -1,75 +1,91 @@
import CodeMirror from "codemirror";
import TernServer from "utils/autocomplete/TernServer";
import KeyboardShortcuts from "constants/KeyboardShortcuts";
-import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
-import { DataTree } from "entities/DataTree/dataTreeFactory";
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
import { HintHelper } from "components/editorComponents/CodeEditor/EditorConfig";
import AnalyticsUtil from "utils/AnalyticsUtil";
-//TODO : add currentRow to data ?? currentRow: Object of columns ie first row
-export const bindingHint: HintHelper = (editor, data, additionalData) => {
- const ternServer = new TernServer(data, additionalData);
+import { customTreeTypeDefCreator } from "../../../utils/autocomplete/customTreeTypeDefCreator";
+
+export const bindingHint: HintHelper = (editor, dataTree, additionalData) => {
+ if (additionalData) {
+ const customTreeDef = customTreeTypeDefCreator(additionalData);
+ TernServer.updateDef("customDataTree", customTreeDef);
+ }
+
editor.setOption("extraKeys", {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available
...editor.options.extraKeys,
- [KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (cm: CodeMirror.Editor) =>
- ternServer.complete(cm),
+ [KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (
+ cm: CodeMirror.Editor,
+ expected: string,
+ entity: string,
+ ) => TernServer.complete(cm, expected, entity),
[KeyboardShortcuts.CodeEditor.ShowTypeAndInfo]: (cm: CodeMirror.Editor) => {
- ternServer.showType(cm);
+ TernServer.showType(cm);
},
[KeyboardShortcuts.CodeEditor.OpenDocsLink]: (cm: CodeMirror.Editor) => {
- ternServer.showDocs(cm);
+ TernServer.showDocs(cm);
},
});
return {
- update: (data: DataTree) => {
- const dataTreeDef = dataTreeTypeDefCreator(data);
- ternServer.updateDef("dataTree", dataTreeDef);
- },
- showHint: (editor: CodeMirror.Editor) => {
- let cursorBetweenBinding = false;
- const cursor = editor.getCursor();
- const value = editor.getValue();
- let cursorIndex = cursor.ch;
- if (cursor.line > 0) {
- for (let lineIndex = 0; lineIndex < cursor.line; lineIndex++) {
- const line = editor.getLine(lineIndex);
- // Add line length + 1 for new line character
- cursorIndex = cursorIndex + line.length + 1;
- }
- }
- const stringSegments = getDynamicStringSegments(value);
- // count of chars processed
- let cumulativeCharCount = 0;
- stringSegments.forEach((segment: string) => {
- const start = cumulativeCharCount;
- const dynamicStart = segment.indexOf("{{");
- const dynamicDoesStart = dynamicStart > -1;
- const dynamicEnd = segment.indexOf("}}");
- const dynamicDoesEnd = dynamicEnd > -1;
- const dynamicStartIndex = dynamicStart + start + 2;
- const dynamicEndIndex = dynamicEnd + start;
- if (
- dynamicDoesStart &&
- cursorIndex >= dynamicStartIndex &&
- ((dynamicDoesEnd && cursorIndex <= dynamicEndIndex) ||
- (!dynamicDoesEnd && cursorIndex >= dynamicStartIndex))
- ) {
- cursorBetweenBinding = true;
- }
- cumulativeCharCount = start + segment.length;
- });
-
- const shouldShow = cursorBetweenBinding;
+ showHint: (
+ editor: CodeMirror.Editor,
+ expected: string,
+ entityName: string,
+ ): boolean => {
+ const shouldShow = checkIfCursorInsideBinding(editor);
if (shouldShow) {
- AnalyticsUtil.logEvent("AUTO_COMPELTE_SHOW", {});
- ternServer.complete(editor);
- } else {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore: No types available
- editor.closeHint();
+ AnalyticsUtil.logEvent("AUTO_COMPLETE_SHOW", {});
+ TernServer.complete(editor, expected, entityName);
+ return true;
}
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore: No types available
+ editor.closeHint();
+ return false;
},
};
};
+
+const computeCursorIndex = (editor: CodeMirror.Editor) => {
+ const cursor = editor.getCursor();
+ let cursorIndex = cursor.ch;
+ if (cursor.line > 0) {
+ for (let lineIndex = 0; lineIndex < cursor.line; lineIndex++) {
+ const line = editor.getLine(lineIndex);
+ cursorIndex = cursorIndex + line.length + 1;
+ }
+ }
+ return cursorIndex;
+};
+
+export const checkIfCursorInsideBinding = (
+ editor: CodeMirror.Editor,
+): boolean => {
+ let cursorBetweenBinding = false;
+ const value = editor.getValue();
+ const cursorIndex = computeCursorIndex(editor);
+ const stringSegments = getDynamicStringSegments(value);
+ // count of chars processed
+ let cumulativeCharCount = 0;
+ stringSegments.forEach((segment: string) => {
+ const start = cumulativeCharCount;
+ const dynamicStart = segment.indexOf("{{");
+ const dynamicDoesStart = dynamicStart > -1;
+ const dynamicEnd = segment.indexOf("}}");
+ const dynamicDoesEnd = dynamicEnd > -1;
+ const dynamicStartIndex = dynamicStart + start + 2;
+ const dynamicEndIndex = dynamicEnd + start;
+ if (
+ dynamicDoesStart &&
+ cursorIndex >= dynamicStartIndex &&
+ ((dynamicDoesEnd && cursorIndex <= dynamicEndIndex) ||
+ (!dynamicDoesEnd && cursorIndex >= dynamicStartIndex))
+ ) {
+ cursorBetweenBinding = true;
+ }
+ cumulativeCharCount = start + segment.length;
+ });
+ return cursorBetweenBinding;
+};
diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx
index 8f98ea1af4..f652e5dae0 100644
--- a/app/client/src/components/editorComponents/CodeEditor/index.tsx
+++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx
@@ -1,4 +1,4 @@
-import React, { Component, lazy, Suspense } from "react";
+import React, { Component } from "react";
import { connect } from "react-redux";
import { AppState } from "reducers";
import CodeMirror, { EditorConfiguration } from "codemirror";
@@ -14,7 +14,7 @@ import "codemirror/addon/mode/multiplex";
import "codemirror/addon/tern/tern.css";
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup";
-import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
+import { WrappedFieldInputProps } from "redux-form";
import _ from "lodash";
import {
DataTree,
@@ -31,17 +31,17 @@ import {
EditorTheme,
EditorThemes,
Hinter,
+ HintHelper,
+ MarkHelper,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
import {
DynamicAutocompleteInputWrapper,
EditorWrapper,
- HintStyles,
IconContainer,
} from "components/editorComponents/CodeEditor/styledComponents";
import { bindingMarker } from "components/editorComponents/CodeEditor/markHelpers";
import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers";
-import { retryPromise } from "utils/AppsmithUtils";
import BindingPrompt from "./BindingPrompt";
import { showBindingPrompt } from "./BindingPromptHelper";
import ScrollIndicator from "components/ads/ScrollIndicator";
@@ -49,22 +49,37 @@ import "codemirror/addon/fold/brace-fold";
import "codemirror/addon/fold/foldgutter";
import "codemirror/addon/fold/foldgutter.css";
import * as Sentry from "@sentry/react";
+import {
+ EvaluationError,
+ getEvalErrorPath,
+ getEvalValuePath,
+ PropertyEvaluationErrorType,
+} from "utils/DynamicBindingUtils";
import { removeNewLineChars, getInputValue } from "./codeEditorUtils";
-
-const LightningMenu = lazy(() =>
- retryPromise(() => import("components/editorComponents/LightningMenu")),
-);
+import { commandsHelper } from "./commandsHelper";
+import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
+import Button from "components/ads/Button";
+import styled from "styled-components";
+import { Colors } from "constants/Colors";
+import { getPluginIdToImageLocation } from "sagas/selectors";
const AUTOCOMPLETE_CLOSE_KEY_CODES = [
"Enter",
"Tab",
"Escape",
- "Backspace",
"Comma",
+ "Backspace",
];
interface ReduxStateProps {
dynamicData: DataTree;
+ datasources: any;
+ pluginIdToImageLocation: Record;
+ recentEntities: string[];
+}
+
+interface ReduxDispatchProps {
+ executeCommand: (payload: any) => void;
}
export type EditorStyleProps = {
@@ -72,7 +87,6 @@ export type EditorStyleProps = {
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
height?: string | number;
- meta?: Partial;
showLineNumbers?: boolean;
className?: string;
leftImage?: string;
@@ -99,7 +113,9 @@ export type EditorProps = EditorStyleProps &
hideEvaluatedValue?: boolean;
};
-type Props = ReduxStateProps & EditorProps;
+type Props = ReduxStateProps &
+ EditorProps &
+ ReduxDispatchProps & { dispatch?: () => void };
type State = {
isFocused: boolean;
@@ -107,13 +123,26 @@ type State = {
autoCompleteVisible: boolean;
};
+const CommandBtnContainer = styled.div<{ isFocused: boolean }>`
+ position: absolute;
+ right: 1px;
+ height: 33px;
+ width: 33px;
+ top: 1px;
+ display: none;
+ transition: 0.3s all ease;
+ align-items: center;
+ justify-content: center;
+ background: ${(props) => (props.isFocused ? Colors.MERCURY : "#fafafa")};
+ z-index: 2;
+`;
class CodeEditor extends Component {
static defaultProps = {
marking: [bindingMarker],
- hinting: [bindingHint],
+ hinting: [bindingHint, commandsHelper],
};
- textArea = React.createRef();
+ codeEditorTarget = React.createRef();
editor!: CodeMirror.Editor;
hinters: Hinter[] = [];
private editorWrapperRef = React.createRef();
@@ -129,7 +158,7 @@ class CodeEditor extends Component {
}
componentDidMount(): void {
- if (this.textArea.current) {
+ if (this.codeEditorTarget.current) {
const options: EditorConfiguration = {
mode: this.props.mode,
theme: EditorThemes[this.props.theme],
@@ -143,6 +172,7 @@ class CodeEditor extends Component {
matchBrackets: false,
scrollbarStyle:
this.props.size !== EditorSize.COMPACT ? "native" : "null",
+ placeholder: this.props.placeholder,
};
if (!this.props.input.onChange || this.props.disabled) {
@@ -165,71 +195,103 @@ class CodeEditor extends Component {
},
};
}
- this.editor = CodeMirror.fromTextArea(this.textArea.current, options);
- this.editor.on("change", _.debounce(this.handleChange, 300));
- this.editor.on("change", this.handleAutocompleteVisibility);
- this.editor.on("change", this.onChangeTrigger);
- this.editor.on("keyup", this.handleAutocompleteHide);
- this.editor.on("focus", this.handleEditorFocus);
- this.editor.on("cursorActivity", this.handleCursorMovement);
- this.editor.on("focus", this.onFocusTrigger);
- this.editor.on("blur", this.handleEditorBlur);
- if (this.props.height) {
- this.editor.setSize(0, this.props.height);
- } else {
- this.editor.setSize(0, "auto");
- }
// Set value of the editor
const inputValue = getInputValue(this.props.input.value || "");
if (this.props.size === EditorSize.COMPACT) {
- this.editor.setValue(removeNewLineChars(inputValue));
+ options.value = removeNewLineChars(inputValue);
} else {
- this.editor.setValue(inputValue);
+ options.value = inputValue;
}
- this.updateMarkings();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore This is an undocumented option of codemirror available
+ // with the Codemirror Constructor
+ options.finishInit = (editor: CodeMirror.Editor) => {
+ // If you need to do something with the editor right after itβs been created,
+ // put that code here.
+ //
+ // This helps with performance: finishInit() is called inside
+ // CodeMirrorβs `operation()` (https://codemirror.net/doc/manual.html#operation
+ // which means CodeMirror recalculates itself only one time, once all CodeMirror
+ // changes here are completed
+ //
- this.startAutocomplete();
+ editor.on("beforeChange", this.handleBeforeChange);
+ editor.on("change", _.debounce(this.handleChange, 300));
+ editor.on("change", this.handleAutocompleteVisibility);
+ editor.on("change", this.onChangeTrigger);
+ editor.on("keyup", this.handleAutocompleteHide);
+ editor.on("focus", this.handleEditorFocus);
+ editor.on("cursorActivity", this.handleCursorMovement);
+ editor.on("focus", this.onFocusTrigger);
+ editor.on("blur", this.handleEditorBlur);
+ editor.on("postPick", () => this.handleAutocompleteVisibility(editor));
+ if (this.props.height) {
+ editor.setSize("100%", this.props.height);
+ } else {
+ editor.setSize("100%", "100%");
+ }
+
+ CodeEditor.updateMarkings(editor, this.props.marking);
+
+ this.hinters = CodeEditor.startAutocomplete(
+ editor,
+ this.props.hinting,
+ this.props.dynamicData,
+ this.props.showLightningMenu,
+ this.props.additionalDynamicData,
+ );
+ };
+
+ // Finally create the Codemirror editor
+ this.editor = CodeMirror(this.codeEditorTarget.current, options);
+ // DO NOT ADD CODE BELOW. If you need to do something with the editor right after itβs created,
+ // put that code into `options.finishInit()`.
}
}
componentDidUpdate(prevProps: Props): void {
- this.editor.refresh();
- if (!this.state.isFocused) {
- // const currentMode = this.editor.getOption("mode");
- const editorValue = this.editor.getValue();
- // Safe update of value of the editor when value updated outside the editor
- const inputValue = getInputValue(this.props.input.value);
- if (!!inputValue || inputValue === "") {
- if (this.props.size === EditorSize.COMPACT) {
- this.editor.setValue(removeNewLineChars(inputValue));
- } else if (inputValue !== editorValue) {
- this.editor.setValue(inputValue);
+ this.editor.operation(() => {
+ if (!this.state.isFocused) {
+ // const currentMode = this.editor.getOption("mode");
+ const editorValue = this.editor.getValue();
+ // Safe update of value of the editor when value updated outside the editor
+ const inputValue = getInputValue(this.props.input.value);
+ if (!!inputValue || inputValue === "") {
+ if (this.props.size === EditorSize.COMPACT) {
+ this.editor.setValue(removeNewLineChars(inputValue));
+ } else if (inputValue !== editorValue) {
+ this.editor.setValue(inputValue);
+ }
+ }
+ CodeEditor.updateMarkings(this.editor, this.props.marking);
+ } else {
+ // Update the dynamic bindings for autocomplete
+ if (prevProps.dynamicData !== this.props.dynamicData) {
+ this.hinters.forEach(
+ (hinter) => hinter.update && hinter.update(this.props.dynamicData),
+ );
}
}
- this.updateMarkings();
-
- // if (currentMode !== this.props.mode) {
- // this.editor.setOption("mode", this.props?.mode);
- // }
- } else {
- // Update the dynamic bindings for autocomplete
- if (prevProps.dynamicData !== this.props.dynamicData) {
- this.hinters.forEach(
- (hinter) => hinter.update && hinter.update(this.props.dynamicData),
- );
- }
- }
+ });
}
- startAutocomplete() {
- this.hinters = this.props.hinting.map((helper) => {
- return helper(
- this.editor,
- this.props.dynamicData,
- this.props.additionalDynamicData,
- );
+ componentWillUnmount() {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore: No types available
+ this.editor.closeHint();
+ }
+
+ static startAutocomplete(
+ editor: CodeMirror.Editor,
+ hinting: Array,
+ dynamicData: DataTree,
+ showLightningMenu?: boolean,
+ additionalDynamicData?: Record>,
+ ) {
+ return hinting.map((helper) => {
+ return helper(editor, dynamicData, additionalDynamicData);
});
}
@@ -263,13 +325,16 @@ class CodeEditor extends Component {
handleEditorFocus = () => {
this.setState({ isFocused: true });
- this.editor.refresh();
if (this.props.size === EditorSize.COMPACT) {
- const inputValue = this.props.input.value;
- this.editor.setOption("lineWrapping", true);
- this.editor.setValue(inputValue);
- this.editor.setCursor(inputValue.length);
+ this.editor.operation(() => {
+ const inputValue = this.props.input.value;
+ this.editor.setOption("lineWrapping", true);
+ this.editor.setValue(inputValue);
+ this.editor.setCursor(inputValue.length);
+ });
}
+ if (this.editor.getValue().length === 0)
+ this.handleAutocompleteVisibility(this.editor);
};
handleEditorBlur = () => {
@@ -278,30 +343,70 @@ class CodeEditor extends Component {
if (this.props.size === EditorSize.COMPACT) {
this.editor.setOption("lineWrapping", false);
}
-
this.editor.setOption("matchBrackets", false);
};
+ handleBeforeChange(
+ cm: CodeMirror.Editor,
+ change: CodeMirror.EditorChangeCancellable,
+ ) {
+ if (change.origin === "paste") {
+ // Remove all non ASCII quotes since they are invalid in Javascript
+ const formattedText = change.text.map((line) => {
+ let formattedLine = line.replace(/[ββ]/g, "'");
+ formattedLine = formattedLine.replace(/[ββ]/g, '"');
+ return formattedLine;
+ });
+ if (change.update) {
+ change.update(undefined, undefined, formattedText);
+ }
+ }
+ }
+
handleChange = (instance?: any, changeObj?: any) => {
- const value = this.editor.getValue();
+ const value = this.editor.getValue() || "";
if (changeObj && changeObj.origin === "complete") {
AnalyticsUtil.logEvent("AUTO_COMPLETE_SELECT", {
searchString: changeObj.text[0],
});
}
- const inputValue = this.props.input.value;
+ const inputValue = this.props.input.value || "";
if (
this.props.input.onChange &&
- value !== inputValue &&
+ (value !== inputValue ||
+ _.get(this.editor, "state.completionActive.startLen") === 0) &&
this.state.isFocused
) {
this.props.input.onChange(value);
}
- this.updateMarkings();
+ CodeEditor.updateMarkings(this.editor, this.props.marking);
};
handleAutocompleteVisibility = (cm: CodeMirror.Editor) => {
- this.hinters.forEach((hinter) => hinter.showHint(cm));
+ if (!this.state.isFocused) return;
+ const expected = this.props.expected ? this.props.expected : "";
+ const { entityName } = getEntityNameAndPropertyPath(
+ this.props.dataTreePath || "",
+ );
+ let hinterOpen = false;
+ for (let i = 0; i < this.hinters.length; i++) {
+ hinterOpen = this.hinters[i].showHint(cm, expected, entityName, {
+ datasources: this.props.datasources.list,
+ pluginIdToImageLocation: this.props.pluginIdToImageLocation,
+ recentEntities: this.props.recentEntities,
+ update: this.props.input.onChange?.bind(this),
+ executeCommand: (payload: any) => {
+ this.props.executeCommand({
+ ...payload,
+ callback: (binding: string) => {
+ const value = this.editor.getValue() + binding;
+ this.updatePropertyValue(value, value.length);
+ },
+ });
+ },
+ });
+ if (hinterOpen) break;
+ }
};
handleAutocompleteHide = (cm: any, event: KeyboardEvent) => {
@@ -310,59 +415,96 @@ class CodeEditor extends Component {
}
};
- updateMarkings = () => {
- this.props.marking.forEach((helper) => this.editor && helper(this.editor));
+ static updateMarkings = (
+ editor: CodeMirror.Editor,
+ marking: Array,
+ ) => {
+ marking.forEach((helper) => helper(editor));
};
- updatePropertyValue(value: string, cursor?: number) {
+ updatePropertyValue(
+ value: string,
+ cursor?: number,
+ preventAutoComplete = false,
+ ) {
if (value) {
this.editor.setValue(value);
}
this.editor.focus();
- if (cursor === undefined) {
- if (value) {
- cursor = value.length - 2;
- } else {
- cursor = 1;
- }
- }
this.editor.setCursor({
- line: 0,
- ch: cursor,
+ line: cursor || this.editor.lineCount() - 1,
+ ch: this.editor.getLine(this.editor.lineCount() - 1).length - 2,
});
this.setState({ isFocused: true }, () => {
+ if (preventAutoComplete) return;
this.handleAutocompleteVisibility(this.editor);
});
}
+ getPropertyValidation = (
+ dataTree: DataTree,
+ dataTreePath?: string,
+ ): {
+ isInvalid: boolean;
+ errors: EvaluationError[];
+ pathEvaluatedValue: unknown;
+ } => {
+ if (!dataTreePath) {
+ return {
+ isInvalid: false,
+ errors: [],
+ pathEvaluatedValue: undefined,
+ };
+ }
+
+ const errors = _.get(
+ dataTree,
+ getEvalErrorPath(dataTreePath),
+ [],
+ ) as EvaluationError[];
+ const filteredLintErrors = errors.filter(
+ (error) => error.errorType !== PropertyEvaluationErrorType.LINT,
+ );
+ const pathEvaluatedValue = _.get(dataTree, getEvalValuePath(dataTreePath));
+
+ return {
+ isInvalid: filteredLintErrors.length > 0,
+ errors: filteredLintErrors,
+ pathEvaluatedValue,
+ };
+ };
+
render() {
const {
- input,
- meta,
- theme,
- disabled,
- className,
- placeholder,
- showLightningMenu,
- dataTreePath,
- dynamicData,
- expected,
- size,
- evaluatedValue,
- height,
- borderLess,
border,
- hoverInteraction,
- fill,
- useValidationMessage,
- hideEvaluatedValue,
+ borderLess,
+ className,
+ dataTreePath,
+ disabled,
+ dynamicData,
+ evaluatedValue,
evaluationSubstitutionType,
+ expected,
+ fill,
+ height,
+ hideEvaluatedValue,
+ hoverInteraction,
+ input,
+ showLightningMenu,
+ size,
+ theme,
+ useValidationMessage,
} = this.props;
- const hasError = !!(meta && meta.error);
+ const {
+ errors,
+ isInvalid,
+ pathEvaluatedValue,
+ } = this.getPropertyValidation(dynamicData, dataTreePath);
let evaluated = evaluatedValue;
if (dataTreePath) {
- evaluated = _.get(dynamicData, dataTreePath);
+ evaluated = pathEvaluatedValue;
}
+
const showEvaluatedValue =
this.state.isFocused &&
("evaluatedValue" in this.props ||
@@ -370,36 +512,37 @@ class CodeEditor extends Component {
return (
{showLightningMenu !== false && !this.state.isFocused && (
- }>
- {
- this.setState({ isOpened: false });
+
+ {
+ const newValue =
+ typeof this.props.input.value === "string"
+ ? this.props.input.value + "/"
+ : "/";
+ this.updatePropertyValue(newValue, newValue.length);
}}
- onOpenLightningMenu={() => {
- this.setState({ isOpened: true });
- }}
- skin={
- this.props.theme === EditorTheme.DARK ? Skin.DARK : Skin.LIGHT
- }
- updateDynamicInputValue={this.updatePropertyValue}
+ tag="button"
+ text="/"
/>
-
+
)}
{
disabled={disabled}
editorTheme={this.props.theme}
fill={fill}
- hasError={hasError}
+ hasError={isInvalid}
height={height}
hoverInteraction={hoverInteraction}
isFocused={this.state.isFocused}
@@ -420,7 +563,6 @@ class CodeEditor extends Component {
ref={this.editorWrapperRef}
size={size}
>
-
{this.props.leftIcon && (
{this.props.leftIcon}
)}
@@ -432,12 +574,17 @@ class CodeEditor extends Component {
src={this.props.leftImage}
/>
)}
-
+
+
+
{this.props.link && (
{
{this.props.rightIcon && (
{this.props.rightIcon}
)}
-
@@ -466,6 +608,15 @@ class CodeEditor extends Component {
const mapStateToProps = (state: AppState): ReduxStateProps => ({
dynamicData: getDataTreeForAutocomplete(state),
+ datasources: state.entities.datasources,
+ pluginIdToImageLocation: getPluginIdToImageLocation(state),
+ recentEntities: state.ui.globalSearch.recentEntities.map((r) => r.id),
});
-export default Sentry.withProfiler(connect(mapStateToProps)(CodeEditor));
+const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
+ executeCommand: (payload) => dispatch({ type: "EXECUTE_COMMAND", payload }),
+});
+
+export default Sentry.withProfiler(
+ connect(mapStateToProps, mapDispatchToProps)(CodeEditor),
+);
diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
index 3272c2f646..64d6947c5e 100644
--- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
@@ -1,4 +1,4 @@
-import styled, { createGlobalStyle } from "styled-components";
+import styled from "styled-components";
import {
CodeEditorBorder,
EditorSize,
@@ -7,167 +7,6 @@ import {
import { Skin, Theme } from "constants/DefaultTheme";
import { Colors } from "constants/Colors";
-export const HintStyles = createGlobalStyle<{
- editorTheme: EditorTheme;
- theme: Theme;
-}>`
- .CodeMirror-hints {
- position: absolute;
- z-index: 20;
- overflow: hidden;
- list-style: none;
- margin-top: ${(props) => props.theme.spaces[3]}px;
- padding: 0px 0px;
- font-size: 90%;
- font-family: monospace;
- max-height: 20em;
- overflow-y: auto;
- background: ${(props) =>
- props.editorTheme === EditorTheme.LIGHT ? "#FAFAFA" : "#262626"};
- box-shadow: 0px 12px 28px -6px rgba(0, 0, 0, 0.32);
- border-radius: 0px;
- }
-
- .CodeMirror-hint {
- height: 24px;
- color: ${(props) =>
- props.editorTheme === EditorTheme.LIGHT ? "#090707" : "#FFFFFF"};
- cursor: pointer;
- display: flex;
- width: 220px;
- align-items: center;
- font-size: 12px;
- line-height: 15px;
- letter-spacing: -0.24px;
- &:hover {
- background: ${(props) =>
- props.editorTheme === EditorTheme.LIGHT ? "#6A86CE" : "#157A96"};
- border-radius: 0px;
- color: #fff;
- &:after {
- color: #fff;
- }
- }
- }
-
- .datasource-hint {
- padding: 10px;
- display: block;
- width: 500px;
- height: 32px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-
- &.invalid {
- color: ${(props) => props.theme.colors.errorMessage};
- }
- }
- .CodeMirror-Tern-completion {
- padding-left: ${(props) => props.theme.spaces[11]}px !important;
- &:hover{
- background: ${(props) =>
- props.editorTheme === EditorTheme.LIGHT ? "#6A86CE" : "#157A96"};
- }
- }
- .CodeMirror-Tern-completion:before {
- left: 7px;
- bottom: 6px;
- height: 12px;
- width: 12px;
- border-radius: 0;
- font-size: 10px;
- line-height: 12px;
- font-weight: normal;
- text-align: center;
- color: ${(props) => props.theme.colors.codeMirror.dataType.shortForm};
- margin-right: ${(props) => props.theme.spaces[13]}px;
- }
- .CodeMirror-Tern-completion-fn:before {
- background: ${(props) => props.theme.colors.dataTypeBg.function};
- }
- .CodeMirror-Tern-completion-object:before {
- background: ${(props) => props.theme.colors.dataTypeBg.object};
- }
- .CodeMirror-Tern-completion-unknown:before {
- background: ${(props) => props.theme.colors.dataTypeBg.unknown};
- }
- .CodeMirror-Tern-completion-array:before {
- background: ${(props) => props.theme.colors.dataTypeBg.array};
- }
- .CodeMirror-Tern-completion-number:before, .CodeMirror-Tern-completion-string:before, .CodeMirror-Tern-completion-bool:before {
- background: ${(props) => props.theme.colors.dataTypeBg.number};
- }
- .CodeMirror-Tern-completion:after {
- position: absolute;
- right: 8px;
- bottom: 6px;
- font-style: italic;
- font-weight: normal;
- font-size: 10px;
- line-height: 13px;
- letter-spacing: -0.24px;
- color: ${(props) => props.theme.colors.codeMirror.dataType.fullForm};
- }
- .CodeMirror-Tern-completion-fn:after {
- content: "Function";
- }
- .CodeMirror-Tern-completion-object:after {
- content: "Object";
- }
- .CodeMirror-Tern-completion-unknown:after {
- content: "Unknown";
- }
- .CodeMirror-Tern-completion-array:after {
- content: "Array";
- }
- .CodeMirror-Tern-completion-number:after {
- content: "Number";
- }
- .CodeMirror-Tern-completion-string:after {
- content: "String";
- }
- .CodeMirror-Tern-completion-bool:after {
- content: "Boolean";
- }
- .CodeMirror-Tern-tooltip {
- z-index: 20 !important;
- }
- li.CodeMirror-hint-active {
- background: ${(props) =>
- props.theme.colors.codeMirror.background.hoverState};
- border-radius: 0px;
- color: #fff;
- &:after {
- color: #fff;
- }
- }
- .CodeMirror-Tern-hint-doc {
- display: none;
- &.visible {
- display: block;
- background-color: ${(props) =>
- props.editorTheme === EditorTheme.DARK ? "#23292e" : "#fff"} !important;
- color: ${(props) =>
- props.editorTheme === EditorTheme.DARK
- ? "#F4F4F4"
- : "#1E242B"} !important;
- max-height: 150px;
- width: 250px;
- font-size: 12px;
- padding: 5px !important;
- border: 1px solid !important;
- border-color: ${(props) =>
- props.editorTheme === EditorTheme.DARK
- ? "#23292e"
- : "#DEDEDE"} !important;
- box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.12) !important;
- overflow: scroll;
- }
-
- }
-`;
-
const getBorderStyle = (
props: { theme: Theme } & {
editorTheme?: EditorTheme;
@@ -391,6 +230,20 @@ export const EditorWrapper = styled.div<{
text-shadow: none;
font: inherit;
}
+ .CodeEditorTarget {
+ width: 100%;
+ ${(props) =>
+ props.size === EditorSize.COMPACT
+ ? `
+ position: absolute;
+ left: 0;
+ right: 0;
+ `
+ : `
+ position: relative;
+ `}
+ ${(props) => (props.isFocused ? `z-index: 3;` : `z-index: 0;`)}
+ }
`;
export const IconContainer = styled.div`
@@ -422,20 +275,13 @@ export const DynamicAutocompleteInputWrapper = styled.div<{
height: 100%;
flex: 1;
position: relative;
- border-color: ${(props) =>
- !props.isError && props.isActive && props.skin === Skin.DARK
- ? Colors.ALABASTER
- : "transparent"};
+ border: 1px solid ${(props) => (!props.isError ? "transparent" : "red")};
> span:first-of-type {
width: 30px;
position: absolute;
right: 0px;
}
&:hover {
- border-color: ${(props) =>
- !props.isError && props.skin === Skin.DARK
- ? Colors.ALABASTER
- : "transparent"};
.lightning-menu {
background: ${(props) => (!props.isNotHover ? "#090707" : "")};
svg {
@@ -450,10 +296,28 @@ export const DynamicAutocompleteInputWrapper = styled.div<{
}
}
}
+ .slash-commands {
+ display: flex;
+ }
}
- border: 0px;
border-radius: 0px;
.lightning-menu {
z-index: 1 !important;
}
+ .commands-button {
+ z-index: 2;
+ width: 24px;
+ height: 24px;
+ background: transparent;
+ color: #f86a2b;
+ border: none;
+ font-weight: bold;
+ font-size: 15px;
+ font-style: italic;
+ padding: 0 0 3px;
+ &:hover {
+ background: #f86a2b;
+ color: white;
+ }
+ }
`;
diff --git a/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx b/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx
index 4ae6fb592f..cbc2dcf921 100644
--- a/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx
+++ b/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx
@@ -3,8 +3,10 @@ import styled from "styled-components";
import { isUndefined } from "lodash";
import { Severity } from "entities/AppsmithConsole";
import FilterHeader from "./FilterHeader";
-import { BlankState, useFilteredLogs, usePagination } from "./helpers";
+import { BlankState } from "./helpers";
import LogItem, { getLogItemProps } from "./LogItem";
+import { usePagination, useFilteredLogs } from "./hooks";
+import { createMessage, NO_LOGS } from "constants/messages";
const LIST_HEADER_HEIGHT = "38px";
@@ -37,7 +39,7 @@ function DebbuggerLogs(props: Props) {
const [filter, setFilter] = useState("");
const [searchQuery, setSearchQuery] = useState(props.searchQuery);
const filteredLogs = useFilteredLogs(searchQuery, filter);
- const { paginatedData, next } = usePagination(filteredLogs);
+ const { next, paginatedData } = usePagination(filteredLogs);
const listRef = useRef(null);
const selectedFilter = useMemo(
() => LOGS_FILTER_OPTIONS.find((option) => option.value === filter),
@@ -79,7 +81,10 @@ function DebbuggerLogs(props: Props) {
{!paginatedData.length ? (
-
+
) : (
paginatedData.map((e, index: number) => {
const logItemProps = getLogItemProps(e);
diff --git a/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx b/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx
index a7641165a9..a541ee2cbb 100644
--- a/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx
+++ b/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx
@@ -8,6 +8,14 @@ import { showDebugger } from "actions/debuggerActions";
import Errors from "./Errors";
import Resizer, { ResizerCSS } from "./Resizer";
import AnalyticsUtil from "utils/AnalyticsUtil";
+import EntityDeps from "./EntityDependecies";
+import {
+ createMessage,
+ DEBUGGER_ERRORS,
+ DEBUGGER_LOGS,
+ INSPECT_ENTITY,
+} from "constants/messages";
+import { stopEventPropagation } from "utils/AppsmithUtils";
const TABS_HEADER_HEIGHT = 36;
@@ -41,14 +49,19 @@ type DebuggerTabsProps = {
const DEBUGGER_TABS = [
{
key: "ERROR",
- title: "Errors",
+ title: createMessage(DEBUGGER_ERRORS),
panelComponent: ,
},
{
key: "LOGS",
- title: "Logs",
+ title: createMessage(DEBUGGER_LOGS),
panelComponent: ,
},
+ {
+ key: "INSPECT_ELEMENTS",
+ title: createMessage(INSPECT_ENTITY),
+ panelComponent: ,
+ },
];
function DebuggerTabs(props: DebuggerTabsProps) {
@@ -65,7 +78,7 @@ function DebuggerTabs(props: DebuggerTabsProps) {
const onClose = () => dispatch(showDebugger(false));
return (
-
+
`
+ margin-left: ${(props) => props.step * 10}px;
+ padding-top: ${(props) => props.theme.spaces[3]}px;
+
+ .label-wrapper {
+ display: flex;
+ flex-direction: row;
+ font-weight: ${(props) => props.theme.fontWeights[2]};
+
+ span {
+ margin-left: ${(props) => props.theme.spaces[3] - 1}px;
+ }
+ }
+
+ .${Classes.ICON} {
+ ${(props) => !props.isOpen && `transform: rotate(-90deg);`}
+ }
+`;
+
+const DependenciesWrapper = styled.div`
+ padding: ${(props) => props.theme.spaces[7]}px
+ ${(props) => props.theme.spaces[13] + 1}px;
+ color: ${(props) => props.theme.colors.debugger.inspectElement.color};
+
+ .no-dependencies {
+ margin-left: ${(props) => props.theme.spaces[4]}px;
+ }
+`;
+
+const StyledSpan = styled.div<{ step: number }>`
+ padding-top: ${(props) => props.theme.spaces[3]}px;
+ padding-left: ${(props) => props.theme.spaces[6] + 1}px;
+ margin-left: ${(props) => props.theme.spaces[4]}px;
+ border-left: solid 1px rgba(147, 144, 144, 0.7);
+ text-decoration-line: underline;
+ cursor: pointer;
+`;
+
+const BlankStateContainer = styled.div`
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ flex-direction: column;
+ color: ${(props) => props.theme.colors.debugger.blankState.color};
+
+ span {
+ margin-top: ${(props) => props.theme.spaces[9] + 1}px;
+ }
+`;
+
+function EntityDeps() {
+ const deps = useSelector((state: AppState) => state.evaluations.dependencies);
+ const selectedEntity = useSelectedEntity();
+
+ const entityDependencies: {
+ directDependencies: string[];
+ inverseDependencies: string[];
+ } | null = useMemo(
+ () =>
+ getDependenciesFromInverseDependencies(
+ deps.inverseDependencyMap,
+ selectedEntity ? selectedEntity.name : null,
+ ),
+ [selectedEntity, deps.inverseDependencyMap],
+ );
+
+ if (!selectedEntity || !entityDependencies) return ;
+
+ return (
+
+
+
+
+ );
+}
+
+function BlankState() {
+ return (
+
+
+ {createMessage(INSPECT_ENTITY_BLANK_STATE)}
+
+ );
+}
+
+function DependencyHierarchy(props: {
+ dependencies: string[];
+ entityName: string;
+ selectedEntity: SourceEntity;
+ type: string;
+}) {
+ const { navigateToEntity } = useEntityLink();
+ const label = props.dependencies.length
+ ? props.entityName
+ : `No ${props.type} exist for ${props.selectedEntity.name}`;
+
+ return (
+
+ {props.dependencies.length ? (
+
+ {props.dependencies.map((item) => {
+ return (
+ {
+ e.stopPropagation();
+ navigateToEntity(item);
+ }}
+ step={2}
+ >
+ {item}
+
+ );
+ })}
+
+ ) : (
+ {label}
+ )}
+
+ );
+}
+const MemoizedDependencyHierarchy = memo(DependencyHierarchy);
+
+function Collapsible(props: {
+ label: string;
+ step: number;
+ children: ReactNode;
+}) {
+ const [isOpen, setIsOpen] = useState(true);
+
+ return (
+ {
+ e.stopPropagation();
+ setIsOpen(!isOpen);
+ }}
+ step={props.step}
+ >
+
+
+ {props.label}
+
+ {props.children}
+
+ );
+}
+
+export default EntityDeps;
diff --git a/app/client/src/components/editorComponents/Debugger/EntityLink.tsx b/app/client/src/components/editorComponents/Debugger/EntityLink.tsx
index 56959a7842..51d0ec2765 100644
--- a/app/client/src/components/editorComponents/Debugger/EntityLink.tsx
+++ b/app/client/src/components/editorComponents/Debugger/EntityLink.tsx
@@ -2,7 +2,7 @@ import { DATA_SOURCES_EDITOR_ID_URL } from "constants/routes";
import { PluginType } from "entities/Action";
import { ENTITY_TYPE, SourceEntity } from "entities/AppsmithConsole";
import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
-import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/WidgetEntity";
+import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/useNavigateToWidget";
import React, { useCallback } from "react";
import { useSelector } from "react-redux";
import { AppState } from "reducers";
@@ -25,10 +25,11 @@ function ActionLink(props: EntityLinkProps) {
const onClick = useCallback(() => {
if (action) {
- const { pageId, pluginType, id } = action;
+ const { id, pageId, pluginType } = action;
const actionConfig = getActionConfig(pluginType);
const url =
- applicationId && actionConfig?.getURL(applicationId, pageId, id);
+ applicationId &&
+ actionConfig?.getURL(applicationId, pageId, id, pluginType);
if (url) {
history.push(url);
@@ -71,7 +72,7 @@ function WidgetLink(props: EntityLinkProps) {
AnalyticsUtil.logEvent("DEBUGGER_ENTITY_NAVIGATION", {
entityType: "WIDGET",
});
- }, []);
+ }, [navigateToWidget]);
return (
{!Object.values(errors).length ? (
-
+
) : (
Object.values(errors).map((e, index) => {
const logItemProps = getLogItemProps(e);
diff --git a/app/client/src/components/editorComponents/Debugger/LogItem.tsx b/app/client/src/components/editorComponents/Debugger/LogItem.tsx
index ebf355ba41..ba57bef865 100644
--- a/app/client/src/components/editorComponents/Debugger/LogItem.tsx
+++ b/app/client/src/components/editorComponents/Debugger/LogItem.tsx
@@ -5,7 +5,6 @@ import { Message, Severity, SourceEntity } from "entities/AppsmithConsole";
import React, { useCallback, useState } from "react";
import ReactJson from "react-json-view";
import styled from "styled-components";
-import { isString } from "lodash";
import EntityLink, { DebuggerLinkUI } from "./EntityLink";
import { SeverityIcon, SeverityIconColor } from "./helpers";
import { useDispatch } from "react-redux";
@@ -102,7 +101,7 @@ const Log = styled.div<{ collapsed: boolean }>`
`;
const JsonWrapper = styled.div`
- padding-top: 4px;
+ padding-top: ${(props) => props.theme.spaces[1]}px;
svg {
color: ${(props) => props.theme.colors.debugger.jsonIcon} !important;
height: 12px !important;
@@ -139,6 +138,10 @@ const StyledSearchIcon = styled(Icon)`
}
`;
+const MessageWrapper = styled.div`
+ padding-top: ${(props) => props.theme.spaces[1]}px;
+`;
+
export const getLogItemProps = (e: Message) => {
return {
icon: SeverityIcon[e.severity] as IconName,
@@ -149,9 +152,9 @@ export const getLogItemProps = (e: Message) => {
timeTaken: e.timeTaken ? `${e.timeTaken}ms` : "",
severity: e.severity,
text: e.text,
- message: e.message && isString(e.message) ? e.message : "",
state: e.state,
id: e.source ? e.source.id : undefined,
+ messages: e.messages,
};
};
@@ -163,11 +166,11 @@ type LogItemProps = {
timeTaken: string;
severity: Severity;
text: string;
- message: string;
state?: Record;
id?: string;
source?: SourceEntity;
expand?: boolean;
+ messages: Message["messages"];
};
function LogItem(props: LogItemProps) {
@@ -182,12 +185,12 @@ function LogItem(props: LogItemProps) {
},
collapsed: 1,
};
- const showToggleIcon = props.state || props.message;
+ const showToggleIcon = props.state || props.messages;
const dispatch = useDispatch();
- const openHelpModal = useCallback((e) => {
+ const openHelpModal = useCallback((e, message?: string) => {
e.stopPropagation();
- const text = props.message || props.text;
+ const text = message || props.text;
AnalyticsUtil.logEvent("OPEN_OMNIBAR", {
source: "DEBUGGER",
@@ -196,6 +199,7 @@ function LogItem(props: LogItemProps) {
dispatch(setGlobalSearchQuery(text || ""));
dispatch(toggleShowGlobalSearchModal());
}, []);
+ const messages = props.messages || [];
return (
- {props.message && (
-
-
- {props.message}
-
-
- )}
+ {messages.map((e) => {
+ return (
+
+ openHelpModal(event, e.message)}
+ >
+ {e.message}
+
+
+ );
+ })}
{props.state && (
;
+ setContainerDimensions?: (height: number) => void;
};
function Resizer(props: ResizerProps) {
@@ -41,9 +42,15 @@ function Resizer(props: ResizerProps) {
updatedHeight > minHeight
) {
panel.style.height = `${height - movementY}px`;
+ props.setContainerDimensions &&
+ props.setContainerDimensions(height - movementY);
}
};
+ useEffect(() => {
+ handleResize(0);
+ }, []);
+
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
e.preventDefault();
diff --git a/app/client/src/components/editorComponents/Debugger/helpers.test.ts b/app/client/src/components/editorComponents/Debugger/helpers.test.ts
new file mode 100644
index 0000000000..fa7e67d134
--- /dev/null
+++ b/app/client/src/components/editorComponents/Debugger/helpers.test.ts
@@ -0,0 +1,88 @@
+import { DependencyMap } from "utils/DynamicBindingUtils";
+import {
+ getDependenciesFromInverseDependencies,
+ getDependencyChain,
+} from "./helpers";
+
+describe("getDependencies", () => {
+ it("Check if getDependencies returns in a correct format", () => {
+ const input = {
+ "Button1.text": ["Input1.defaultText", "Button1"],
+ "Input1.defaultText": ["Input1.text", "Input1"],
+ "Input1.inputType": ["Input1.isValid", "Input1"],
+ "Input1.text": ["Input1.isValid", "Input1.value", "Input1"],
+ "Input1.isRequired": ["Input1.isValid", "Input1"],
+ "Input1.isValid": ["Button1.isVisible", "Input1"],
+ "Button1.isVisible": ["Button1"],
+ Button1: ["Chart1.chartName"],
+ "Chart1.chartName": ["Chart1"],
+ "Input1.value": ["Input1"],
+ };
+ const output = {
+ directDependencies: ["Input1"],
+ inverseDependencies: ["Input1", "Chart1"],
+ };
+
+ expect(
+ getDependenciesFromInverseDependencies(input, "Button1"),
+ ).toStrictEqual(output);
+ });
+
+ it("Get dependency chain", () => {
+ const inputs: string[] = ["Button1.text", "DatePicker1.value"];
+ const inverseDependencies: DependencyMap[] = [
+ {
+ "Button1.text": ["Input1.defaultText", "Button1"],
+ "Input1.defaultText": ["Checkbox1.label", "Input1.text", "Input1"],
+ "Checkbox1.LEFT": ["Checkbox1.alignWidget", "Checkbox1"],
+ "Checkbox1.defaultCheckedState": ["Checkbox1.isChecked", "Checkbox1"],
+ "Checkbox1.isRequired": ["Checkbox1.isValid", "Checkbox1"],
+ "Checkbox1.isChecked": [
+ "Checkbox1.isValid",
+ "Checkbox1.value",
+ "Checkbox1",
+ ],
+ "Checkbox1.value": ["Checkbox1"],
+ "Checkbox1.isValid": ["Checkbox1"],
+ "Checkbox1.alignWidget": ["Checkbox1"],
+ "Checkbox1.label": ["Checkbox1"],
+ "Input1.text": ["Input1.value", "Input1.isValid", "Input1"],
+ "Input1.inputType": ["Input1.isValid", "Input1"],
+ "Input1.isRequired": ["Input1.isValid", "Input1"],
+ "Input1.isValid": ["Input1"],
+ "Input1.value": ["Input1"],
+ },
+ {
+ "DatePicker1.defaultDate": [
+ "DatePicker1.value",
+ "DatePicker1.selectedDate",
+ "DatePicker1.formattedDate",
+ "DatePicker1",
+ ],
+ "DatePicker1.value": [
+ "DatePicker1.selectedDate",
+ "DatePicker1.formattedDate",
+ "DatePicker1",
+ ],
+ "DatePicker1.selectedDate": [
+ "Text1.text",
+ "DatePicker1.isValid",
+ "DatePicker1",
+ ],
+ "Text1.text": ["Text1.value", "Text1"],
+ "Text1.value": ["Text1"],
+ "DatePicker1.dateFormat": ["DatePicker1.formattedDate", "DatePicker1"],
+ "DatePicker1.isRequired": ["DatePicker1.isValid", "DatePicker1"],
+ "DatePicker1.isValid": ["DatePicker1"],
+ "DatePicker1.formattedDate": ["DatePicker1"],
+ },
+ ];
+ const output = [["Input1.defaultText", "Checkbox1.label"], ["Text1.text"]];
+
+ inputs.map((input: any, index) => {
+ expect(getDependencyChain(input, inverseDependencies[index])).toEqual(
+ output[index],
+ );
+ });
+ });
+});
diff --git a/app/client/src/components/editorComponents/Debugger/helpers.tsx b/app/client/src/components/editorComponents/Debugger/helpers.tsx
index 29e019b9cf..0424c4145a 100644
--- a/app/client/src/components/editorComponents/Debugger/helpers.tsx
+++ b/app/client/src/components/editorComponents/Debugger/helpers.tsx
@@ -1,15 +1,15 @@
-import { Message, Severity } from "entities/AppsmithConsole";
-import React, { useCallback, useEffect, useState } from "react";
-import { useSelector } from "react-redux";
-import { AppState } from "reducers";
+import { Severity } from "entities/AppsmithConsole";
+import React from "react";
import styled from "styled-components";
import { getTypographyByKey } from "constants/DefaultTheme";
+import { createMessage, OPEN_THE_DEBUGGER, PRESS } from "constants/messages";
+import { DependencyMap } from "utils/DynamicBindingUtils";
import {
- createMessage,
- NO_LOGS,
- OPEN_THE_DEBUGGER,
- PRESS,
-} from "constants/messages";
+ API_EDITOR_URL,
+ BUILDER_PAGE_URL,
+ QUERIES_EDITOR_URL,
+} from "constants/routes";
+import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
const BlankStateWrapper = styled.div`
overflow: auto;
@@ -26,7 +26,10 @@ const BlankStateWrapper = styled.div`
}
`;
-export function BlankState(props: { hasShortCut?: boolean }) {
+export function BlankState(props: {
+ placeholderText?: string;
+ hasShortCut?: boolean;
+}) {
return (
{props.hasShortCut ? (
@@ -36,7 +39,7 @@ export function BlankState(props: { hasShortCut?: boolean }) {
{createMessage(OPEN_THE_DEBUGGER)}
) : (
- {createMessage(NO_LOGS)}
+ {props.placeholderText}
)}
);
@@ -54,46 +57,98 @@ export const SeverityIconColor: Record = {
[Severity.WARNING]: "rgb(224, 179, 14)",
};
-export const useFilteredLogs = (query: string, filter?: any) => {
- let logs = useSelector((state: AppState) => state.ui.debugger.logs);
+export function getDependenciesFromInverseDependencies(
+ deps: DependencyMap,
+ entityName: string | null,
+) {
+ if (!entityName) return null;
- if (filter) {
- logs = logs.filter((log: Message) => log.severity === filter);
- }
+ const directDependencies = new Set();
+ const inverseDependencies = new Set();
- if (query) {
- logs = logs.filter((log: Message) => {
- if (log.source?.name)
- return (
- log.source?.name.toUpperCase().indexOf(query.toUpperCase()) !== -1
- );
+ Object.entries(deps).forEach(([dependant, dependencies]) => {
+ (dependencies as any).map((dependency: any) => {
+ if (!dependant.includes(entityName) && dependency.includes(entityName)) {
+ const entity = dependant
+ .split(".")
+ .slice(0, 1)
+ .join("");
+
+ directDependencies.add(entity);
+ } else if (
+ dependant.includes(entityName) &&
+ !dependency.includes(entityName)
+ ) {
+ const entity = dependency
+ .split(".")
+ .slice(0, 1)
+ .join("");
+
+ inverseDependencies.add(entity);
+ }
});
- }
+ });
- return logs;
+ return {
+ inverseDependencies: Array.from(inverseDependencies),
+ directDependencies: Array.from(directDependencies),
+ };
+}
+
+// Recursively find out dependency chain from
+// the inverse dependency map
+export function getDependencyChain(
+ propertyPath: string,
+ inverseMap: DependencyMap,
+) {
+ let currentChain: string[] = [];
+ const dependents = inverseMap[propertyPath];
+
+ if (!dependents) return currentChain;
+
+ const dependentInfo = getEntityNameAndPropertyPath(propertyPath);
+
+ dependents.map((e) => {
+ if (!e.includes(dependentInfo.entityName)) {
+ currentChain.push(e);
+ }
+
+ if (e !== dependentInfo.entityName) {
+ currentChain = currentChain.concat(getDependencyChain(e, inverseMap));
+ }
+ });
+ return currentChain;
+}
+
+export const onApiEditor = (
+ applicationId: string | undefined,
+ currentPageId: string | undefined,
+) => {
+ return (
+ window.location.pathname.indexOf(
+ API_EDITOR_URL(applicationId, currentPageId),
+ ) > -1
+ );
};
-export const usePagination = (data: Message[], itemsPerPage = 50) => {
- const [currentPage, setCurrentPage] = useState(1);
- const [paginatedData, setPaginatedData] = useState([]);
- const maxPage = Math.ceil(data.length / itemsPerPage);
-
- useEffect(() => {
- const data = currentData();
- setPaginatedData(data);
- }, [currentPage, data.length]);
-
- const currentData = useCallback(() => {
- const end = currentPage * itemsPerPage;
- return data.slice(0, end);
- }, [data]);
-
- const next = useCallback(() => {
- setCurrentPage((currentPage) => {
- const newCurrentPage = Math.min(currentPage + 1, maxPage);
- return newCurrentPage <= 0 ? 1 : newCurrentPage;
- });
- }, []);
-
- return { next, paginatedData };
+export const onQueryEditor = (
+ applicationId: string | undefined,
+ currentPageId: string | undefined,
+) => {
+ return (
+ window.location.pathname.indexOf(
+ QUERIES_EDITOR_URL(applicationId, currentPageId),
+ ) > -1
+ );
+};
+
+export const onCanvas = (
+ applicationId: string | undefined,
+ currentPageId: string | undefined,
+) => {
+ return (
+ window.location.pathname.indexOf(
+ BUILDER_PAGE_URL(applicationId, currentPageId),
+ ) > -1
+ );
};
diff --git a/app/client/src/components/editorComponents/Debugger/hooks.ts b/app/client/src/components/editorComponents/Debugger/hooks.ts
new file mode 100644
index 0000000000..f03c180549
--- /dev/null
+++ b/app/client/src/components/editorComponents/Debugger/hooks.ts
@@ -0,0 +1,149 @@
+import { useCallback, useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+import { useParams } from "react-router";
+import { ENTITY_TYPE, Message } from "entities/AppsmithConsole";
+import { AppState } from "reducers";
+import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
+import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/useNavigateToWidget";
+import { getWidget } from "sagas/selectors";
+import { getDataTree } from "selectors/dataTreeSelectors";
+import {
+ getCurrentApplicationId,
+ getCurrentPageId,
+} from "selectors/editorSelectors";
+import { getAction } from "selectors/entitiesSelector";
+import {
+ getCurrentWidgetId,
+ getIsPropertyPaneVisible,
+} from "selectors/propertyPaneSelectors";
+import { isWidget, isAction } from "workers/evaluationUtils";
+import { onApiEditor, onQueryEditor, onCanvas } from "./helpers";
+import history from "utils/history";
+
+export const useFilteredLogs = (query: string, filter?: any) => {
+ let logs = useSelector((state: AppState) => state.ui.debugger.logs);
+
+ if (filter) {
+ logs = logs.filter((log: Message) => log.severity === filter);
+ }
+
+ if (query) {
+ logs = logs.filter((log: Message) => {
+ if (log.source?.name)
+ return (
+ log.source?.name.toUpperCase().indexOf(query.toUpperCase()) !== -1
+ );
+ });
+ }
+
+ return logs;
+};
+
+export const usePagination = (data: Message[], itemsPerPage = 50) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [paginatedData, setPaginatedData] = useState([]);
+ const maxPage = Math.ceil(data.length / itemsPerPage);
+
+ useEffect(() => {
+ const data = currentData();
+ setPaginatedData(data);
+ }, [currentPage, data.length]);
+
+ const currentData = useCallback(() => {
+ const end = currentPage * itemsPerPage;
+ return data.slice(0, end);
+ }, [data]);
+
+ const next = useCallback(() => {
+ setCurrentPage((currentPage) => {
+ const newCurrentPage = Math.min(currentPage + 1, maxPage);
+ return newCurrentPage <= 0 ? 1 : newCurrentPage;
+ });
+ }, []);
+
+ return { next, paginatedData };
+};
+
+export const useSelectedEntity = () => {
+ const applicationId = useSelector(getCurrentApplicationId);
+ const currentPageId = useSelector(getCurrentPageId);
+
+ const params: any = useParams();
+ const action = useSelector((state: AppState) => {
+ if (
+ onApiEditor(applicationId, currentPageId) ||
+ onQueryEditor(applicationId, currentPageId)
+ ) {
+ const id = params.apiId || params.queryId;
+
+ return getAction(state, id);
+ }
+
+ return null;
+ });
+
+ const isPropertyPaneVisible = useSelector(getIsPropertyPaneVisible);
+ const selectedWidget = useSelector(getCurrentWidgetId);
+ const widget = useSelector((state: AppState) => {
+ if (onCanvas(applicationId, currentPageId) && isPropertyPaneVisible) {
+ return selectedWidget ? getWidget(state, selectedWidget) : null;
+ }
+
+ return null;
+ });
+
+ if (
+ onApiEditor(applicationId, currentPageId) ||
+ onQueryEditor(applicationId, currentPageId)
+ ) {
+ return {
+ name: action?.name ?? "",
+ type: ENTITY_TYPE.ACTION,
+ id: action?.id ?? "",
+ };
+ } else if (onCanvas(applicationId, currentPageId)) {
+ return {
+ name: widget?.widgetName ?? "",
+ type: ENTITY_TYPE.WIDGET,
+ id: widget?.widgetId ?? "",
+ };
+ }
+
+ return null;
+};
+
+export const useEntityLink = () => {
+ const dataTree = useSelector(getDataTree);
+ const applicationId = useSelector(getCurrentApplicationId);
+ const pageId = useSelector(getCurrentPageId);
+
+ const { navigateToWidget } = useNavigateToWidget();
+
+ const navigateToEntity = useCallback(
+ (name) => {
+ const entity = dataTree[name];
+ if (isWidget(entity)) {
+ navigateToWidget(entity.widgetId, entity.type, pageId || "");
+ } else if (isAction(entity)) {
+ const actionConfig = getActionConfig(entity.pluginType);
+ const url =
+ applicationId &&
+ actionConfig?.getURL(
+ applicationId,
+ pageId || "",
+ entity.actionId,
+ entity.pluginType,
+ );
+
+ if (url) {
+ history.push(url);
+ }
+ }
+ },
+ [dataTree],
+ );
+
+ return {
+ navigateToEntity,
+ };
+};
diff --git a/app/client/src/components/editorComponents/Debugger/index.tsx b/app/client/src/components/editorComponents/Debugger/index.tsx
index 2e7ebb0fb7..86d987f914 100644
--- a/app/client/src/components/editorComponents/Debugger/index.tsx
+++ b/app/client/src/components/editorComponents/Debugger/index.tsx
@@ -10,15 +10,18 @@ import { showDebugger as showDebuggerAction } from "actions/debuggerActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { Colors } from "constants/Colors";
import { getTypographyByKey } from "constants/DefaultTheme";
+import { Layers } from "constants/Layers";
+import { stopEventPropagation } from "utils/AppsmithUtils";
const Container = styled.div<{ errorCount: number }>`
+ z-index: ${Layers.debugger};
background-color: ${(props) =>
props.theme.colors.debugger.floatingButton.background};
position: fixed;
right: 20px;
bottom: 20px;
cursor: pointer;
- padding: 19px;
+ padding: ${(props) => props.theme.spaces[6]}px;
color: ${(props) => props.theme.colors.debugger.floatingButton.color};
border-radius: 50px;
box-shadow: ${(props) => props.theme.colors.debugger.floatingButton.shadow};
@@ -33,11 +36,9 @@ const Container = styled.div<{ errorCount: number }>`
.debugger-count {
color: ${Colors.WHITE};
- font-size: 14px;
- font-weight: 500;
${(props) => getTypographyByKey(props, "h6")}
- height: 20px;
- padding: 6px;
+ height: 16px;
+ padding: ${(props) => props.theme.spaces[1]}px;
background-color: ${(props) =>
!!props.errorCount
? props.theme.colors.debugger.floatingButton.errorCount
@@ -61,11 +62,12 @@ function Debugger() {
(state: AppState) => state.ui.debugger.isOpen,
);
- const onClick = () => {
+ const onClick = (e: any) => {
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
source: "CANVAS",
});
dispatch(showDebuggerAction(true));
+ stopEventPropagation(e);
};
if (!showDebugger)
@@ -75,7 +77,7 @@ function Debugger() {
errorCount={errorCount}
onClick={onClick}
>
-
+
{errorCount}
);
diff --git a/app/client/src/components/editorComponents/DragLayerComponent.tsx b/app/client/src/components/editorComponents/DragLayerComponent.tsx
index c9fb4ab1f8..a26bae3b02 100644
--- a/app/client/src/components/editorComponents/DragLayerComponent.tsx
+++ b/app/client/src/components/editorComponents/DragLayerComponent.tsx
@@ -4,11 +4,14 @@ import { useDragLayer, XYCoord } from "react-dnd";
import DropZone from "./Dropzone";
import { noCollision, currentDropRow } from "utils/WidgetPropsUtils";
import { OccupiedSpace } from "constants/editorConstants";
-import { CONTAINER_GRID_PADDING } from "constants/WidgetConstants";
+import {
+ CONTAINER_GRID_PADDING,
+ GridDefaults,
+} from "constants/WidgetConstants";
import { DropTargetContext } from "./DropTargetComponent";
import { scrollElementIntoParentCanvasView } from "utils/helpers";
import { getNearestParentCanvas } from "utils/generators";
-
+const GRID_POINT_SIZE = 1;
const WrappedDragLayer = styled.div<{
columnWidth: number;
rowHeight: number;
@@ -17,22 +20,28 @@ const WrappedDragLayer = styled.div<{
}>`
position: absolute;
pointer-events: none;
- left: ${(props) => (props.noPad ? "0" : `${CONTAINER_GRID_PADDING}px;`)};
- top: ${(props) => (props.noPad ? "0" : `${CONTAINER_GRID_PADDING}px;`)};
+ left: ${(props) =>
+ props.noPad ? "0" : `${CONTAINER_GRID_PADDING - GRID_POINT_SIZE}px;`};
+ top: ${(props) =>
+ props.noPad ? "0" : `${CONTAINER_GRID_PADDING - GRID_POINT_SIZE}px;`};
height: ${(props) =>
- props.noPad ? `100%` : `calc(100% - ${CONTAINER_GRID_PADDING}px)`};
+ props.noPad
+ ? `100%`
+ : `calc(100% - ${(CONTAINER_GRID_PADDING - GRID_POINT_SIZE) * 2}px)`};
width: ${(props) =>
- props.noPad ? `100%` : `calc(100% - ${CONTAINER_GRID_PADDING}px)`};
+ props.noPad
+ ? `100%`
+ : `calc(100% - ${(CONTAINER_GRID_PADDING - GRID_POINT_SIZE) * 2}px)`};
background-image: radial-gradient(
- circle,
- ${(props) => props.theme.colors.grid} 1px,
+ circle at ${GRID_POINT_SIZE}px ${GRID_POINT_SIZE}px,
+ ${(props) => props.theme.colors.grid} ${GRID_POINT_SIZE}px,
transparent 0
);
- background-size: ${(props) => props.columnWidth}px
- ${(props) => props.rowHeight}px;
- background-position: -${(props) => props.columnWidth / 2}px -${(props) =>
- props.rowHeight / 2}px;
+ background-size: ${(props) =>
+ props.columnWidth - GRID_POINT_SIZE / GridDefaults.DEFAULT_GRID_COLUMNS}px
+ ${(props) =>
+ props.rowHeight - GRID_POINT_SIZE / GridDefaults.DEFAULT_GRID_COLUMNS}px;
`;
type DragLayerProps = {
@@ -71,7 +80,7 @@ function DragLayerComponent(props: DragLayerProps) {
x: 0,
y: 0,
});
- const { isDragging, currentOffset, widget, canDrop } = useDragLayer(
+ const { canDrop, currentOffset, isDragging, widget } = useDragLayer(
(monitor) => ({
isDragging: monitor.isDragging(),
currentOffset: monitor.getSourceClientOffset(),
diff --git a/app/client/src/components/editorComponents/DraggableComponent.tsx b/app/client/src/components/editorComponents/DraggableComponent.tsx
index a5cbe41ed3..19f0b04e9b 100644
--- a/app/client/src/components/editorComponents/DraggableComponent.tsx
+++ b/app/client/src/components/editorComponents/DraggableComponent.tsx
@@ -7,11 +7,12 @@ import { useSelector } from "react-redux";
import { AppState } from "reducers";
import { getColorWithOpacity } from "constants/DefaultTheme";
import {
- useWidgetSelection,
useShowPropertyPane,
useWidgetDragResize,
} from "utils/hooks/dragResizeHooks";
import AnalyticsUtil from "utils/AnalyticsUtil";
+import { commentModeSelector } from "selectors/commentsSelectors";
+import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
const DraggableWrapper = styled.div`
display: block;
@@ -50,8 +51,11 @@ export const canDrag = (
isResizing: boolean,
isDraggingDisabled: boolean,
props: any,
+ isCommentMode: boolean,
) => {
- return !isResizing && !isDraggingDisabled && !props.dragDisabled;
+ return (
+ !isResizing && !isDraggingDisabled && !props?.dragDisabled && !isCommentMode
+ );
};
function DraggableComponent(props: DraggableComponentProps) {
@@ -59,7 +63,9 @@ function DraggableComponent(props: DraggableComponentProps) {
const showPropertyPane = useShowPropertyPane();
// Dispatch hook handy to set a widget as focused/selected
- const { selectWidget, focusWidget } = useWidgetSelection();
+ const { focusWidget, selectWidget } = useWidgetSelection();
+
+ const isCommentMode = useSelector(commentModeSelector);
// Dispatch hook handy to set any `DraggableComponent` as dragging/ not dragging
// The value is boolean
@@ -68,9 +74,12 @@ function DraggableComponent(props: DraggableComponentProps) {
// This state tells us which widget is selected
// The value is the widgetId of the selected widget
const selectedWidget = useSelector(
- (state: AppState) => state.ui.widgetDragResize.selectedWidget,
+ (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget,
);
+ const selectedWidgets = useSelector(
+ (state: AppState) => state.ui.widgetDragResize.selectedWidgets,
+ );
// This state tels us which widget is focused
// The value is the widgetId of the focused widget.
const focusedWidget = useSelector(
@@ -135,23 +144,13 @@ function DraggableComponent(props: DraggableComponentProps) {
},
canDrag: () => {
// Dont' allow drag if we're resizing or the drag of `DraggableComponent` is disabled
- return canDrag(isResizing, isDraggingDisabled, props);
+ return canDrag(isResizing, isDraggingDisabled, props, isCommentMode);
},
});
// True when any widget is dragging or resizing, including this one
const isResizingOrDragging = !!isResizing || !!isDragging;
- // When the draggable is clicked
- const handleClick = (e: any) => {
- if (!isResizingOrDragging) {
- selectWidget &&
- selectedWidget !== props.widgetId &&
- selectWidget(props.widgetId);
- }
- e.stopPropagation();
- };
-
// When mouse is over this draggable
const handleMouseOver = (e: any) => {
focusWidget &&
@@ -160,10 +159,12 @@ function DraggableComponent(props: DraggableComponentProps) {
focusWidget(props.widgetId);
e.stopPropagation();
};
-
+ const shouldRenderComponent = !(
+ selectedWidgets.includes(props.widgetId) && isDragging
+ );
// Display this draggable based on the current drag state
const style: CSSProperties = {
- display: isCurrentWidgetDragging ? "none" : "flex",
+ display: isCurrentWidgetDragging ? "none" : "block",
};
// WidgetBoundaries
@@ -172,6 +173,10 @@ function DraggableComponent(props: DraggableComponentProps) {
style={{
opacity:
isResizingOrDragging && selectedWidget !== props.widgetId ? 1 : 0,
+ position: "absolute",
+ transform: `translate(-50%, -50%)`,
+ top: "50%",
+ left: "50%",
}}
/>
);
@@ -183,14 +188,9 @@ function DraggableComponent(props: DraggableComponentProps) {
const className = `${classNameForTesting}`;
- const shouldRenderComponent = !(
- selectedWidget === props.widgetId && isDragging
- );
-
return (
state.ui.widgetDragResize.selectedWidget,
+ (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget,
);
const isResizing = useSelector(
(state: AppState) => state.ui.widgetDragResize.isResizing,
@@ -99,7 +99,7 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
const [rows, setRows] = useState(snapRows);
const showPropertyPane = useShowPropertyPane();
- const { selectWidget, focusWidget } = useWidgetSelection();
+ const { deselectAll, focusWidget, selectWidget } = useWidgetSelection();
const updateCanvasSnapRows = useCanvasSnapRowsUpdateHook();
useEffect(() => {
@@ -237,12 +237,9 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
const handleFocus = (e: any) => {
if (!isResizing && !isDragging) {
if (!props.parentId) {
- selectWidget && selectWidget(props.widgetId);
+ deselectAll();
focusWidget && focusWidget(props.widgetId);
showPropertyPane && showPropertyPane();
- } else {
- selectWidget && selectWidget(props.parentId);
- focusWidget && focusWidget(props.parentId);
}
}
// commenting this out to allow propagation of click events
@@ -253,13 +250,12 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
? `${Math.max(rows * props.snapRowSpace, props.minHeight)}px`
: "100%";
- const border =
+ const boxShadow =
(isResizing || isDragging) &&
isExactlyOver &&
props.widgetId === MAIN_CONTAINER_WIDGET_ID
- ? "1px solid #DDDDDD"
- : "1px solid transparent";
-
+ ? "0px 0px 0px 1px #DDDDDD"
+ : "0px 0px 0px 1px transparent";
const dropRef = !props.dropDisabled ? drop : undefined;
return (
@@ -272,7 +268,7 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
ref={dropRef}
style={{
height,
- border,
+ boxShadow,
}}
>
{props.children}
diff --git a/app/client/src/components/editorComponents/Dropzone.tsx b/app/client/src/components/editorComponents/Dropzone.tsx
index 29a83003d3..78cd278b8c 100644
--- a/app/client/src/components/editorComponents/Dropzone.tsx
+++ b/app/client/src/components/editorComponents/Dropzone.tsx
@@ -4,6 +4,7 @@ import styled from "styled-components";
import { snapToGrid } from "utils/helpers";
import { IntentColors } from "constants/DefaultTheme";
import { useSpring, animated, interpolate, config } from "react-spring";
+import { Layers } from "constants/Layers";
const SPRING_CONFIG = {
...config.gentle,
@@ -124,6 +125,7 @@ export const DropZone = forwardRef(
height={props.height * props.parentRowHeight}
ref={ref}
style={{
+ zIndex: Layers.animatedDropZone,
transform: interpolate(
[X, Y],
(x: number, y: number) => `translate3d(${x}px,${y}px,0)`,
@@ -135,6 +137,7 @@ export const DropZone = forwardRef(
candrop={props.canDrop}
height={props.height * props.parentRowHeight}
style={{
+ zIndex: Layers.animatedSnappingDropZone,
transform: interpolate(
[snappedX, snappedY],
(x: number, y: number) => `translate3d(${x}px,${y}px,0)`,
diff --git a/app/client/src/components/editorComponents/EditableText.tsx b/app/client/src/components/editorComponents/EditableText.tsx
index b8c2983a02..baa81e42c0 100644
--- a/app/client/src/components/editorComponents/EditableText.tsx
+++ b/app/client/src/components/editorComponents/EditableText.tsx
@@ -92,10 +92,25 @@ const TextContainer = styled.div<{ isValid: boolean; minimal: boolean }>`
`;
export function EditableText(props: EditableTextProps) {
- const [isEditing, setIsEditing] = useState(!!props.isEditingDefault);
- const [value, setStateValue] = useState(props.defaultValue);
+ const {
+ beforeUnmount,
+ className,
+ defaultValue,
+ editInteractionKind,
+ forceDefault,
+ hideEditIcon,
+ isEditingDefault,
+ isInvalid,
+ minimal,
+ onBlur,
+ onTextChanged,
+ placeholder,
+ updating,
+ valueTransform,
+ } = props;
+ const [isEditing, setIsEditing] = useState(!!isEditingDefault);
+ const [value, setStateValue] = useState(defaultValue);
const inputValRef = useRef("");
- const { beforeUnmount } = props;
const setValue = useCallback((value) => {
inputValRef.current = value;
@@ -103,16 +118,16 @@ export function EditableText(props: EditableTextProps) {
}, []);
useEffect(() => {
- setValue(props.defaultValue);
- }, [props.defaultValue]);
+ setValue(defaultValue);
+ }, [defaultValue]);
useEffect(() => {
- setIsEditing(!!props.isEditingDefault);
- }, [props.defaultValue, props.isEditingDefault]);
+ setIsEditing(!!isEditingDefault);
+ }, [defaultValue, isEditingDefault]);
useEffect(() => {
- if (props.forceDefault === true) setValue(props.defaultValue);
- }, [props.forceDefault, props.defaultValue]);
+ if (forceDefault === true) setValue(defaultValue);
+ }, [forceDefault, defaultValue]);
// at times onTextChange is not fired
// for example when the modal is closed on clicking the overlay
@@ -128,58 +143,63 @@ export function EditableText(props: EditableTextProps) {
e.preventDefault();
e.stopPropagation();
};
- const onChange = (_value: string) => {
- props.onBlur && props.onBlur();
- const isInvalid = props.isInvalid ? props.isInvalid(_value) : false;
- if (!isInvalid) {
- props.onTextChanged(_value);
- setIsEditing(false);
- } else {
- Toaster.show({
- text: "Invalid name",
- variant: Variant.danger,
- });
- }
- };
+ const onChange = useCallback(
+ (_value: string) => {
+ onBlur && onBlur();
+ const _isInvalid = isInvalid ? isInvalid(_value) : false;
+ if (!_isInvalid) {
+ onTextChanged(_value);
+ setIsEditing(false);
+ } else {
+ Toaster.show({
+ text: "Invalid name",
+ variant: Variant.danger,
+ });
+ }
+ },
+ [isInvalid],
+ );
- const onInputchange = (_value: string) => {
- let finalVal: string = _value;
- if (props.valueTransform) {
- finalVal = props.valueTransform(_value);
- }
- setValue(finalVal);
- };
+ const onInputchange = useCallback(
+ (_value: string) => {
+ let finalVal: string = _value;
+ if (valueTransform) {
+ finalVal = valueTransform(_value);
+ }
+ setValue(finalVal);
+ },
+ [valueTransform],
+ );
- const errorMessage = props.isInvalid && props.isInvalid(value);
+ const errorMessage = isInvalid && isInvalid(value);
const error = errorMessage ? errorMessage : undefined;
return (
-
+
- {!props.minimal &&
- !props.hideEditIcon &&
- !props.updating &&
- !isEditing && }
+ {!minimal && !hideEditIcon && !updating && !isEditing && (
+
+ )}
diff --git a/app/client/src/components/editorComponents/EditorContextProvider.tsx b/app/client/src/components/editorComponents/EditorContextProvider.tsx
index b2dd17cc84..028b03ae9f 100644
--- a/app/client/src/components/editorComponents/EditorContextProvider.tsx
+++ b/app/client/src/components/editorComponents/EditorContextProvider.tsx
@@ -1,4 +1,4 @@
-import React, { Context, createContext, ReactNode } from "react";
+import React, { Context, createContext, ReactNode, useMemo } from "react";
import { connect } from "react-redux";
import { WidgetOperation } from "widgets/BaseWidget";
@@ -55,76 +55,68 @@ type EditorContextProviderProps = EditorContextType & {
function EditorContextProvider(props: EditorContextProviderProps) {
const {
- executeAction,
- updateWidget,
- updateWidgetProperty,
- updateWidgetMetaProperty,
- disableDrag,
- children,
- resetChildrenMetaProperty,
- deleteWidgetProperty,
batchUpdateWidgetProperty,
+ children,
+ deleteWidgetProperty,
+ disableDrag,
+ executeAction,
+ resetChildrenMetaProperty,
+ updateWidget,
+ updateWidgetMetaProperty,
+ updateWidgetProperty,
} = props;
+
+ // Memoize the context provider to prevent
+ // unnecessary renders
+ const contextValue = useMemo(
+ () => ({
+ executeAction,
+ updateWidget,
+ updateWidgetProperty,
+ updateWidgetMetaProperty,
+ disableDrag,
+ resetChildrenMetaProperty,
+ deleteWidgetProperty,
+ batchUpdateWidgetProperty,
+ }),
+ [
+ executeAction,
+ updateWidget,
+ updateWidgetProperty,
+ updateWidgetMetaProperty,
+ disableDrag,
+ resetChildrenMetaProperty,
+ deleteWidgetProperty,
+ batchUpdateWidgetProperty,
+ ],
+ );
return (
-
+
{children}
);
}
-const mapDispatchToProps = (dispatch: any) => {
- return {
- updateWidgetProperty: (
- widgetId: string,
- propertyName: string,
- propertyValue: any,
- ) =>
- dispatch(
- updateWidgetPropertyRequest(
- widgetId,
- propertyName,
- propertyValue,
- RenderModes.CANVAS,
- ),
- ),
- executeAction: (actionPayload: ExecuteActionPayload) =>
- dispatch(executeAction(actionPayload)),
- updateWidget: (
- operation: WidgetOperation,
- widgetId: string,
- payload: any,
- ) => dispatch(updateWidget(operation, widgetId, payload)),
- updateWidgetMetaProperty: (
- widgetId: string,
- propertyName: string,
- propertyValue: any,
- ) =>
- dispatch(updateWidgetMetaProperty(widgetId, propertyName, propertyValue)),
- resetChildrenMetaProperty: (widgetId: string) =>
- dispatch(resetChildrenMetaProperty(widgetId)),
- disableDrag: (disable: boolean) => {
- dispatch(disableDragAction(disable));
- },
- deleteWidgetProperty: (widgetId: string, propertyPaths: string[]) =>
- dispatch(deletePropertyAction(widgetId, propertyPaths)),
- batchUpdateWidgetProperty: (
- widgetId: string,
- updates: BatchPropertyUpdatePayload,
- ) => {
- dispatch(batchUpdatePropertyAction(widgetId, updates));
- },
- };
+const mapDispatchToProps = {
+ updateWidgetProperty: (
+ widgetId: string,
+ propertyName: string,
+ propertyValue: any,
+ ) =>
+ updateWidgetPropertyRequest(
+ widgetId,
+ propertyName,
+ propertyValue,
+ RenderModes.CANVAS,
+ ),
+
+ executeAction,
+ updateWidget,
+ updateWidgetMetaProperty,
+ resetChildrenMetaProperty,
+ disableDrag: disableDragAction,
+ deleteWidgetProperty: deletePropertyAction,
+ batchUpdateWidgetProperty: batchUpdatePropertyAction,
};
export default connect(null, mapDispatchToProps)(EditorContextProvider);
diff --git a/app/client/src/components/editorComponents/EntityNameComponent.tsx b/app/client/src/components/editorComponents/EntityNameComponent.tsx
index a664c78f94..e2e6654039 100644
--- a/app/client/src/components/editorComponents/EntityNameComponent.tsx
+++ b/app/client/src/components/editorComponents/EntityNameComponent.tsx
@@ -127,10 +127,10 @@ class EntityNameComponent extends React.Component<
const { focused } = this.state;
const {
isValid,
+ onChange,
+ placeholder,
validationMessage,
value,
- placeholder,
- onChange,
} = this.props;
return (
diff --git a/app/client/src/components/editorComponents/GlobalSearch/ActionLink.tsx b/app/client/src/components/editorComponents/GlobalSearch/ActionLink.tsx
index 2e6d4c1a10..8b3c590ed5 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/ActionLink.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/ActionLink.tsx
@@ -13,9 +13,9 @@ export const StyledActionLink = styled.span<{ isActiveItem?: boolean }>`
export const ActionLink = withTheme(
({
+ isActiveItem,
item,
theme,
- isActiveItem,
}: {
item: SearchItem;
theme: Theme;
diff --git a/app/client/src/components/editorComponents/GlobalSearch/AlgoliaSearchWrapper.tsx b/app/client/src/components/editorComponents/GlobalSearch/AlgoliaSearchWrapper.tsx
index 9425debd85..7dd45b584e 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/AlgoliaSearchWrapper.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/AlgoliaSearchWrapper.tsx
@@ -12,7 +12,7 @@ type SearchProps = {
children: React.ReactNode;
};
-function Search({ query, children }: SearchProps) {
+function Search({ children, query }: SearchProps) {
const [queryInState, setQueryInState] = useState(query);
const debouncedSetQueryInState = useCallback(
debounce(setQueryInState, 100),
diff --git a/app/client/src/components/editorComponents/GlobalSearch/Description.tsx b/app/client/src/components/editorComponents/GlobalSearch/Description.tsx
index e9af253255..cae754bb9e 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/Description.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/Description.tsx
@@ -1,8 +1,8 @@
-import React, { useCallback, useEffect } from "react";
+import React, { useEffect } from "react";
import styled from "styled-components";
import ActionLink from "./ActionLink";
import Highlight from "./Highlight";
-import { getItemTitle, SEARCH_ITEM_TYPES } from "./utils";
+import { algoliaHighlightTag, getItemTitle, SEARCH_ITEM_TYPES } from "./utils";
import { getTypographyByKey } from "constants/DefaultTheme";
import { SearchItem } from "./utils";
import parseDocumentationContent from "./parseDocumentationContent";
@@ -74,27 +74,57 @@ const Container = styled.div`
}
`;
-const DocumentationDescription = ({ item }: { item: SearchItem }) => {
- try {
- const {
- _highlightResult: {
- document: { value: rawDocument },
- title: { value: rawTitle },
- },
- } = item;
- const content = parseDocumentationContent({
- rawDocument: rawDocument,
- rawTitle: rawTitle,
- path: item.path,
- });
+function DocumentationDescription({
+ item,
+ query,
+}: {
+ item: SearchItem;
+ query: string;
+}) {
+ const {
+ _highlightResult: {
+ document: { value: rawDocument },
+ title: { value: rawTitle },
+ },
+ } = item;
+ const content = parseDocumentationContent({
+ rawDocument: rawDocument,
+ rawTitle: rawTitle,
+ path: item.path,
+ query,
+ });
+ const containerRef = React.useRef(null);
+ useEffect(() => {
+ scrollToMatchedValue();
+ }, [content]);
- return content ? (
-
- ) : null;
- } catch (e) {
- return null;
- }
-};
+ const scrollToMatchedValue = () => {
+ const root = containerRef.current;
+ if (!root) return;
+ const list = root.getElementsByTagName(algoliaHighlightTag);
+ if (list.length) {
+ const bestMatch = Array.from(list).reduce((accumulator, currentValue) => {
+ if (
+ currentValue.textContent &&
+ accumulator.textContent &&
+ currentValue.textContent.length > accumulator.textContent.length
+ )
+ return currentValue;
+ return accumulator;
+ }, list[0]);
+
+ bestMatch.scrollIntoView();
+ } else {
+ setTimeout(() => {
+ root.firstElementChild?.scrollIntoView();
+ }, 0);
+ }
+ };
+
+ return content ? (
+
+ ) : null;
+}
const StyledHitEnterMessageContainer = styled.div`
background: ${(props) =>
@@ -142,28 +172,12 @@ const descriptionByType = {
function Description(props: Props) {
const { activeItem, activeItemType } = props;
- const containerRef = React.useRef(null);
-
- const onScroll = useCallback((e: React.UIEvent) => {
- if (
- props.scrollPositionRef?.current ||
- props.scrollPositionRef?.current === 0
- ) {
- props.scrollPositionRef.current = (e.target as HTMLDivElement).scrollTop;
- }
- }, []);
-
- useEffect(() => {
- if (containerRef.current) {
- containerRef.current.scrollTop = props.scrollPositionRef?.current;
- }
- }, [containerRef.current, activeItem]);
if (!activeItemType || !activeItem) return null;
const Component = descriptionByType[activeItemType];
return (
-
+
);
diff --git a/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx b/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx
new file mode 100644
index 0000000000..ad5dfb33b4
--- /dev/null
+++ b/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+import styled from "styled-components";
+
+const Wrapper = styled.div`
+ span {
+ color: white;
+ font-variant: all-small-caps;
+ font-size: ${(props) => props.theme.fontSizes[2]}px;
+ margin-right: ${(props) => props.theme.spaces[1]}px;
+ }
+ div {
+ margin-right: ${(props) => props.theme.spaces[7]}px;
+ font-size: ${(props) => props.theme.fontSizes[2]}px;
+ text-align: center;
+ }
+ color: ${(props) => props.theme.colors.globalSearch.searchItemText};
+ padding: ${(props) => props.theme.spaces[1]}px
+ ${(props) => props.theme.spaces[6]}px;
+ display: flex;
+ flex-direction: row;
+`;
+
+const FOOTER_INFO = [
+ {
+ action: "\u2191\u2193",
+ description: "Select",
+ },
+ {
+ action: "ENTER",
+ description: "Open",
+ },
+];
+
+function Footer() {
+ return (
+
+ {FOOTER_INFO.map((info) => {
+ return (
+
+ {info.action}
+ {info.description}
+
+ );
+ })}
+
+ );
+}
+
+export default Footer;
diff --git a/app/client/src/components/editorComponents/GlobalSearch/GlobalSearchHotKeys.tsx b/app/client/src/components/editorComponents/GlobalSearch/GlobalSearchHotKeys.tsx
index 43686d95ce..fd79706b8a 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/GlobalSearchHotKeys.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/GlobalSearchHotKeys.tsx
@@ -53,7 +53,7 @@ class GlobalSearchHotKeys extends React.Component {
return (
{this.hotKeysConfig.map(
- ({ combo, onKeyDown, allowInInput, label, group }, index) => (
+ ({ allowInInput, combo, group, label, onKeyDown }, index) => (
{text};
+function Highlight({
+ className,
+ match,
+ text,
+}: {
+ className?: string;
+ match: string;
+ text: string;
+}) {
+ if (!match) return {text} ;
const regEx = new RegExp(match, "ig");
const parts = text?.split(regEx);
- if (parts?.length === 1) return {text} ;
+ if (parts?.length === 1) return {text} ;
let lastIndex = 0;
return (
-
+
{parts?.map((part, index) => {
lastIndex += Math.max(part.length, 0);
const result = (
diff --git a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx
index f1af2aa4fc..84766c4720 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx
@@ -4,6 +4,7 @@ import NoSearchDataImage from "assets/images/no_search_data.png";
import { NO_SEARCH_DATA_TEXT } from "constants/messages";
import { getTypographyByKey } from "constants/DefaultTheme";
import { ReactComponent as DiscordIcon } from "assets/icons/help/discord.svg";
+import AnalyticsUtil from "utils/AnalyticsUtil";
const Container = styled.div`
display: flex;
@@ -49,6 +50,7 @@ function ResultsNotFound() {
className="discord-link"
onClick={() => {
window.open("https://discord.gg/rBTTVJp", "_blank");
+ AnalyticsUtil.logEvent("DISCORD_LINK_CLICK");
}}
>
diff --git a/app/client/src/components/editorComponents/GlobalSearch/SearchModal.tsx b/app/client/src/components/editorComponents/GlobalSearch/SearchModal.tsx
index bc2afc5b01..7ac95ec680 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/SearchModal.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/SearchModal.tsx
@@ -26,7 +26,7 @@ type Props = {
children: React.ReactNode;
};
-function DocsSearchModal({ modalOpen, toggleShow, children }: Props) {
+function DocsSearchModal({ children, modalOpen, toggleShow }: Props) {
return (
props.theme.colors.globalSearch.searchItemSubText};
+ font-size: ${(props) => props.theme.fontSizes[1]}px;
+ margin-right: ${(props) => `${props.theme.spaces[2]}px`};
+ display: ${(props) => (props.isActiveItem ? "inline" : "none")};
+ max-width: 50px;
+ ${overflowCSS}
+ }
+
&:hover {
background-color: ${(props) =>
props.itemType !== SEARCH_ITEM_TYPES.sectionTitle &&
@@ -59,6 +81,10 @@ export const SearchItemContainer = styled.div<{
${StyledActionLink} {
visibility: visible;
}
+
+ .subtext {
+ display: inline;
+ }
}
${(props) => getTypographyByKey(props, "p3")};
@@ -90,6 +116,12 @@ const StyledDocumentIcon = styled(DocumentIcon)`
display: flex;
`;
+const TextWrapper = styled.div`
+ flex: 1;
+ display: flex;
+ justify-content: space-between;
+`;
+
function DocumentationItem(props: { item: SearchItem; isActiveItem: boolean }) {
return (
<>
@@ -122,17 +154,20 @@ function WidgetItem(props: {
item: SearchItem;
isActiveItem: boolean;
}) {
- const { query, item } = props;
+ const { item, query } = props;
const { type } = item || {};
- let title = getItemTitle(item);
+ const title = getItemTitle(item);
const pageName = usePageName(item.pageId);
- title = `${title} / ${pageName}`;
+ const subText = `${pageName}`;
return (
<>
{getWidgetIcon(type)}
-
+
+
+
+
>
@@ -158,20 +193,26 @@ function ActionItem(props: {
return state.entities.plugins.list;
});
const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]);
- const icon = getActionConfig(pluginType)?.getIcon(
- item.config,
- pluginGroups[item.config.datasource.pluginId],
- );
+ const icon =
+ pluginType === PluginType.API
+ ? apiIcon
+ : getActionConfig(pluginType)?.getIcon(
+ item.config,
+ pluginGroups[item.config.datasource.pluginId],
+ );
- let title = getItemTitle(item);
+ const title = getItemTitle(item);
const pageName = usePageName(config.pageId);
- title = `${title} / ${pageName}`;
+ const subText = `${pageName}`;
return (
<>
{icon}
-
+
+
+
+
>
@@ -206,7 +247,7 @@ function PageItem(props: {
item: SearchItem;
isActiveItem: boolean;
}) {
- const { query, item } = props;
+ const { item, query } = props;
const title = getItemTitle(item);
const icon = item.isDefault ? homePageIcon : pageIcon;
@@ -265,7 +306,7 @@ type ItemProps = {
};
function SearchItemComponent(props: ItemProps) {
- const { item, index, query } = props;
+ const { index, item, query } = props;
const itemRef = useRef(null);
const searchContext = useContext(SearchContext);
const activeItemIndex = searchContext?.activeItemIndex;
@@ -293,9 +334,7 @@ function SearchItemComponent(props: ItemProps) {
itemType !== SEARCH_ITEM_TYPES.placeholder
) {
setActiveItemIndex(index);
- if (itemType !== SEARCH_ITEM_TYPES.document) {
- searchContext?.handleItemLinkClick(item, "SEARCH_ITEM");
- }
+ searchContext?.handleItemLinkClick(item, "SEARCH_ITEM");
}
}}
ref={itemRef}
@@ -312,8 +351,8 @@ const SearchResultsContainer = styled.div`
`;
function SearchResults({
- searchResults,
query,
+ searchResults,
}: {
searchResults: SearchItem[];
query: string;
diff --git a/app/client/src/components/editorComponents/GlobalSearch/index.tsx b/app/client/src/components/editorComponents/GlobalSearch/index.tsx
index 735efbdefa..648b2d2a5d 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/index.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/index.tsx
@@ -20,7 +20,7 @@ import SearchContext from "./GlobalSearchContext";
import Description from "./Description";
import ResultsNotFound from "./ResultsNotFound";
import { getActions, getAllPageWidgets } from "selectors/entitiesSelector";
-import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/WidgetEntity";
+import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/useNavigateToWidget";
import {
toggleShowGlobalSearchModal,
setGlobalSearchQuery,
@@ -46,6 +46,9 @@ import { keyBy, noop } from "lodash";
import EntitiesIcon from "assets/icons/ads/entities.svg";
import DocsIcon from "assets/icons/ads/docs.svg";
import RecentIcon from "assets/icons/ads/recent.svg";
+import Footer from "./Footer";
+
+import { getCurrentPageId } from "selectors/editorSelectors";
const StyledContainer = styled.div`
width: 750px;
@@ -63,12 +66,8 @@ const StyledContainer = styled.div`
${algoliaHighlightTag},
& .ais-Highlight-highlighted,
& .search-highlighted {
- background: unset;
- color: ${(props) => props.theme.colors.globalSearch.searchItemHighlight};
+ background-color: #6287b0;
font-style: normal;
- text-decoration: underline;
- text-decoration-color: ${(props) =>
- props.theme.colors.globalSearch.highlightedTextUnderline};
}
`;
@@ -92,7 +91,35 @@ const getSectionTitle = (title: string, icon: any) => ({
icon,
});
+const getIsInCurrentPage = (
+ isWidget: boolean,
+ entity: any,
+ currentPageId?: string,
+) =>
+ isWidget
+ ? entity.pageId === currentPageId
+ : entity?.config?.pageId === currentPageId;
+
+const sortActionsAndWidgets = (a: any, b: any, currentPageId?: string) => {
+ const isAWidget = !!a.widgetId;
+ const isBWidget = !!b.widgetId;
+
+ const aInCurrentPage = getIsInCurrentPage(isAWidget, a, currentPageId);
+ const bInCurrentPage = getIsInCurrentPage(isBWidget, b, currentPageId);
+
+ // page entites on top
+ if (aInCurrentPage && !bInCurrentPage) return -1;
+ if (!aInCurrentPage && bInCurrentPage) return 1;
+
+ // actions before widgets
+ if (isAWidget && !isBWidget) return 1;
+ if (!isAWidget && isBWidget) return -1;
+
+ return 0;
+};
+
function GlobalSearch() {
+ const currentPageId = useSelector(getCurrentPageId);
const modalOpen = useSelector(isModalOpenSelector);
const defaultDocs = useDefaultDocumentationResults(modalOpen);
const params = useParams();
@@ -166,13 +193,18 @@ function GlobalSearch() {
setQuery(resetSearchQuery);
} else {
dispatch(setGlobalSearchQuery(""));
- if (!query) setActiveItemIndex(1);
+ if (!query)
+ recentEntities.length > 1
+ ? setActiveItemIndex(2)
+ : setActiveItemIndex(1);
}
}, [modalOpen]);
useEffect(() => {
- setActiveItemIndex(1);
- }, [query]);
+ !query && recentEntities.length > 1
+ ? setActiveItemIndex(2)
+ : setActiveItemIndex(1);
+ }, [query, recentEntities.length]);
const filteredWidgets = useMemo(() => {
if (!query) return searchableWidgets;
@@ -208,7 +240,7 @@ function GlobalSearch() {
);
}, [pages, query]);
- const recentsSectionTitle = getSectionTitle("Recents", RecentIcon);
+ const recentsSectionTitle = getSectionTitle("Recent Entities", RecentIcon);
const docsSectionTitle = getSectionTitle("Documentation Links", DocsIcon);
const entitiesSectionTitle = getSectionTitle("Entities", EntitiesIcon);
@@ -231,11 +263,15 @@ function GlobalSearch() {
const results = [];
+ const actionsAndWidgetsSorted = [
+ ...filteredActions,
+ ...filteredWidgets,
+ ].sort((a, b) => sortActionsAndWidgets(a, b, currentPageId));
+
const entities = [
entitiesSectionTitle,
+ ...actionsAndWidgetsSorted,
...filteredPages,
- ...filteredWidgets,
- ...filteredActions,
...filteredDatasources,
];
@@ -313,9 +349,14 @@ function GlobalSearch() {
const handleActionClick = (item: SearchItem) => {
const { config } = item;
- const { pageId, pluginType, id } = config;
+ const { id, pageId, pluginType } = config;
const actionConfig = getActionConfig(pluginType);
- const url = actionConfig?.getURL(params.applicationId, pageId, id);
+ const url = actionConfig?.getURL(
+ params.applicationId,
+ pageId,
+ id,
+ pluginType,
+ );
toggleShow();
url && history.push(url);
};
@@ -401,6 +442,7 @@ function GlobalSearch() {
)}
+ {!query && }
diff --git a/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.test.ts b/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.test.ts
index bc7fa299e8..2f1e3be00f 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.test.ts
+++ b/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.test.ts
@@ -37,6 +37,7 @@ describe("parseDocumentationContent", () => {
rawTitle: sampleTitleResponse,
rawDocument: sampleDocumentResponse,
path: "master/security",
+ query: "Security",
};
const result = parseDocumentationContent(sampleItem);
expect(result).toStrictEqual(expectedResult);
diff --git a/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.ts b/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.ts
index 43e2dccf6c..7a83e3ba2c 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.ts
+++ b/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.ts
@@ -1,6 +1,7 @@
import marked from "marked";
import { HelpBaseURL } from "constants/HelpConstants";
import { algoliaHighlightTag } from "./utils";
+import log from "loglevel";
/**
* @param {String} HTML representing a single element
@@ -39,7 +40,7 @@ const getDocumentationCTA = (path: any) => {
* Append open documentation button to title
*/
const updateDocumentDescriptionTitle = (documentObj: any, item: any) => {
- const { rawTitle, path } = item;
+ const { path, rawTitle } = item;
Array.from(documentObj.querySelectorAll("h1")).forEach((match: any) => {
match.outerHTML = `${match.innerHTML} `;
@@ -66,9 +67,29 @@ const updateDocumentDescriptionTitle = (documentObj: any, item: any) => {
// append documentation button after title:
const ctaElement = getDocumentationCTA(path) as Node;
firstChild.appendChild(ctaElement);
+
+ removeBadHighlights(firstChild, item.query);
}
};
+// Remove highlights if they don't match well
+const removeBadHighlights = (node: HTMLElement | Document, query: string) => {
+ Array.from(
+ node.querySelectorAll(algoliaHighlightTag) as NodeListOf,
+ ).forEach((match) => {
+ // If the length of the highlighted node is less than the 1/2 length of
+ // the query we remove the highlighted tag.
+ // E.g query: "store any" won't highlight "any" nodes
+ if (
+ match.textContent &&
+ match.textContent.length < Math.floor(query.length / 2)
+ ) {
+ const innerHtml = match.innerHTML;
+ match.replaceWith(innerHtml);
+ }
+ });
+};
+
const replaceHintTagsWithCode = (text: string) => {
let result = text.replace(/{% hint .*?%}/, "```");
result = result.replace(/{% endhint .*?%}/, "```");
@@ -78,10 +99,14 @@ const replaceHintTagsWithCode = (text: string) => {
const parseDocumentationContent = (item: any): string | undefined => {
try {
- const { rawDocument } = item;
+ const { query, rawDocument } = item;
let value = rawDocument;
if (!value) return;
+ const aisTag = new RegExp(
+ `<${algoliaHighlightTag}>|</${algoliaHighlightTag}>`,
+ "g",
+ );
value = stripMarkdown(value);
value = replaceHintTagsWithCode(value);
@@ -91,10 +116,7 @@ const parseDocumentationContent = (item: any): string | undefined => {
const documentObj = domparser.parseFromString(parsedDocument, "text/html");
// remove algolia highlight within code sections
- const aisTag = new RegExp(
- `<${algoliaHighlightTag}>|</${algoliaHighlightTag}>`,
- "g",
- );
+
Array.from(documentObj.querySelectorAll("code")).forEach((match) => {
match.innerHTML = match.innerHTML.replace(aisTag, "");
});
@@ -117,13 +139,80 @@ const parseDocumentationContent = (item: any): string | undefined => {
} catch (e) {}
});
+ // Combine adjacent highlighted nodes into a single one
+ let adjacentMatches: string[] = [];
+ const letterRegex = /[a-zA-Z]/g;
+ // Get highlighted tags
+ Array.from(documentObj.querySelectorAll(algoliaHighlightTag)).forEach(
+ (match) => {
+ // If adjacent element is an `algoliaHighlightTag` and the next
+ // text content does not include a letter
+ if (
+ match.nextSibling?.textContent &&
+ !letterRegex.test(match.nextSibling?.textContent) &&
+ match.nextElementSibling?.nodeName.toLowerCase() ===
+ algoliaHighlightTag &&
+ !adjacentMatches.length &&
+ match.textContent
+ ) {
+ // Store the matched word and the text content
+ adjacentMatches = adjacentMatches.concat([
+ match.textContent,
+ match.nextSibling?.textContent,
+ ]);
+ // Remove the text node as we have stored this above
+ match.nextSibling.remove();
+ // Remove the node as we have it's text content
+ match.remove();
+ }
+ // If this is part of a group of highligted words
+ else if (adjacentMatches.length && match.textContent) {
+ // store it's text content
+ adjacentMatches.push(match.textContent);
+
+ // If there are more adjacent highlight nodes ahead
+ if (
+ match.nextElementSibling?.nodeName.toLowerCase() ===
+ algoliaHighlightTag &&
+ match.nextSibling?.textContent &&
+ !letterRegex.test(match.nextSibling?.textContent)
+ ) {
+ // store the text content
+ adjacentMatches.push(match.nextSibling?.textContent);
+ match.nextSibling.remove();
+ // delete the node
+ match.remove();
+ }
+ // We are at the last node of the group
+ else {
+ // Create a algoliaHighlightTag element and add the
+ // grouped text
+ const highlightTag = document.createElement(algoliaHighlightTag);
+ highlightTag.innerText = adjacentMatches.join("");
+
+ // Simply replace(or remove) the last node with the
+ // newly created algoliaHighlightTag
+ match.replaceWith(highlightTag);
+ // Reset
+ adjacentMatches = [];
+ }
+ } else {
+ // Reset adjacentMatches. We are no longer part of a group of adjacent nodes.
+ // Start afresh
+ adjacentMatches = [];
+ }
+ },
+ );
+
+ // Remove highlight for nodes that don't match well
+ removeBadHighlights(documentObj, query);
// update description title
updateDocumentDescriptionTitle(documentObj, item);
const content = strip(documentObj.body.innerHTML).trim();
return content;
} catch (e) {
- console.log(e, "err");
+ log.error(e);
return;
}
};
diff --git a/app/client/src/components/editorComponents/GlobalSearch/useRecentEntities.tsx b/app/client/src/components/editorComponents/GlobalSearch/useRecentEntities.tsx
index a2957bf538..7943051a54 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/useRecentEntities.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/useRecentEntities.tsx
@@ -18,9 +18,9 @@ const useResentEntities = () => {
const pages = useSelector(getPageList) || [];
- const populatedRecentEntities = recentEntities
+ const populatedRecentEntities = (recentEntities || [])
.map((entity) => {
- const { type, id, params } = entity;
+ const { id, params, type } = entity;
if (type === "page") {
const result = pages.find((page) => page.pageId === id);
if (result) {
diff --git a/app/client/src/components/editorComponents/NavBarItem.tsx b/app/client/src/components/editorComponents/NavBarItem.tsx
index 54a556063c..7e1ad0c060 100644
--- a/app/client/src/components/editorComponents/NavBarItem.tsx
+++ b/app/client/src/components/editorComponents/NavBarItem.tsx
@@ -77,14 +77,14 @@ const ItemContainer = styled.div`
class NavBarItem extends React.Component {
render(): React.ReactNode {
const {
- title,
- icon,
- path,
exact,
- width,
height,
- onClick,
+ icon,
isActive,
+ onClick,
+ path,
+ title,
+ width,
} = this.props;
return (
diff --git a/app/client/src/components/editorComponents/PaneWrapper.tsx b/app/client/src/components/editorComponents/PaneWrapper.tsx
index e238bc5120..7a84236223 100644
--- a/app/client/src/components/editorComponents/PaneWrapper.tsx
+++ b/app/client/src/components/editorComponents/PaneWrapper.tsx
@@ -4,7 +4,6 @@ export default styled.div`
background-color: ${(props) => props.theme.colors.propertyPane.bg};
border-radius: ${(props) => props.theme.radii[0]}px;
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2), 0px 2px 10px rgba(0, 0, 0, 0.1);
- padding: 24px 16px;
color: ${(props) => props.theme.colors.textOnDarkBG};
text-transform: capitalize;
`;
diff --git a/app/client/src/components/editorComponents/PreventInteractionsOverlay.tsx b/app/client/src/components/editorComponents/PreventInteractionsOverlay.tsx
index c92e7ae5dd..345632ce35 100644
--- a/app/client/src/components/editorComponents/PreventInteractionsOverlay.tsx
+++ b/app/client/src/components/editorComponents/PreventInteractionsOverlay.tsx
@@ -15,6 +15,10 @@ const Container = styled.div`
width: 100%;
height: 100%;
position: relative;
+
+ > div {
+ height: 100%;
+ }
`;
type Props = {
diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx
index 9c07e1fe6e..7bfc735b25 100644
--- a/app/client/src/components/editorComponents/ResizableComponent.tsx
+++ b/app/client/src/components/editorComponents/ResizableComponent.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useRef, memo } from "react";
+import React, { useContext, useRef, memo, useMemo } from "react";
import { XYCoord } from "react-dnd";
import {
@@ -16,13 +16,13 @@ import {
} from "./ResizableUtils";
import {
useShowPropertyPane,
- useWidgetSelection,
useWidgetDragResize,
} from "utils/hooks/dragResizeHooks";
import { useSelector } from "react-redux";
import { AppState } from "reducers";
import Resizable from "resizable";
-import { isDropZoneOccupied, getSnapColumns } from "utils/WidgetPropsUtils";
+import { omit, get } from "lodash";
+import { getSnapColumns, isDropZoneOccupied } from "utils/WidgetPropsUtils";
import {
VisibilityContainer,
LeftHandleStyles,
@@ -38,27 +38,35 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import { scrollElementIntoParentCanvasView } from "utils/helpers";
import { getNearestParentCanvas } from "utils/generators";
import { getOccupiedSpaces } from "selectors/editorSelectors";
+import { commentModeSelector } from "selectors/commentsSelectors";
+import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
export type ResizableComponentProps = WidgetProps & {
paddingOffset: number;
};
-/* eslint-disable react/display-name */
-export const ResizableComponent = memo((props: ResizableComponentProps) => {
+export const ResizableComponent = memo(function ResizableComponent(
+ props: ResizableComponentProps,
+) {
const resizableRef = useRef(null);
// Fetch information from the context
const { updateWidget } = useContext(EditorContext);
const occupiedSpaces = useSelector(getOccupiedSpaces);
- const { updateDropTargetRows, persistDropTargetRows } = useContext(
+ const { persistDropTargetRows, updateDropTargetRows } = useContext(
DropTargetContext,
);
+ const isCommentMode = useSelector(commentModeSelector);
+
const showPropertyPane = useShowPropertyPane();
const { selectWidget } = useWidgetSelection();
const { setIsResizing } = useWidgetDragResize();
const selectedWidget = useSelector(
- (state: AppState) => state.ui.widgetDragResize.selectedWidget,
+ (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget,
+ );
+ const selectedWidgets = useSelector(
+ (state: AppState) => state.ui.widgetDragResize.selectedWidgets,
);
const focusedWidget = useSelector(
(state: AppState) => state.ui.widgetDragResize.focusedWidget,
@@ -70,6 +78,7 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
const isResizing = useSelector(
(state: AppState) => state.ui.widgetDragResize.isResizing,
);
+
const occupiedSpacesBySiblingWidgets =
occupiedSpaces && props.parentId && occupiedSpaces[props.parentId]
? occupiedSpaces[props.parentId]
@@ -77,7 +86,9 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
// isFocused (string | boolean) -> isWidgetFocused (boolean)
const isWidgetFocused =
- focusedWidget === props.widgetId || selectedWidget === props.widgetId;
+ focusedWidget === props.widgetId ||
+ selectedWidget === props.widgetId ||
+ selectedWidgets.includes(props.widgetId);
// Calculate the dimensions of the widget,
// The ResizableContainer's size prop is controlled
@@ -103,15 +114,18 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
possibleBoundingElements.length > 0
? possibleBoundingElements[0]
: undefined;
- const boundingElementClientRect = boundingElement
- ? boundingElement.getBoundingClientRect()
- : undefined;
// onResize handler
// Checks if the current resize position has any collisions
// If yes, set isColliding flag to true.
// If no, set isColliding flag to false.
const isColliding = (newDimensions: UIElementSize, position: XYCoord) => {
+ // Moving the bounding element calculations inside
+ // to make this expensive operation only whne
+ const boundingElementClientRect = boundingElement
+ ? boundingElement.getBoundingClientRect()
+ : undefined;
+
const bottom =
props.topRow +
position.y / props.parentRowSpace +
@@ -139,9 +153,10 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
return true;
}
+ // Minimum row and columns to be set to a widget.
if (
- newRowCols.rightColumn - newRowCols.leftColumn < 1 ||
- newRowCols.bottomRow - newRowCols.topRow < 1
+ newRowCols.rightColumn - newRowCols.leftColumn < 2 ||
+ newRowCols.bottomRow - newRowCols.topRow < 4
) {
return true;
}
@@ -246,27 +261,38 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
widgetType: props.type,
});
};
+ const handles = useMemo(() => {
+ const allHandles = {
+ left: LeftHandleStyles,
+ top: TopHandleStyles,
+ bottom: BottomHandleStyles,
+ right: RightHandleStyles,
+ bottomRight: BottomRightHandleStyles,
+ topLeft: TopLeftHandleStyles,
+ topRight: TopRightHandleStyles,
+ bottomLeft: BottomLeftHandleStyles,
+ };
+
+ return omit(allHandles, get(props, "disabledResizeHandles", []));
+ }, [props]);
+
+ const isEnabled =
+ !isDragging && isWidgetFocused && !props.resizeDisabled && !isCommentMode;
return (
{
);
});
-
export default ResizableComponent;
diff --git a/app/client/src/components/editorComponents/ResizeStyledComponents.tsx b/app/client/src/components/editorComponents/ResizeStyledComponents.tsx
index cd814f5033..055423c519 100644
--- a/app/client/src/components/editorComponents/ResizeStyledComponents.tsx
+++ b/app/client/src/components/editorComponents/ResizeStyledComponents.tsx
@@ -16,7 +16,6 @@ export const VisibilityContainer = styled.div<{
export const EdgeHandleStyles = css`
position: absolute;
- z-index: 3;
width: ${EDGE_RESIZE_HANDLE_WIDTH}px;
height: ${EDGE_RESIZE_HANDLE_WIDTH}px;
&::before {
diff --git a/app/client/src/components/editorComponents/Sidebar.tsx b/app/client/src/components/editorComponents/Sidebar.tsx
index 8282baf2ff..73093e7658 100644
--- a/app/client/src/components/editorComponents/Sidebar.tsx
+++ b/app/client/src/components/editorComponents/Sidebar.tsx
@@ -7,12 +7,13 @@ import * as Sentry from "@sentry/react";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
+import { Layers } from "constants/Layers";
const SidebarWrapper = styled.div`
background-color: ${Colors.MINE_SHAFT};
padding: 0;
width: ${(props) => props.theme.sidebarWidth};
- z-index: 3;
+ z-index: ${Layers.sideBar};
color: ${(props) => props.theme.colors.textOnDarkBG};
overflow-y: auto;
diff --git a/app/client/src/components/editorComponents/StoreAsDatasource.tsx b/app/client/src/components/editorComponents/StoreAsDatasource.tsx
index 35abcea5ff..989d82323c 100644
--- a/app/client/src/components/editorComponents/StoreAsDatasource.tsx
+++ b/app/client/src/components/editorComponents/StoreAsDatasource.tsx
@@ -7,12 +7,12 @@ import Icon, { IconSize } from "components/ads/Icon";
import { Classes } from "components/ads/common";
export const DatasourceIcon = styled.div<{ enable?: boolean }>`
- position: absolute;
- right: -155px;
- top: 7px;
display: flex;
align-items: center;
cursor: pointer;
+ margin-left: 10px;
+ height: 100%;
+ min-height: 37px;
.${Classes.TEXT} {
color: ${(props) => props.theme.colors.text.heading};
}
diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx
index b33a28bd0f..e0409bc315 100644
--- a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx
+++ b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx
@@ -1,5 +1,6 @@
import React, { CSSProperties } from "react";
import { ControlIcons } from "icons/ControlIcons";
+import Icon, { IconSize } from "components/ads/Icon";
import { Colors } from "constants/Colors";
import styled from "styled-components";
import { Tooltip, Classes } from "@blueprintjs/core";
@@ -34,18 +35,41 @@ const SettingsWrapper = styled.div`
`;
const WidgetName = styled.span`
- margin-right: 5px;
+ margin-right: ${(props) => props.theme.spaces[1] + 1}px;
+ margin-left: ${(props) => props.theme.spaces[3]}px;
+`;
+
+const StyledErrorIcon = styled(Icon)`
+ &:hover {
+ svg {
+ path {
+ fill: ${Colors.WHITE};
+ }
+ }
+ }
+ margin-right: ${(props) => props.theme.spaces[1]}px;
`;
type SettingsControlProps = {
toggleSettings: (e: any) => void;
activity: Activities;
name: string;
+ errorCount: number;
};
const SettingsIcon = ControlIcons.SETTINGS_CONTROL;
-const getStyles = (activity: Activities): CSSProperties | undefined => {
+const getStyles = (
+ activity: Activities,
+ errorCount: number,
+): CSSProperties | undefined => {
+ if (errorCount > 0) {
+ return {
+ background: "red",
+ color: Colors.WHITE,
+ };
+ }
+
switch (activity) {
case Activities.ACTIVE:
return {
@@ -69,7 +93,9 @@ export function SettingsControl(props: SettingsControlProps) {
const settingsIcon = (
);
+ const errorIcon = (
+
+ );
return (
+ {!!props.errorCount && (
+ <>
+ {errorIcon}
+ {props.errorCount}
+ >
+ )}
{props.name}
{settingsIcon}
diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx
index 0e5a653bde..61bf20578f 100644
--- a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx
+++ b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx
@@ -4,24 +4,23 @@ import { useSelector } from "react-redux";
import { AppState } from "reducers";
import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer";
import SettingsControl, { Activities } from "./SettingsControl";
-import {
- useShowPropertyPane,
- useWidgetSelection,
-} from "utils/hooks/dragResizeHooks";
+import { useShowPropertyPane } from "utils/hooks/dragResizeHooks";
import AnalyticsUtil from "utils/AnalyticsUtil";
-import { WidgetType } from "constants/WidgetConstants";
+import { WidgetType, WidgetTypes } from "constants/WidgetConstants";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
+import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
-const PositionStyle = styled.div`
+const PositionStyle = styled.div<{ topRow: number }>`
position: absolute;
- top: -${(props) => props.theme.spaces[10]}px;
+ top: ${(props) =>
+ props.topRow > 2 ? `${-1 * props.theme.spaces[10]}px` : "calc(100%)"};
height: ${(props) => props.theme.spaces[10]}px;
- width: 100%;
- left: 0;
+ right: 0;
display: flex;
padding: 0 4px;
+ cursor: pointer;
`;
const ControlGroup = styled.div`
@@ -41,6 +40,8 @@ type WidgetNameComponentProps = {
parentId?: string;
type: WidgetType;
showControls?: boolean;
+ topRow: number;
+ errorCount: number;
};
export function WidgetNameComponent(props: WidgetNameComponentProps) {
@@ -51,7 +52,10 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) {
(state: AppState) => state.ui.propertyPane,
);
const selectedWidget = useSelector(
- (state: AppState) => state.ui.widgetDragResize.selectedWidget,
+ (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget,
+ );
+ const selectedWidgets = useSelector(
+ (state: AppState) => state.ui.widgetDragResize.selectedWidgets,
);
const focusedWidget = useSelector(
(state: AppState) => state.ui.widgetDragResize.focusedWidget,
@@ -72,6 +76,9 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) {
) {
PerformanceTracker.startTracking(
PerformanceTransactionName.OPEN_PROPERTY_PANE,
+ { widgetId: props.widgetId },
+ true,
+ [{ name: "widget_type", value: props.type }],
);
AnalyticsUtil.logEvent("PROPERTY_PANE_OPEN_CLICK", {
widgetType: props.type,
@@ -90,27 +97,39 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) {
e.preventDefault();
e.stopPropagation();
};
+ const showAsSelected =
+ selectedWidget === props.widgetId ||
+ selectedWidgets.includes(props.widgetId);
const showWidgetName =
props.showControls ||
- ((focusedWidget === props.widgetId || selectedWidget === props.widgetId) &&
+ ((focusedWidget === props.widgetId || showAsSelected) &&
!isDragging &&
- !isResizing);
+ !isResizing) ||
+ !!props.errorCount;
- let currentActivity = Activities.NONE;
+ let currentActivity =
+ props.type === WidgetTypes.MODAL_WIDGET
+ ? Activities.HOVERING
+ : Activities.NONE;
if (focusedWidget === props.widgetId) currentActivity = Activities.HOVERING;
- if (selectedWidget === props.widgetId) currentActivity = Activities.SELECTED;
+ if (showAsSelected) currentActivity = Activities.SELECTED;
if (
+ showAsSelected &&
propertyPaneState.isVisible &&
propertyPaneState.widgetId === props.widgetId
)
currentActivity = Activities.ACTIVE;
return showWidgetName ? (
-
+
diff --git a/app/client/src/components/editorComponents/form/FormDialogComponent.tsx b/app/client/src/components/editorComponents/form/FormDialogComponent.tsx
index 5a9b2375a1..5a50a66494 100644
--- a/app/client/src/components/editorComponents/form/FormDialogComponent.tsx
+++ b/app/client/src/components/editorComponents/form/FormDialogComponent.tsx
@@ -1,6 +1,8 @@
-import React, { ReactNode, useState, useCallback } from "react";
+import React, { ReactNode, useState, useEffect } from "react";
import { isPermitted } from "pages/Applications/permissionHelpers";
import Dialog from "components/ads/DialogComponent";
+import { useDispatch } from "react-redux";
+import { setShowAppInviteUsersDialog } from "actions/applicationActions";
type FormDialogComponentProps = {
isOpen?: boolean;
@@ -16,16 +18,17 @@ type FormDialogComponentProps = {
};
export function FormDialogComponent(props: FormDialogComponentProps) {
- const [isOpen, setIsOpen] = useState(!!props.isOpen);
+ const [isOpen, setIsOpenState] = useState(!!props.isOpen);
+ const dispatch = useDispatch();
- const onClose = useCallback(() => {
- setIsOpen(false);
- }, []);
+ const setIsOpen = (isOpen: boolean) => {
+ setIsOpenState(isOpen);
+ dispatch(setShowAppInviteUsersDialog(isOpen));
+ };
- // track if the dialog is open to close it when clicking cancel within the form
- const onOpening = useCallback(() => {
- setIsOpen(true);
- }, []);
+ useEffect(() => {
+ setIsOpen(!!props.isOpen);
+ }, [props.isOpen]);
const Form = props.Form;
@@ -40,14 +43,15 @@ export function FormDialogComponent(props: FormDialogComponentProps) {
setIsOpen(true)}
setMaxWidth={props.setMaxWidth}
+ setModalClose={() => setIsOpen(false)}
title={props.title}
trigger={props.trigger}
>
diff --git a/app/client/src/components/editorComponents/form/fields/DropdownFieldWrapper.tsx b/app/client/src/components/editorComponents/form/fields/DropdownFieldWrapper.tsx
index 38c510a93a..4664ecb6b1 100644
--- a/app/client/src/components/editorComponents/form/fields/DropdownFieldWrapper.tsx
+++ b/app/client/src/components/editorComponents/form/fields/DropdownFieldWrapper.tsx
@@ -1,4 +1,5 @@
import Dropdown from "components/ads/Dropdown";
+import { Colors } from "constants/Colors";
import React, { useEffect, useState } from "react";
import { WrappedFieldInputProps } from "redux-form";
@@ -39,6 +40,7 @@ function DropdownFieldWrapper(props: DropdownWrapperProps) {
return (
{
render() {
diff --git a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
index 1355fcb2e8..e44ee2dee8 100644
--- a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
+++ b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
@@ -1,4 +1,5 @@
import React, { ChangeEvent } from "react";
+import ReactDOM from "react-dom";
import {
BaseFieldProps,
change,
@@ -12,7 +13,8 @@ import CodeEditor, {
import { API_EDITOR_FORM_NAME } from "constants/forms";
import { AppState } from "reducers";
import { connect } from "react-redux";
-import _ from "lodash";
+import get from "lodash/get";
+import merge from "lodash/merge";
import {
DEFAULT_DATASOURCE,
EmbeddedRestDatasource,
@@ -36,6 +38,8 @@ import { DATA_SOURCES_EDITOR_ID_URL } from "constants/routes";
import Icon, { IconSize } from "components/ads/Icon";
import Text, { TextType } from "components/ads/Text";
import history from "utils/history";
+import { getDatasourceInfo } from "pages/Editor/APIEditor/DatasourceList";
+import * as FontFamilies from "constants/Fonts";
type ReduxStateProps = {
orgId: string;
@@ -59,26 +63,84 @@ type Props = EditorProps &
const DatasourceContainer = styled.div`
display: flex;
position: relative;
- width: calc(100% - 155px);
`;
+
+const hintContainerStyles: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ fontFamily: `${FontFamilies.TextFonts}`,
+};
+
+const mainContainerStyles: React.CSSProperties = {
+ display: "flex",
+ justifyContent: "space-between",
+ marginBottom: "6px",
+};
+
+const datasourceNameStyles: React.CSSProperties = {
+ fontSize: "14px",
+ fontWeight: 500,
+ color: "#090707",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+};
+const datasourceInfoStyles: React.CSSProperties = {
+ color: "#4B4848",
+ fontWeight: 400,
+ fontSize: "12px",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+};
+const italicInfoStyles = {
+ ...datasourceInfoStyles,
+ flexShrink: 0,
+ fontStyle: "italic",
+};
+
+//Avoiding styled components since ReactDOM.render cannot directly work with it
+function CustomHint(props: { datasource: Datasource }) {
+ return (
+
+
+ {props.datasource.name}
+
+ {getDatasourceInfo(props.datasource)}
+
+
+
+ {get(props.datasource, "datasourceConfiguration.url")}
+
+
+ );
+}
+
+const apiFormValueSelector = formValueSelector(API_EDITOR_FORM_NAME);
class EmbeddedDatasourcePathComponent extends React.Component {
handleDatasourceUrlUpdate = (datasourceUrl: string) => {
- const { datasource, pluginId, orgId } = this.props;
+ const { datasource, orgId, pluginId } = this.props;
const urlHasUpdated =
datasourceUrl !== datasource.datasourceConfiguration?.url;
if (urlHasUpdated) {
- this.props.updateDatasource({
- ...DEFAULT_DATASOURCE(pluginId, orgId),
+ const isDatasourceRemoved =
+ datasourceUrl.indexOf(datasource.datasourceConfiguration?.url) === -1;
+ let newDatasource = isDatasourceRemoved
+ ? { ...DEFAULT_DATASOURCE(pluginId, orgId) }
+ : { ...datasource };
+ newDatasource = {
+ ...newDatasource,
datasourceConfiguration: {
- ...datasource.datasourceConfiguration,
+ ...newDatasource.datasourceConfiguration,
url: datasourceUrl,
},
- });
+ };
+ this.props.updateDatasource(newDatasource);
}
};
handlePathUpdate = (path: string) => {
- const { value, onChange } = this.props.input;
+ const { onChange, value } = this.props.input;
if (onChange && value !== path) {
onChange(path);
}
@@ -95,8 +157,8 @@ class EmbeddedDatasourcePathComponent extends React.Component {
path: "",
};
}
- if ("id" in datasource && datasource.id) {
- const datasourceUrl = datasource.datasourceConfiguration.url;
+ if (datasource && datasource.hasOwnProperty("id")) {
+ const datasourceUrl = get(datasource, "datasourceConfiguration.url", "");
if (value.includes(datasourceUrl)) {
return {
datasourceUrl,
@@ -128,7 +190,7 @@ class EmbeddedDatasourcePathComponent extends React.Component {
typeof valueOrEvent === "string"
? valueOrEvent
: valueOrEvent.target.value;
- const { path, datasourceUrl } = this.parseInputValue(value);
+ const { datasourceUrl, path } = this.parseInputValue(value);
this.handlePathUpdate(path);
this.handleDatasourceUrlUpdate(datasourceUrl);
};
@@ -142,7 +204,7 @@ class EmbeddedDatasourcePathComponent extends React.Component {
"id" in datasource &&
datasource.id
) {
- const end = datasource.datasourceConfiguration.url.length;
+ const end = get(datasource, "datasourceConfiguration.url", "").length;
editorInstance.markText(
{ ch: 0, line: 0 },
{ ch: end, line: 0 },
@@ -172,17 +234,23 @@ class EmbeddedDatasourcePathComponent extends React.Component {
completeSingle: false,
hint: () => {
const list = datasourceList
- .filter((datasource) =>
+ .filter((datasource: Datasource) =>
(datasource.datasourceConfiguration?.url || "").includes(
parsed.datasourceUrl,
),
)
- .map((datasource) => ({
+ .map((datasource: Datasource) => ({
text: datasource.datasourceConfiguration.url,
data: datasource,
className: !datasource.isValid
- ? "datasource-hint invalid"
- : "datasource-hint",
+ ? "datasource-hint custom invalid"
+ : "datasource-hint custom",
+ render: (element: HTMLElement, self: any, data: any) => {
+ ReactDOM.render(
+ ,
+ element,
+ );
+ },
}));
const hints = {
list,
@@ -213,7 +281,7 @@ class EmbeddedDatasourcePathComponent extends React.Component {
datasource,
input: { value },
} = this.props;
- const datasourceUrl = _.get(datasource, "datasourceConfiguration.url", "");
+ const datasourceUrl = get(datasource, "datasourceConfiguration.url", "");
const displayValue = `${datasourceUrl}${value}`;
const input = {
...this.props.input,
@@ -237,7 +305,7 @@ class EmbeddedDatasourcePathComponent extends React.Component {
return (
- {datasource && !("id" in datasource) ? (
+ {displayValue && datasource && !("id" in datasource) ? (
) : datasource && "id" in datasource ? (
{
}
}
-const apiFormValueSelector = formValueSelector(API_EDITOR_FORM_NAME);
-
const mapStateToProps = (
state: AppState,
ownProps: { pluginId: string },
@@ -275,7 +341,7 @@ const mapStateToProps = (
(d) => d.id === datasourceFromAction.id,
);
if (datasourceFromDataSourceList) {
- datasourceMerged = _.merge(
+ datasourceMerged = merge(
{},
datasourceFromAction,
datasourceFromDataSourceList,
diff --git a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx
index 514a02a016..387dc51dad 100644
--- a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx
+++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx
@@ -14,8 +14,12 @@ import {
import Text, { Case, TextType } from "components/ads/Text";
import { Classes } from "components/ads/common";
-const KeyValueStackContainer = styled.div`
- padding: ${(props) => props.theme.spaces[4]}px
+type CustomStack = {
+ removeTopPadding?: boolean;
+};
+
+const KeyValueStackContainer = styled.div`
+ padding: ${(props) => (props.removeTopPadding ? 0 : props.theme.spaces[4])}px
${(props) => props.theme.spaces[14]}px
${(props) => props.theme.spaces[11] + 1}px
${(props) => props.theme.spaces[11] + 2}px;
@@ -89,19 +93,21 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) {
}, [props.fields, props.pushFields]);
return (
-
-
-
-
- Key
-
-
-
-
- Value
-
-
-
+
+ {!props.hideHeader && (
+
+
+
+ Key
+
+
+
+
+ Value
+
+
+
+ )}
{props.fields.length > 0 && (
<>
{props.fields.map((field: any, index: number) => {
@@ -130,7 +136,6 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) {
hoverInteraction
name={`${field}.key`}
placeholder={`Key ${index + 1}`}
- showLightningMenu={false}
theme={props.theme}
/>
@@ -224,6 +229,7 @@ type Props = {
placeholder?: string;
pushFields?: boolean;
dataTreePath?: string;
+ hideHeader?: boolean;
theme?: EditorTheme;
};
diff --git a/app/client/src/components/formControls/BaseControl.tsx b/app/client/src/components/formControls/BaseControl.tsx
index 9087cb0178..4c9f6d26d3 100644
--- a/app/client/src/components/formControls/BaseControl.tsx
+++ b/app/client/src/components/formControls/BaseControl.tsx
@@ -51,6 +51,7 @@ export interface ControlData {
isRequired?: boolean;
hidden?: HiddenType;
placeholderText?: string;
+ schema?: any;
}
export interface ControlFunctions {
diff --git a/app/client/src/components/formControls/CheckboxControl.tsx b/app/client/src/components/formControls/CheckboxControl.tsx
index 072ab52186..d2a7e02fe8 100644
--- a/app/client/src/components/formControls/CheckboxControl.tsx
+++ b/app/client/src/components/formControls/CheckboxControl.tsx
@@ -17,7 +17,7 @@ class CheckboxControl extends BaseControl {
}
render() {
- const { configProperty, label, info } = this.props;
+ const { configProperty, info, label } = this.props;
return ;
}
diff --git a/app/client/src/components/formControls/DropDownControl.tsx b/app/client/src/components/formControls/DropDownControl.tsx
index cf27809189..c62fc612b0 100644
--- a/app/client/src/components/formControls/DropDownControl.tsx
+++ b/app/client/src/components/formControls/DropDownControl.tsx
@@ -49,7 +49,7 @@ const customSelectStyles = {
class DropDownControl extends BaseControl {
render() {
- const { configProperty, options, label, isRequired, subtitle } = this.props;
+ const { configProperty, isRequired, label, options, subtitle } = this.props;
return (
diff --git a/app/client/src/components/formControls/DynamicInputTextControl.tsx b/app/client/src/components/formControls/DynamicInputTextControl.tsx
index 856f9b7c25..d6caaf97c3 100644
--- a/app/client/src/components/formControls/DynamicInputTextControl.tsx
+++ b/app/client/src/components/formControls/DynamicInputTextControl.tsx
@@ -16,11 +16,11 @@ export function InputText(props: {
name: string;
actionName: string;
}) {
- const { actionName, name, placeholder, label, isRequired } = props;
+ const { actionName, isRequired, label, name, placeholder } = props;
const dataTreePath = actionPathFromName(actionName, name);
return (
-
+
{label} {isRequired && "*"}
@@ -36,7 +36,7 @@ export function InputText(props: {
class DynamicInputTextControl extends BaseControl
{
render() {
- const { label, placeholderText, configProperty, actionName } = this.props;
+ const { actionName, configProperty, label, placeholderText } = this.props;
return (
{
);
const createTemplateButton = screen.getByText("Create");
userEvent.click(createTemplateButton);
- expect(screen.getByText("test plugin template")).toBeDefined();
+ // Test each word separately because they are in different spans
+ expect(screen.getByText("test")).toBeDefined();
+ expect(screen.getByText("plugin")).toBeDefined();
+ expect(screen.getByText("template")).toBeDefined();
waitFor(async () => {
await expect(screen.findByText("Create")).toBeNull();
});
diff --git a/app/client/src/components/formControls/DynamicTextFieldControl.tsx b/app/client/src/components/formControls/DynamicTextFieldControl.tsx
index 6d3d2b111e..ff5a441f4b 100644
--- a/app/client/src/components/formControls/DynamicTextFieldControl.tsx
+++ b/app/client/src/components/formControls/DynamicTextFieldControl.tsx
@@ -61,12 +61,12 @@ class DynamicTextControl extends BaseControl<
render() {
const {
- responseType,
- label,
- placeholderText,
actionName,
configProperty,
evaluationSubstitutionType,
+ label,
+ placeholderText,
+ responseType,
} = this.props;
const dataTreePath = actionPathFromName(actionName, configProperty);
const isNewQuery =
@@ -122,6 +122,7 @@ export interface DynamicTextFieldProps extends ControlProps {
responseType: string;
placeholderText?: string;
evaluationSubstitutionType: EvaluationSubstitutionType;
+ mutedHinting?: boolean;
}
const mapStateToProps = (state: AppState, props: DynamicTextFieldProps) => {
diff --git a/app/client/src/components/formControls/FieldArrayControl.tsx b/app/client/src/components/formControls/FieldArrayControl.tsx
new file mode 100644
index 0000000000..6dba80f6b7
--- /dev/null
+++ b/app/client/src/components/formControls/FieldArrayControl.tsx
@@ -0,0 +1,106 @@
+import React, { useEffect } from "react";
+import FormControl from "pages/Editor/FormControl";
+import Text, { TextType } from "components/ads/Text";
+import Icon, { IconSize } from "components/ads/Icon";
+import { Classes } from "components/ads/common";
+import styled from "styled-components";
+import { FieldArray } from "redux-form";
+import FormLabel from "components/editorComponents/FormLabel";
+import { ControlProps } from "./BaseControl";
+
+const CenteredIcon = styled(Icon)`
+ margin-top: 25px;
+ &.hide {
+ opacity: 0;
+ pointer-events: none;
+ }
+`;
+
+const PrimaryBox = styled.div`
+ display: flex;
+ flex-direction: column;
+ border: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg};
+ padding: 10px;
+ border-radius: 5px;
+`;
+
+const SecondaryBox = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: 5px;
+`;
+
+const AddMoreAction = styled.div`
+ width: fit-content;
+ cursor: pointer;
+ display: flex;
+ margin-top: 16px;
+ .${Classes.TEXT} {
+ margin-left: 8px;
+ color: #03b365;
+ }
+`;
+
+function NestedComponents(props: any) {
+ useEffect(() => {
+ if (props.fields.length < 1) {
+ props.fields.push({});
+ }
+ }, [props.fields.length]);
+ return (
+