diff --git a/.env.example b/.env.example index a98011bb3d..6dfdfab187 100644 --- a/.env.example +++ b/.env.example @@ -57,4 +57,9 @@ APPSMITH_MAIL_SMTP_TLS_ENABLED= #APPSMITH_SENTRY_ENVIRONMENT= # Configure cloud services -# APPSMITH_CLOUD_SERVICES_BASE_URL="https://release-cs.appsmith.com" \ No newline at end of file +# APPSMITH_CLOUD_SERVICES_BASE_URL="https://release-cs.appsmith.com" + +# Google Recaptcha Config +APPSMITH_RECAPTCHA_SITE_KEY= +APPSMITH_RECAPTCHA_SECRET_KEY= +APPSMITH_RECAPTCHA_ENABLED= \ No newline at end of file diff --git a/.github/config.json b/.github/config.json index e92c4b9c7a..fd21c1db49 100644 --- a/.github/config.json +++ b/.github/config.json @@ -170,6 +170,10 @@ "color": "50ba23", "description": "" }, + "Import-Export-App": { + "name": "Import-Export-App", + "color": "50ba23" + }, "JS": { "name": "JS", "color": "ffc1c2", @@ -669,6 +673,11 @@ "label": "Debugger", "value": true }, + { + "type": "hasLabel", + "label": "Import-Export-App", + "value": true + }, { "type": "hasLabel", "label": "Omnibar", diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index df2e2f8f51..162b9ead0c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,10 +3,12 @@ > Use this template to quickly create a well written pull request. Delete all quotes before creating the pull request. ## Description -> Please include a summary of the changes and which issue has been fixed. Please also include relevant motivation + +> Please include a summary of the changes and which issue has been fixed. Please also include relevant motivation > and context. List any dependencies that are required for this change. Fixes # (issue) + > if no issue exists, please create an issue and ask the maintainers about this first ## Type of change @@ -20,7 +22,7 @@ Fixes # (issue) ## How Has This Been Tested? -> Please describe the tests that you ran to verify your changes. Provide instructions, so we can reproduce. +> Please describe the tests that you ran to verify your changes. Provide instructions, so we can reproduce. > Please also list any relevant details for your test configuration. - Test A diff --git a/.github/workflows/build-rts.yml b/.github/workflows/build-rts.yml index 6ee9672092..9fc4aaca53 100644 --- a/.github/workflows/build-rts.yml +++ b/.github/workflows/build-rts.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: push: - branches: [release, master] + branches: [release, release-frozen, master] # Only trigger if files have changed in this specific path paths: - "app/rts/**" @@ -68,6 +68,14 @@ jobs: echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-rts:${{steps.vars.outputs.tag}} + # Build release-frozen Docker image and push to Docker Hub + - name: Push release-frozen image to Docker Hub + if: success() && github.ref == 'refs/heads/release-frozen' + run: | + docker build -t ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-rts:${{steps.vars.outputs.tag}} . + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin + docker push ${{ secrets.DOCKER_HUB_ORGANIZATION }}/appsmith-rts:${{steps.vars.outputs.tag}} + # Build master Docker image and push to Docker Hub - name: Push master image to Docker Hub with commit tag if: success() && github.ref == 'refs/heads/master' diff --git a/.github/workflows/client-test.yml b/.github/workflows/client-test.yml index 994845a1df..572f4a43b6 100644 --- a/.github/workflows/client-test.yml +++ b/.github/workflows/client-test.yml @@ -13,7 +13,7 @@ on: # trigger for pushes to release and master push: - branches: [release, master] + branches: [release, release-frozen, master] paths: - "app/client/**" - "!app/client/cypress/manual_TestSuite/**" @@ -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') diff --git a/.github/workflows/external-client-test.yml b/.github/workflows/external-client-test.yml index b59ac9c054..d882d70dbc 100644 --- a/.github/workflows/external-client-test.yml +++ b/.github/workflows/external-client-test.yml @@ -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/. 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 diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 3ac0df6d93..ee7f215bd3 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: push: - branches: [release, master] + branches: [release, release-frozen, master] # Only trigger if files have changed in this specific path paths: - "app/server/**" @@ -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' diff --git a/README.md b/README.md index 8a7d411b00..270d0bc01c 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ - Appsmith - The Frontend Tool for Backend DevsAppsmith - The Frontend Tool for Backend Devs + Appsmith - The Frontend Tool for Backend DevsAppsmith - The Frontend Tool for Backend Devs

Start Building - • - Features - • + • Documentation - · + • + Community + • Tutorials - · - Blog - · + • + Events + • Youtube - · + • Discord
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.
+

We're launching the Appsmith Community! Be a part of the community that will help shape the future of Appsmith! +


@@ -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)

## 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) @@ -154,7 +157,7 @@ We love our contributors! We're committed to fostering an open and welcoming env wayne Forde Abhishek - +Ashok
diff --git a/app.json b/app.json index d6bf73c26b..1a1fb89536 100644 --- a/app.json +++ b/app.json @@ -98,6 +98,21 @@ "value": "", "required": false }, + "APPSMITH_RECAPTCHA_SITE_KEY": { + "description" : "Google reCAPTCHA v3 site key, it is required if you wish to enable protection against spam/abusive users. Read more at: https://developers.google.com/recaptcha/docs/v3", + "value": "", + "required": false + }, + "APPSMITH_RECAPTCHA_SECRET_KEY": { + "description" : "Google reCAPTCHA v3 verification secret key, it is required if you wish to enable spam protection in your backend server.", + "value": "", + "required": false + }, + "APPSMITH_RECAPTCHA_ENABLED": { + "description" : "Boolean config to enable or disable Google reCAPTCHA v3 verification feature. If set to true, both site key and secret key should be provided.", + "value": "", + "required": false + }, "APPSMITH_DISABLE_TELEMETRY": { "description" : "We want to be transparent and request that you share anonymous usage data with us. This data is purely statistical in nature and helps us understand your needs & provide better support to your self-hosted instance. You can read more about what information is collected in our documentation https://docs.appsmith.com/v/v1.2.1/setup/telemetry", "value": "false" diff --git a/app/client/README.md b/app/client/README.md index 2a275ea504..4a5ba02f8e 100755 --- a/app/client/README.md +++ b/app/client/README.md @@ -6,4 +6,4 @@ This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). -For details on setting up your development machine, please refer to the [Setup Guide](https://github.com/appsmithorg/appsmith/blob/release/contributions/ClientSetup.md) +For details on setting up your development machine, please refer to the [Setup Guide](../../contributions/ClientSetup.md) diff --git a/app/client/cypress/fixtures/application-file.json b/app/client/cypress/fixtures/application-file.json new file mode 100644 index 0000000000..5a1193ac1d --- /dev/null +++ b/app/client/cypress/fixtures/application-file.json @@ -0,0 +1,174 @@ +{ + "exportedApplication": { + "userPermissions": [ + "canComment:applications", + "manage:applications", + "read:applications", + "publish:applications", + "makePublic:applications" + ], + "name": "testing app - pk", + "isPublic": false, + "appIsExample": false, + "color": "#FE9F44", + "icon": "heart", + "new": true + }, + "datasourceList": [], + "pageList": [ + { + "userPermissions": [ + "read:pages", + "manage:pages" + ], + "unpublishedPage": { + "name": "Page1", + "layouts": [ + { + "id": "60a77186cdbfc9440388285c", + "userPermissions": [], + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1118, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 18, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicTriggerPathList": [], + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "widgetName": "Button1", + "rightColumn": 4, + "isDefaultClickDisabled": true, + "widgetId": "g7jf8v3wkq", + "buttonStyle": "PRIMARY_BUTTON", + "topRow": 0, + "bottomRow": 1, + "parentRowSpace": 40, + "isVisible": true, + "type": "BUTTON_WIDGET", + "version": 1, + "parentId": "0", + "isLoading": false, + "parentColumnSpace": 67.375, + "leftColumn": 2, + "text": "Submit", + "isDisabled": false + }, + { + "widgetName": "Chart1", + "rightColumn": 8, + "allowHorizontalScroll": false, + "widgetId": "ow55pc4z0z", + "topRow": 5, + "bottomRow": 13, + "parentRowSpace": 40, + "isVisible": true, + "type": "CHART_WIDGET", + "version": 1, + "parentId": "0", + "isLoading": false, + "chartData": { + "pftw37090s": { + "seriesName": "Sales", + "data": [ + { + "x": "Mon", + "y": 10000 + }, + { + "x": "Tue", + "y": 12000 + }, + { + "x": "Wed", + "y": 32000 + }, + { + "x": "Thu", + "y": 28000 + }, + { + "x": "Fri", + "y": 14000 + }, + { + "x": "Sat", + "y": 19000 + }, + { + "x": "Sun", + "y": 36000 + } + ] + } + }, + "yAxisName": "Total Order Revenue $", + "parentColumnSpace": 67.375, + "chartName": "Last week's revenue", + "leftColumn": 2, + "xAxisName": "Last Week", + "chartType": "LINE_CHART" + } + ] + }, + "layoutOnLoadActions": [], + "new": false + } + ], + "userPermissions": [] + }, + "publishedPage": { + "name": "Page1", + "layouts": [ + { + "id": "60a77186cdbfc9440388285c", + "userPermissions": [], + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1254, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 4, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [] + }, + "new": false + } + ], + "userPermissions": [] + }, + "new": true + } + ], + "publishedDefaultPageName": "Page1", + "unpublishedDefaultPageName": "Page1", + "actionList": [], + "decryptedFields": {}, + "publishedLayoutmongoEscapedWidgets": {}, + "unpublishedLayoutmongoEscapedWidgets": {} +} \ No newline at end of file diff --git a/app/client/cypress/fixtures/basicDsl.json b/app/client/cypress/fixtures/basicDsl.json new file mode 100644 index 0000000000..a1a2ec69a7 --- /dev/null +++ b/app/client/cypress/fixtures/basicDsl.json @@ -0,0 +1 @@ +{"dsl":{"widgetName":"MainContainer","backgroundColor":"none","rightColumn":966,"snapColumns":64,"detachFromLayout":true,"widgetId":"0","topRow":0,"bottomRow":320,"containerStyle":"none","snapRows":125,"parentRowSpace":1,"type":"CANVAS_WIDGET","canExtend":true,"version":23,"minHeight":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":[]}} \ No newline at end of file diff --git a/app/client/cypress/fixtures/debuggerDependencyDsl.json b/app/client/cypress/fixtures/debuggerDependencyDsl.json new file mode 100644 index 0000000000..d023180e24 --- /dev/null +++ b/app/client/cypress/fixtures/debuggerDependencyDsl.json @@ -0,0 +1,58 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 9, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "text": "Submit", + "buttonStyle": "PRIMARY_BUTTON", + "widgetName": "Button1", + "isDisabled": false, + "isDefaultClickDisabled": true, + "type": "BUTTON_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 5, + "rightColumn": 7, + "topRow": 2, + "bottomRow": 3, + "parentId": "0", + "widgetId": "3qg87le9t4" + }, + { + "isVisible": true, + "inputType": "TEXT", + "label": "", + "widgetName": "Input1", + "type": "INPUT_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 2, + "rightColumn": 7, + "topRow": 0, + "bottomRow": 1, + "parentId": "0", + "widgetId": "2lhsjdd5sg" + } + ] + } + } \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/ExecutionParams_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/ExecutionParams_spec.js index 6e4a234a3b..249f28f43b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/ExecutionParams_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/ExecutionParams_spec.js @@ -31,8 +31,12 @@ describe("API Panel Test Functionality", function() { cy.contains(".t--datasource-name", datasourceName) .find(queryLocators.createQuery) .click(); - cy.get(queryLocators.templateMenu).click(); + cy.get(queryLocators.settings).click({ force: true }); + cy.get(queryLocators.switch) + .last() + .click({ force: true }); + cy.get(queryLocators.query).click({ force: true }); cy.get(".CodeMirror textarea") .first() .focus() @@ -43,6 +47,7 @@ describe("API Panel Test Functionality", function() { cy.WaitAutoSave(); cy.runQuery(); }); + it("Will pass execution params", function() { // Bind the table cy.SearchEntityandOpen("Table1"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js index 7cc05bf94f..c133dd319f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js @@ -1,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), + ); }); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ExportApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ExportApplication_spec.js new file mode 100644 index 0000000000..3b58ec8dde --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ExportApplication_spec.js @@ -0,0 +1,159 @@ +const dsl = require("../../../../fixtures/displayWidgetDsl.json"); +const homePage = require("../../../../locators/HomePage.json"); +const commonlocators = require("../../../../locators/commonlocators.json"); + +describe("Export application as a JSON file", function() { + let orgid; + let appid; + let currentUrl; + let newOrganizationName; + let appname; + + before(() => { + cy.addDsl(dsl); + }); + + it("Check if exporting app flow works as expected", function() { + cy.get(commonlocators.homeIcon).click({ force: true }); + appname = localStorage.getItem("AppName"); + cy.get(homePage.searchInput).type(appname); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + + cy.get(homePage.applicationCard) + .first() + .trigger("mouseover"); + cy.get(homePage.appMoreIcon) + .first() + .click({ force: true }); + cy.get(homePage.exportAppFromMenu).click({ force: true }); + cy.get(homePage.toastMessage).should("contain", "Successfully exported"); + cy.LogOut(); + }); + + it("User with admin access,should be able to export the app", function() { + cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); + cy.NavigateToHome(); + cy.generateUUID().then((uid) => { + orgid = uid; + appid = uid; + localStorage.setItem("OrgName", orgid); + cy.createOrg(); + cy.wait("@createOrg").then((interception) => { + newOrganizationName = interception.response.body.data.name; + cy.renameOrg(newOrganizationName, orgid); + }); + cy.CreateAppForOrg(orgid, appid); + cy.wait("@getPagesForCreateApp").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get("h2").contains("Drag and drop a widget here"); + cy.get(homePage.shareApp).click({ force: true }); + cy.shareApp(Cypress.env("TESTUSERNAME1"), homePage.adminRole); + + cy.LogOut(); + + cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); + cy.NavigateToHome(); + cy.wait(2000); + cy.log({ appid }); + cy.get(homePage.searchInput).type(appid); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + + cy.get(homePage.applicationCard) + .first() + .trigger("mouseover"); + cy.get(homePage.appMoreIcon) + .first() + .click({ force: true }); + cy.get(homePage.exportAppFromMenu).should("be.visible"); + }); + cy.LogOut(); + }); + + it("User with developer access,should not be able to export the app", function() { + cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); + cy.NavigateToHome(); + cy.generateUUID().then((uid) => { + orgid = uid; + appid = uid; + localStorage.setItem("OrgName", orgid); + cy.createOrg(); + cy.wait("@createOrg").then((interception) => { + newOrganizationName = interception.response.body.data.name; + cy.renameOrg(newOrganizationName, orgid); + }); + cy.CreateAppForOrg(orgid, appid); + cy.wait("@getPagesForCreateApp").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get("h2").contains("Drag and drop a widget here"); + cy.get(homePage.shareApp).click({ force: true }); + cy.shareApp(Cypress.env("TESTUSERNAME1"), homePage.developerRole); + + cy.LogOut(); + + cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); + cy.NavigateToHome(); + cy.wait(2000); + cy.log({ appid }); + cy.get(homePage.searchInput).type(appid); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + + cy.get(homePage.applicationCard) + .first() + .trigger("mouseover"); + cy.get(homePage.appMoreIcon) + .first() + .click({ force: true }); + cy.get(homePage.exportAppFromMenu).should("not.exist"); + }); + cy.LogOut(); + }); + + it("User with viewer access,should not be able to export the app", function() { + cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); + cy.NavigateToHome(); + cy.generateUUID().then((uid) => { + orgid = uid; + appid = uid; + localStorage.setItem("OrgName", orgid); + cy.createOrg(); + cy.wait("@createOrg").then((interception) => { + newOrganizationName = interception.response.body.data.name; + cy.renameOrg(newOrganizationName, orgid); + }); + cy.CreateAppForOrg(orgid, appid); + cy.wait("@getPagesForCreateApp").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get("h2").contains("Drag and drop a widget here"); + cy.get(homePage.shareApp).click({ force: true }); + cy.shareApp(Cypress.env("TESTUSERNAME1"), homePage.viewerRole); + + cy.LogOut(); + + cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); + cy.NavigateToHome(); + cy.wait(2000); + cy.log({ appid }); + cy.get(homePage.searchInput).type(appid); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + + cy.get(homePage.applicationCard) + .first() + .trigger("mouseover"); + cy.get(homePage.appEditIcon).should("not.exist"); + }); + cy.LogOut(); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js index 91f4ece082..1a33fcb0da 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js @@ -1,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(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Inspect_Element_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Inspect_Element_spec.js new file mode 100644 index 0000000000..c492494e88 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Inspect_Element_spec.js @@ -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"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Derived_Column_Data_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Derived_Column_Data_validation_spec.js index 2104fa8e76..d57c23fac3 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Derived_Column_Data_validation_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Derived_Column_Data_validation_spec.js @@ -43,13 +43,12 @@ describe("Test Create Api and Bind to Table widget", function() { // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); cy.toggleJsAndUpdate("tabledata", "Green"); - cy.get(commonlocators.editPropCrossButton).click(); + cy.get(".t--property-pane-back-btn").click({ force: true }); cy.wait("@updateLayout"); cy.readTabledataValidateCSS("1", "0", "background-color", "rgb(0, 128, 0)"); }); it("Edit column name and validate test for computed value based on column type selected", function() { - cy.SearchEntityandOpen("Table1"); cy.editColumn("customColumn1"); cy.readTabledataPublish("1", "9").then((tabData) => { const tabValue = tabData; diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js index 1bfbed6a6b..4fabdef2c4 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_spec.js @@ -3,7 +3,6 @@ const widgetsPage = require("../../../../locators/Widgets.json"); const commonlocators = require("../../../../locators/commonlocators.json"); const publish = require("../../../../locators/publishWidgetspage.json"); const dsl = require("../../../../fixtures/tableWidgetDsl.json"); -const pages = require("../../../../locators/Pages.json"); describe("Table Widget Functionality", function() { before(() => { @@ -83,7 +82,9 @@ describe("Table Widget Functionality", function() { cy.wait(5000); cy.get(publish.searchInput) .first() - .clear() + .within(() => { + return cy.get("input").clear(); + }) .type("7434532"); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); @@ -97,7 +98,9 @@ describe("Table Widget Functionality", function() { it("Table Widget Functionality To Filter The Data", function() { cy.get(publish.searchInput) .first() - .clear(); + .within(() => { + return cy.get("input").clear(); + }); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); cy.isSelectRow(1); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_spec.js index cf610c78f5..e0e0885887 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_spec.js @@ -36,7 +36,19 @@ describe("Text Widget Functionality", function() { .should("have.css", "font-size", "24px"); }); + it("Text Email Parsing Validation", function() { + cy.testCodeMirror("ab.end@domain.com"); + cy.wait("@updateLayout"); + cy.PublishtheApp(); + cy.get(commonlocators.headingTextStyle + " a").should( + "have.attr", + "href", + "mailto:ab.end@domain.com", + ); + }); + it("Text-TextStyle Label Validation", function() { + cy.testCodeMirror(this.data.TextLabelValue); //Changing the Text Style's and validating cy.ChangeTextStyle( this.data.TextLabel, diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js index 06b63c3340..ae64c14c37 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js @@ -1,5 +1,6 @@ const testdata = require("../../../../fixtures/testdata.json"); const apiwidget = require("../../../../locators/apiWidgetslocator.json"); +const widgetsPage = require("../../../../locators/Widgets.json"); const explorer = require("../../../../locators/explorerlocators.json"); const commonlocators = require("../../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../../locators/FormWidgets.json"); @@ -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 */ diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js index 178635df67..1b0841806c 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js @@ -31,10 +31,9 @@ describe("Tab widget test", function() { cy.RenameEntity(tabname); cy.validateMessage(tabname); cy.deleteEntity(); - cy.get(commonlocators.entityExplorersearch).should("be.visible"); cy.get(commonlocators.entityExplorersearch) - .clear() - .type("Tab 2"); + .clear({ force: true }) + .type("Tab 2", { force: true }); cy.get( commonlocators.entitySearchResult.concat("Tab 2").concat("')"), ).should("not.exist"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js index 50bef3f6c9..70cef20567 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js @@ -3,6 +3,7 @@ const formWidgetsPage = require("../../../../locators/FormWidgets.json"); const publish = require("../../../../locators/publishWidgetspage.json"); const dsl = require("../../../../fixtures/formdsl.json"); const pages = require("../../../../locators/Pages.json"); +const widgetsPage = require("../../../../locators/Widgets.json"); describe("Form Widget Functionality", function() { before(() => { @@ -23,10 +24,13 @@ describe("Form Widget Functionality", function() { /** * @param{Text} Random Colour */ - cy.testCodeMirror(this.data.colour); + cy.get(widgetsPage.backgroundcolorPicker) + .first() + .click({ force: true }); + cy.xpath(widgetsPage.greenColor).click(); cy.get(formWidgetsPage.formD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); /** * @param{toggleButton Css} Assert to be checked */ @@ -40,7 +44,7 @@ describe("Form Widget Functionality", function() { it("Form Widget Functionality To Verify The Colour", function() { cy.get(formWidgetsPage.formD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); }); it("Form Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/LeaveOrganizationTest_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/LeaveOrganizationTest_spec.js index 73758abae9..325789bc62 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/LeaveOrganizationTest_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/LeaveOrganizationTest_spec.js @@ -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); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgImportApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgImportApplication_spec.js new file mode 100644 index 0000000000..620b05e0aa --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OrganisationTests/OrgImportApplication_spec.js @@ -0,0 +1,39 @@ +const homePage = require("../../../../locators/HomePage.json"); + +describe("Organization Import Application", function() { + let orgid; + let newOrganizationName; + const fixtureDummyAppPath = "application-file.json"; + it("Can Import Application", function() { + cy.NavigateToHome(); + cy.generateUUID().then((uid) => { + orgid = uid; + localStorage.setItem("OrgName", orgid); + cy.createOrg(); + cy.wait("@createOrg").then((createOrgInterception) => { + newOrganizationName = createOrgInterception.response.body.data.name; + cy.renameOrg(newOrganizationName, orgid); + cy.get(homePage.orgImportAppOption).click({ force: true }); + + cy.get(homePage.orgImportAppModal).should("be.visible"); + cy.xpath(homePage.uploadLogo).attachFile(fixtureDummyAppPath); + + cy.get(homePage.orgImportAppButton).click({ force: true }); + cy.wait("@importNewApplication").then((interception) => { + let appId = interception.response.body.data.id; + let defaultPage = interception.response.body.data.pages.find( + (eachPage) => !!eachPage.isDefault, + ); + cy.get(homePage.toastMessage).should( + "contain", + "Application imported successfully", + ); + cy.url().should( + "include", + `/applications/${appId}/pages/${defaultPage.id}/edit`, + ); + }); + }); + }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js index bf7a8c4c50..da45d8d1a5 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js @@ -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})`) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js index ff72c2c1c5..fccf4a4aa8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js @@ -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); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateOrgTests_spec.js index 2b7c53bdf6..d50dc6924b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateOrgTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/CreateOrgTests_spec.js @@ -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); diff --git a/app/client/cypress/locators/HomePage.json b/app/client/cypress/locators/HomePage.json index fbc854261f..d6b0808e4b 100644 --- a/app/client/cypress/locators/HomePage.json +++ b/app/client/cypress/locators/HomePage.json @@ -9,6 +9,7 @@ "appMoreIcon": ".bp3-popover-wrapper.more .bp3-popover-target", "duplicateApp": "[data-cy=t--duplicate]", "forkAppFromMenu": "[data-cy=t--fork-app]", + "exportAppFromMenu": "[data-cy=t--export-app]", "forkAppOrgList": ".radio-group", "forkAppOrgButton": "[data-cy=t--fork-app-to-org-button]", "selectAction": "#Base", @@ -59,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]", diff --git a/app/client/cypress/locators/QueryEditor.json b/app/client/cypress/locators/QueryEditor.json index 28929954ba..88f760a234 100644 --- a/app/client/cypress/locators/QueryEditor.json +++ b/app/client/cypress/locators/QueryEditor.json @@ -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" +} \ No newline at end of file diff --git a/app/client/cypress/locators/publishWidgetspage.json b/app/client/cypress/locators/publishWidgetspage.json index b4a75d5dad..d1eb7eb40b 100644 --- a/app/client/cypress/locators/publishWidgetspage.json +++ b/app/client/cypress/locators/publishWidgetspage.json @@ -22,7 +22,7 @@ "pickMyLocation": ".t--widget-mapwidget div[title='Pick My Location']", "rectChart": ".t--widget-chartwidget g rect", "chartLab": ".t--widget-chartwidget g:nth-child(5) text", - "searchInput": "input", + "searchInput": ".t--search-input", "downloadBtn": ".t--table-download-btn", "filterBtn": ".t--table-filter-toggle-btn", "attributeDropdown": ".t--table-filter-columns-dropdown", diff --git a/app/client/cypress/manual_TestSuite/Tab_Widget_Spec.js b/app/client/cypress/manual_TestSuite/Tab_Widget_Spec.js new file mode 100644 index 0000000000..6e88a6da63 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Tab_Widget_Spec.js @@ -0,0 +1,85 @@ +const dsl = require("../../../fixtures/TabWidgetDsl.json"); + +describe("Tab widget", function() { + it("Movement of tabs inside Tab widget ", function() { + // Drag and drop the Tab widget + // click on "Add a Tab" + // Add multiple Tabs + // Hold and move the Tab + // and observe if the tab are moved in the same + }); + + it(" Deletion of Tabs and adding them back with Undo", function() { + // Drag and drop the Tab widget + // click on "Add a Tab" + // Add multiple Tabs + // Click on delete option of the Tab + // ensure the tab is deleted + // Click on delete option of the Tab + // Ensure an info message is dispalyed to user + // Now click on "UNDO" + //and observe that the Tab is added back + }); + + it("Test Ideas for testing the Visible option for tabs ", function() { + // Drag and drop the Tab widget + // click on "Add a Tab" + // Click on Property pane of the tab widget + // Click on the Control Pane of the tab + // Now click on JS option + // Now add it false + // and observe the Tab is blured on edit Mode + // Now click on Deploy + // Observe the tab is not dispalyed to user + // Now come back to Edit mode + // Click on the Control Pane of the tab + // Now click on JS option + // enable the button + // Now observe the Tab must be visible and normal + }); + + it("Test Ideas for testing the Show Tabs Feature ", function() { + // Drag and drop the Tab widget + // Click on Property pane of the tab widget + // Scroll down to Show Tabs option + // Now click on JS option + // Now add it false + // and observe the Tab widget does not show any Tabs + // Now click on Deploy + // Observe the Tab widget does not show any Tabs + // Now come back to Edit mode + // Now click on JS option + // enable the button + // Now observe the Tab must be visible + }); + + it("Adding multiple widgets inside the Tab widget", function() { + // Drag and drop the Tab widget + // Ensure default 2 Tabs are dispalyed to user + // Add date picker, Text and Button into Tab1 + // Click on Tab2 and ensure it is empty + // Add image widget , radio, Check widget + // Click on Deploy + // Ensure the Tab widget with widgets are displayed to user + }); + + it("Adding action while changing the Tab ", function() { + // Drag and drop the Tab widget + // Click on Property pane of the tab widget + // Navigate to Action section + // Select "Show a modal" + // Click on "Add a Modal" + // Assign related action on the modal + // Now change the tab + // and observe the modal pop up is displayed to user + }); + + it("Binding the Tab to widget ", function() { + // Drag and drop the Tab widget + // Click on Property pane of the tab widget + // Navigate to control pane + // Convert a Visible option to JS + // Add binding to the widget + // Ensure user is dispalyed on the TAB widget + }); +}); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 75ea622e30..5d7ead70f5 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -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", ); diff --git a/app/client/docker/templates/nginx-app.conf.template b/app/client/docker/templates/nginx-app.conf.template index 4b7c5fdf4e..6b8ed93474 100644 --- a/app/client/docker/templates/nginx-app.conf.template +++ b/app/client/docker/templates/nginx-app.conf.template @@ -48,6 +48,7 @@ server { sub_filter __APPSMITH_MAIL_ENABLED__ '${APPSMITH_MAIL_ENABLED}'; sub_filter __APPSMITH_DISABLE_TELEMETRY__ '${APPSMITH_DISABLE_TELEMETRY}'; sub_filter __APPSMITH_CLOUD_SERVICES_BASE_URL__ '${APPSMITH_CLOUD_SERVICES_BASE_URL}'; + sub_filter __APPSMITH_RECAPTCHA_SITE_KEY__ '${APPSMITH_RECAPTCHA_SITE_KEY}'; } location /f { diff --git a/app/client/netlify.toml b/app/client/netlify.toml index 435adaafc5..9c900dc92d 100644 --- a/app/client/netlify.toml +++ b/app/client/netlify.toml @@ -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" diff --git a/app/client/package.json b/app/client/package.json index a1b874beab..e5835567de 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -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", diff --git a/app/client/patches/@blueprintjs+core+3.36.0.patch b/app/client/patches/@blueprintjs+core+3.36.0.patch new file mode 100644 index 0000000000..3342cbfefc --- /dev/null +++ b/app/client/patches/@blueprintjs+core+3.36.0.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js b/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js +index 84f03fa..5e5488a 100644 +--- a/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js ++++ b/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js +@@ -188,7 +188,16 @@ var EditableText = /** @class */ (function (_super) { + if (this.state.isEditing && !prevState.isEditing) { + (_b = (_a = this.props).onEdit) === null || _b === void 0 ? void 0 : _b.call(_a, this.state.value); + } +- this.updateInputDimensions(); ++ // updateInputDimensions is an expensive method. Call it only when the props ++ // it depends on change ++ if (this.state.value !== prevState.value || ++ this.props.alwaysRenderInput !== prevProps.alwaysRenderInput || ++ this.props.maxLines !== prevProps.maxLines || ++ this.props.minLines !== prevProps.minLines || ++ this.props.minWidth !== prevProps.minWidth || ++ this.props.multiline !== prevProps.multiline) { ++ this.updateInputDimensions(); ++ } + }; + EditableText.prototype.renderInput = function (value) { + var _a = this.props, disabled = _a.disabled, maxLength = _a.maxLength, multiline = _a.multiline, type = _a.type, placeholder = _a.placeholder; diff --git a/app/client/public/index.html b/app/client/public/index.html index 8079bbba08..df6675bb58 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -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__"), }; diff --git a/app/client/src/actions/applicationActions.ts b/app/client/src/actions/applicationActions.ts index 1dda8b55d8..e4cf360f65 100644 --- a/app/client/src/actions/applicationActions.ts +++ b/app/client/src/actions/applicationActions.ts @@ -1,6 +1,9 @@ import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; import { APP_MODE } from "../reducers/entityReducers/appReducer"; -import { UpdateApplicationPayload } from "api/ApplicationApi"; +import { + UpdateApplicationPayload, + ImportApplicationRequest, +} from "api/ApplicationApi"; export const setDefaultApplicationPageSuccess = ( pageId: string, @@ -77,6 +80,13 @@ export const duplicateApplication = (applicationId: string) => { }; }; +export const importApplication = (appDetails: ImportApplicationRequest) => { + return { + type: ReduxActionTypes.IMPORT_APPLICATION_INIT, + payload: appDetails, + }; +}; + export const getAllApplications = () => { return { type: ReduxActionTypes.GET_ALL_APPLICATION_INIT, diff --git a/app/client/src/actions/commentActions.ts b/app/client/src/actions/commentActions.ts index 114e1277b0..39d0d071aa 100644 --- a/app/client/src/actions/commentActions.ts +++ b/app/client/src/actions/commentActions.ts @@ -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, diff --git a/app/client/src/actions/notificationActions.ts b/app/client/src/actions/notificationActions.ts new file mode 100644 index 0000000000..a44b88b47b --- /dev/null +++ b/app/client/src/actions/notificationActions.ts @@ -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; +}) => ({ + 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, +}); diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index 3bf4ab40ab..736b901e8c 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -104,9 +104,10 @@ export const updateAndSaveLayout = ( }; }; -export const saveLayout = () => { +export const saveLayout = (isRetry?: boolean) => { return { type: ReduxActionTypes.SAVE_PAGE_INIT, + payload: { isRetry }, }; }; diff --git a/app/client/src/api/ApplicationApi.tsx b/app/client/src/api/ApplicationApi.tsx index 0347802ff0..5ad7f57fd7 100644 --- a/app/client/src/api/ApplicationApi.tsx +++ b/app/client/src/api/ApplicationApi.tsx @@ -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 { + const formData = new FormData(); + if (request.applicationFile) { + formData.append("file", request.applicationFile); + } + return Api.post("v1/applications/import/" + request.orgId, formData, null, { + headers: { + "Content-Type": "multipart/form-data", + }, + onUploadProgress: request.progress, + }); + } } export default ApplicationApi; diff --git a/app/client/src/api/NotificationsAPI.tsx b/app/client/src/api/NotificationsAPI.tsx new file mode 100644 index 0000000000..c1f981c4e5 --- /dev/null +++ b/app/client/src/api/NotificationsAPI.tsx @@ -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 { + return Api.get(NotificaitonsApi.baseURL); + } + + // TODO update mark all as read notifications api + static markAllNotificationsAsRead(): AxiosPromise { + return Api.get(NotificaitonsApi.baseURL); + } +} + +export default NotificaitonsApi; diff --git a/app/client/src/assets/icons/ads/bell.svg b/app/client/src/assets/icons/ads/bell.svg new file mode 100644 index 0000000000..397af1d327 --- /dev/null +++ b/app/client/src/assets/icons/ads/bell.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/download.svg b/app/client/src/assets/icons/ads/download.svg new file mode 100644 index 0000000000..0cbe360e6c --- /dev/null +++ b/app/client/src/assets/icons/ads/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/upload_success.svg b/app/client/src/assets/icons/ads/upload_success.svg new file mode 100644 index 0000000000..578e9a3171 --- /dev/null +++ b/app/client/src/assets/icons/ads/upload_success.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/images/InspectElement.svg b/app/client/src/assets/images/InspectElement.svg new file mode 100644 index 0000000000..75a86413ce --- /dev/null +++ b/app/client/src/assets/images/InspectElement.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/client/src/assets/images/comments-onboarding/step-4.png b/app/client/src/assets/images/comments-onboarding/step-4.png index e8a6bd6274..fd20595d7a 100644 Binary files a/app/client/src/assets/images/comments-onboarding/step-4.png and b/app/client/src/assets/images/comments-onboarding/step-4.png differ diff --git a/app/client/src/assets/images/comments-onboarding/step-5.png b/app/client/src/assets/images/comments-onboarding/step-5.png new file mode 100644 index 0000000000..e8a6bd6274 Binary files /dev/null and b/app/client/src/assets/images/comments-onboarding/step-5.png differ diff --git a/app/client/src/assets/lottie/pulse-dot.json b/app/client/src/assets/lottie/pulse-dot.json new file mode 100644 index 0000000000..a4901959e1 --- /dev/null +++ b/app/client/src/assets/lottie/pulse-dot.json @@ -0,0 +1 @@ +{"v":"5.5.0","fr":29.9700012207031,"ip":0,"op":70.0000028511585,"w":250,"h":250,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":90,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[118.549,117.468,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,2.889]},"t":0,"s":[83.62,83.62,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,-0.549]},"t":27,"s":[105.62,105.62,100]},{"t":52.0000021180034,"s":[83.62,83.62,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[106.672,106.672],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9490196078431372,0.16862745098039217,0.16862745098039217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[7.715,9.008],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":72.0000029326201,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"w1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":25.0000010182709,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[125.156,124.494,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[83.6,83.6,100]},{"t":25.0000010182709,"s":[234.048,234.048,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[69.508,69.508],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9490196078431372,0.16862745098039217,0.16862745098039217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.187,0.605],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[139.443,139.443],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"w2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":30.0000012219251,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[125.326,124.285,0],"ix":2},"a":{"a":0,"k":[-44.248,-66.371,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[92.661,92.661,100]},{"t":30.0000012219251,"s":[229.892,229.892,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[86.801,86.801],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9490196078431372,0.16862745098039217,0.16862745098039217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-44.6,-65.6],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/client/src/comments/AppComments/AppCommentThreads.tsx b/app/client/src/comments/AppComments/AppCommentThreads.tsx index 755c760d39..d4b5dad56b 100644 --- a/app/client/src/comments/AppComments/AppCommentThreads.tsx +++ b/app/client/src/comments/AppComments/AppCommentThreads.tsx @@ -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(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 ( -
- {commentThreadIds.map((commentThreadId: string) => ( - - ))} -
- {appThreadsHeightEqZero && } + {commentThreadIds.length > 0 && ( + ( + /** Keeping this as a fail safe: since zero + * height elements throw an error + * */ +
+ +
+ )} + /> + )} + {commentThreadIds.length === 0 && }
); } diff --git a/app/client/src/comments/AppComments/AppComments.tsx b/app/client/src/comments/AppComments/AppComments.tsx index b4057d7566..c78e9df639 100644 --- a/app/client/src/comments/AppComments/AppComments.tsx +++ b/app/client/src/comments/AppComments/AppComments.tsx @@ -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 ( - + diff --git a/app/client/src/comments/AppComments/AppCommentsFilterPopover.tsx b/app/client/src/comments/AppComments/AppCommentsFilterPopover.tsx index 637cece82d..f3a4fe2466 100644 --- a/app/client/src/comments/AppComments/AppCommentsFilterPopover.tsx +++ b/app/client/src/comments/AppComments/AppCommentsFilterPopover.tsx @@ -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" > - - - + ); } diff --git a/app/client/src/comments/AppComments/Container.tsx b/app/client/src/comments/AppComments/Container.tsx index 7d898a9690..3aaf327c6c 100644 --- a/app/client/src/comments/AppComments/Container.tsx +++ b/app/client/src/comments/AppComments/Container.tsx @@ -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; diff --git a/app/client/src/comments/CommentCard/CommentCard.tsx b/app/client/src/comments/CommentCard/CommentCard.tsx index 398fedc206..c73f417f6d 100644 --- a/app/client/src/comments/CommentCard/CommentCard.tsx +++ b/app/client/src/comments/CommentCard/CommentCard.tsx @@ -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)); } diff --git a/app/client/src/comments/CommentCard/ResolveCommentButton.tsx b/app/client/src/comments/CommentCard/ResolveCommentButton.tsx index 32871cebe9..2c3062ba0e 100644 --- a/app/client/src/comments/CommentCard/ResolveCommentButton.tsx +++ b/app/client/src/comments/CommentCard/ResolveCommentButton.tsx @@ -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 ( diff --git a/app/client/src/comments/CommentThread/CommentThread.tsx b/app/client/src/comments/CommentThread/CommentThread.tsx index 9bdb8f72e4..4068d6f8a2 100644 --- a/app/client/src/comments/CommentThread/CommentThread.tsx +++ b/app/client/src/comments/CommentThread/CommentThread.tsx @@ -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 ? ( - - -
- - {parentComment && ( - - )} - {!hideChildren && - childComments && - childComments.length > 0 && ( - - {childComments.map((comment) => ( - - ))} - - )} -
- - {!isScrolledToBottom && ( - - )} -
- {!hideInput && ( - - )} - - - ) : null, + return isThreadVisible ? ( + +
+ + {parentComment && ( + + )} + {!hideChildren && childComments && childComments.length > 0 && ( + + {childComments.map((comment) => ( + + ))} + + )} +
+ + {!isScrolledToBottom && ( + + )} +
+ {!hideInput && ( + )} - - ); + + ) : null; } export default CommentThreadContainer; diff --git a/app/client/src/comments/CommentsShowcaseCarousel/CommentsCarouselModal.tsx b/app/client/src/comments/CommentsShowcaseCarousel/CommentsCarouselModal.tsx index 4597b03550..26c34a56dd 100644 --- a/app/client/src/comments/CommentsShowcaseCarousel/CommentsCarouselModal.tsx +++ b/app/client/src/comments/CommentsShowcaseCarousel/CommentsCarouselModal.tsx @@ -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} diff --git a/app/client/src/comments/CommentsShowcaseCarousel/index.tsx b/app/client/src/comments/CommentsShowcaseCarousel/index.tsx index d9725a62fc..c4eabed38f 100644 --- a/app/client/src/comments/CommentsShowcaseCarousel/index.tsx +++ b/app/client/src/comments/CommentsShowcaseCarousel/index.tsx @@ -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, diff --git a/app/client/src/comments/init.ts b/app/client/src/comments/init.ts deleted file mode 100644 index f3db7d4a4b..0000000000 --- a/app/client/src/comments/init.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { updateAndSaveLayout } from "actions/pageActions"; -import { uniqueId } from "lodash"; - -const dsl = require("./dsl.json"); - -export const updateLayout = () => updateAndSaveLayout(dsl.widgets as any); - -export const getTestComments = () => { - const commentThreads = Object.entries(dsl.widgets).map(([widgetId]) => { - return { - refId: widgetId, - position: { top: 10, left: 15 }, - id: uniqueId(), - comments: [{ body: widgetId, authorName: uniqueId() }], - }; - }); - - // const [widgetId] = Object.entries(dsl.widgets)[0]; - // const commentThreads = [ - // { - // refId: widgetId, - // meta: { - // position: { top: 10, left: 15 }, - // }, - // id: `${1}`, - // comments: [{ body: widgetId }], - // }, - // ]; - - return commentThreads; -}; diff --git a/app/client/src/comments/inlineComments/Comments.tsx b/app/client/src/comments/inlineComments/Comments.tsx index 822c45e7b9..5533ffa719 100644 --- a/app/client/src/comments/inlineComments/Comments.tsx +++ b/app/client/src/comments/inlineComments/Comments.tsx @@ -1,12 +1,53 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { useSelector } from "react-redux"; import UnpublishedCommentThread from "./UnpublishedCommentThread"; import InlineCommentPin from "./InlineCommentPin"; import { + commentThreadsSelector, refCommentThreadsSelector, unpublishedCommentThreadSelector, } from "../../selectors/commentsSelectors"; -import { getCurrentApplicationId } from "selectors/editorSelectors"; +import { + getCurrentApplicationId, + getCurrentPageId, +} from "selectors/editorSelectors"; +import { useLocation } from "react-router"; + +// TODO refactor application comment threads by page id to optimise +// if lists turn out to be expensive +function InlinePageCommentPin({ + commentThreadId, + focused, +}: { + commentThreadId: string; + focused: boolean; +}) { + const commentThread = useSelector(commentThreadsSelector(commentThreadId)); + const currentPageId = useSelector(getCurrentPageId); + + if (commentThread && commentThread.pageId !== currentPageId) return null; + + return ( + + ); +} + +const MemoisedInlinePageCommentPin = React.memo(InlinePageCommentPin); + +const useSelectCommentThreadUsingQuery = () => { + const location = useLocation(); + const [commentThreadIdInUrl, setCommentThreadIdInUrl] = useState< + string | null + >(); + + useEffect(() => { + const searchParams = new URL(window.location.href).searchParams; + const commentThreadIdInUrl = searchParams.get("commentThreadId"); + setCommentThreadIdInUrl(commentThreadIdInUrl); + }, [location]); + + return commentThreadIdInUrl; +}; /** * Renders comment threads associated with a refId (for example widgetId) @@ -21,13 +62,15 @@ function Comments({ refId }: { refId: string }) { const unpublishedCommentThread = useSelector( unpublishedCommentThreadSelector(refId), ); + const commentThreadIdInUrl = useSelectCommentThreadUsingQuery(); return ( <> {Array.isArray(commentsThreadIds) && commentsThreadIds.map((commentsThreadId: any) => ( - ))} diff --git a/app/client/src/comments/inlineComments/InlineCommentPin.tsx b/app/client/src/comments/inlineComments/InlineCommentPin.tsx index 4e1fe69035..30e7adbb2a 100644 --- a/app/client/src/comments/inlineComments/InlineCommentPin.tsx +++ b/app/client/src/comments/inlineComments/InlineCommentPin.tsx @@ -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 ? ( - - { - // capture clicks so that create new thread is not triggered - e.preventDefault(); - e.stopPropagation(); - }} - top={top} - > - - - - } - 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" - > - - - - - ) : null, - )} - - ); + return isPinVisible ? ( + { + // capture clicks so that create new thread is not triggered + e.preventDefault(); + e.stopPropagation(); + }} + top={top} + > + + } + enforceFocus={false} + hasBackdrop + // isOpen is controlled so that newly created threads are set to be visible + isOpen={!!isCommentThreadVisible} + minimal + modifiers={modifiers} + onInteraction={(nextOpenState: boolean) => { + if (nextOpenState) { + dispatch(setVisibleThread(commentThreadId)); + } else { + dispatch(resetVisibleThread(commentThreadId)); + } + }} + placement={"right-start"} + popoverClassName="comment-thread" + portalClassName="inline-comment-thread" + > + + + + ) : null; } export default InlineCommentPin; diff --git a/app/client/src/comments/tour/commentsTourSteps.ts b/app/client/src/comments/tour/commentsTourSteps.ts index e9804059d0..fdf40b8bfd 100644 --- a/app/client/src/comments/tour/commentsTourSteps.ts +++ b/app/client/src/comments/tour/commentsTourSteps.ts @@ -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; diff --git a/app/client/src/comments/utils.ts b/app/client/src/comments/utils.ts index 3c8ab11dcd..2a863d77ea 100644 --- a/app/client/src/comments/utils.ts +++ b/app/client/src/comments/utils.ts @@ -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 = { + commentThreadId, + isCommentMode: true, + }; + + if (isResolved) { + queryParams.isResolved = true; + } + + const url = new URL( + `${window.location.origin}${BUILDER_PAGE_URL( + applicationId, + pageId, + queryParams, + )}`, + ); + + return url; +}; diff --git a/app/client/src/components/ads/DisplayImageUpload.tsx b/app/client/src/components/ads/DisplayImageUpload.tsx index f982914e4b..fb35df09fe 100644 --- a/app/client/src/components/ads/DisplayImageUpload.tsx +++ b/app/client/src/components/ads/DisplayImageUpload.tsx @@ -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 ( setIsModalOpen(true)}> ) : ( { - console.log(e, "error"); + onError={() => { setLoadError(true); }} onLoad={() => setLoadError(false)} diff --git a/app/client/src/components/ads/FilePicker.tsx b/app/client/src/components/ads/FilePicker.tsx index 1d5d6936b4..4ccffaa125 100644 --- a/app/client/src/components/ads/FilePicker.tsx +++ b/app/client/src/components/ads/FilePicker.tsx @@ -3,28 +3,52 @@ import styled from "styled-components"; import Button, { Category, Size } from "./Button"; import axios from "axios"; import { ReactComponent as UploadIcon } from "../../assets/icons/ads/upload.svg"; +import { ReactComponent as UploadSuccessIcon } from "../../assets/icons/ads/upload_success.svg"; import { DndProvider, useDrop, DropTargetMonitor } from "react-dnd"; import HTML5Backend, { NativeTypes } from "react-dnd-html5-backend"; import Text, { TextType } from "./Text"; import { Classes, Variant } from "./common"; import { Toaster } from "./Toast"; -import { createMessage, ERROR_FILE_TOO_LARGE } from "constants/messages"; - +import { + createMessage, + ERROR_FILE_TOO_LARGE, + REMOVE_FILE_TOOL_TIP, +} from "constants/messages"; +import TooltipComponent from "components/ads/Tooltip"; +import { Position } from "@blueprintjs/core/lib/esm/common/position"; +import Icon, { IconSize } from "./Icon"; const CLOUDINARY_PRESETS_NAME = ""; const CLOUDINARY_CLOUD_NAME = ""; +const FileEndings = { + IMAGE: ".jpeg,.png,.svg", + JSON: ".json", + TEXT: ".txt", + ANY: "*", +}; + +export enum FileType { + IMAGE = "IMAGE", + JSON = "JSON", + TEXT = "TEXT", + ANY = "ANY", +} + type FilePickerProps = { onFileUploaded?: (fileUrl: string) => void; onFileRemoved?: () => void; fileUploader?: FileUploader; url?: string; logoUploadError?: string; + fileType: FileType; + delayedUpload?: boolean; }; const ContainerDiv = styled.div<{ isUploaded: boolean; isActive: boolean; canDrop: boolean; + fileType: FileType; }>` width: 320px; height: 190px; @@ -41,7 +65,7 @@ const ContainerDiv = styled.div<{ color: ${(props) => props.theme.colors.filePicker.color}; } - .bg-image { + .upload-form-container { width: 100%; height: 100%; display: grid; @@ -51,15 +75,36 @@ const ContainerDiv = styled.div<{ background-size: contain; } + .centered { + justify-content: center; + flex-direction: column; + align-items: center; + + .success-container { + display: flex; + align-items: center; + .success-icon { + margin-right: ${(props) => props.theme.spaces[4]}px; + } + + .success-text { + color: #03b365; + margin-right: ${(props) => props.theme.spaces[4]}px; + } + } + } + .file-description { width: 95%; - margin-top: auto; + margin: 0 auto; + margin-top: ${(props) => + props.fileType === FileType.IMAGE ? "auto" : "0px"}; margin-bottom: ${(props) => props.theme.spaces[6] + 1}px; display: none; } .file-spec { - margin-bottom: ${(props) => props.theme.spaces[2]}px; + margin-bottom: ${(props) => props.theme.spaces[3]}px; span { margin-right: ${(props) => props.theme.spaces[4]}px; } @@ -116,6 +161,11 @@ const ContainerDiv = styled.div<{ } `; +const IconWrapper = styled.div` + width: ${(props) => props.theme.spaces[9]}px; + padding-left: ${(props) => props.theme.spaces[2]}px; +`; + export type SetProgress = (percentage: number) => void; export type UploadCallback = (url: string) => void; export type FileUploader = ( @@ -159,7 +209,7 @@ export function CloudinaryUploader( } function FilePickerComponent(props: FilePickerProps) { - const { logoUploadError } = props; + const { fileType, logoUploadError } = props; const [fileInfo, setFileInfo] = useState<{ name: string; size: number }>({ name: "", size: 0, @@ -207,7 +257,7 @@ function FilePickerComponent(props: FilePickerProps) { } if (uploadPercentage === 100) { setIsUploaded(true); - if (fileDescRef.current && bgRef.current) { + if (fileDescRef.current && bgRef.current && fileType === FileType.IMAGE) { fileDescRef.current.style.display = "none"; bgRef.current.style.opacity = "1"; } @@ -219,6 +269,35 @@ function FilePickerComponent(props: FilePickerProps) { } function handleFileUpload(files: FileList | null) { + if (fileType === FileType.IMAGE) { + handleImageFileUpload(files); + } else { + handleOtherFileUpload(files); + } + } + + function handleOtherFileUpload(files: FileList | null) { + const file = files && files[0]; + let fileSize = 0; + if (!file) { + return; + } + fileSize = Math.floor(file.size / 1024); + setFileInfo({ name: file.name, size: fileSize }); + if (props.delayedUpload) { + setIsUploaded(true); + setProgress(100); + } + if (fileDescRef.current) { + fileDescRef.current.style.display = "flex"; + } + if (fileContainerRef.current) { + fileContainerRef.current.style.display = "none"; + } + props.fileUploader && props.fileUploader(file, setProgress, onUpload); + } + + function handleImageFileUpload(files: FileList | null) { const file = files && files[0]; let fileSize = 0; @@ -253,10 +332,15 @@ function FilePickerComponent(props: FilePickerProps) { } function removeFile() { - if (fileContainerRef.current && bgRef.current) { + if (fileContainerRef.current) { setFileUrl(""); + if (fileDescRef.current) { + fileDescRef.current.style.display = "none"; + } fileContainerRef.current.style.display = "flex"; - bgRef.current.style.backgroundImage = "url('')"; + if (bgRef.current) { + bgRef.current.style.backgroundImage = "url('')"; + } setIsUploaded(false); props.onFileRemoved && props.onFileRemoved(); } @@ -275,8 +359,9 @@ function FilePickerComponent(props: FilePickerProps) { } }, [props.url]); + // Following hook should be used only if file type is image. useEffect(() => { - if (fileUrl && !isUploaded) { + if (fileUrl && !isUploaded && fileType === FileType.IMAGE) { setIsUploaded(true); if (bgRef.current) { bgRef.current.style.backgroundImage = `url(${fileUrl})`; @@ -291,42 +376,47 @@ function FilePickerComponent(props: FilePickerProps) { } }, [fileUrl, logoUploadError]); - return ( - -
-
- - - Drag & Drop files to upload or - -
- handleFileUpload(el.target.files)} - ref={inputRef} - type="file" - value={""} - /> -
+ // + + const uploadFileForm = ( +
+ + + Drag & Drop files to upload or + +
+ handleFileUpload(el.target.files)} + ref={inputRef} + type="file" + value={""} + /> +
+ ); + + const uploadStatus = ( +
+ {fileInfo.name} + {fileInfo.size}KB +
+ ); + + const imageUploadComponent = ( + <> +
+ {uploadFileForm}
-
- {fileInfo.name} - {fileInfo.size}KB -
+ {uploadStatus}
@@ -341,6 +431,45 @@ function FilePickerComponent(props: FilePickerProps) { text="remove" />
+ + ); + + const uploadComponent = ( +
+ {uploadFileForm} +
+ {uploadStatus} +
+ + + Successfully Uploaded! + + + removeFile()}> + + + +
+
+
+ ); + + return ( + + {fileType === FileType.IMAGE ? imageUploadComponent : uploadComponent} ); } diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index e02af7fe68..ecca134ab8 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -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 = ; break; + case "upload": + returnIcon = ; + break; + + case "download": + returnIcon = ; + break; + default: returnIcon = null; break; diff --git a/app/client/src/components/ads/MentionsInput.tsx b/app/client/src/components/ads/MentionsInput.tsx index e62b5fe676..87d4ed8a39 100644 --- a/app/client/src/components/ads/MentionsInput.tsx +++ b/app/client/src/components/ads/MentionsInput.tsx @@ -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 ( - - Invite {props.mention.name} - + + + +
+ {createMessage(INVITE_A_NEW_USER)} + {props.mention.name} +
); } diff --git a/app/client/src/components/ads/Menu.tsx b/app/client/src/components/ads/Menu.tsx index dc3437cba9..b235273c38 100644 --- a/app/client/src/components/ads/Menu.tsx +++ b/app/client/src/components/ads/Menu.tsx @@ -12,6 +12,8 @@ type MenuProps = CommonComponentProps & { onOpening?: (node: HTMLElement) => void; onClosing?: (node: HTMLElement) => void; modifiers?: PopperModifiers; + isOpen?: boolean; + onClose?: () => void; }; const MenuWrapper = styled.div` @@ -30,8 +32,10 @@ function Menu(props: MenuProps) { className={props.className} data-cy={props.cypressSelector} disabled={props.disabled} + isOpen={props.isOpen} minimal modifiers={props.modifiers} + onClose={props.onClose} onClosing={props.onClosing} onOpening={props.onOpening} portalClassName={props.className} diff --git a/app/client/src/components/ads/Toast.tsx b/app/client/src/components/ads/Toast.tsx index 34154c8fb8..d0a78223e3 100644 --- a/app/client/src/components/ads/Toast.tsx +++ b/app/client/src/components/ads/Toast.tsx @@ -97,6 +97,7 @@ const ToastBody = styled.div<{ color: ${props.theme.colors.toast.undo}; line-height: 18px; font-weight: 600; + white-space: nowrap } ` : null} diff --git a/app/client/src/components/ads/Tooltip.tsx b/app/client/src/components/ads/Tooltip.tsx index 5adec64426..277665dfa2 100644 --- a/app/client/src/components/ads/Tooltip.tsx +++ b/app/client/src/components/ads/Tooltip.tsx @@ -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} diff --git a/app/client/src/components/ads/tour/TourTooltipWrapper.tsx b/app/client/src/components/ads/tour/TourTooltipWrapper.tsx index 3446348638..abd14b7a81 100644 --- a/app/client/src/components/ads/tour/TourTooltipWrapper.tsx +++ b/app/client/src/components/ads/tour/TourTooltipWrapper.tsx @@ -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(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 ( -
- - {tourStepConfig?.data.message} - - } - isOpen={!!isOpen} - position={Position.BOTTOM} - > - {children} - -
+ <> + {/* A crude overlay which won't work with containers having overflow hidden */} + {isOpen && props.hasOverlay && } + + {isOpen && props.showPulse && ( + + )} + + {tourStepConfig?.data.message} + + } + isOpen={!!isOpen} + modifiers={props.modifiers} + position={Position.BOTTOM} + > + {children} + + + ); } diff --git a/app/client/src/components/designSystems/appsmith/Dropdown.tsx b/app/client/src/components/designSystems/appsmith/Dropdown.tsx index 92e1a76645..2053b0310f 100644 --- a/app/client/src/components/designSystems/appsmith/Dropdown.tsx +++ b/app/client/src/components/designSystems/appsmith/Dropdown.tsx @@ -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 ( { return !!multipleWidgetsSelected; } + public onOnmnibarHotKeyDown(e: KeyboardEvent) { + e.preventDefault(); + this.props.toggleShowGlobalSearchModal(); + AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "HOTKEY_COMBO" }); + } + public renderHotkeys() { return ( @@ -79,16 +82,14 @@ class GlobalHotKeys extends React.Component { global label="Search entities" onKeyDown={(e: any) => { - const entitySearchInput = document.getElementById( - ENTITY_EXPLORER_SEARCH_ID, - ); const widgetSearchInput = document.getElementById( WIDGETS_SEARCH_ID, ); - if (entitySearchInput) entitySearchInput.focus(); - if (widgetSearchInput) widgetSearchInput.focus(); - e.preventDefault(); - e.stopPropagation(); + if (widgetSearchInput) { + widgetSearchInput.focus(); + e.preventDefault(); + e.stopPropagation(); + } }} /> { combo="mod + k" global label="Show omnibar" - onKeyDown={(e: KeyboardEvent) => { - console.log("toggleShowGlobalSearchModal"); - e.preventDefault(); - this.props.toggleShowGlobalSearchModal(); - AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "HOTKEY_COMBO" }); - }} + onKeyDown={(e) => this.onOnmnibarHotKeyDown(e)} + /> + this.onOnmnibarHotKeyDown(e)} /> { onPositionChange = noop, themeMode = props.themeMode || ThemeMode.LIGHT, } = props; - const popperTheme = useSelector((state: AppState) => - getThemeDetails(state, themeMode), + // Meomoizing to avoid rerender of draggable icon. + // What is the cost of memoizing? + const popperTheme = useMemo( + () => getThemeDetails({} as AppState, themeMode), + [themeMode], ); + useEffect(() => { const parentElement = props.targetNode && props.targetNode.parentElement; if ( @@ -133,7 +136,7 @@ export default (props: PopperProps) => { }, [ props.targetNode, props.isOpen, - props.modifiers, + JSON.stringify(props.modifiers), props.placement, disablePopperEvents, ]); diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index 0d718d56c3..004f287434 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -47,7 +47,14 @@ import CloseEditor from "components/editorComponents/CloseEditor"; import { setGlobalSearchQuery } from "actions/globalSearchActions"; import { toggleShowGlobalSearchModal } from "actions/globalSearchActions"; import { omnibarDocumentationHelper } from "constants/OmnibarDocumentationConstants"; +import EntityDeps from "components/editorComponents/Debugger/EntityDependecies"; import { isHidden } from "components/formControls/utils"; +import { + createMessage, + DEBUGGER_ERRORS, + DEBUGGER_LOGS, + INSPECT_ENTITY, +} from "constants/messages"; const QueryFormContainer = styled.form` display: flex; @@ -461,7 +468,7 @@ export function EditorJSONtoForm(props: Props) { const renderEachConfig = (formName: string) => (section: any): any => { return section.children.map((formControlOrSection: ControlProps) => { if (isHidden(props.formData, section.hidden)) return null; - if ("children" in formControlOrSection) { + if (formControlOrSection.hasOwnProperty("children")) { return renderEachConfig(formName)(formControlOrSection); } else { try { @@ -537,14 +544,19 @@ export function EditorJSONtoForm(props: Props) { }, { key: "ERROR", - title: "Errors", + title: createMessage(DEBUGGER_ERRORS), panelComponent: , }, { key: "LOGS", - title: "Logs", + title: createMessage(DEBUGGER_LOGS), panelComponent: , }, + { + key: "ENTITY_DEPENDENCIES", + title: createMessage(INSPECT_ENTITY), + panelComponent: , + }, ]; const onTabSelect = (index: number) => { diff --git a/app/client/src/pages/Editor/ToggleModeButton.tsx b/app/client/src/pages/Editor/ToggleModeButton.tsx index 74b24be777..55199fa4e5 100644 --- a/app/client/src/pages/Editor/ToggleModeButton.tsx +++ b/app/client/src/pages/Editor/ToggleModeButton.tsx @@ -6,6 +6,7 @@ import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper"; import { ReactComponent as Pen } from "assets/icons/comments/pen.svg"; import { ReactComponent as CommentModeUnread } from "assets/icons/comments/comment-mode-unread-indicator.svg"; import { ReactComponent as CommentMode } from "assets/icons/comments/chat.svg"; +import { Indices } from "constants/Layers"; import { setCommentMode as setCommentModeAction, @@ -54,6 +55,7 @@ const ModeButton = styled.div<{ active: boolean }>` const Container = styled.div` display: flex; flex: 1; + z-index: ${Indices.Layer1}; `; /** @@ -133,47 +135,73 @@ function ToggleCommentModeButton() { return ( - setCommentModeInUrl(false)} - > - - Edit Mode - V - - } - hoverOpenDelay={1000} - position={Position.BOTTOM} - > - - - { - proceedToNextTourStep(); + hasOverlay + modifiers={{ + offset: { enabled: true, offset: "3, 20" }, + arrow: { + enabled: true, + fn: (data) => ({ + ...data, + offsets: { + ...data.offsets, + arrow: { + top: -8, + left: 80, + }, + }, + }), + }, }} + pulseStyles={{ + top: 20, + left: 28, + height: 30, + width: 30, + }} + showPulse tourIndex={0} tourType={TourType.COMMENTS_TOUR} > - setCommentModeInUrl(true)} - > - - Comment Mode - C - - } - hoverOpenDelay={1000} - position={Position.BOTTOM} +
+ setCommentModeInUrl(false)} > - - - + + Edit Mode + V + + } + hoverOpenDelay={1000} + position={Position.BOTTOM} + > + + + + { + setCommentModeInUrl(true); + proceedToNextTourStep(); + }} + > + + Comment Mode + C + + } + hoverOpenDelay={1000} + position={Position.BOTTOM} + > + + + +
); diff --git a/app/client/src/pages/UserAuth/SignUp.tsx b/app/client/src/pages/UserAuth/SignUp.tsx index 97604e2671..74c74ebcbc 100644 --- a/app/client/src/pages/UserAuth/SignUp.tsx +++ b/app/client/src/pages/UserAuth/SignUp.tsx @@ -35,7 +35,6 @@ import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers"; import { SignupFormValues } from "./helpers"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import { getAppsmithConfigs } from "configs"; import { SIGNUP_SUBMIT_PATH } from "constants/ApiConstants"; import { connect } from "react-redux"; import { AppState } from "reducers"; @@ -45,6 +44,8 @@ import PerformanceTracker, { import { useIntiateOnboarding } from "components/editorComponents/Onboarding/utils"; import { SIGNUP_FORM_EMAIL_FIELD_NAME } from "constants/forms"; +import { getAppsmithConfigs } from "configs"; +import { useScript, ScriptStatus, AddScriptTo } from "utils/hooks/useScript"; const { enableGithubOAuth, enableGoogleOAuth } = getAppsmithConfigs(); const SocialLoginList: string[] = []; @@ -54,6 +55,13 @@ if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB); import { withTheme } from "styled-components"; import { Theme } from "constants/DefaultTheme"; +declare global { + interface Window { + grecaptcha: any; + } +} +const { googleRecaptchaSiteKey } = getAppsmithConfigs(); + const validate = (values: SignupFormValues) => { const errors: SignupFormValues = {}; if (!values.password || isEmptyString(values.password)) { @@ -82,6 +90,11 @@ export function SignUp(props: SignUpFormProps) { const location = useLocation(); const initiateOnboarding = useIntiateOnboarding(); + const recaptchaStatus = useScript( + `https://www.google.com/recaptcha/api.js?render=${googleRecaptchaSiteKey.apiKey}`, + AddScriptTo.HEAD, + ); + let showError = false; let errorMessage = ""; const queryParams = new URLSearchParams(location.search); @@ -118,7 +131,37 @@ export function SignUp(props: SignUpFormProps) { {SocialLoginList.length > 0 && ( )} - + { + e.preventDefault(); + const formElement: HTMLFormElement = document.getElementById( + "signup-form", + ) as HTMLFormElement; + if ( + googleRecaptchaSiteKey.enabled && + recaptchaStatus === ScriptStatus.READY + ) { + window.grecaptcha + .execute(googleRecaptchaSiteKey.apiKey, { + action: "submit", + }) + .then(function(token: any) { + formElement && + formElement.setAttribute( + "action", + `${signupURL}?recaptchaToken=${token}`, + ); + formElement && formElement.submit(); + }); + } else { + formElement && formElement.submit(); + } + return false; + }} + > @@ -58,19 +65,22 @@ export function PageHeader(props: PageHeaderProps) { {user && ( - - {user.username === ANONYMOUS_USERNAME ? ( -
+ + + + + \ No newline at end of file diff --git a/app/server/appsmith-server/src/main/resources/email/commentResolvedTemplate.html b/app/server/appsmith-server/src/main/resources/email/commentResolvedTemplate.html new file mode 100644 index 0000000000..5c28f11d85 --- /dev/null +++ b/app/server/appsmith-server/src/main/resources/email/commentResolvedTemplate.html @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + +
+
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+
+
+
+ Hi {{App_User_Name}}, +
+
+ {{Commenter_Name}} has resolved the comment in {{Application_Name}} in the {{Organization_Name}} organization. +
+ +
+ Please follow this link to view and re-open the comment. +
+
+
+
+ + + + + + +
+ + + + + + +
+ Go to Comment +
+
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/app/server/appsmith-server/src/main/resources/email/userTaggedInCommentTemplate.html b/app/server/appsmith-server/src/main/resources/email/userTaggedInCommentTemplate.html new file mode 100644 index 0000000000..541e1348f7 --- /dev/null +++ b/app/server/appsmith-server/src/main/resources/email/userTaggedInCommentTemplate.html @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + +
+
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+
+
+
+ Hi {{App_User_Name}}, +
+
+ {{Commenter_Name}} tagged you in a comment in {{Application_Name}} in the {{Organization_Name}} organization. +
+
+ {{#Comment_Body}} +
{{ . }}
+ {{/Comment_Body}} +
+
+ Please follow this link to view and respond to the comment. +
+
+
+
+ + + + + + +
+ + + + + + +
+ Go to Comment +
+
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/CommentUtilsTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/CommentUtilsTest.java new file mode 100644 index 0000000000..4c549c3c79 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/CommentUtilsTest.java @@ -0,0 +1,91 @@ +package com.appsmith.server.helpers; + +import com.appsmith.server.domains.Comment; +import org.junit.Assert; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class CommentUtilsTest { + @Test + void getCommentBody_WhenBodyIsNull_ReturnsEmptyList() { + Comment comment = new Comment(); + Assert.assertEquals(0, CommentUtils.getCommentBody(comment).size()); + } + + @Test + void getCommentBody_WhenBodyHasMultipleBlocks_ReturnsValidBodies() { + Comment.Block commentBlock1 = new Comment.Block(); + commentBlock1.setText("First line"); + Comment.Block commentBlock2 = new Comment.Block(); + commentBlock1.setText("Second line"); + + Comment.Body commentBody = new Comment.Body(); + commentBody.setBlocks(List.of(commentBlock1, commentBlock2)); + + Comment comment = new Comment(); + comment.setBody(commentBody); + + Assert.assertEquals(2, CommentUtils.getCommentBody(comment).size()); + Assert.assertEquals(commentBlock1.getText(), CommentUtils.getCommentBody(comment).get(0)); + Assert.assertEquals(commentBlock2.getText(), CommentUtils.getCommentBody(comment).get(1)); + } + + @Test + public void isUserMentioned_WhenBodyIsNull_ReturnsFalse() { + Comment comment = new Comment(); + Assert.assertFalse(CommentUtils.isUserMentioned(comment, "user@abc.com")); + } + + @Test + public void isUserMentioned_WhenBodyHasNoMention_ReturnsFalse() { + Comment.Body body = new Comment.Body(); + Comment comment = new Comment(); + comment.setBody(body); + Assert.assertFalse(CommentUtils.isUserMentioned(comment, "user@abc.com")); + + Comment.Entity entity = new Comment.Entity(); + Map entityMap = new HashMap<>(); + entityMap.put("abc", entity); + body.setEntityMap(entityMap); + comment.setBody(body); + Assert.assertFalse(CommentUtils.isUserMentioned(comment, "user@abc.com")); + } + + private Map createEntityMapForUsers(List mentionedUserNames) { + Map entityMap = new HashMap<>(); + for (String username: mentionedUserNames) { + Comment.EntityData.EntityUser entityUser = new Comment.EntityData.EntityUser(); + entityUser.setUsername(username); + Comment.EntityData.Mention mention = new Comment.EntityData.Mention(); + mention.setUser(entityUser); + + Comment.EntityData entityData = new Comment.EntityData(); + entityData.setMention(mention); + + Comment.Entity entity = new Comment.Entity(); + entity.setType("mention"); + entity.setData(entityData); + entityMap.put(username, entity); + } + return entityMap; + } + + @Test + public void isUserMentioned_WhenSomeoneIsMentioned_ReturnsCorrectValue() { + Map entityMap = createEntityMapForUsers( + List.of("1", "2", "3") + ); + Comment.Body body = new Comment.Body(); + body.setEntityMap(entityMap); + Comment comment = new Comment(); + comment.setBody(body); + + Assert.assertTrue(CommentUtils.isUserMentioned(comment, "1")); + Assert.assertTrue(CommentUtils.isUserMentioned(comment, "2")); + Assert.assertTrue(CommentUtils.isUserMentioned(comment, "3")); + Assert.assertFalse(CommentUtils.isUserMentioned(comment, "4")); + } +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/repositories/OrganizationRepositoryTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/repositories/OrganizationRepositoryTest.java new file mode 100644 index 0000000000..b36cb6edbd --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/repositories/OrganizationRepositoryTest.java @@ -0,0 +1,83 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.Organization; +import com.appsmith.server.domains.UserRole; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.util.function.Tuple2; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Slf4j +@DirtiesContext +class OrganizationRepositoryTest { + + @Autowired + private OrganizationRepository organizationRepository; + + @Test + void updateUserRoleNames_WhenUserIdMatched_AllOrgsUpdated() { + String oldUserName = "Old name", + newUserName = "New name", + userId = "user1"; + UserRole userRole = new UserRole(); + userRole.setName(oldUserName); + userRole.setUserId(userId); + + List userRoles = new ArrayList<>(); + userRoles.add(userRole); + + Organization org1 = new Organization(); + org1.setId(UUID.randomUUID().toString()); + org1.setSlug(org1.getId()); + org1.setUserRoles(userRoles); + + Organization org2 = new Organization(); + org2.setId(UUID.randomUUID().toString()); + org2.setSlug(org2.getId()); + org2.setUserRoles(userRoles); + + // create two orgs + Mono> aveOrgsMonoZip = Mono.zip( + organizationRepository.save(org1), organizationRepository.save(org2) + ); + + Mono> updatedOrgTupleMono = aveOrgsMonoZip.flatMap(objects -> { + // update the user names + return organizationRepository.updateUserRoleNames(userId, newUserName).thenReturn(objects); + }).flatMap(organizationTuple2 -> { + // fetch the two orgs again + Mono updatedOrg1Mono = organizationRepository.findBySlug(org1.getId()); + Mono updatedOrg2Mono = organizationRepository.findBySlug(org2.getId()); + return Mono.zip(updatedOrg1Mono, updatedOrg2Mono); + }); + + StepVerifier.create(updatedOrgTupleMono).assertNext(orgTuple -> { + Organization o1 = orgTuple.getT1(); + Assert.assertEquals(1, o1.getUserRoles().size()); + UserRole userRole1 = o1.getUserRoles().get(0); + Assert.assertEquals(userId, userRole1.getUserId()); + Assert.assertEquals(newUserName, userRole1.getName()); + + Organization o2 = orgTuple.getT2(); + Assert.assertEquals(1, o2.getUserRoles().size()); + UserRole userRole2 = o2.getUserRoles().get(0); + Assert.assertEquals(userId, userRole2.getUserId()); + Assert.assertEquals(newUserName, userRole2.getName()); + }).verifyComplete(); + } +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CommentServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CommentServiceTest.java index 7c29eb748d..2a46b7195d 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CommentServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CommentServiceTest.java @@ -57,7 +57,7 @@ public class CommentServiceTest { thread.setComments(List.of( makePlainTextComment("comment one") )); - return commentService.createThread(thread); + return commentService.createThread(thread, "https://app.appsmith.com"); }) .zipWhen(thread -> commentService.getThreadsByApplicationId(thread.getApplicationId())); @@ -128,7 +128,7 @@ public class CommentServiceTest { thread.setComments(List.of( makePlainTextComment("Test Comment") )); - return commentService.createThread(thread); + return commentService.createThread(thread, "https://app.appsmith.com"); }) .flatMap(commentThread -> Mono.just(commentThread.getComments().get(0))) .cache(); @@ -163,7 +163,7 @@ public class CommentServiceTest { final CommentThread thread = new CommentThread(); thread.setApplicationId(application.getId()); thread.setComments(List.of(makePlainTextComment("Test Comment"))); - return commentService.createThread(thread); + return commentService.createThread(thread, "https://app.appsmith.com"); }) .flatMap(commentThread -> Mono.just(commentThread.getComments().get(0))) .flatMap(comment -> { @@ -202,7 +202,7 @@ public class CommentServiceTest { final CommentThread thread = new CommentThread(); thread.setApplicationId(application.getId()); thread.setComments(List.of(makePlainTextComment("Test Comment"))); - return commentService.createThread(thread); + return commentService.createThread(thread, "https://app.appsmith.com"); }) .flatMap(commentThread -> Mono.just(commentThread.getComments().get(0))) .flatMap(comment -> { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceUnitTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceUnitTest.java new file mode 100644 index 0000000000..076af9e999 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceUnitTest.java @@ -0,0 +1,92 @@ +package com.appsmith.server.services; + +import com.appsmith.server.acl.RoleGraph; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Organization; +import com.appsmith.server.domains.UserRole; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.repositories.AssetRepository; +import com.appsmith.server.repositories.OrganizationRepository; +import com.appsmith.server.repositories.PluginRepository; +import com.appsmith.server.repositories.UserRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.test.StepVerifier; + +import javax.validation.Validator; +import java.util.List; + +import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS; + +@RunWith(SpringJUnit4ClassRunner.class) +public class OrganizationServiceUnitTest { + + @MockBean PluginRepository pluginRepository; + @MockBean SessionUserService sessionUserService; + @MockBean UserOrganizationService userOrganizationService; + @MockBean UserRepository userRepository; + @MockBean RoleGraph roleGraph; + @MockBean AssetRepository assetRepository; + @MockBean AssetService assetService; + @MockBean Scheduler scheduler; + @MockBean MongoConverter mongoConverter; + @MockBean ReactiveMongoTemplate reactiveMongoTemplate; + @MockBean OrganizationRepository organizationRepository; + @MockBean Validator validator; + @MockBean AnalyticsService analyticsService; + + OrganizationService organizationService; + + @Before + public void setUp() { + organizationService = new OrganizationServiceImpl(scheduler, validator, mongoConverter, reactiveMongoTemplate, + organizationRepository, analyticsService, pluginRepository, sessionUserService, userOrganizationService, + userRepository, roleGraph, assetRepository, assetService + ); + } + + @Test + public void getOrganizationMembers_WhenRoleIsNull_ReturnsEmptyList() { + // create a organization object + Organization testOrg = new Organization(); + testOrg.setName("Get All Members For Organization Test"); + testOrg.setDomain("test.com"); + testOrg.setWebsite("https://test.com"); + testOrg.setId("test-org-id"); + + // mock repository methods so that they return the objects we've created + Mockito.when(organizationRepository.findById("test-org-id", ORGANIZATION_INVITE_USERS)) + .thenReturn(Mono.just(testOrg)); + + Mono> organizationMembers = organizationService.getOrganizationMembers(testOrg.getId()); + StepVerifier + .create(organizationMembers) + .assertNext(userRoles -> { + Assert.assertEquals(0, userRoles.size()); + }) + .verifyComplete(); + } + + @Test + public void getOrganizationMembers_WhenNoOrgFound_ThrowsException() { + String sampleOrgId = "test-org-id"; + // mock repository methods so that they return the objects we've created + Mockito.when(organizationRepository.findById(sampleOrgId, ORGANIZATION_INVITE_USERS)) + .thenReturn(Mono.empty()); + + Mono> organizationMembers = organizationService.getOrganizationMembers(sampleOrgId); + StepVerifier + .create(organizationMembers) + .expectErrorMessage(AppsmithError.NO_RESOURCE_FOUND.getMessage(FieldName.ORGANIZATION, sampleOrgId)) + .verify(); + } +} diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/EmailEventHandlerTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/EmailEventHandlerTest.java new file mode 100644 index 0000000000..bdb0e0eea2 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/EmailEventHandlerTest.java @@ -0,0 +1,198 @@ +package com.appsmith.server.solutions; + +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Comment; +import com.appsmith.server.domains.CommentThread; +import com.appsmith.server.domains.Organization; +import com.appsmith.server.domains.UserRole; +import com.appsmith.server.events.CommentAddedEvent; +import com.appsmith.server.events.CommentThreadClosedEvent; +import com.appsmith.server.notifications.EmailSender; +import com.appsmith.server.repositories.ApplicationRepository; +import com.appsmith.server.repositories.OrganizationRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.eq; + +@RunWith(SpringJUnit4ClassRunner.class) +public class EmailEventHandlerTest { + + private static final String COMMENT_ADDED_EMAIL_TEMPLATE = "email/commentAddedTemplate.html"; + private static final String USER_MENTIONED_EMAIL_TEMPLATE = "email/userTaggedInCommentTemplate.html"; + private static final String THREAD_RESOLVED_EMAIL_TEMPLATE = "email/commentResolvedTemplate.html"; + + @MockBean + private ApplicationEventPublisher applicationEventPublisher; + @MockBean + private EmailSender emailSender; + @MockBean + private OrganizationRepository organizationRepository; + @MockBean + private ApplicationRepository applicationRepository; + + EmailEventHandler emailEventHandler; + private Application application; + private Organization organization; + + String authorUserName = "abc"; + String originHeader = "efg"; + String applicationId = "application-id"; + String organizationId = "organization-id"; + String emailReceiverUsername = "email-receiver"; + + @Before + public void setUp() { + emailEventHandler = new EmailEventHandler( + applicationEventPublisher, emailSender, organizationRepository, applicationRepository + ); + application = new Application(); + application.setName("Test application for comment"); + application.setOrganizationId(organizationId); + organization = new Organization(); + + // add a role with email receiver username + UserRole userRole = new UserRole(); + userRole.setUsername(emailReceiverUsername); + organization.setUserRoles(List.of(userRole)); + + Mockito.when(applicationRepository.findById(applicationId)).thenReturn(Mono.just(application)); + Mockito.when(organizationRepository.findById(organizationId)).thenReturn(Mono.just(organization)); + } + + @Test + public void publish_WhenValidCommentProvided_ReturnsTrue() { + Comment comment = new Comment(); + CommentAddedEvent commentAddedEvent = new CommentAddedEvent( + authorUserName, organization, application, originHeader, comment + ); + + Mockito.doNothing().when(applicationEventPublisher).publishEvent(commentAddedEvent); + + Mono booleanMono = emailEventHandler.publish(authorUserName, applicationId, comment, originHeader); + StepVerifier.create(booleanMono).assertNext(aBoolean -> { + Assert.assertEquals(Boolean.TRUE, aBoolean); + }).verifyComplete(); + } + + @Test + public void publish_WhenValidCommentThreadProvided_ReturnsTrue() { + CommentThread commentThread = new CommentThread(); + CommentThreadClosedEvent commentThreadClosedEvent = new CommentThreadClosedEvent( + authorUserName, organization, application, originHeader, commentThread + ); + Mockito.doNothing().when(applicationEventPublisher).publishEvent(commentThreadClosedEvent); + + Mono booleanMono = emailEventHandler.publish(authorUserName, applicationId, commentThread, originHeader); + StepVerifier.create(booleanMono).assertNext(aBoolean -> { + Assert.assertEquals(Boolean.TRUE, aBoolean); + }).verifyComplete(); + } + + @Test + public void handle_WhenValidCommentAddedEvent_ReturnsTrue() { + Comment sampleComment = new Comment(); + sampleComment.setAuthorUsername(authorUserName); + sampleComment.setAuthorName("Test Author"); + + // send the event + CommentAddedEvent commentAddedEvent = new CommentAddedEvent( + authorUserName, organization, application, originHeader, sampleComment + ); + emailEventHandler.handle(commentAddedEvent); + + String expectedEmailSubject = String.format( + "New comment from %s in %s", sampleComment.getAuthorName(), application.getName() + ); + // check email sender was called with expected template and subject + Mockito.verify(emailSender, Mockito.times(1)).sendMail( + eq(emailReceiverUsername), eq(expectedEmailSubject), eq(COMMENT_ADDED_EMAIL_TEMPLATE), Mockito.anyMap() + ); + } + + private Map createEntityMapForUsers(List mentionedUserNames) { + Map entityMap = new HashMap<>(); + for (String username: mentionedUserNames) { + Comment.EntityData.EntityUser entityUser = new Comment.EntityData.EntityUser(); + entityUser.setUsername(username); + Comment.EntityData.Mention mention = new Comment.EntityData.Mention(); + mention.setUser(entityUser); + + Comment.EntityData entityData = new Comment.EntityData(); + entityData.setMention(mention); + + Comment.Entity entity = new Comment.Entity(); + entity.setType("mention"); + entity.setData(entityData); + entityMap.put(username, entity); + } + return entityMap; + } + + @Test + public void handle_WhenUserMentionedEvent_ReturnsTrue() { + Comment sampleComment = new Comment(); + sampleComment.setAuthorUsername(authorUserName); + sampleComment.setAuthorName("Test Author"); + + // mention the emailReceiverUsername in the sample comment + Map entityMap = createEntityMapForUsers(List.of(emailReceiverUsername)); + Comment.Body body = new Comment.Body(); + body.setEntityMap(entityMap); + sampleComment.setBody(body); + + // send the event + CommentAddedEvent commentAddedEvent = new CommentAddedEvent( + authorUserName, organization, application, originHeader, sampleComment + ); + emailEventHandler.handle(commentAddedEvent); + + // check if expectation meets + String expectedEmailSubject = String.format("New comment for you from %s", sampleComment.getAuthorName()); + + // check email sender was called with expected template and subject + Mockito.verify(emailSender, Mockito.times(1)).sendMail( + eq(emailReceiverUsername), eq(expectedEmailSubject), eq(USER_MENTIONED_EMAIL_TEMPLATE), Mockito.anyMap() + ); + } + + @Test + public void handle_WhenThreadClosed_ReturnsTrue() { + // add comment thread with a resolved state where resolver is `authorUserName` + String resolverName = "Test Author"; + CommentThread.CommentThreadState resolveState = new CommentThread.CommentThreadState(); + resolveState.setAuthorUsername(authorUserName); + resolveState.setAuthorName(resolverName); + resolveState.setActive(true); + + CommentThread commentThread = new CommentThread(); + commentThread.setResolvedState(resolveState); + + // send the event + CommentThreadClosedEvent commentAddedEvent = new CommentThreadClosedEvent( + authorUserName, organization, application, originHeader, commentThread + ); + emailEventHandler.handle(commentAddedEvent); + + // check if expectation meets + String expectedEmailSubject = String.format( + "%s has resolved comment in %s", resolveState.getAuthorName(), application.getName() + ); + // check email sender was called with expected template and subject + Mockito.verify(emailSender, Mockito.times(1)).sendMail( + eq(emailReceiverUsername), eq(expectedEmailSubject), eq(THREAD_RESOLVED_EMAIL_TEMPLATE), Mockito.anyMap() + ); + } +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java index bf54cf7efc..85c12176ab 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java @@ -473,6 +473,7 @@ public class ExamplesOrganizationClonerTests { final Datasource ds1 = new Datasource(); ds1.setName("datasource 1"); ds1.setOrganizationId(organization.getId()); + ds1.setPluginId(installedPlugin.getId()); final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); ds1.setDatasourceConfiguration(datasourceConfiguration); datasourceConfiguration.setUrl("http://httpbin.org/get"); @@ -483,6 +484,7 @@ public class ExamplesOrganizationClonerTests { final Datasource ds2 = new Datasource(); ds2.setName("datasource 2"); ds2.setOrganizationId(organization.getId()); + ds2.setPluginId(installedPlugin.getId()); ds2.setDatasourceConfiguration(new DatasourceConfiguration()); DBAuth auth = new DBAuth(); auth.setPassword("answer-to-life"); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java index a18f2ab5e6..f788a28a36 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java @@ -17,7 +17,6 @@ import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.PluginType; -import com.appsmith.server.domains.User; import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; @@ -67,6 +66,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static com.appsmith.server.acl.AclPermission.EXPORT_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; @@ -139,9 +139,18 @@ public class ImportExportApplicationServiceTests { public void setup() { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); installedPlugin = pluginRepository.findByPackageName("installed-plugin").block(); - User apiUser = userService.findByEmail("api_user").block(); - orgId = apiUser.getOrganizationIds().iterator().next(); - + + Organization organization = new Organization(); + organization.setName("Import-Export-Test-Organization"); + Organization savedOrganization = organizationService.create(organization).block(); + orgId = savedOrganization.getId(); + + Application testApplication = new Application(); + testApplication.setName("Export-Application-Test-Application"); + testApplication.setOrganizationId(orgId); + Application savedApplication = applicationPageService.createApplication(testApplication, orgId).block(); + testAppId = savedApplication.getId(); + invalid_json_file = importExportApplicationService.INVALID_JSON_FILE; Datasource ds1 = new Datasource(); @@ -168,6 +177,7 @@ public class ImportExportApplicationServiceTests { } @Test + @WithUserDetails(value = "api_user") public void exportApplicationWithNullApplicationIdTest() { Mono resultMono = importExportApplicationService.exportApplicationById(null); @@ -182,11 +192,7 @@ public class ImportExportApplicationServiceTests { @WithUserDetails(value = "api_user") public void createExportAppJsonWithoutActionsAndDatasourceTest() { - Application testApplication = new Application(); - testApplication.setName("Export Application TestApp"); - - final Mono resultMono = applicationPageService.createApplication(testApplication, orgId) - .flatMap(application -> importExportApplicationService.exportApplicationById(application.getId())); + final Mono resultMono = importExportApplicationService.exportApplicationById(testAppId); StepVerifier.create(resultMono) .assertNext(applicationJson -> { @@ -197,10 +203,11 @@ public class ImportExportApplicationServiceTests { NewPage defaultPage = pageList.get(0); - assertThat(exportedApp.getName()).isEqualTo(testApplication.getName()); + assertThat(exportedApp.getId()).isNull(); assertThat(exportedApp.getOrganizationId()).isNull(); assertThat(exportedApp.getPages()).isNull(); assertThat(exportedApp.getPolicies().size()).isEqualTo(0); + assertThat(exportedApp.getUserPermissions()).contains(EXPORT_APPLICATIONS.getValue()); assertThat(pageList.isEmpty()).isFalse(); assertThat(defaultPage.getApplicationId()).isNull(); @@ -378,6 +385,7 @@ public class ImportExportApplicationServiceTests { } @Test + @WithUserDetails(value = "api_user") public void importApplicationFromInvalidFileTest() { FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); Flux dataBufferFlux = DataBufferUtils @@ -396,6 +404,7 @@ public class ImportExportApplicationServiceTests { } @Test + @WithUserDetails(value = "api_user") public void importApplicationWithNullOrganizationIdTest() { FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); diff --git a/contributions/ClientSetup.md b/contributions/ClientSetup.md index 13da4c58cf..5eb3fb1a3d 100644 --- a/contributions/ClientSetup.md +++ b/contributions/ClientSetup.md @@ -8,6 +8,18 @@ On your development machine, please ensure that: 1. You have `docker` installed in your system. If not, please visit: [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/) 1. You have `mkcert` installed. Please visit: [https://github.com/FiloSottile/mkcert#installation](https://github.com/FiloSottile/mkcert#installation) for details. For `mkcert` to work with Firefox you may require the `nss` utility to be installed. Details are in the link above. + + Note: + - On Linux you can easily install `mkcert` using the following command + ``` + curl -s https://api.github.com/repos/FiloSottile/mkcert/releases/latest \ + | grep "browser_download_url.*linux-amd64" \ + | cut -d : -f 2,3 | tr -d \" \ + | wget -i - -O mkcert + chmod +x mkcert + sudo mv mkcert /usr/local/bin + ``` + 1. You have `envsubst` installed. use `brew install gettext` on MacOS. Linux machines usually have this installed. 1. You have cloned the repo in your local machine. 1. You have yarn installed as a global npm package i.e. `npm install -g yarn` @@ -71,7 +83,6 @@ On your development machine, please ensure that: - By default your client app points to the local api server - `http://host.docker.internal:8080` for MacOS or `http://localhost:8080` for Linux. Your page will load with errors if you don't have the api server running on your local system. To setup the api server on your local system please follow the instructions [here](https://github.com/appsmithorg/appsmith/blob/release/contributions/ServerSetup.md) - In case you are unable to setup the api server on your local system, you can also [use Appsmith's staging API server](#if-you-would-like-to-hit-a-different-appsmith-server). -- In case you are using a M1 chip Macbook please run the client with `yarn start-m1`. #### If yarn start throws mismatch node version error diff --git a/contributions/ServerSetup.md b/contributions/ServerSetup.md index 16cfb1eaa7..c49108d4b7 100644 --- a/contributions/ServerSetup.md +++ b/contributions/ServerSetup.md @@ -1,30 +1,60 @@ ## Running Server Codebase -The server codebase is written in Java and is powered by Spring + WebFlux. This document explains how you can setup a development environment to make changes and test your changes. +This document explains how you can setup a development environment for Appsmith server. As the server codebase is written in Java and is powered by Spring + WebFlux we need Java and Maven installed to build the code. In addition we also need one instance of MongoDB and Redis each to run Appsmith server. Lastly, we will set up IntelliJ IDEA to let you edit the code. Let's get those prerequisites installed on your machine. ->For details on setting up with `Docker`, please refer to the [Setup Guide](../app/server/README.md#run-locally-with-docker) +>If you are not setting up a development environment you can get the Appsmith server up and running quickly with `Docker`. Please refer to the [Setup Guide](../app/server/README.md#run-locally-with-docker) on how to do that. ## Pre-requisites -- Java --- OpenJDK 11. -- Maven --- version 3+ (preferably 3.6). -- A MongoDB database --- A simple way to get this up is explained [further down in this document](#setting-up-a-local-mongodb). -- A Redis instance --- A simple way to get this up is explained [further down in this document](#setting-up-a-local-redis). -- An IDE --- We use IntelliJ IDEA as our primary IDE for backend development. +Before you can start to hack on the Appsmith server, your machine should have the following installed: -## Steps for Setup +- Java - OpenJDK 11. +- Maven - Version 3+ (preferably 3.6). +- A MongoDB database - Refer to the [Setting up a local MongoDB instance](#setting-up-a-local-mongodb-instance) section to setup a MongoDB instance using `Docker`. +- A Redis instance - Refer to the [Setting up a local Redis instance](#setting-up-a-local-redis-instance) section to setup a Redis instance using `Docker`. +- An IDE - We use IntelliJ IDEA as our primary IDE for backend development. To set it up, refer to the [Setting up IntelliJ IDEA](#setting-up-intellij-idea) section. -1. After cloning the repository, change your directory to `app/server` +This document doesn't provide instructions to install Java and Maven because these vary between different operating systems and distributions. Please refer to the documentation of your operating system or package manager to install these. Next we will setup MondoDB and Redis using `Docker`. -2. Run the command +### Setting up a local MongoDB instance + +The following command will start a MongoDB docker instance locally: + +```sh +docker run -p 127.0.0.1:27017:27017 --name appsmith-mongodb -e MONGO_INITDB_DATABASE=appsmith -v /path/to/store/data:/data/db mongo +``` + +Please change the `/path/to/store/data` to a valid path on your system. This is where MongoDB will persist it's data across runs of this container. + +Note that this command doesn't set any username or password on the database so we make it accessible only from localhost using the `127.0.0.1:` part in the port mapping argument. Please refer to the documentation of this image to learn [how to set a username and password](https://hub.docker.com/_/mongo). + +MongoDB will now be running on `mongodb://localhost:27017/appsmith`. + +### Setting up a local Redis instance + +The following command will start a Redis docker instance locally: + +```sh +docker run -p 127.0.0.1:6379:6379 --name appsmith-redis redis +``` + +Redis will now be running on `redis://localhost:6379`. + +With the prerequisites met, let's build the code. + +## Building and running the code + +1. Clone Appsmith repository. +2. Change your directory to `app/server`. +3. Run the following command: ```sh mvn clean compile ``` - + This generates a bunch of classes required by IntelliJ for compiling the rest of the source code. Without this step, your IDE may complain about missing classes and will be unable to compile the code. -3. Create a copy of the `envs/dev.env.example` +4. Create a copy of the `envs/dev.env.example` ```sh cp envs/dev.env.example .env @@ -32,14 +62,14 @@ cp envs/dev.env.example .env This command creates a `.env` file in the `app/server` folder. All run scripts pick up environment configuration from this file. -4. Modify the property values in the file `.env` to point to your local running instance of MongoDB and Redis. +5. Ensure that the environment variables `APPSMITH_MONGODB_URI` and `APPSMITH_REDIS_URI` in the file `.env` point to your local running instances of MongoDB and Redis. -5. In order to create the final JAR for the Appsmith server, run the command: +6. Run the following command to create the final JAR for the Appsmith server: ``` ./build.sh ``` -This command will create a `dist` folder which contains the final packaged jar along with multiple jars for the binaries for plugins as well. +This command will create a `dist` folder which contains the final packaged jar along with multiple jars for plugins as well. Note: - If you want to skip tests, you can pass `-DskipTests` flag to the build cmd. @@ -55,7 +85,7 @@ Check failed: Docker environment should have more than 2GB free disk space. There are two ways to resolve this issue: (1) free up more space (2) change docker's data root path. -6. Start the Java server by running +7. Start the Java server by running ``` ./scripts/start-dev-server.sh @@ -63,35 +93,13 @@ There are two ways to resolve this issue: (1) free up more space (2) change dock By default, the server will start on port 8080. -7. When the server starts, it automatically runs migrations on MongoDB and will populate it with some initial required data. +8. When the server starts, it automatically runs migrations on MongoDB and will populate it with some initial required data. -8. You can check the status of the server by hitting the endpoint: [http://localhost:8080](http://localhost:8080) on your browser. By default you should see an HTTP 401 error. +9. You can check the status of the server by hitting the endpoint: [http://localhost:8080](http://localhost:8080) on your browser. By default you should see an HTTP 401 error. -## Setting up a local MongoDB +Now the last bit, let's get your Intellij IDEA up and running. -The following command can bring up a MongoDB docker instance locally. - -```sh -docker run -p 127.0.0.1:27017:27017 --name appsmith-mongodb -e MONGO_INITDB_DATABASE=appsmith -v /path/to/store/data:/data/db mongo -``` - -Please change the `/path/to/store/data` to a valid path on your system. This is where MongoDB will persist it's data across runs of this container. - -Note that this command doesn't set any username or password on the database so we make it accessible only from localhost using the `127.0.0.1:` part in the port mapping argument. Please refer to the documentation of this image to learn [how to set a username and password](https://hub.docker.com/_/mongo). - -When using this command, the value of `APPSMITH_MONGODB_URI` should be set to `mongodb://localhost:27017/appsmith` (which is what's provided in the example env file). - -## Setting up a local Redis - -The following command can bring up a Redis docker instance locally. - -```sh -docker run -p 127.0.0.1:6379:6379 --name appsmith-redis redis -``` - -When using this command, the value of `APPSMITH_REDIS_URI` should be set to `redis://localhost:6379`. - -## Seting up IntelliJ IDE +## Setting up IntelliJ IDEA To run the project from within the IDE, you will need to make use of the run configuration that is part of the repository. The run configuration uses the [EnvFile](https://plugins.jetbrains.com/plugin/7861-envfile) plugin to include environment variables in the path. Any and all tests can be run within the IDE by cloning this run configuration. @@ -109,7 +117,8 @@ Please note when setting **Working directory** option. If the path is not correc 3. Load your env file by going to the EnvFile Tab in the Run/Debug configuration settings for your server. Screenshot 2021-03-03 at 1 49 17 PM +Happy hacking. -## Need Assistance +## Need Assistance? - If you are unable to resolve any issue while doing the setup, please feel free to ask questions on our [Discord channel](https://discord.com/invite/rBTTVJp) or initiate a [Github discussion](https://github.com/appsmithorg/appsmith/discussions) or send an email to `support@appsmith.com`. We'll be happy to help you. - In case you notice any discrepancy, please raise an issue on Github and/or send an email to support@appsmith.com. diff --git a/deploy/ansible/appsmith_playbook/appsmith-vars.yml b/deploy/ansible/appsmith_playbook/appsmith-vars.yml index 81dd498371..bca918e2f0 100644 --- a/deploy/ansible/appsmith_playbook/appsmith-vars.yml +++ b/deploy/ansible/appsmith_playbook/appsmith-vars.yml @@ -39,4 +39,7 @@ tnc_pp: '' version_id: '' version_release_date: '' intercom_app_id: '' +google_recaptcha_site_key: '' +google_recaptcha_secret_key: '' +google_recaptcha_enabled: 'false' diff --git a/deploy/ansible/appsmith_playbook/roles/generate_template/templates/docker.env.j2 b/deploy/ansible/appsmith_playbook/roles/generate_template/templates/docker.env.j2 index f62bf401da..7bd7a504fc 100644 --- a/deploy/ansible/appsmith_playbook/roles/generate_template/templates/docker.env.j2 +++ b/deploy/ansible/appsmith_playbook/roles/generate_template/templates/docker.env.j2 @@ -45,3 +45,9 @@ APPSMITH_MONGODB_URI=mongodb://{{ mongo_root_user }}:{{ mongo_root_password }}@{ # ******************************* APPSMITH_DISABLE_TELEMETRY={{ disable_telemetry }} + +# ******** Google Recaptcha Keys *********** +APPSMITH_RECAPTCHA_SITE_KEY= {{ google_recaptcha_site_key }} +APPSMITH_RECAPTCHA_SECRET_KEY= {{ google_recaptcha_secrete_key }} +APPSMITH_RECAPTCHA_ENABLED= {{ google_recaptcha_enabled }} +# ******************************** diff --git a/deploy/ansible/appsmith_playbook/roles/generate_template/templates/nginx-app.conf.j2 b/deploy/ansible/appsmith_playbook/roles/generate_template/templates/nginx-app.conf.j2 index c46d49e702..0436cadb49 100644 --- a/deploy/ansible/appsmith_playbook/roles/generate_template/templates/nginx-app.conf.j2 +++ b/deploy/ansible/appsmith_playbook/roles/generate_template/templates/nginx-app.conf.j2 @@ -37,6 +37,9 @@ sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '{{ version_release_date }}'; sub_filter __APPSMITH_INTERCOM_APP_ID__ '{{ intercom_app_id }}'; sub_filter __APPSMITH_MAIL_ENABLED__ '{{ mail_enabled }}'; + sub_filter __APPSMITH_RECAPTCHA_SITE_KEY__ '{{ google_recaptcha_site_key }}'; + sub_filter __APPSMITH_RECAPTCHA_SECRET_KEY__ '{{ google_recaptcha_secrete_key }}'; + sub_filter __APPSMITH_RECAPTCHA_ENABLED__ '{{ google_recaptcha_enabled }}'; } location /f { @@ -93,6 +96,9 @@ {{ ssl_cmt }} sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '{{ version_release_date }}'; {{ ssl_cmt }} sub_filter __APPSMITH_INTERCOM_APP_ID__ '{{ intercom_app_id }}'; {{ ssl_cmt }} sub_filter __APPSMITH_MAIL_ENABLED__ '{{ mail_enabled }}'; +{{ ssl_cmt }} sub_filter __APPSMITH_RECAPTCHA_SITE_KEY__ '{{ google_recaptcha_site_key }}'; +{{ ssl_cmt }} sub_filter __APPSMITH_RECAPTCHA_SECRET_KEY__ '{{ google_recaptcha_secrete_key }}'; +{{ ssl_cmt }} sub_filter __APPSMITH_RECAPTCHA_ENABLED__ '{{ google_recaptcha_enabled }}'; {{ ssl_cmt }} } {{ ssl_cmt }} location /f { diff --git a/deploy/heroku/README.MD b/deploy/heroku/README.MD index 14b6740d62..552bb15245 100644 --- a/deploy/heroku/README.MD +++ b/deploy/heroku/README.MD @@ -33,6 +33,10 @@ Quickly set up Appsmith to explore product functionality using Heroku. - `APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET`: Client secret provided by Github for OAuth2 login - `APPSMITH_GOOGLE_MAPS_API_KEY`: Google Maps API key which is required if you wish to leverage Google Maps widget. Read more at: https://docs.appsmith.com/v/v1.2.1/setup/docker/google-maps - `APPSMITH_DISABLE_TELEMETRY`: 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 + - Google reCAPTCHA v3 Configuration: + - `APPSMITH_RECAPTCHA_SITE_KEY`: 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 + - `APPSMITH_RECAPTCHA_SECRET_KEY`: Google reCAPTCHA v3 verification secret key, it is required if you wish to enable spam protection in your backend server. + - `APPSMITH_RECAPTCHA_ENABLED`: Boolean config to enable or disable Google reCAPTCHA v3 verification feature. If set to true, both site key and secret key should be provided. After Heroku finishes setting up the app, click “View” and your Appsmith should be up and running. You will be taken to the account creation page, where you can enter credentials to create an account and get started. diff --git a/deploy/heroku/bootstrap.sh b/deploy/heroku/bootstrap.sh index 8e07179d37..4275fb890b 100755 --- a/deploy/heroku/bootstrap.sh +++ b/deploy/heroku/bootstrap.sh @@ -42,6 +42,12 @@ if [[ -z "${APPSMITH_GOOGLE_MAPS_API_KEY}" ]]; then unset APPSMITH_GOOGLE_MAPS_API_KEY fi +if [[ -z "${APPSMITH_RECAPTCHA_SITE_KEY}" ]] || [[ -z "${APPSMITH_RECAPTCHA_SECRET_KEY}" ]] || [[ -z "${APPSMITH_RECAPTCHA_ENABLED}" ]]; then + unset APPSMITH_RECAPTCHA_SITE_KEY # If this field is empty is might cause application crash + unset APPSMITH_RECAPTCHA_SECRET_KEY + unset APPSMITH_RECAPTCHA_ENABLED +fi + cat /etc/nginx/conf.d/default.conf.template | envsubst "$(printf '$%s,' $(env | grep -Eo '^APPSMITH_[A-Z0-9_]+'))" | sed -e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' > /etc/nginx/conf.d/default.conf.template.1 envsubst "\$PORT" < /etc/nginx/conf.d/default.conf.template.1 > /etc/nginx/conf.d/default.conf diff --git a/deploy/heroku/default.conf.template b/deploy/heroku/default.conf.template index 8362ccc505..490cacb32d 100644 --- a/deploy/heroku/default.conf.template +++ b/deploy/heroku/default.conf.template @@ -32,6 +32,9 @@ server { sub_filter __APPSMITH_INTERCOM_APP_ID__ '${APPSMITH_INTERCOM_APP_ID}'; sub_filter __APPSMITH_MAIL_ENABLED__ '${APPSMITH_MAIL_ENABLED}'; sub_filter __APPSMITH_DISABLE_TELEMETRY__ '${APPSMITH_DISABLE_TELEMETRY}'; + sub_filter __APPSMITH_RECAPTCHA_SITE_KEY__ '${APPSMITH_RECAPTCHA_SITE_KEY}'; + sub_filter __APPSMITH_RECAPTCHA_SECRET_KEY__ '${APPSMITH_RECAPTCHA_SECRET_KEY}'; + sub_filter __APPSMITH_RECAPTCHA_ENABLED__ '${APPSMITH_RECAPTCHA_ENABLED}'; } location /f { diff --git a/deploy/k8s/scripts/appsmith-configmap.yaml.sh b/deploy/k8s/scripts/appsmith-configmap.yaml.sh index d664292f6f..bf7b314546 100644 --- a/deploy/k8s/scripts/appsmith-configmap.yaml.sh +++ b/deploy/k8s/scripts/appsmith-configmap.yaml.sh @@ -30,4 +30,7 @@ data: APPSMITH_REDIS_URL: redis://redis-service:6379 APPSMITH_MONGODB_URI: $mongo_protocol$encoded_mongo_root_user:$encoded_mongo_root_password@$mongo_host/$mongo_db?retryWrites=true&authSource=admin APPSMITH_DISABLE_TELEMETRY: "$disable_telemetry" + APPSMITH_RECAPTCHA_SITE_KEY= "" + APPSMITH_RECAPTCHA_SECRET_KEY= "" + APPSMITH_RECAPTCHA_ENABLED= "false" EOF diff --git a/deploy/k8s/scripts/nginx-configmap.yaml b/deploy/k8s/scripts/nginx-configmap.yaml index b9253d36bf..eef8665258 100644 --- a/deploy/k8s/scripts/nginx-configmap.yaml +++ b/deploy/k8s/scripts/nginx-configmap.yaml @@ -37,6 +37,9 @@ data: 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}'; + sub_filter __APPSMITH_RECAPTCHA_SECRET_KEY__ '${APPSMITH_RECAPTCHA_SECRET_KEY}'; + sub_filter __APPSMITH_RECAPTCHA_ENABLED__ '${APPSMITH_RECAPTCHA_ENABLED}'; } location /f { diff --git a/deploy/template/docker.env.sh b/deploy/template/docker.env.sh index 6bbeb5e901..d1561b8902 100644 --- a/deploy/template/docker.env.sh +++ b/deploy/template/docker.env.sh @@ -59,4 +59,10 @@ APPSMITH_DISABLE_TELEMETRY=$disable_telemetry # APPSMITH_CODEC_SIZE= # ******************************* +# ***** Google Recaptcha Config ****** +# APPSMITH_RECAPTCHA_SITE_KEY= +# APPSMITH_RECAPTCHA_SECRET_KEY= +# APPSMITH_RECAPTCHA_ENABLED= +# ************************************ + EOF diff --git a/deploy/template/nginx_app.conf.sh b/deploy/template/nginx_app.conf.sh index cb0a0928b4..8954753b82 100644 --- a/deploy/template/nginx_app.conf.sh +++ b/deploy/template/nginx_app.conf.sh @@ -49,6 +49,9 @@ $NGINX_SSL_CMNT server_name $custom_domain ; sub_filter __APPSMITH_INTERCOM_APP_ID__ '\${APPSMITH_INTERCOM_APP_ID}'; sub_filter __APPSMITH_MAIL_ENABLED__ '\${APPSMITH_MAIL_ENABLED}'; sub_filter __APPSMITH_DISABLE_TELEMETRY__ '\${APPSMITH_DISABLE_TELEMETRY}'; + sub_filter __APPSMITH_RECAPTCHA_SITE_KEY__ '\${APPSMITH_RECAPTCHA_SITE_KEY}'; + sub_filter __APPSMITH_RECAPTCHA_SECRET_KEY__ '\${APPSMITH_RECAPTCHA_SECRET_KEY}'; + sub_filter __APPSMITH_RECAPTCHA_ENABLED__ '\${APPSMITH_RECAPTCHA_ENABLED}'; } location /f { @@ -106,6 +109,9 @@ $NGINX_SSL_CMNT sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '\${APPSMITH $NGINX_SSL_CMNT sub_filter __APPSMITH_INTERCOM_APP_ID__ '\${APPSMITH_INTERCOM_APP_ID}'; $NGINX_SSL_CMNT sub_filter __APPSMITH_MAIL_ENABLED__ '\${APPSMITH_MAIL_ENABLED}'; $NGINX_SSL_CMNT sub_filter __APPSMITH_DISABLE_TELEMETRY__ '\${APPSMITH_DISABLE_TELEMETRY}'; +$NGINX_SSL_CMNT sub_filter __APPSMITH_RECAPTCHA_SITE_KEY__ '\${APPSMITH_RECAPTCHA_SITE_KEY}; +$NGINX_SSL_CMNT sub_filter __APPSMITH_RECAPTCHA_SECRET_KEY__ '\${APPSMITH_RECAPTCHA_SECRET_KEY}; +$NGINX_SSL_CMNT sub_filter __APPSMITH_RECAPTCHA_ENABLED__ '\${APPSMITH_RECAPTCHA_ENABLED}'; $NGINX_SSL_CMNT } $NGINX_SSL_CMNT $NGINX_SSL_CMNT location /f { diff --git a/office_hours.md b/office_hours.md index 4ee2e13ea7..996a95769e 100644 --- a/office_hours.md +++ b/office_hours.md @@ -28,6 +28,26 @@ You can find the archives of the calls below with a brief summary of each sessio ## Archives +Community Call Jun 10, 2021: New Upcoming Widgets and Latest Releases + +Video Link + +#### Summary + +Somangshu and Abhishek talked about the latest releases from the past two weeks. Somangshu also demoed a few of our upcoming widgets and share designs for those that are in the works. + +------------------ + +Appsmith Live Demo #4, 3rd Jun 2021:  Building a CMS with the Notion API + +Video Link + +#### Summary + +Akshay and Confidence show the community how we internally used the recently released Notion API to build a CMS with features not available natively in notion. This demo also shows how to work with APIs having complex responses. + +------------------ + Appsmith Live Demo #3, 29th May 2021: Stitching APIs together to automate workflows Video Link