Merge remote-tracking branch 'origin/release' into FIX/4731-dragging-and-dropping-table-widget-error-in-debugger
|
|
@ -57,4 +57,9 @@ APPSMITH_MAIL_SMTP_TLS_ENABLED=
|
|||
#APPSMITH_SENTRY_ENVIRONMENT=
|
||||
|
||||
# Configure cloud services
|
||||
# APPSMITH_CLOUD_SERVICES_BASE_URL="https://release-cs.appsmith.com"
|
||||
# APPSMITH_CLOUD_SERVICES_BASE_URL="https://release-cs.appsmith.com"
|
||||
|
||||
# Google Recaptcha Config
|
||||
APPSMITH_RECAPTCHA_SITE_KEY=
|
||||
APPSMITH_RECAPTCHA_SECRET_KEY=
|
||||
APPSMITH_RECAPTCHA_ENABLED=
|
||||
9
.github/config.json
vendored
|
|
@ -170,6 +170,10 @@
|
|||
"color": "50ba23",
|
||||
"description": ""
|
||||
},
|
||||
"Import-Export-App": {
|
||||
"name": "Import-Export-App",
|
||||
"color": "50ba23"
|
||||
},
|
||||
"JS": {
|
||||
"name": "JS",
|
||||
"color": "ffc1c2",
|
||||
|
|
@ -669,6 +673,11 @@
|
|||
"label": "Debugger",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "hasLabel",
|
||||
"label": "Import-Export-App",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "hasLabel",
|
||||
"label": "Omnibar",
|
||||
|
|
|
|||
6
.github/pull_request_template.md
vendored
|
|
@ -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
|
||||
|
|
|
|||
10
.github/workflows/build-rts.yml
vendored
|
|
@ -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'
|
||||
|
|
|
|||
58
.github/workflows/client-test.yml
vendored
|
|
@ -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/**"
|
||||
|
|
@ -29,10 +29,10 @@ jobs:
|
|||
# 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_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)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
|
|
@ -116,6 +116,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 +198,7 @@ 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 \
|
||||
|
|
@ -211,8 +212,23 @@ jobs:
|
|||
--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 \
|
||||
--env APPSMITH_MONGODB_URI=mongodb://localhost:27017/appsmith \
|
||||
--env APPSMITH_REDIS_URL=redis://localhost:6379 \
|
||||
--env APPSMITH_ENCRYPTION_PASSWORD=password \
|
||||
--env APPSMITH_ENCRYPTION_SALT=salt \
|
||||
--env APPSMITH_IS_SELF_HOSTED=false \
|
||||
--env APPSMITH_CLOUD_SERVICES_BASE_URL= \
|
||||
--env APPSMITH_CLOUD_SERVICES_USERNAME= \
|
||||
--env APPSMITH_CLOUD_SERVICES_PASSWORD= \
|
||||
${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-server:release-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 \
|
||||
|
|
@ -297,15 +313,15 @@ jobs:
|
|||
|
||||
- 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;
|
||||
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:
|
||||
|
|
@ -315,7 +331,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
|
||||
|
|
@ -349,6 +365,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')
|
||||
|
|
|
|||
59
.github/workflows/external-client-test.yml
vendored
|
|
@ -294,7 +294,7 @@ 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
|
||||
|
|
@ -310,8 +310,25 @@ jobs:
|
|||
--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
|
||||
|
||||
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= \
|
||||
--env APPSMITH_CLOUD_SERVICES_USERNAME= \
|
||||
--env APPSMITH_CLOUD_SERVICES_PASSWORD= \
|
||||
${{ 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
|
||||
|
|
@ -461,45 +478,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/<branch_name>. 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
|
||||
|
|
|
|||
10
.github/workflows/server.yml
vendored
|
|
@ -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/**"
|
||||
|
|
@ -102,6 +102,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'
|
||||
|
|
|
|||
25
README.md
|
|
@ -1,22 +1,24 @@
|
|||
<a href="https://app.appsmith.com/signup/?utm_source=github&utm_medium=social&utm_content=website&utm_campaign=null&utm_term=website"> <img src="static/images/git-banner-left.png" width="410px" alt="Appsmith - The Frontend Tool for Backend Devs"><img src="static/images/git-banner-right.png" width="410px" alt="Appsmith - The Frontend Tool for Backend Devs"></a>
|
||||
<a href="https://app.appsmith.com/signup/?utm_source=github&utm_medium=social&utm_content=website&utm_campaign=null&utm_term=website"> <img src="https://user-images.githubusercontent.com/30255708/120593421-1b107c80-c45d-11eb-86c9-06bdd66a143a.png" width="410px" alt="Appsmith - The Frontend Tool for Backend Devs"><img src="static/images/git-banner-right.png" width="410px" alt="Appsmith - The Frontend Tool for Backend Devs"></a>
|
||||
|
||||
<p>
|
||||
<b><a href="https://app.appsmith.com/signup/?utm_source=github&utm_medium=social&utm_content=website&utm_campaign=null&utm_term=website">Start Building</a></b>
|
||||
•
|
||||
<a href="https://www.appsmith.com/?utm_source=github&utm_medium=social&utm_content=website&utm_campaign=null&utm_term=website">Features</a>
|
||||
•
|
||||
•
|
||||
<a href="https://docs.appsmith.com/?utm_source=github&utm_medium=social&utm_content=appsmith_docs&utm_campaign=null&utm_term=appsmith_docs">Documentation</a>
|
||||
·
|
||||
•
|
||||
<a href="https://community.appsmith.com/">Community</a>
|
||||
•
|
||||
<a href="https://github.com/appsmithorg/appsmith/tree/update/readme#tutorials">Tutorials</a>
|
||||
·
|
||||
<a href="https://blog.appsmith.com/">Blog</a>
|
||||
·
|
||||
•
|
||||
<a href="https://app.appsmith.com/applications/602b8aef12ba0d29d3ec151c/pages/602b8aef12ba0d29d3ec151e">Events</a>
|
||||
•
|
||||
<a href="https://www.youtube.com/appsmith">Youtube</a>
|
||||
·
|
||||
•
|
||||
<a href="https://discord.gg/rBTTVJp">Discord</a>
|
||||
<br>
|
||||
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.
|
||||
<br>
|
||||
<p>We're launching the <a href = "https://www.notion.so/Betasmith-Join-the-Appsmith-Beta-Community-5c288dfd57bd4c4781c3bf02ddf9aa8a">Appsmith Community</a>! Be a part of the community that will help shape the future of Appsmith!
|
||||
<br>
|
||||
<br><p><img src="static/images/integrations.png" width="320px"></p>
|
||||
<br>
|
||||
</p>
|
||||
|
|
@ -72,6 +74,7 @@ Issues are inevitable. When you have one, our entire team is around to help—
|
|||
- 💬 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)
|
||||
<br><br>
|
||||
|
||||
## Demos
|
||||
|
|
@ -112,7 +115,7 @@ We love our contributors! We're committed to fostering an open and welcoming env
|
|||
- 👾 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)
|
||||
|
||||
#### Currently Contributing (36)
|
||||
#### Top Contributors (36)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove / modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
|
|
@ -154,7 +157,7 @@ We love our contributors! We're committed to fostering an open and welcoming env
|
|||
<a href="http://dwayne.io"><img src="https://avatars.githubusercontent.com/u/347097?v=4?s=100" width="60px;" alt="wayne Forde"/></a>
|
||||
<a href="https://github.com/monarch0111"><img src="https://avatars.githubusercontent.com/u/2965013?v=4?s=100" width="60px;" alt="Abhishek"/></a>
|
||||
<a href="http://www.navdeepsingh.in/"><img src="https://avatars.githubusercontent.com/u/2968787?v=4?s=100" width="60px;" alt=""/></a>
|
||||
|
||||
<a href="https://github.com/marks0351"><img src="https://avatars.githubusercontent.com/u/35134347?v=4?s=100" width="60px;" alt="Ashok"/></a>
|
||||
<br>
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
|
|
|||
15
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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
174
app/client/cypress/fixtures/application-file.json
Normal file
|
|
@ -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": {}
|
||||
}
|
||||
1
app/client/cypress/fixtures/basicDsl.json
Normal file
|
|
@ -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":330,"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":[]}}
|
||||
58
app/client/cypress/fixtures/debuggerDependencyDsl.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
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(() => {
|
||||
dsl.dsl.version = 23; // latest migrated version
|
||||
cy.addDsl(dsl);
|
||||
});
|
||||
|
||||
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.get(widgetsPage.defaultInput).type("A");
|
||||
cy.get(commonlocators.editPropCrossButton).click({ force: true });
|
||||
cy.wait("@updateLayout").then((httpResponse) => {
|
||||
parentApplicationDsl = httpResponse.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");
|
||||
|
|
@ -24,17 +32,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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,21 +1,30 @@
|
|||
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(() => {
|
||||
dsl.dsl.version = 23; // latest migrated version
|
||||
cy.addDsl(dsl);
|
||||
});
|
||||
|
||||
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.get(widgetsPage.defaultInput).type("A");
|
||||
cy.get(commonlocators.editPropCrossButton).click({ force: true });
|
||||
cy.wait("@updateLayout").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");
|
||||
|
|
@ -29,18 +38,20 @@ 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);
|
||||
});
|
||||
cy.get("@getPage").then((httpResponse) => {
|
||||
expect(httpResponse.status).to.deep.equal(200);
|
||||
});
|
||||
// check that forked application has same dsl
|
||||
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),
|
||||
);
|
||||
});
|
||||
cy.NavigateToHome();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
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.openPropertyPane("inputwidget");
|
||||
cy.contains(".t--dependencies-item", "Button1").click();
|
||||
cy.contains(".t--references-item", "Input1");
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
@ -30,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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe("Leave organization test spec", function() {
|
|||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -88,7 +88,7 @@ describe("Entity explorer tests related to copy query", function() {
|
|||
});
|
||||
|
||||
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})`)
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ describe("Entity explorer datasource structure", function() {
|
|||
200,
|
||||
);
|
||||
|
||||
cy.get(commonlocators.entityExplorersearch).clear();
|
||||
cy.get(commonlocators.entityExplorersearch).clear({ force: true });
|
||||
|
||||
cy.deleteDatasource(datasourceName);
|
||||
});
|
||||
|
|
@ -102,7 +102,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)
|
||||
|
|
@ -158,7 +158,7 @@ describe("Entity explorer datasource structure", function() {
|
|||
200,
|
||||
);
|
||||
|
||||
cy.get(commonlocators.entityExplorersearch).clear();
|
||||
cy.get(commonlocators.entityExplorersearch).clear({ force: true });
|
||||
cy.deleteDatasource(datasourceName);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,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);
|
||||
|
|
@ -95,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);
|
||||
|
|
@ -129,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);
|
||||
|
|
|
|||
|
|
@ -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,6 +60,11 @@
|
|||
"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]",
|
||||
|
|
|
|||
|
|
@ -7,5 +7,8 @@
|
|||
"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",
|
||||
"settings": "li:contains('Settings')",
|
||||
"query": "li:contains('Query')",
|
||||
"switch": ".t--form-control-SWITCH input"
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
85
app/client/cypress/manual_TestSuite/Tab_Widget_Spec.js
Normal file
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
|
@ -167,7 +167,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")
|
||||
|
|
@ -455,10 +459,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(
|
||||
|
|
@ -470,10 +473,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(
|
||||
|
|
@ -630,8 +633,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(
|
||||
|
|
@ -645,8 +647,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);
|
||||
|
|
@ -656,10 +658,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(
|
||||
|
|
@ -1644,6 +1645,7 @@ Cypress.Commands.add("addDsl", (dsl) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
cy.wait("@updateLayout");
|
||||
});
|
||||
|
||||
Cypress.Commands.add("DeleteAppByApi", () => {
|
||||
|
|
@ -2210,6 +2212,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",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
"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",
|
||||
|
|
@ -75,8 +75,8 @@
|
|||
"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",
|
||||
"json-fn": "^1.1.1",
|
||||
"lint-staged": "^9.2.5",
|
||||
|
|
@ -125,6 +125,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",
|
||||
|
|
@ -152,7 +153,6 @@
|
|||
"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",
|
||||
"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",
|
||||
|
|
@ -164,7 +164,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"
|
||||
|
|
@ -237,6 +238,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",
|
||||
|
|
|
|||
22
app/client/patches/@blueprintjs+core+3.36.0.patch
Normal file
|
|
@ -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;
|
||||
|
|
@ -204,7 +204,8 @@
|
|||
intercomAppID: APP_ID,
|
||||
mailEnabled: parseConfig("__APPSMITH_MAIL_ENABLED__"),
|
||||
disableTelemetry: DISABLE_TELEMETRY === "" || DISABLE_TELEMETRY,
|
||||
cloudServicesBaseUrl: parseConfig("__APPSMITH_CLOUD_SERVICES_BASE_URL__") || "https://cs.appsmith.com"
|
||||
cloudServicesBaseUrl: parseConfig("__APPSMITH_CLOUD_SERVICES_BASE_URL__") || "https://cs.appsmith.com",
|
||||
googleRecaptchaSiteKey: parseConfig("__APPSMITH_RECAPTCHA_SITE_KEY__"),
|
||||
};
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -19,17 +19,6 @@ 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,
|
||||
|
|
|
|||
31
app/client/src/actions/notificationActions.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import { AppsmithNotification } from "entities/Notification";
|
||||
|
||||
export const fetchNotificationsRequest = () => ({
|
||||
type: ReduxActionTypes.FETCH_NOTIFICATIONS_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchNotificationsSuccess = (payload: {
|
||||
notifications: Array<AppsmithNotification>;
|
||||
}) => ({
|
||||
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,
|
||||
});
|
||||
|
|
@ -104,9 +104,10 @@ export const updateAndSaveLayout = (
|
|||
};
|
||||
};
|
||||
|
||||
export const saveLayout = () => {
|
||||
export const saveLayout = (isRetry?: boolean) => {
|
||||
return {
|
||||
type: ReduxActionTypes.SAVE_PAGE_INIT,
|
||||
payload: { isRetry },
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,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}`;
|
||||
|
|
@ -212,6 +219,21 @@ class ApplicationApi extends Api {
|
|||
request.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
static importApplicationToOrg(
|
||||
request: ImportApplicationRequest,
|
||||
): AxiosPromise<ApiResponse> {
|
||||
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;
|
||||
|
|
|
|||
18
app/client/src/api/NotificationsAPI.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { AxiosPromise } from "axios";
|
||||
import Api from "./Api";
|
||||
import { ApiResponse } from "./ApiResponses";
|
||||
|
||||
class NotificaitonsApi extends Api {
|
||||
static baseURL = "v1/notifications";
|
||||
|
||||
static fetchNotifications(): AxiosPromise<ApiResponse> {
|
||||
return Api.get(NotificaitonsApi.baseURL);
|
||||
}
|
||||
|
||||
// TODO update mark all as read notifications api
|
||||
static markAllNotificationsAsRead(): AxiosPromise<ApiResponse> {
|
||||
return Api.get(NotificaitonsApi.baseURL);
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificaitonsApi;
|
||||
3
app/client/src/assets/icons/ads/bell.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.64706 6.35294C1.64706 2.84431 4.49137 0 8 0C11.5086 0 14.3529 2.84431 14.3529 6.35294V12.25H16V14.5H0V12.25H1.64706V6.35294ZM8 18C9.36447 18 10.4706 16.8807 10.4706 15.5H5.52941C5.52941 16.8807 6.63553 18 8 18Z" fill="#939090"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 384 B |
3
app/client/src/assets/icons/ads/download.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4547 16.4284H13.117H6.88326H4.54559C2.8243 16.4284 1.42871 14.8933 1.42871 12.9999C1.42871 11.3987 2.43157 10.0641 3.7804 9.68786C3.93001 6.28329 6.47884 3.57129 9.61053 3.57129C12.7072 3.57129 15.2349 6.22243 15.4352 9.57386C17.183 9.56015 18.5716 11.1167 18.5716 12.9999C18.5716 14.8933 17.176 16.4284 15.4547 16.4284ZM12.7266 11.4286L9.99929 14.8572L7.27202 11.4286L8.83045 11.4286L8.83045 8.00004L11.1681 8.00003V11.4286L12.7266 11.4286Z" fill="#939090"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 616 B |
3
app/client/src/assets/icons/ads/upload_success.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.6364 16H16.3636L13.6364 16V11.7333H15.8182L12.0001 7.46667L8.18188 11.7333H10.3636V16L7.63636 16H4.36364C1.95382 16 0 14.0896 0 11.7333C0 9.7408 1.404 8.08 3.29236 7.61173C3.50182 3.37493 7.07018 0 11.4545 0C15.7898 0 19.3287 3.2992 19.6091 7.46987C22.056 7.4528 24 9.38987 24 11.7333C24 14.0896 22.0462 16 19.6364 16Z" fill="#03B365"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
32
app/client/src/assets/images/InspectElement.svg
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<svg width="121" height="125" viewBox="0 0 121 125" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="59.5699" cy="117.637" rx="53.0523" ry="6.40901" fill="#C5C5C5" fill-opacity="0.6"/>
|
||||
<circle cx="21.2045" cy="34.0209" r="3.2045" stroke="#C5C5C5" stroke-width="1.78028"/>
|
||||
<circle cx="112.219" cy="91.6024" r="2.02719" stroke="#939090" stroke-width="1.1584"/>
|
||||
<path d="M79.8922 11.0464V18.8308" stroke="#939090" stroke-width="1.70907"/>
|
||||
<path d="M76 14.9385H83.7844" stroke="#939090" stroke-width="1.70907"/>
|
||||
<path d="M8.66328 84.0464V93.7769" stroke="#939090" stroke-width="2.13634"/>
|
||||
<path d="M3.79785 88.9118H13.5284" stroke="#939090" stroke-width="2.13634"/>
|
||||
<circle cx="50.0014" cy="3.2045" r="3.2045" fill="#C5C5C5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M35 34.0464C35 31.8372 36.7909 30.0464 39 30.0464H117C119.209 30.0464 121 31.8372 121 34.0464V55.0464C121 57.2555 119.209 59.0464 117 59.0464H39C36.7909 59.0464 35 57.2555 35 55.0464V34.0464Z" fill="#C5C5C5"/>
|
||||
<circle cx="50" cy="44.0464" r="9" fill="#E8E8E8"/>
|
||||
<path d="M45.6006 43.6451L49.0763 47.0461L55.1865 40.2441" stroke="#A9A7A7" stroke-width="2.50226"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M65 41.0464C65 39.9418 65.8954 39.0464 67 39.0464H92C93.1046 39.0464 94 39.9418 94 41.0464C94 42.151 93.1046 43.0464 92 43.0464H67C65.8954 43.0464 65 42.151 65 41.0464Z" fill="#A9A7A7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M95 41.0464C95 39.9418 95.8954 39.0464 97 39.0464H109C110.105 39.0464 111 39.9418 111 41.0464C111 42.151 110.105 43.0464 109 43.0464H97C95.8954 43.0464 95 42.151 95 41.0464Z" fill="#A9A7A7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M88 48.0464C88 46.9418 88.8954 46.0464 90 46.0464H99C100.105 46.0464 101 46.9418 101 48.0464C101 49.151 100.105 50.0464 99 50.0464H90C88.8954 50.0464 88 49.151 88 48.0464Z" fill="#A9A7A7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 73.0464C19 70.8372 20.7909 69.0464 23 69.0464H92C94.2091 69.0464 96 70.8372 96 73.0464V94.0464C96 96.2555 94.2091 98.0464 92 98.0464H23C20.7909 98.0464 19 96.2555 19 94.0464V73.0464Z" fill="#C5C5C5"/>
|
||||
<circle cx="34" cy="85.0464" r="9" fill="#E8E8E8"/>
|
||||
<path d="M29.6006 84.6451L33.0763 88.0461L39.1865 81.2441" stroke="#A9A7A7" stroke-width="2.50226"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M49 82.0464C49 80.9418 49.8954 80.0464 51 80.0464H60C61.1046 80.0464 62 80.9418 62 82.0464C62 83.151 61.1046 84.0464 60 84.0464H51C49.8954 84.0464 49 83.151 49 82.0464Z" fill="#A9A7A7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M63 82.0464C63 80.9418 63.8954 80.0464 65 80.0464H81C82.1046 80.0464 83 80.9418 83 82.0464C83 83.151 82.1046 84.0464 81 84.0464H65C63.8954 84.0464 63 83.151 63 82.0464Z" fill="#A9A7A7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M68 89.0464C68 87.9418 67.1046 87.0464 66 87.0464H51C49.8954 87.0464 49 87.9418 49 89.0464C49 90.151 49.8954 91.0464 51 91.0464H66C67.1046 91.0464 68 90.151 68 89.0464Z" fill="#A9A7A7"/>
|
||||
<rect y="47.0464" width="86" height="30" rx="4" fill="#F1F0EE"/>
|
||||
<circle cx="15" cy="62.0464" r="9" fill="#C5C5C5"/>
|
||||
<path d="M10.1123 60.0712L14.9998 64.5083L24.4993 53.9629" stroke="#716E6E" stroke-width="3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 59.0464C30 57.9418 30.8954 57.0464 32 57.0464H43C44.1046 57.0464 45 57.9418 45 59.0464C45 60.151 44.1046 61.0464 43 61.0464H32C30.8954 61.0464 30 60.151 30 59.0464Z" fill="#918E8E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M46 59.0464C46 57.9418 46.8954 57.0464 48 57.0464H74C75.1046 57.0464 76 57.9418 76 59.0464C76 60.151 75.1046 61.0464 74 61.0464H48C46.8954 61.0464 46 60.151 46 59.0464Z" fill="#918E8E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 66.0464C30 64.9418 30.8954 64.0464 32 64.0464H50C51.1046 64.0464 52 64.9418 52 66.0464C52 67.151 51.1046 68.0464 50 68.0464H32C30.8954 68.0464 30 67.151 30 66.0464Z" fill="#C5C5C5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M53 66.0464C53 64.9418 53.8954 64.0464 55 64.0464H64C65.1046 64.0464 66 64.9418 66 66.0464C66 67.151 65.1046 68.0464 64 68.0464H55C53.8954 68.0464 53 67.151 53 66.0464Z" fill="#C5C5C5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M105.707 79.8053L89.4521 110.376L79.7396 105.211L95.9941 74.6411L105.707 79.8053Z" fill="#716E6E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M86.3197 116.267L89.0288 111.172L79.3164 106.007L76.6073 111.102C74.9988 114.128 84.7112 119.292 86.3197 116.267Z" fill="#C5C5C5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M96.6612 73.7703L107.195 65.4831L106.214 78.8499L96.6612 73.7703Z" fill="#C5C5C5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 36 KiB |
BIN
app/client/src/assets/images/comments-onboarding/step-5.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
1
app/client/src/assets/lottie/pulse-dot.json
Normal file
|
|
@ -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":[]}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo, useRef, useState } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
|
||||
|
|
@ -10,14 +10,16 @@ import {
|
|||
shouldShowResolved as shouldShowResolvedSelector,
|
||||
appCommentsFilter as appCommentsFilterSelector,
|
||||
} from "selectors/commentsSelectors";
|
||||
import { getCurrentApplicationId } from "selectors/editorSelectors";
|
||||
import {
|
||||
getCurrentApplicationId,
|
||||
getCurrentPageId,
|
||||
} from "selectors/editorSelectors";
|
||||
|
||||
import CommentThread from "comments/CommentThread/connectedCommentThread";
|
||||
import AppCommentsPlaceholder from "./AppCommentsPlaceholder";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
|
||||
import useResizeObserver from "utils/hooks/useResizeObserver";
|
||||
import { get } from "lodash";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -39,14 +41,7 @@ function AppCommentThreads() {
|
|||
const currentUser = useSelector(getCurrentUser);
|
||||
const currentUsername = currentUser?.username;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [appThreadsHeightEqZero, setAppThreadsHeightEqZero] = useState(true);
|
||||
|
||||
useResizeObserver(containerRef.current, (entries) => {
|
||||
const { height } = get(entries, "0.contentRect", {});
|
||||
setAppThreadsHeightEqZero(height === 0);
|
||||
});
|
||||
const currentPageId = useSelector(getCurrentPageId);
|
||||
|
||||
const commentThreadIds = useMemo(
|
||||
() =>
|
||||
|
|
@ -56,6 +51,7 @@ function AppCommentThreads() {
|
|||
shouldShowResolved,
|
||||
appCommentsFilter,
|
||||
currentUsername,
|
||||
currentPageId,
|
||||
),
|
||||
[
|
||||
appCommentThreadIds,
|
||||
|
|
@ -63,23 +59,32 @@ function AppCommentThreads() {
|
|||
shouldShowResolved,
|
||||
appCommentsFilter,
|
||||
currentUsername,
|
||||
currentPageId,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div ref={containerRef}>
|
||||
{commentThreadIds.map((commentThreadId: string) => (
|
||||
<CommentThread
|
||||
commentThreadId={commentThreadId}
|
||||
hideChildren
|
||||
hideInput
|
||||
key={commentThreadId}
|
||||
showSubheader
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{appThreadsHeightEqZero && <AppCommentsPlaceholder />}
|
||||
{commentThreadIds.length > 0 && (
|
||||
<Virtuoso
|
||||
data={commentThreadIds}
|
||||
itemContent={(_index, commentThreadId) => (
|
||||
/** Keeping this as a fail safe: since zero
|
||||
* height elements throw an error
|
||||
* */
|
||||
<div style={{ minHeight: 1 }}>
|
||||
<CommentThread
|
||||
commentThreadId={commentThreadId}
|
||||
hideChildren
|
||||
hideInput
|
||||
key={commentThreadId}
|
||||
showSubheader
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{commentThreadIds.length === 0 && <AppCommentsPlaceholder />}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import AppCommentsHeader from "./AppCommentsHeader";
|
|||
import AppCommentThreads from "./AppCommentThreads";
|
||||
import Container from "./Container";
|
||||
|
||||
function AppComments() {
|
||||
function AppComments(props: { isInline?: boolean }) {
|
||||
const isCommentMode = useSelector(commentModeSelector);
|
||||
|
||||
if (!isCommentMode) return null;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container isInline={props.isInline}>
|
||||
<AppCommentsHeader />
|
||||
<AppCommentThreads />
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -18,10 +18,6 @@ import {
|
|||
|
||||
import "@blueprintjs/popover2/lib/css/blueprint-popover2.css";
|
||||
|
||||
import useProceedToNextTourStep from "utils/hooks/useProceedToNextTourStep";
|
||||
import { TourType } from "entities/Tour";
|
||||
import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper";
|
||||
|
||||
export const options = [
|
||||
{ label: "Show all comments", value: "show-all" },
|
||||
{ label: "Show only pinned", value: "show-only-pinned" },
|
||||
|
|
@ -91,10 +87,6 @@ const AppCommentsFilter = withTheme(({ theme }: { theme: Theme }) => {
|
|||
});
|
||||
|
||||
function AppCommentsFilterPopover() {
|
||||
const proceedToNextTourStep = useProceedToNextTourStep(
|
||||
TourType.COMMENTS_TOUR,
|
||||
3,
|
||||
);
|
||||
useSetResolvedFilterFromQuery();
|
||||
|
||||
return (
|
||||
|
|
@ -114,13 +106,7 @@ function AppCommentsFilterPopover() {
|
|||
placement={"bottom-end"}
|
||||
portalClassName="comment-context-menu"
|
||||
>
|
||||
<TourTooltipWrapper
|
||||
onClick={proceedToNextTourStep}
|
||||
tourIndex={3}
|
||||
tourType={TourType.COMMENTS_TOUR}
|
||||
>
|
||||
<Icon name="filter" size={IconSize.LARGE} />
|
||||
</TourTooltipWrapper>
|
||||
<Icon name="filter" size={IconSize.LARGE} />
|
||||
</Popover2>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,19 @@ 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: 250px;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: ${(props) => props.theme.smallHeaderHeight};
|
||||
${(props) =>
|
||||
!props.isInline
|
||||
? `
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: ${props.theme.smallHeaderHeight};
|
||||
`
|
||||
: `
|
||||
position: unset;
|
||||
`}
|
||||
z-index: ${Layers.appComments};
|
||||
height: calc(100% - ${(props) => props.theme.smallHeaderHeight});
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import history from "utils/history";
|
|||
|
||||
import UserApi from "api/UserApi";
|
||||
|
||||
import { getCommentThreadURL } from "../utils";
|
||||
|
||||
import {
|
||||
deleteCommentRequest,
|
||||
markThreadAsReadRequest,
|
||||
|
|
@ -47,6 +49,10 @@ 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,
|
||||
getCurrentPageId,
|
||||
} from "selectors/editorSelectors";
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
|
|
@ -268,26 +274,23 @@ function CommentCard({
|
|||
pinnedBy = "You";
|
||||
}
|
||||
|
||||
const getCommentURL = () => {
|
||||
const url = new URL(window.location.href);
|
||||
// we only link the comment thread currently
|
||||
// url.searchParams.set("commentId", commentId);
|
||||
url.searchParams.set("commentThreadId", commentThreadId);
|
||||
url.searchParams.set("isCommentMode", "true");
|
||||
if (commentThread.resolvedState?.active) {
|
||||
url.searchParams.set("isResolved", "true");
|
||||
}
|
||||
return url;
|
||||
};
|
||||
const pageId = useSelector(getCurrentPageId);
|
||||
const applicationId = useSelector(getCurrentApplicationId);
|
||||
|
||||
const copyCommentLink = useCallback(() => {
|
||||
const url = getCommentURL();
|
||||
copy(url.toString());
|
||||
const commentThreadURL = getCommentThreadURL({
|
||||
applicationId,
|
||||
commentThreadId,
|
||||
isResolved: !!commentThread?.resolvedState?.active,
|
||||
pageId,
|
||||
});
|
||||
|
||||
const copyCommentLink = () => {
|
||||
copy(commentThreadURL.toString());
|
||||
Toaster.show({
|
||||
text: createMessage(LINK_COPIED_SUCCESSFULLY),
|
||||
variant: Variant.success,
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const pin = useCallback(() => {
|
||||
dispatch(
|
||||
|
|
@ -330,8 +333,9 @@ function CommentCard({
|
|||
// Dont make inline cards clickable
|
||||
const handleCardClick = () => {
|
||||
if (inline) return;
|
||||
const url = getCommentURL();
|
||||
history.push(`${url.pathname}${url.search}${url.hash}`);
|
||||
history.push(
|
||||
`${commentThreadURL.pathname}${commentThreadURL.search}${commentThreadURL.hash}`,
|
||||
);
|
||||
if (!commentThread.isViewed) {
|
||||
dispatch(markThreadAsReadRequest(commentThreadId));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import React from "react";
|
|||
import styled, { withTheme } from "styled-components";
|
||||
import Icon, { IconSize } from "components/ads/Icon";
|
||||
import { Theme } from "constants/DefaultTheme";
|
||||
import { TourType } from "entities/Tour";
|
||||
import useProceedToNextTourStep from "utils/hooks/useProceedToNextTourStep";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -47,15 +45,9 @@ const ResolveCommentButton = withTheme(
|
|||
const strokeColorPath = resolved ? resolvedPathColor : unresolvedColor;
|
||||
const fillColor = resolved ? resolvedFillColor : unresolvedFillColor;
|
||||
|
||||
const proceedToNextTourStep = useProceedToNextTourStep(
|
||||
TourType.COMMENTS_TOUR,
|
||||
2,
|
||||
);
|
||||
|
||||
const _handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleClick();
|
||||
proceedToNextTourStep();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { CommentThread } from "entities/Comments/CommentsInterfaces";
|
|||
import { RawDraftContentState } from "draft-js";
|
||||
|
||||
import styled from "styled-components";
|
||||
import { animated, useTransition } from "react-spring";
|
||||
import { animated } from "react-spring";
|
||||
import { AppState } from "reducers";
|
||||
|
||||
const ThreadContainer = styled(animated.div)<{
|
||||
|
|
@ -74,22 +74,6 @@ function CommentThreadContainer({
|
|||
const isThreadVisible =
|
||||
shouldShowResolved || !commentThread?.resolvedState?.active;
|
||||
|
||||
const config = inline
|
||||
? {
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 },
|
||||
config: { duration: 300 },
|
||||
}
|
||||
: {
|
||||
from: { opacity: 0, transform: "translateX(-100%)" },
|
||||
enter: { opacity: 1, transform: "translateX(0)" },
|
||||
leave: { opacity: 0, transform: "translateX(-100%)" },
|
||||
config: { duration: 300 },
|
||||
};
|
||||
|
||||
const transition = useTransition(isThreadVisible, null, config);
|
||||
|
||||
const isVisible = useSelector(
|
||||
(state: AppState) =>
|
||||
state.ui.comments.visibleCommentThreadId === commentThreadId,
|
||||
|
|
@ -140,69 +124,55 @@ function CommentThreadContainer({
|
|||
|
||||
if (!commentThread) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{transition.map(
|
||||
({ item: show, props: springProps }: { item: boolean; props: any }) =>
|
||||
show ? (
|
||||
<animated.div key={commentThread.id} style={springProps}>
|
||||
<ThreadContainer
|
||||
inline={inline}
|
||||
pinned={commentThread.pinnedState?.active}
|
||||
tabIndex={0}
|
||||
visible={isVisible}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<CommentsContainer inline={inline} ref={commentsContainerRef}>
|
||||
{parentComment && (
|
||||
<CommentCard
|
||||
comment={parentComment}
|
||||
commentThreadId={commentThreadId}
|
||||
inline={inline}
|
||||
isParentComment
|
||||
key={parentComment.id}
|
||||
numberOfReplies={numberOfReplies}
|
||||
resolved={!!commentThread.resolvedState?.active}
|
||||
showReplies={hideChildren}
|
||||
showSubheader={showSubheader}
|
||||
toggleResolved={resolveCommentThread}
|
||||
unread={!commentThread.isViewed}
|
||||
visible={isVisible}
|
||||
/>
|
||||
)}
|
||||
{!hideChildren &&
|
||||
childComments &&
|
||||
childComments.length > 0 && (
|
||||
<ChildComments>
|
||||
{childComments.map((comment) => (
|
||||
<CommentCard
|
||||
comment={comment}
|
||||
commentThreadId={commentThreadId}
|
||||
inline={inline}
|
||||
key={comment.id}
|
||||
visible={isVisible}
|
||||
/>
|
||||
))}
|
||||
</ChildComments>
|
||||
)}
|
||||
<div ref={messagesBottomRef} />
|
||||
</CommentsContainer>
|
||||
{!isScrolledToBottom && (
|
||||
<ScrollToLatest scrollToBottom={scrollToBottom} />
|
||||
)}
|
||||
</div>
|
||||
{!hideInput && (
|
||||
<AddCommentInput
|
||||
onCancel={handleCancel}
|
||||
onSave={addComment}
|
||||
/>
|
||||
)}
|
||||
</ThreadContainer>
|
||||
</animated.div>
|
||||
) : null,
|
||||
return isThreadVisible ? (
|
||||
<ThreadContainer
|
||||
inline={inline}
|
||||
pinned={commentThread.pinnedState?.active}
|
||||
tabIndex={0}
|
||||
visible={isVisible}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<CommentsContainer inline={inline} ref={commentsContainerRef}>
|
||||
{parentComment && (
|
||||
<CommentCard
|
||||
comment={parentComment}
|
||||
commentThreadId={commentThreadId}
|
||||
inline={inline}
|
||||
isParentComment
|
||||
key={parentComment.id}
|
||||
numberOfReplies={numberOfReplies}
|
||||
resolved={!!commentThread.resolvedState?.active}
|
||||
showReplies={hideChildren}
|
||||
showSubheader={showSubheader}
|
||||
toggleResolved={resolveCommentThread}
|
||||
unread={!commentThread.isViewed}
|
||||
visible={isVisible}
|
||||
/>
|
||||
)}
|
||||
{!hideChildren && childComments && childComments.length > 0 && (
|
||||
<ChildComments>
|
||||
{childComments.map((comment) => (
|
||||
<CommentCard
|
||||
comment={comment}
|
||||
commentThreadId={commentThreadId}
|
||||
inline={inline}
|
||||
key={comment.id}
|
||||
visible={isVisible}
|
||||
/>
|
||||
))}
|
||||
</ChildComments>
|
||||
)}
|
||||
<div ref={messagesBottomRef} />
|
||||
</CommentsContainer>
|
||||
{!isScrolledToBottom && (
|
||||
<ScrollToLatest scrollToBottom={scrollToBottom} />
|
||||
)}
|
||||
</div>
|
||||
{!hideInput && (
|
||||
<AddCommentInput onCancel={handleCancel} onSave={addComment} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
</ThreadContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default CommentThreadContainer;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import ModalComponent from "components/designSystems/blueprint/ModalComponent";
|
||||
import { Layers } from "constants/Layers";
|
||||
|
||||
function ShowcaseCarouselModal({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
|
@ -10,12 +11,14 @@ function ShowcaseCarouselModal({ children }: { children: React.ReactNode }) {
|
|||
data-cy={"help-modal"}
|
||||
hasBackDrop={false}
|
||||
isOpen
|
||||
left={25}
|
||||
onClose={() => {
|
||||
console.log("handle close");
|
||||
}}
|
||||
right={25}
|
||||
overlayClassName="comments-onboarding-carousel"
|
||||
scrollContents
|
||||
width={325}
|
||||
zIndex={Layers.appComments}
|
||||
>
|
||||
{children}
|
||||
</ModalComponent>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import CommentsOnboardingStep1 from "assets/images/comments-onboarding/step-1.pn
|
|||
import CommentsOnboardingStep2 from "assets/images/comments-onboarding/step-2.png";
|
||||
import CommentsOnboardingStep3 from "assets/images/comments-onboarding/step-3.png";
|
||||
import CommentsOnboardingStep4 from "assets/images/comments-onboarding/step-4.png";
|
||||
import CommentsOnboardingStep5 from "assets/images/comments-onboarding/step-5.png";
|
||||
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import { Theme } from "constants/DefaultTheme";
|
||||
|
|
@ -25,19 +26,39 @@ import { setCommentsIntroSeen } from "utils/storage";
|
|||
|
||||
import { updateUserDetails } from "actions/userActions";
|
||||
|
||||
const title1 = "Introducing Live Comments";
|
||||
const title2 = "Give feedback";
|
||||
const title3 = "Invite other people to your conversations";
|
||||
const title4 = "You are all set!";
|
||||
|
||||
const content1 =
|
||||
"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.";
|
||||
const content2 =
|
||||
"Comment on your co-worker’s work and share your thoughts on what works and what needs change.";
|
||||
const content3 =
|
||||
"When leaving a comment you can tag oter 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.";
|
||||
const content4 =
|
||||
"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.";
|
||||
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: CommentsOnboardingStep1,
|
||||
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: CommentsOnboardingStep2,
|
||||
},
|
||||
{
|
||||
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: CommentsOnboardingStep3,
|
||||
},
|
||||
{
|
||||
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: CommentsOnboardingStep4,
|
||||
},
|
||||
{
|
||||
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: CommentsOnboardingStep5,
|
||||
},
|
||||
];
|
||||
|
||||
const IntroContentContainer = styled.div`
|
||||
padding: ${(props) => props.theme.spaces[5]}px;
|
||||
|
|
@ -83,31 +104,10 @@ const getSteps = (
|
|||
initialProfileFormValues: { emailAddress?: string; displayName?: string },
|
||||
emailDisabled: boolean,
|
||||
) => [
|
||||
{
|
||||
...introSteps.slice(0, 4).map((stepConfig: any) => ({
|
||||
props: stepConfig,
|
||||
component: IntroStepThemed,
|
||||
props: {
|
||||
title: title1,
|
||||
content: content1,
|
||||
banner: CommentsOnboardingStep1,
|
||||
hideBackBtn: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: IntroStepThemed,
|
||||
props: {
|
||||
title: title2,
|
||||
content: content2,
|
||||
banner: CommentsOnboardingStep2,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: IntroStepThemed,
|
||||
props: {
|
||||
title: title3,
|
||||
content: content3,
|
||||
banner: CommentsOnboardingStep3,
|
||||
},
|
||||
},
|
||||
})),
|
||||
{
|
||||
component: ProfileForm,
|
||||
props: {
|
||||
|
|
@ -120,9 +120,7 @@ const getSteps = (
|
|||
{
|
||||
component: IntroStepThemed,
|
||||
props: {
|
||||
title: title4,
|
||||
content: content4,
|
||||
banner: CommentsOnboardingStep4,
|
||||
...introSteps[4],
|
||||
hideBackBtn: true,
|
||||
nextBtnText: "Start Tutorial",
|
||||
onSubmit: startTutorial,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<InlineCommentPin commentThreadId={commentThreadId} focused={focused} />
|
||||
);
|
||||
}
|
||||
|
||||
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) => (
|
||||
<InlineCommentPin
|
||||
<MemoisedInlinePageCommentPin
|
||||
commentThreadId={commentsThreadId}
|
||||
focused={commentThreadIdInUrl === commentsThreadId}
|
||||
key={commentsThreadId}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ import {
|
|||
resetVisibleThread,
|
||||
markThreadAsReadRequest,
|
||||
} from "actions/commentActions";
|
||||
import { useTransition, animated } from "react-spring";
|
||||
import { useLocation } from "react-router";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import { AppState } from "reducers";
|
||||
|
||||
|
|
@ -33,33 +31,6 @@ const CommentTriggerContainer = styled.div<{ top: number; left: number }>`
|
|||
z-index: 1;
|
||||
`;
|
||||
|
||||
const useSelectCommentThreadUsingQuery = (commentThreadId: string) => {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
|
||||
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];
|
||||
if (commentPin) {
|
||||
scrollIntoView(commentPin, {
|
||||
scrollMode: "if-needed",
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
});
|
||||
}
|
||||
// set comment thread visible after scrollIntoView is complete
|
||||
setTimeout(() => {
|
||||
dispatch(setVisibleThread(commentThreadId));
|
||||
});
|
||||
}
|
||||
}, [location]);
|
||||
};
|
||||
|
||||
const StyledPinContainer = styled.div<{ unread?: boolean }>`
|
||||
position: relative;
|
||||
& .pin-id {
|
||||
|
|
@ -111,11 +82,46 @@ function Pin({
|
|||
|
||||
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)
|
||||
*/
|
||||
function InlineCommentPin({ commentThreadId }: { commentThreadId: string }) {
|
||||
function InlineCommentPin({
|
||||
commentThreadId,
|
||||
focused,
|
||||
}: {
|
||||
commentThreadId: string;
|
||||
focused: boolean;
|
||||
}) {
|
||||
const commentThread = useSelector(commentThreadsSelector(commentThreadId));
|
||||
const { left, top } = get(commentThread, "position", {
|
||||
top: 0,
|
||||
|
|
@ -124,98 +130,84 @@ function InlineCommentPin({ commentThreadId }: { commentThreadId: string }) {
|
|||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useSelectCommentThreadUsingQuery(commentThreadId);
|
||||
const isPinVisible = useSelector(
|
||||
(state: AppState) =>
|
||||
shouldShowResolvedSelector(state) ||
|
||||
!commentThread?.resolvedState?.active,
|
||||
);
|
||||
|
||||
const shouldShowResolved = useSelector(shouldShowResolvedSelector);
|
||||
const isPinVisible =
|
||||
shouldShowResolved || !commentThread?.resolvedState?.active;
|
||||
const isCommentThreadVisible = useSelector(
|
||||
(state: AppState) =>
|
||||
state.ui.comments.visibleCommentThreadId === commentThreadId,
|
||||
);
|
||||
|
||||
const transition = useTransition(isPinVisible, null, {
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 },
|
||||
config: { duration: 300 },
|
||||
});
|
||||
|
||||
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 (
|
||||
<>
|
||||
{transition.map(
|
||||
({ item: show, props: springProps }: { item: boolean; props: any }) =>
|
||||
show ? (
|
||||
<animated.div key={commentThreadId} style={springProps}>
|
||||
<CommentTriggerContainer
|
||||
data-cy="inline-comment-pin"
|
||||
draggable="true"
|
||||
left={left}
|
||||
onClick={(e: any) => {
|
||||
// capture clicks so that create new thread is not triggered
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
top={top}
|
||||
>
|
||||
<Popover2
|
||||
autoFocus
|
||||
boundary={Container as HTMLDivElement}
|
||||
canEscapeKeyClose
|
||||
content={
|
||||
<animated.div style={springProps}>
|
||||
<CommentThread
|
||||
commentThread={commentThread}
|
||||
inline
|
||||
isOpen={!!isCommentThreadVisible}
|
||||
/>
|
||||
</animated.div>
|
||||
}
|
||||
hasBackdrop
|
||||
isOpen={!!isCommentThreadVisible}
|
||||
minimal
|
||||
// isOpen is controlled so that newly created threads are set to be visible
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: true },
|
||||
offset: {
|
||||
enabled: true,
|
||||
options: {
|
||||
offset: [-8, 10],
|
||||
},
|
||||
},
|
||||
}}
|
||||
onInteraction={(nextOpenState: boolean) => {
|
||||
if (nextOpenState) {
|
||||
dispatch(setVisibleThread(commentThreadId));
|
||||
} else {
|
||||
dispatch(resetVisibleThread(commentThreadId));
|
||||
}
|
||||
}}
|
||||
placement={"right-start"}
|
||||
popoverClassName="comment-thread"
|
||||
portalClassName="inline-comment-thread"
|
||||
>
|
||||
<Pin
|
||||
commentThreadId={commentThreadId}
|
||||
onClick={handlePinClick}
|
||||
sequenceId={commentThread.sequenceId}
|
||||
unread={!commentThread.isViewed}
|
||||
/>
|
||||
</Popover2>
|
||||
</CommentTriggerContainer>
|
||||
</animated.div>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return isPinVisible ? (
|
||||
<CommentTriggerContainer
|
||||
data-cy="inline-comment-pin"
|
||||
draggable="true"
|
||||
left={left}
|
||||
onClick={(e: any) => {
|
||||
// capture clicks so that create new thread is not triggered
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
top={top}
|
||||
>
|
||||
<Popover2
|
||||
autoFocus={false}
|
||||
boundary={Container as HTMLDivElement}
|
||||
canEscapeKeyClose
|
||||
content={
|
||||
<CommentThread
|
||||
commentThread={commentThread}
|
||||
inline
|
||||
isOpen={!!isCommentThreadVisible}
|
||||
/>
|
||||
}
|
||||
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"
|
||||
>
|
||||
<Pin
|
||||
commentThreadId={commentThreadId}
|
||||
onClick={handlePinClick}
|
||||
sequenceId={commentThread.sequenceId}
|
||||
unread={!commentThread.isViewed}
|
||||
/>
|
||||
</Popover2>
|
||||
</CommentTriggerContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default InlineCommentPin;
|
||||
|
|
|
|||
|
|
@ -7,19 +7,5 @@ const steps = [
|
|||
id: "CREATE_UNPUBLISHED_COMMENT",
|
||||
data: { message: "Click anywhere on the canvas \n and leave a comment." },
|
||||
},
|
||||
{
|
||||
id: "RESOLVE_COMMENT",
|
||||
data: {
|
||||
message:
|
||||
"Great job! You can resolve this \n comment by clicking on the \n resolve button.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "COMMENTS_SECTION_FILTER",
|
||||
data: {
|
||||
message:
|
||||
"You will be able to see all of the \n resolved comments when you \n filter the comment section.",
|
||||
},
|
||||
},
|
||||
];
|
||||
export default steps;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { CommentThread } from "entities/Comments/CommentsInterfaces";
|
||||
import { BUILDER_PAGE_URL } from "constants/routes";
|
||||
|
||||
// used for dev
|
||||
export const reduceCommentsByRef = (comments: any[]) => {
|
||||
|
|
@ -78,3 +79,34 @@ export const getOffsetPos = (
|
|||
top: offsetTopPercent,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCommentThreadURL = ({
|
||||
applicationId,
|
||||
commentThreadId,
|
||||
isResolved,
|
||||
pageId,
|
||||
}: {
|
||||
applicationId?: string;
|
||||
commentThreadId: string;
|
||||
isResolved?: boolean;
|
||||
pageId?: string;
|
||||
}) => {
|
||||
const queryParams: Record<string, any> = {
|
||||
commentThreadId,
|
||||
isCommentMode: true,
|
||||
};
|
||||
|
||||
if (isResolved) {
|
||||
queryParams.isResolved = true;
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`${window.location.origin}${BUILDER_PAGE_URL(
|
||||
applicationId,
|
||||
pageId,
|
||||
queryParams,
|
||||
)}`,
|
||||
);
|
||||
|
||||
return url;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
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";
|
||||
|
|
@ -135,6 +135,10 @@ export default function DisplayImageUpload({ onChange, submit, value }: Props) {
|
|||
return uppy;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (value) setLoadError(false);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Container onClick={() => setIsModalOpen(true)}>
|
||||
<Dialog
|
||||
|
|
@ -149,8 +153,7 @@ export default function DisplayImageUpload({ onChange, submit, value }: Props) {
|
|||
<ProfileImagePlaceholder />
|
||||
) : (
|
||||
<img
|
||||
onError={(e) => {
|
||||
console.log(e, "error");
|
||||
onError={() => {
|
||||
setLoadError(true);
|
||||
}}
|
||||
onLoad={() => setLoadError(false)}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ContainerDiv
|
||||
canDrop={canDrop}
|
||||
isActive={isActive}
|
||||
isUploaded={isUploaded}
|
||||
ref={drop}
|
||||
>
|
||||
<div className="bg-image" ref={bgRef}>
|
||||
<div className="button-wrapper" ref={fileContainerRef}>
|
||||
<UploadIcon />
|
||||
<Text className="drag-drop-text" type={TextType.P2}>
|
||||
Drag & Drop files to upload or
|
||||
</Text>
|
||||
<form>
|
||||
<input
|
||||
accept=".jpeg,.png,.svg"
|
||||
id="fileInput"
|
||||
multiple={false}
|
||||
onChange={(el) => handleFileUpload(el.target.files)}
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
value={""}
|
||||
/>
|
||||
<Button
|
||||
category={Category.tertiary}
|
||||
onClick={(el) => ButtonClick(el)}
|
||||
size={Size.medium}
|
||||
text="Browse"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
// <UploadSuccessIcon />
|
||||
|
||||
const uploadFileForm = (
|
||||
<div className="button-wrapper" ref={fileContainerRef}>
|
||||
<UploadIcon />
|
||||
<Text className="drag-drop-text" type={TextType.P2}>
|
||||
Drag & Drop files to upload or
|
||||
</Text>
|
||||
<form>
|
||||
<input
|
||||
accept={FileEndings[fileType]}
|
||||
id="fileInput"
|
||||
multiple={false}
|
||||
onChange={(el) => handleFileUpload(el.target.files)}
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
value={""}
|
||||
/>
|
||||
<Button
|
||||
category={Category.tertiary}
|
||||
onClick={(el) => ButtonClick(el)}
|
||||
size={Size.medium}
|
||||
text="Browse"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const uploadStatus = (
|
||||
<div className="file-spec">
|
||||
<Text type={TextType.H6}>{fileInfo.name}</Text>
|
||||
<Text type={TextType.H6}>{fileInfo.size}KB</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
const imageUploadComponent = (
|
||||
<>
|
||||
<div className="upload-form-container" ref={bgRef}>
|
||||
{uploadFileForm}
|
||||
<div className="file-description" id="fileDesc" ref={fileDescRef}>
|
||||
<div className="file-spec">
|
||||
<Text type={TextType.H6}>{fileInfo.name}</Text>
|
||||
<Text type={TextType.H6}>{fileInfo.size}KB</Text>
|
||||
</div>
|
||||
{uploadStatus}
|
||||
<div className="progress-container">
|
||||
<div className="progress-inner" ref={progressRef} />
|
||||
</div>
|
||||
|
|
@ -341,6 +431,45 @@ function FilePickerComponent(props: FilePickerProps) {
|
|||
text="remove"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const uploadComponent = (
|
||||
<div className="upload-form-container">
|
||||
{uploadFileForm}
|
||||
<div
|
||||
className="file-description centered"
|
||||
id="fileDesc"
|
||||
ref={fileDescRef}
|
||||
>
|
||||
{uploadStatus}
|
||||
<div className="success-container">
|
||||
<UploadSuccessIcon className="success-icon" />
|
||||
<Text className="success-text" type={TextType.H4}>
|
||||
Successfully Uploaded!
|
||||
</Text>
|
||||
<TooltipComponent
|
||||
content={REMOVE_FILE_TOOL_TIP()}
|
||||
position={Position.TOP}
|
||||
>
|
||||
<IconWrapper className="icon-wrapper" onClick={() => removeFile()}>
|
||||
<Icon name="close" size={IconSize.XL} />
|
||||
</IconWrapper>
|
||||
</TooltipComponent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ContainerDiv
|
||||
canDrop={canDrop}
|
||||
fileType={fileType}
|
||||
isActive={isActive}
|
||||
isUploaded={isUploaded}
|
||||
ref={drop}
|
||||
>
|
||||
{fileType === FileType.IMAGE ? imageUploadComponent : uploadComponent}
|
||||
</ContainerDiv>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ 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 styled from "styled-components";
|
||||
import { CommonComponentProps, Classes } from "./common";
|
||||
import { noop } from "lodash";
|
||||
|
|
@ -117,6 +119,8 @@ export const sizeHandler = (size?: IconSize) => {
|
|||
};
|
||||
|
||||
export const IconCollection = [
|
||||
"upload",
|
||||
"download",
|
||||
"book",
|
||||
"bug",
|
||||
"cancel",
|
||||
|
|
@ -477,6 +481,14 @@ const Icon = forwardRef(
|
|||
returnIcon = <Reaction2 />;
|
||||
break;
|
||||
|
||||
case "upload":
|
||||
returnIcon = <Upload />;
|
||||
break;
|
||||
|
||||
case "download":
|
||||
returnIcon = <Download />;
|
||||
break;
|
||||
|
||||
default:
|
||||
returnIcon = null;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import "draft-js/dist/Draft.css";
|
|||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import { EntryComponentProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry";
|
||||
import UserApi from "api/UserApi";
|
||||
import Text, { TextType } from "./Text";
|
||||
|
||||
import Icon from "components/ads/Icon";
|
||||
|
||||
import { INVITE_A_NEW_USER, createMessage } from "constants/messages";
|
||||
|
||||
const StyledMention = styled.span`
|
||||
color: ${(props) => props.theme.colors.comments.mention};
|
||||
|
|
@ -62,6 +65,21 @@ const Username = styled.div`
|
|||
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;
|
||||
|
|
@ -70,9 +88,13 @@ function SuggestionComponent(props: EntryComponentProps) {
|
|||
if (props.mention?.isInviteTrigger) {
|
||||
return (
|
||||
<StyledSuggestionsComponent {...parentProps}>
|
||||
<Text type={TextType.P2}>
|
||||
Invite <b>{props.mention.name}</b>
|
||||
</Text>
|
||||
<PlusCircle>
|
||||
<Icon fillColor="#fff" name="plus" />
|
||||
</PlusCircle>
|
||||
<div>
|
||||
<Name>{createMessage(INVITE_A_NEW_USER)}</Name>
|
||||
<Username>{props.mention.name}</Username>
|
||||
</div>
|
||||
</StyledSuggestionsComponent>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ const ToastBody = styled.div<{
|
|||
color: ${props.theme.colors.toast.undo};
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap
|
||||
}
|
||||
`
|
||||
: null}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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";
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ type TooltipProps = CommonComponentProps & {
|
|||
autoFocus?: boolean;
|
||||
hoverOpenDelay?: number;
|
||||
minimal?: boolean;
|
||||
modifiers?: Modifiers;
|
||||
isOpen?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -33,6 +35,7 @@ function TooltipComponent(props: TooltipProps) {
|
|||
minimal={props.minimal}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
...props.modifiers,
|
||||
}}
|
||||
openOnTargetFocus={props.openOnTargetFocus}
|
||||
popoverClassName={GLOBAL_STYLE_TOOLTIP_CLASSNAME}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import TooltipComponent from "components/ads/Tooltip";
|
||||
import { useSelector } from "react-redux";
|
||||
import Text, { TextType } from "../Text";
|
||||
|
|
@ -8,14 +8,44 @@ 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(
|
||||
|
|
@ -27,30 +57,53 @@ function TourTooltipWrapper(props: Props) {
|
|||
const tourStepsConfig = TourStepsByType[tourType as TourType];
|
||||
const tourStepConfig = tourStepsConfig[tourIndex];
|
||||
const isOpen = isCurrentStepActive && isCurrentTourActive;
|
||||
const dotRef = useRef<HTMLDivElement>(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 (
|
||||
<div onClick={props.onClick ? props.onClick : noop}>
|
||||
<TooltipComponent
|
||||
boundary={"viewport"}
|
||||
content={
|
||||
<Text
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
color: "#fff",
|
||||
display: "flex",
|
||||
textAlign: "center",
|
||||
}}
|
||||
type={TextType.P3}
|
||||
>
|
||||
{tourStepConfig?.data.message}
|
||||
</Text>
|
||||
}
|
||||
isOpen={!!isOpen}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
{children}
|
||||
</TooltipComponent>
|
||||
</div>
|
||||
<>
|
||||
{/* A crude overlay which won't work with containers having overflow hidden */}
|
||||
{isOpen && props.hasOverlay && <Overlay />}
|
||||
<Container onClick={props.onClick ? props.onClick : noop}>
|
||||
{isOpen && props.showPulse && (
|
||||
<PulseDot ref={dotRef} style={props.pulseStyles} />
|
||||
)}
|
||||
<TooltipComponent
|
||||
boundary={"viewport"}
|
||||
content={
|
||||
<Text
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
color: "#fff",
|
||||
display: "flex",
|
||||
textAlign: "center",
|
||||
}}
|
||||
type={TextType.P3}
|
||||
>
|
||||
{tourStepConfig?.data.message}
|
||||
</Text>
|
||||
}
|
||||
isOpen={!!isOpen}
|
||||
modifiers={props.modifiers}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
{children}
|
||||
</TooltipComponent>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,12 +50,15 @@ const selectStyles = {
|
|||
padding: "5px",
|
||||
}),
|
||||
indicatorSeparator: () => ({}),
|
||||
menu: (provided: any) => ({ ...provided, zIndex: 2 }),
|
||||
menuPortal: (base: any) => ({ ...base, zIndex: 2 }),
|
||||
};
|
||||
|
||||
export function BaseDropdown(props: DropdownProps) {
|
||||
const { customSelectStyles, input } = props;
|
||||
return (
|
||||
<Select
|
||||
menuPortalTarget={document.body}
|
||||
styles={{ ...selectStyles, ...customSelectStyles }}
|
||||
{...input}
|
||||
isDisabled={props.isDisabled}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function AutoToolTipComponent(props: Props) {
|
|||
updateToolTip(false);
|
||||
}
|
||||
}, [ref]);
|
||||
if (props.columnType === ColumnTypes.URL) {
|
||||
if (props.columnType === ColumnTypes.URL && props.title) {
|
||||
return <LinkWrapper {...props} />;
|
||||
}
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ interface TableProps {
|
|||
applyFilter: (filters: ReactTableFilter[]) => void;
|
||||
compactMode?: CompactMode;
|
||||
updateCompactMode: (compactMode: CompactMode) => void;
|
||||
isVisibleCompactMode?: boolean;
|
||||
isVisibleDownload?: boolean;
|
||||
isVisibleFilters?: boolean;
|
||||
isVisiblePagination?: boolean;
|
||||
isVisibleSearch?: boolean;
|
||||
}
|
||||
|
||||
const defaultColumn = {
|
||||
|
|
@ -161,65 +166,83 @@ export function Table(props: TableProps) {
|
|||
const tableWrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const tableBodyRef = useRef<HTMLDivElement | null>(null);
|
||||
const tableHeaderWrapperRef = React.createRef<HTMLDivElement>();
|
||||
const isHeaderVisible =
|
||||
props.isVisibleSearch ||
|
||||
props.isVisibleFilters ||
|
||||
props.isVisibleDownload ||
|
||||
props.isVisibleCompactMode ||
|
||||
props.isVisiblePagination;
|
||||
|
||||
return (
|
||||
<TableWrapper
|
||||
backgroundColor={Colors.ATHENS_GRAY_DARKER}
|
||||
height={props.height}
|
||||
id={`table${props.widgetId}`}
|
||||
isHeaderVisible={isHeaderVisible}
|
||||
tableSizes={tableSizes}
|
||||
triggerRowSelection={props.triggerRowSelection}
|
||||
width={props.width}
|
||||
>
|
||||
<TableHeaderWrapper
|
||||
backgroundColor={Colors.WHITE}
|
||||
ref={tableHeaderWrapperRef}
|
||||
serverSidePaginationEnabled={props.serverSidePaginationEnabled}
|
||||
tableSizes={tableSizes}
|
||||
width={props.width}
|
||||
>
|
||||
<Scrollbars
|
||||
renderThumbHorizontal={ScrollbarHorizontalThumb}
|
||||
renderThumbVertical={ScrollbarVerticalThumb}
|
||||
style={{ width: props.width, height: 38 }}
|
||||
{isHeaderVisible && (
|
||||
<TableHeaderWrapper
|
||||
backgroundColor={Colors.WHITE}
|
||||
ref={tableHeaderWrapperRef}
|
||||
serverSidePaginationEnabled={props.serverSidePaginationEnabled}
|
||||
tableSizes={tableSizes}
|
||||
width={props.width}
|
||||
>
|
||||
<TableHeaderInnerWrapper
|
||||
backgroundColor={Colors.WHITE}
|
||||
serverSidePaginationEnabled={props.serverSidePaginationEnabled}
|
||||
tableSizes={tableSizes}
|
||||
width={props.width}
|
||||
<Scrollbars
|
||||
renderThumbHorizontal={ScrollbarHorizontalThumb}
|
||||
renderThumbVertical={ScrollbarVerticalThumb}
|
||||
style={{ width: props.width, height: 38 }}
|
||||
>
|
||||
<TableHeader
|
||||
applyFilter={props.applyFilter}
|
||||
columns={tableHeadercolumns}
|
||||
compactMode={props.compactMode}
|
||||
currentPageIndex={currentPageIndex}
|
||||
editMode={props.editMode}
|
||||
filters={props.filters}
|
||||
nextPageClick={props.nextPageClick}
|
||||
pageCount={pageCount}
|
||||
pageNo={props.pageNo}
|
||||
pageOptions={pageOptions}
|
||||
prevPageClick={props.prevPageClick}
|
||||
searchKey={props.searchKey}
|
||||
searchTableData={props.searchTableData}
|
||||
<TableHeaderInnerWrapper
|
||||
backgroundColor={Colors.WHITE}
|
||||
serverSidePaginationEnabled={props.serverSidePaginationEnabled}
|
||||
tableColumns={columns}
|
||||
tableData={props.data}
|
||||
tableSizes={tableSizes}
|
||||
updateCompactMode={props.updateCompactMode}
|
||||
updatePageNo={props.updatePageNo}
|
||||
widgetName={props.widgetName}
|
||||
/>
|
||||
</TableHeaderInnerWrapper>
|
||||
</Scrollbars>
|
||||
</TableHeaderWrapper>
|
||||
width={props.width}
|
||||
>
|
||||
<TableHeader
|
||||
applyFilter={props.applyFilter}
|
||||
columns={tableHeadercolumns}
|
||||
compactMode={props.compactMode}
|
||||
currentPageIndex={currentPageIndex}
|
||||
editMode={props.editMode}
|
||||
filters={props.filters}
|
||||
isVisibleCompactMode={props.isVisibleCompactMode}
|
||||
isVisibleDownload={props.isVisibleDownload}
|
||||
isVisibleFilters={props.isVisibleFilters}
|
||||
isVisiblePagination={props.isVisiblePagination}
|
||||
isVisibleSearch={props.isVisibleSearch}
|
||||
nextPageClick={props.nextPageClick}
|
||||
pageCount={pageCount}
|
||||
pageNo={props.pageNo}
|
||||
pageOptions={pageOptions}
|
||||
prevPageClick={props.prevPageClick}
|
||||
searchKey={props.searchKey}
|
||||
searchTableData={props.searchTableData}
|
||||
serverSidePaginationEnabled={props.serverSidePaginationEnabled}
|
||||
tableColumns={columns}
|
||||
tableData={props.data}
|
||||
tableSizes={tableSizes}
|
||||
updateCompactMode={props.updateCompactMode}
|
||||
updatePageNo={props.updatePageNo}
|
||||
widgetName={props.widgetName}
|
||||
/>
|
||||
</TableHeaderInnerWrapper>
|
||||
</Scrollbars>
|
||||
</TableHeaderWrapper>
|
||||
)}
|
||||
<div
|
||||
className={props.isLoading ? Classes.SKELETON : "tableWrap"}
|
||||
ref={tableWrapperRef}
|
||||
>
|
||||
<Scrollbars
|
||||
renderThumbHorizontal={ScrollbarHorizontalThumb}
|
||||
style={{ width: props.width, height: props.height - 48 }}
|
||||
style={{
|
||||
width: props.width,
|
||||
height: isHeaderVisible ? props.height - 48 : props.height,
|
||||
}}
|
||||
>
|
||||
<div {...getTableProps()} className="table">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -110,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 (
|
||||
<>
|
||||
<SearchComponent
|
||||
onSearch={props.searchTableData}
|
||||
placeholder="Search..."
|
||||
value={props.searchKey}
|
||||
/>
|
||||
<CommonFunctionsMenuWrapper tableSizes={props.tableSizes}>
|
||||
<TableFilters
|
||||
applyFilter={props.applyFilter}
|
||||
columns={props.columns}
|
||||
editMode={props.editMode}
|
||||
filters={props.filters}
|
||||
{props.isVisibleSearch && (
|
||||
<SearchComponent
|
||||
onSearch={props.searchTableData}
|
||||
placeholder="Search..."
|
||||
value={props.searchKey}
|
||||
/>
|
||||
<TableDataDownload
|
||||
columns={props.tableColumns}
|
||||
data={props.tableData}
|
||||
widgetName={props.widgetName}
|
||||
/>
|
||||
<TableCompactMode
|
||||
compactMode={props.compactMode}
|
||||
updateCompactMode={props.updateCompactMode}
|
||||
/>
|
||||
</CommonFunctionsMenuWrapper>
|
||||
{props.serverSidePaginationEnabled && (
|
||||
)}
|
||||
{(props.isVisibleFilters ||
|
||||
props.isVisibleDownload ||
|
||||
props.isVisibleCompactMode) && (
|
||||
<CommonFunctionsMenuWrapper tableSizes={props.tableSizes}>
|
||||
{props.isVisibleFilters && (
|
||||
<TableFilters
|
||||
applyFilter={props.applyFilter}
|
||||
columns={props.columns}
|
||||
editMode={props.editMode}
|
||||
filters={props.filters}
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.isVisibleDownload && (
|
||||
<TableDataDownload
|
||||
columns={props.tableColumns}
|
||||
data={props.tableData}
|
||||
widgetName={props.widgetName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.isVisibleCompactMode && (
|
||||
<TableCompactMode
|
||||
compactMode={props.compactMode}
|
||||
updateCompactMode={props.updateCompactMode}
|
||||
/>
|
||||
)}
|
||||
</CommonFunctionsMenuWrapper>
|
||||
)}
|
||||
|
||||
{props.isVisiblePagination && props.serverSidePaginationEnabled && (
|
||||
<PaginationWrapper>
|
||||
<PaginationItemWrapper
|
||||
className="t--table-widget-prev-page"
|
||||
|
|
@ -162,7 +182,7 @@ function TableHeader(props: TableHeaderProps) {
|
|||
</PaginationItemWrapper>
|
||||
</PaginationWrapper>
|
||||
)}
|
||||
{!props.serverSidePaginationEnabled && (
|
||||
{props.isVisiblePagination && !props.serverSidePaginationEnabled && (
|
||||
<PaginationWrapper>
|
||||
<RowWrapper className="show-page-items">
|
||||
{props.tableData?.length} Records
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const TableWrapper = styled.div<{
|
|||
tableSizes: TableSizes;
|
||||
backgroundColor?: Color;
|
||||
triggerRowSelection: boolean;
|
||||
isHeaderVisible?: boolean;
|
||||
}>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -56,7 +57,8 @@ export const TableWrapper = styled.div<{
|
|||
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};
|
||||
|
|
@ -111,8 +113,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 {
|
||||
|
|
@ -411,6 +415,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;
|
||||
|
|
|
|||
|
|
@ -112,7 +112,9 @@ export const renderCell = (
|
|||
tableWidth={tableWidth}
|
||||
title={value.toString()}
|
||||
>
|
||||
{value.toString()}
|
||||
{value && columnType === ColumnTypes.URL && cellProperties.displayText
|
||||
? cellProperties.displayText
|
||||
: value.toString()}
|
||||
</AutoToolTipComponent>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ interface ReactTableComponentProps {
|
|||
columns: ReactTableColumnProps[];
|
||||
compactMode?: CompactMode;
|
||||
updateCompactMode: (compactMode: CompactMode) => void;
|
||||
isVisibleSearch?: boolean;
|
||||
isVisibleFilters?: boolean;
|
||||
isVisibleDownload?: boolean;
|
||||
isVisibleCompactMode?: boolean;
|
||||
isVisiblePagination?: boolean;
|
||||
}
|
||||
|
||||
function ReactTableComponent(props: ReactTableComponentProps) {
|
||||
|
|
@ -81,6 +86,11 @@ function ReactTableComponent(props: ReactTableComponentProps) {
|
|||
handleResizeColumn,
|
||||
height,
|
||||
isLoading,
|
||||
isVisibleCompactMode,
|
||||
isVisibleDownload,
|
||||
isVisibleFilters,
|
||||
isVisiblePagination,
|
||||
isVisibleSearch,
|
||||
nextPageClick,
|
||||
onRowClick,
|
||||
pageNo,
|
||||
|
|
@ -230,6 +240,11 @@ function ReactTableComponent(props: ReactTableComponentProps) {
|
|||
handleResizeColumn={handleResizeColumn}
|
||||
height={height}
|
||||
isLoading={isLoading}
|
||||
isVisibleCompactMode={isVisibleCompactMode}
|
||||
isVisibleDownload={isVisibleDownload}
|
||||
isVisibleFilters={isVisibleFilters}
|
||||
isVisiblePagination={isVisiblePagination}
|
||||
isVisibleSearch={isVisibleSearch}
|
||||
nextPageClick={nextPageClick}
|
||||
pageNo={pageNo - 1}
|
||||
pageSize={pageSize || 1}
|
||||
|
|
@ -262,6 +277,11 @@ export default React.memo(ReactTableComponent, (prev, next) => {
|
|||
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 &&
|
||||
|
|
|
|||
|
|
@ -180,8 +180,25 @@ function RecaptchaComponent(
|
|||
});
|
||||
props.onClick && props.onClick(event);
|
||||
}
|
||||
|
||||
// Check if a string is a valid JSON string
|
||||
const checkValidJson = (inputString: string): boolean => {
|
||||
try {
|
||||
JSON.parse(inputString);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let validGoogleRecaptchaKey = props.googleRecaptchaKey;
|
||||
|
||||
if (validGoogleRecaptchaKey && checkValidJson(validGoogleRecaptchaKey)) {
|
||||
validGoogleRecaptchaKey = undefined;
|
||||
}
|
||||
|
||||
const status = useScript(
|
||||
`https://www.google.com/recaptcha/api.js?render=${props.googleRecaptchaKey}`,
|
||||
`https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class DatePickerComponent extends React.Component<
|
|||
? new Date(this.props.maxDate)
|
||||
: now
|
||||
.clone()
|
||||
.set({ month: 11, date: 31, year: year + 20 })
|
||||
.set({ month: 11, date: 31, year: year + 100 })
|
||||
.toDate();
|
||||
const isValid = this.state.selectedDate
|
||||
? this.isValidDate(new Date(this.state.selectedDate))
|
||||
|
|
@ -168,6 +168,7 @@ class DatePickerComponent extends React.Component<
|
|||
if (
|
||||
this.props.minDate &&
|
||||
parsedMinDate.isValid() &&
|
||||
!parsedCurrentDate.isSame(parsedMinDate, "day") &&
|
||||
parsedCurrentDate.isBefore(parsedMinDate)
|
||||
) {
|
||||
isValid = false;
|
||||
|
|
@ -179,6 +180,7 @@ class DatePickerComponent extends React.Component<
|
|||
isValid &&
|
||||
this.props.maxDate &&
|
||||
parsedMaxDate.isValid() &&
|
||||
!parsedCurrentDate.isSame(parsedMaxDate, "day") &&
|
||||
parsedCurrentDate.isAfter(parsedMaxDate)
|
||||
) {
|
||||
isValid = false;
|
||||
|
|
|
|||
|
|
@ -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} {
|
||||
|
|
|
|||
|
|
@ -56,11 +56,13 @@ const Content = styled.div<{
|
|||
export type ModalComponentProps = {
|
||||
isOpen: boolean;
|
||||
onClose: (e: any) => void;
|
||||
onModalClose?: () => void;
|
||||
children: ReactNode;
|
||||
width?: number;
|
||||
className?: string;
|
||||
canOutsideClickClose: boolean;
|
||||
canEscapeKeyClose: boolean;
|
||||
overlayClassName?: string;
|
||||
scrollContents: boolean;
|
||||
height?: number;
|
||||
top?: number;
|
||||
|
|
@ -76,6 +78,14 @@ export function ModalComponent(props: ModalComponentProps) {
|
|||
const modalContentRef: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
|
||||
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" });
|
||||
|
|
@ -94,6 +104,7 @@ export function ModalComponent(props: ModalComponentProps) {
|
|||
<Overlay
|
||||
canEscapeKeyClose={props.canEscapeKeyClose}
|
||||
canOutsideClickClose={props.canOutsideClickClose}
|
||||
className={props.overlayClassName}
|
||||
enforceFocus={false}
|
||||
hasBackdrop={
|
||||
props.hasBackDrop !== undefined ? !!props.hasBackDrop : true
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class TextComponent extends React.Component<TextComponentProps> {
|
|||
>
|
||||
<Interweave
|
||||
content={text}
|
||||
matchers={[new UrlMatcher("url"), new EmailMatcher("email")]}
|
||||
matchers={[new EmailMatcher("email"), new UrlMatcher("url")]}
|
||||
/>
|
||||
</StyledText>
|
||||
</TextContainer>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,13 @@ 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,
|
||||
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,6 +31,7 @@ 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";
|
||||
|
||||
const ResponseContainer = styled.div`
|
||||
${ResizerCSS}
|
||||
|
|
@ -228,14 +235,19 @@ function ApiResponseView(props: Props) {
|
|||
},
|
||||
{
|
||||
key: "ERROR",
|
||||
title: "Errors",
|
||||
title: createMessage(DEBUGGER_ERRORS),
|
||||
panelComponent: <ErrorLogs />,
|
||||
},
|
||||
{
|
||||
key: "LOGS",
|
||||
title: "Logs",
|
||||
title: createMessage(DEBUGGER_LOGS),
|
||||
panelComponent: <DebuggerLogs searchQuery={props.apiName} />,
|
||||
},
|
||||
{
|
||||
key: "ENTITY_DEPENDENCIES",
|
||||
title: createMessage(INSPECT_ENTITY),
|
||||
panelComponent: <EntityDeps />,
|
||||
},
|
||||
];
|
||||
|
||||
const onTabSelect = (index: number) => {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,11 @@ export type HintHelper = (
|
|||
additionalData?: Record<string, Record<string, unknown>>,
|
||||
) => Hinter;
|
||||
export type Hinter = {
|
||||
showHint: (editor: CodeMirror.Editor, expected: string) => void;
|
||||
showHint: (
|
||||
editor: CodeMirror.Editor,
|
||||
expected: string,
|
||||
entityName: string,
|
||||
) => void;
|
||||
update?: (data: DataTree) => void;
|
||||
trigger?: (editor: CodeMirror.Editor) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ export const bindingHint: HintHelper = (editor, data, additionalData) => {
|
|||
[KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (
|
||||
cm: CodeMirror.Editor,
|
||||
expected: string,
|
||||
) => ternServer.complete(cm, expected),
|
||||
entity: string,
|
||||
) => ternServer.complete(cm, expected, entity),
|
||||
[KeyboardShortcuts.CodeEditor.ShowTypeAndInfo]: (cm: CodeMirror.Editor) => {
|
||||
ternServer.showType(cm);
|
||||
},
|
||||
|
|
@ -29,7 +30,11 @@ export const bindingHint: HintHelper = (editor, data, additionalData) => {
|
|||
const dataTreeDef = dataTreeTypeDefCreator(data);
|
||||
ternServer.updateDef("dataTree", dataTreeDef);
|
||||
},
|
||||
showHint: (editor: CodeMirror.Editor, expected: string) => {
|
||||
showHint: (
|
||||
editor: CodeMirror.Editor,
|
||||
expected: string,
|
||||
entityName: string,
|
||||
) => {
|
||||
let cursorBetweenBinding = false;
|
||||
const cursor = editor.getCursor();
|
||||
const value = editor.getValue();
|
||||
|
|
@ -66,7 +71,7 @@ export const bindingHint: HintHelper = (editor, data, additionalData) => {
|
|||
const shouldShow = cursorBetweenBinding;
|
||||
if (shouldShow) {
|
||||
AnalyticsUtil.logEvent("AUTO_COMPELTE_SHOW", {});
|
||||
ternServer.complete(editor, expected);
|
||||
ternServer.complete(editor, expected, entityName);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: No types available
|
||||
|
|
|
|||
|
|
@ -50,12 +50,19 @@ import "codemirror/addon/fold/foldgutter";
|
|||
import "codemirror/addon/fold/foldgutter.css";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { removeNewLineChars, getInputValue } from "./codeEditorUtils";
|
||||
import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
|
||||
|
||||
const LightningMenu = lazy(() =>
|
||||
retryPromise(() => import("components/editorComponents/LightningMenu")),
|
||||
);
|
||||
|
||||
const AUTOCOMPLETE_CLOSE_KEY_CODES = ["Enter", "Tab", "Escape", "Comma"];
|
||||
const AUTOCOMPLETE_CLOSE_KEY_CODES = [
|
||||
"Enter",
|
||||
"Tab",
|
||||
"Escape",
|
||||
"Comma",
|
||||
"Backspace",
|
||||
];
|
||||
|
||||
interface ReduxStateProps {
|
||||
dynamicData: DataTree;
|
||||
|
|
@ -255,6 +262,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
};
|
||||
|
||||
handleEditorFocus = () => {
|
||||
if (this.state.isFocused) return;
|
||||
this.setState({ isFocused: true });
|
||||
this.editor.refresh();
|
||||
if (this.props.size === EditorSize.COMPACT) {
|
||||
|
|
@ -265,9 +273,9 @@ class CodeEditor extends Component<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
handleEditorBlur = () => {
|
||||
handleEditorBlur = (cm: CodeMirror.Editor) => {
|
||||
this.handleChange();
|
||||
this.setState({ isFocused: false });
|
||||
if (!cm.state.completionActive) this.setState({ isFocused: false });
|
||||
if (this.props.size === EditorSize.COMPACT) {
|
||||
this.editor.setOption("lineWrapping", false);
|
||||
}
|
||||
|
|
@ -295,7 +303,10 @@ class CodeEditor extends Component<Props, State> {
|
|||
|
||||
handleAutocompleteVisibility = (cm: CodeMirror.Editor) => {
|
||||
const expected = this.props.expected ? this.props.expected : "";
|
||||
this.hinters.forEach((hinter) => hinter.showHint(cm, expected));
|
||||
const { entityName } = getEntityNameAndPropertyPath(
|
||||
this.props.dataTreePath || "",
|
||||
);
|
||||
this.hinters.forEach((hinter) => hinter.showHint(cm, expected, entityName));
|
||||
};
|
||||
|
||||
handleAutocompleteHide = (cm: any, event: KeyboardEvent) => {
|
||||
|
|
@ -340,16 +351,26 @@ class CodeEditor extends Component<Props, State> {
|
|||
if (!dataTreePath) {
|
||||
return { isValid: true, validationMessage: "", jsErrorMessage: "" };
|
||||
}
|
||||
const isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps");
|
||||
const validationMessagePath = dataTreePath.replace(
|
||||
"evaluatedValues",
|
||||
"validationMessages",
|
||||
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
|
||||
dataTreePath,
|
||||
);
|
||||
const jsErrorMessagePath = dataTreePath.replace(
|
||||
"evaluatedValues",
|
||||
"jsErrorMessages",
|
||||
);
|
||||
|
||||
let isValidPath, validationMessagePath, jsErrorMessagePath;
|
||||
if (dataTreePath && dataTreePath.match(/evaluatedValues/g)) {
|
||||
isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps");
|
||||
validationMessagePath = dataTreePath.replace(
|
||||
"evaluatedValues",
|
||||
"validationMessages",
|
||||
);
|
||||
jsErrorMessagePath = dataTreePath.replace(
|
||||
"evaluatedValues",
|
||||
"jsErrorMessages",
|
||||
);
|
||||
} else {
|
||||
isValidPath = entityName + "invalidProps" + propertyPath;
|
||||
validationMessagePath =
|
||||
entityName + ".validationMessages." + propertyPath;
|
||||
jsErrorMessagePath = entityName + ".jsErrorMessages." + propertyPath;
|
||||
}
|
||||
const isValid = !_.get(dataTree, isValidPath, false);
|
||||
const validationMessage = _.get(
|
||||
dataTree,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export const HintStyles = createGlobalStyle<{
|
|||
}
|
||||
|
||||
.datasource-hint {
|
||||
padding: 10px;
|
||||
padding: 10px 20px 10px 10px !important;
|
||||
display: block;
|
||||
width: 500px;
|
||||
height: 32px;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ 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";
|
||||
|
||||
const LIST_HEADER_HEIGHT = "38px";
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ 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";
|
||||
|
||||
const TABS_HEADER_HEIGHT = 36;
|
||||
|
||||
|
|
@ -41,14 +48,19 @@ type DebuggerTabsProps = {
|
|||
const DEBUGGER_TABS = [
|
||||
{
|
||||
key: "ERROR",
|
||||
title: "Errors",
|
||||
title: createMessage(DEBUGGER_ERRORS),
|
||||
panelComponent: <Errors hasShortCut />,
|
||||
},
|
||||
{
|
||||
key: "LOGS",
|
||||
title: "Logs",
|
||||
title: createMessage(DEBUGGER_LOGS),
|
||||
panelComponent: <DebuggerLogs hasShortCut />,
|
||||
},
|
||||
{
|
||||
key: "INSPECT_ELEMENTS",
|
||||
title: createMessage(INSPECT_ENTITY),
|
||||
panelComponent: <EntityDeps />,
|
||||
},
|
||||
];
|
||||
|
||||
function DebuggerTabs(props: DebuggerTabsProps) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
/* eslint-disable prefer-const */
|
||||
import { Collapse } from "@blueprintjs/core";
|
||||
import React, { memo, ReactNode, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "reducers";
|
||||
import styled from "styled-components";
|
||||
import Icon, { IconSize } from "components/ads/Icon";
|
||||
import { Classes } from "components/ads/common";
|
||||
import InspectElement from "assets/images/InspectElement.svg";
|
||||
import { SourceEntity } from "entities/AppsmithConsole";
|
||||
import { createMessage, INSPECT_ENTITY_BLANK_STATE } from "constants/messages";
|
||||
import { getDependenciesFromInverseDependencies } from "./helpers";
|
||||
import { useEntityLink, useSelectedEntity } from "./hooks";
|
||||
|
||||
const CollapsibleWrapper = styled.div<{ step: number; isOpen: boolean }>`
|
||||
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 <BlankState />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MemoizedDependencyHierarchy
|
||||
dependencies={entityDependencies.directDependencies}
|
||||
entityName={`Dependencies of ${selectedEntity.name}`}
|
||||
selectedEntity={selectedEntity}
|
||||
type="dependencies"
|
||||
/>
|
||||
<MemoizedDependencyHierarchy
|
||||
dependencies={entityDependencies.inverseDependencies}
|
||||
entityName={`References of ${selectedEntity.name}`}
|
||||
selectedEntity={selectedEntity}
|
||||
type="references"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlankState() {
|
||||
return (
|
||||
<BlankStateContainer>
|
||||
<img src={InspectElement} />
|
||||
<span>{createMessage(INSPECT_ENTITY_BLANK_STATE)}</span>
|
||||
</BlankStateContainer>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DependenciesWrapper>
|
||||
{props.dependencies.length ? (
|
||||
<Collapsible label={label} step={0}>
|
||||
{props.dependencies.map((item) => {
|
||||
return (
|
||||
<StyledSpan
|
||||
className={`t--${props.type}-item`}
|
||||
key={`${props.selectedEntity.id}-${item}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateToEntity(item);
|
||||
}}
|
||||
step={2}
|
||||
>
|
||||
{item}
|
||||
</StyledSpan>
|
||||
);
|
||||
})}
|
||||
</Collapsible>
|
||||
) : (
|
||||
<span className="no-dependencies">{label}</span>
|
||||
)}
|
||||
</DependenciesWrapper>
|
||||
);
|
||||
}
|
||||
const MemoizedDependencyHierarchy = memo(DependencyHierarchy);
|
||||
|
||||
function Collapsible(props: {
|
||||
label: string;
|
||||
step: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<CollapsibleWrapper
|
||||
isOpen={isOpen}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
step={props.step}
|
||||
>
|
||||
<div className="label-wrapper">
|
||||
<Icon name={"downArrow"} size={IconSize.XXS} />
|
||||
<span>{props.label}</span>
|
||||
</div>
|
||||
<Collapse isOpen={isOpen}>{props.children}</Collapse>
|
||||
</CollapsibleWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default EntityDeps;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { getDependenciesFromInverseDependencies } 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
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 {
|
||||
|
|
@ -10,6 +8,12 @@ import {
|
|||
OPEN_THE_DEBUGGER,
|
||||
PRESS,
|
||||
} from "constants/messages";
|
||||
import { DependencyMap } from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
API_EDITOR_URL,
|
||||
QUERIES_EDITOR_URL,
|
||||
BUILDER_PAGE_URL,
|
||||
} from "constants/routes";
|
||||
|
||||
const BlankStateWrapper = styled.div`
|
||||
overflow: auto;
|
||||
|
|
@ -54,46 +58,73 @@ export const SeverityIconColor: Record<Severity, string> = {
|
|||
[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<string>();
|
||||
const inverseDependencies = new Set<string>();
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
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<Message[]>([]);
|
||||
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
|
||||
);
|
||||
};
|
||||
|
|
|
|||
144
app/client/src/components/editorComponents/Debugger/hooks.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
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/WidgetEntity";
|
||||
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<Message[]>([]);
|
||||
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);
|
||||
|
||||
if (url) {
|
||||
history.push(url);
|
||||
}
|
||||
}
|
||||
},
|
||||
[dataTree],
|
||||
);
|
||||
|
||||
return {
|
||||
navigateToEntity,
|
||||
};
|
||||
};
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
useWidgetDragResize,
|
||||
} from "utils/hooks/dragResizeHooks";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { commentModeSelector } from "selectors/commentsSelectors";
|
||||
|
||||
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) {
|
||||
|
|
@ -61,6 +65,8 @@ function DraggableComponent(props: DraggableComponentProps) {
|
|||
// Dispatch hook handy to set a widget as focused/selected
|
||||
const { focusWidget, selectWidget } = useWidgetSelection();
|
||||
|
||||
const isCommentMode = useSelector(commentModeSelector);
|
||||
|
||||
// Dispatch hook handy to set any `DraggableComponent` as dragging/ not dragging
|
||||
// The value is boolean
|
||||
const { setIsDragging } = useWidgetDragResize();
|
||||
|
|
@ -138,7 +144,7 @@ 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);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<EditableTextWrapper
|
||||
isEditing={isEditing}
|
||||
minimal={!!props.minimal}
|
||||
minimal={!!minimal}
|
||||
onClick={
|
||||
props.editInteractionKind === EditInteractionKind.SINGLE ? edit : _.noop
|
||||
editInteractionKind === EditInteractionKind.SINGLE ? edit : _.noop
|
||||
}
|
||||
onDoubleClick={
|
||||
props.editInteractionKind === EditInteractionKind.DOUBLE ? edit : _.noop
|
||||
editInteractionKind === EditInteractionKind.DOUBLE ? edit : _.noop
|
||||
}
|
||||
>
|
||||
<ErrorTooltip isOpen={!!error} message={errorMessage as string}>
|
||||
<TextContainer isValid={!error} minimal={!!props.minimal}>
|
||||
<TextContainer isValid={!error} minimal={!!minimal}>
|
||||
<BlueprintEditableText
|
||||
className={props.className}
|
||||
className={className}
|
||||
disabled={!isEditing}
|
||||
isEditing={isEditing}
|
||||
onCancel={props.onBlur}
|
||||
onCancel={onBlur}
|
||||
onChange={onInputchange}
|
||||
onConfirm={onChange}
|
||||
placeholder={props.placeholder}
|
||||
placeholder={placeholder}
|
||||
selectAllOnFocus
|
||||
value={value}
|
||||
/>
|
||||
{!props.minimal &&
|
||||
!props.hideEditIcon &&
|
||||
!props.updating &&
|
||||
!isEditing && <EditPen alt="Edit pen" src={Edit} />}
|
||||
{!minimal && !hideEditIcon && !updating && !isEditing && (
|
||||
<EditPen alt="Edit pen" src={Edit} />
|
||||
)}
|
||||
</TextContainer>
|
||||
</ErrorTooltip>
|
||||
</EditableTextWrapper>
|
||||
|
|
|
|||