diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..535bb98225 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,156 @@ +name: CI +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the develop branch + push: + branches: [develop, main] + pull_request: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main') && github.run_number || github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: true + NODE_OPTIONS: "--max-old-space-size=4096" + LOCKBOX_MASTER_KEY: lockbox-master-key + SECRET_KEY_BASE: secrret-key-base + NODE_ENV: test + PG_HOST: postgres + PG_PORT: 5432 + PG_USER: postgres + PG_PASS: postgres + PG_DB: tooljet_test + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build: + runs-on: ubuntu-latest + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Use Node.js 14.17.3 + uses: actions/setup-node@v2 + with: + node-version: 14.17.3 + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - run: npm i -g npm@7.20.0 + - run: npm run build + + lint: + runs-on: ubuntu-latest + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Use Node.js 14.17.3 + uses: actions/setup-node@v2 + with: + node-version: 14.17.3 + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - run: npm i -g npm@7.20.0 + - run: npm --prefix frontend ci && npm --prefix server ci + - run: npm --prefix server run lint && npm --prefix frontend run lint + + unit-test: + runs-on: ubuntu-latest + needs: build + container: node:14.17.3-buster + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - run: apt update && apt install -y postgresql + - run: npm i -g npm@7.20.0 + - run: npm --prefix server ci + - run: npm --prefix server run db:create + - run: npm --prefix server run db:migrate + - run: npm --prefix server run test + + e2e-test: + runs-on: ubuntu-latest + needs: build + container: node:14.17.3-buster + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - run: apt update && apt install -y postgresql + - run: npm i -g npm@7.20.0 + - run: npm --prefix server ci + - run: npm --prefix server run db:create + - run: npm --prefix server run db:migrate + - run: npm --prefix server run test:e2e -- --silent diff --git a/.version b/.version index 027934ea1a..d33c3a2128 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.11.1 \ No newline at end of file +0.12.0 \ No newline at end of file diff --git a/README.md b/README.md index cd2253670a..b37a50bee8 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ You can deploy ToolJet on Heroku for free using the one-click-deployment button [GitHub contributor leaderboard using ToolJet](https://blog.tooljet.io/building-a-github-contributor-leaderboard-using-tooljet/)
[Cryptocurrency dashboard using ToolJet](https://blog.tooljet.com/how-to-build-a-cryptocurrency-dashboard-in-10-minutes/)
+[WhatsApp CRM using ToolJet](https://blog.tooljet.com/build-a-whatsapp-crm-using-tooljet-within-10-mins/)
## Documentation Documentation is available at https://docs.tooljet.com. diff --git a/app.json b/app.json index effbbd8d70..9fcc5e6d93 100644 --- a/app.json +++ b/app.json @@ -40,6 +40,10 @@ "SSO_DISABLE_SIGNUP": { "description": "Disable sign-up via SSO", "value": "" + }, + "DISABLE_PASSWORD_LOGIN": { + "description": "Disable logging in with username and password. (Do not turn this on unless you've configured SSO and additional admins)", + "value": "false" } }, "formation": { @@ -63,4 +67,4 @@ } } } -} +} \ No newline at end of file diff --git a/docs/docs/Enterprise/_category_.json b/docs/docs/Enterprise/_category_.json new file mode 100644 index 0000000000..cf5729e52d --- /dev/null +++ b/docs/docs/Enterprise/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Enterprise", + "position": 8, + "collapsed": true +} \ No newline at end of file diff --git a/docs/docs/Enterprise/audit_logs.md b/docs/docs/Enterprise/audit_logs.md new file mode 100644 index 0000000000..0c673c19fc --- /dev/null +++ b/docs/docs/Enterprise/audit_logs.md @@ -0,0 +1,69 @@ +# Audit logs + +The audit log is the report of all the activities done in your ToolJet account. It will capture and display events automatically by recording who performed an activity, what when, and where the activity was performed, along with other information such as IP address. + +ToolJet - Enterprise - Audit logs + +### Filter audit logs + +Audited events can be filtered using the below characteristics: + +#### Select Users + +Select a specific user from this dropdown to check all their activities. + +#### Select Apps + +The dropdown will list all the apps present in your account. Choose an app to filter the logs associated with that app. + +#### Select Resources + +| Resources | description | +| ----------- | ----------- | +| User | Filter all the User events like `USER_LOGIN`, `USER_SIGNUP`, `USER_INVITE`, AND `USER_INVITE_REDEEM`. | +| App | Filter all the App events like `APP_CREATE`, `APP_UPDATE`,`APP_VIEW`,`APP_DELETE`,`APP_IMPORT`,`APP_EXPORT`,`APP_CLONE`. | +| Data Query | Filters the events associated with Data Query like `DATA_QUERY_RUN`. | +| Group Permission | All the events associated with Group Permissions will be filtered. Group Permissions include `GROUP_CREATE`, `GROUP_UPDATE`, `GROUP_DELETE`. | +| App Group Permission | Within each group, you can set apps for read or edit privileges. These events gets recorded as App Group Permissions. | + +#### Select Actions + +| Actions | description | +| ----------- | ----------- | +| USER_LOGIN | This event is recorded everytime a user logins. | +| USER_SIGNUP | This event is recorded everytime a new signup is made. | +| USER_INVITE | You can invite users to your account from `Manage Users` section and an event is audited everytime an invite is sent. | +| USER_INVITE_REDEEM | This event is recorded whenever an invite is redeemed. | +| APP_CREATE | This event is recorded when a user creates a new app. | +| APP_UPDATE | This event is recorded whenever actions like renaming the app, making the app public, editing shareable link, or deploying the app are made. | +| APP_VIEW | This event is logged when someone views the launched app. (public apps isn't accounted for) | +| APP_DELETE | This event is recorded whenever a user deletes an app from the dashboard. | +| APP_IMPORT | This event is recorded whenever a user imports an app. | +| APP_EXPORT | This event is recorded whenever an app is exported. | +| APP_CLONE | This event is recorded whenever a clone of the existing app is created. | +| DATA_QUERY_RUN | This event is logged whenever a data source is added, a query is created, or a whenever a query is run either from the query editor or from the launched app. | +| GROUP_PERMISSION_CREATE | This event is recorded whenever a group is created. | +| GROUP_PERMISSION_UPDATE | This event is recorded whenever an app or user is added to or removed from a group, or the permissions for a group are updated. | +| GROUP_PERMISSION_DELETE | This event is recorded whenever a user group is deleted from an account. | +| APP_GROUP_PERMISSION_UPDATE | For every app added in a user group, you can set privileges like `View` or `Edit` and whenever these privileges are updated this event is recorded. By default, the permission of an app for a user group is set to `View`. | + +:::info +It is mandatory to set a Data Range in `From` and `To` to filter audit logs. +::: + +### Understanding information from logs + +ToolJet - Enterprise - Reading logs + +| Property | description | +| ----------- | ----------- | +| action_type | It is the type of action that was logged in this event. Refer [this](#select-actions) to know about actions. | +| created_at | Displays the date and time of a logged event. | +| id | Every event logged has a specific event id associated with it. | +| ip_address | Displays the IP address from where the event was logged. | +| metadata | Metadata includes two sub-properties - `tooljet_version` and `user_agent`. `tooljet_version` displays the version of ToolJet used for the logged event and `user_agent` contains information about the device and browser used for that event. | +| organization_id | Every organization in ToolJet has an id associated with it and is recorded when an event occurs. | +| resource_id | There are several [resources](#select-resources) and each resource that is created, an id get associated with it.| +| resource_name | Displays the name of the [resources](#select-resources) that was logged in the event. For example, if an app was created or deleted then it will display the name of the app. | +| resource_type | isplays the type of the [resources](#select-resources) that was logged in the event. | +| user_id | Every user account in ToolJet has an id associated with it and is recorded when an event occurs. | \ No newline at end of file diff --git a/docs/docs/actions/_category_.json b/docs/docs/actions/_category_.json index 0fce8d65a0..f5b2dfe045 100644 --- a/docs/docs/actions/_category_.json +++ b/docs/docs/actions/_category_.json @@ -1,5 +1,5 @@ { "label": "Actions Reference", - "position": 6, + "position": 7, "collapsed": true } \ No newline at end of file diff --git a/docs/docs/contributing-guide/_category_.json b/docs/docs/contributing-guide/_category_.json index 3b65ba91f4..f5469f8da7 100644 --- a/docs/docs/contributing-guide/_category_.json +++ b/docs/docs/contributing-guide/_category_.json @@ -1,5 +1,5 @@ { "label": "Contributing Guide", - "position": 6, + "position": 9, "collapsed": true -} +} \ No newline at end of file diff --git a/docs/docs/data-sources/airtable.md b/docs/docs/data-sources/airtable.md index fe749bcf7c..e76163829d 100644 --- a/docs/docs/data-sources/airtable.md +++ b/docs/docs/data-sources/airtable.md @@ -7,7 +7,7 @@ sidebar_position: 1 ToolJet can connect to your Airtable account to read and write data. Airtable API key is required to create an Airtable datasource on ToolJet. You can generate API key by visiting [Airtable account page](https://airtable.com/account). -ToolJet - ToolJet - Datasource Airtable +ToolJet - Datasource Airtable :::info Airtable API has a rate limit, and at the time of writing this documentation, the limit is five(5) requests per second per base. You can read more about rate limits here [Airtable API]( https://airtable.com/api ). diff --git a/docs/docs/data-sources/sendgrid.md b/docs/docs/data-sources/sendgrid.md new file mode 100644 index 0000000000..e8e8b2d355 --- /dev/null +++ b/docs/docs/data-sources/sendgrid.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 11 +--- + +# SendGrid + +ToolJet can connect to your SendGrid account to send emails. + +ToolJet - Datasource SendGrid + +:::info +The SendGrid API Datasource supports for interaction with the mail endpoint of the [SendGrid v3 API](https://docs.sendgrid.com/api-reference/how-to-use-the-sendgrid-v3-api/authentication). +::: + +## Connection +To add a new SendGrid API datasource, click the Datasource manager icon on the left-sidebar of the app builder and click on the `Add datasource` button, then select SendGrid API from the modal that pops up. + +Enter your **SendGrid API key** in the "API key" field. + +:::tip +SendGrid API key is required to create an SendGrid datasource on ToolJet. You can generate API key by visiting [SendGrid account page](https://app.sendgrid.com/settings/api_keys). +::: + +Click on the 'Save' button to save the datasource. + +## Supported operations +1. Email service + + +### Email service +Required parameters: +- Send email to +- Send email from +- Subject +- Body as text + + +Optional parameters: +- Body as HTML + +ToolJet - Query SendGrid + +:::info +**Send mail to** - accepts a array/list of emails separated by comma. +For example: +`{{["dev@tooljet.io", "admin@tooljet.io"]}}`. +::: + +:::tip +**Send a single email to multiple recipients** - The `Send mail to` field can contain an array of recipients, which will send a single email with all of the recipients in the field. + +**Send multiple individual emails to multiple recipients** - set Multiple recipients field to `{{true}}` and the `Send mail to` field will be split into multiple emails and send to each recipient. +::: + + +:::note +NOTE: Query should be saved before running. +::: \ No newline at end of file diff --git a/docs/docs/data-sources/typesense.md b/docs/docs/data-sources/typesense.md new file mode 100644 index 0000000000..4312ea53f7 --- /dev/null +++ b/docs/docs/data-sources/typesense.md @@ -0,0 +1,28 @@ +# TypeSense +ToolJet can connect to your TypeSense deployment to read and write data. + +## Supported operations + +:::tip +Documentation for each of these operations are available at https://typesense.org/docs/ +::: + +1. Create collection +2. Index document +3. Search documents +4. Get document +5. Update document +6. Delete document + +:::tip +Make sure that you supply JSON strings instead of JavaScript objects for any document or schema that is being passed to the server, in any of the above operations. +::: + +## Connection +Please make sure the host/IP of the TypeSense deployment is accessible from your VPC if you have self-hosted ToolJet. If you are using ToolJet cloud, please whitelist our IP. + +ToolJet requires the following to connect to your TypeSense deployment: +- Host +- Port +- API Key +- Protocol \ No newline at end of file diff --git a/docs/docs/deployment/env-vars.md b/docs/docs/deployment/env-vars.md index e3a093713b..baf61477d2 100644 --- a/docs/docs/deployment/env-vars.md +++ b/docs/docs/deployment/env-vars.md @@ -48,6 +48,14 @@ If you want to restrict the signups and allow new users only by invitations, set You will still be able to see the signup page but won't be able to successfully submit the form. ::: +#### Disable login and signup using username and password + +:::info +Use this feature only if you have configured other methods of authentication, such as SSO. +::: + +If you want to restrict users from logging in using regular username and password, set the environment variable `DISABLE_PASSWORD_LOGIN` to `true`. + #### Serve client as a server end-point ( optional ) By default, the `SERVE_CLIENT` variable will be set to `false` and the server won't serve the client at its `/` end-point. diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 63b8c0ffc1..28b4fd26d4 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -4,9 +4,9 @@ sidebar_position: 1 # Introduction -ToolJet is an **open-source no-code framework** to build and deploy custom internal tools. ToolJet can connect to your data sources such as databases ( PostgreSQL, MongoDB, MySQL, Elasticsearch, Firestore, DynamoDB, Redis and more ), API endpoints ( ToolJet supports OAuth2 authorization ) and external services ( Stripe, Slack, Google Sheets, airtable and more ). Once the data sources are connected, ToolJet can run queries on these data sources to fetch and update data. The data fetched from data sources can be visualised and modified using the UI widgets such as tables, charts, forms, etc. +ToolJet is an **open-source low-code framework** to build and deploy custom internal tools. ToolJet can connect to your data sources such as databases ( PostgreSQL, MongoDB, MySQL, Elasticsearch, Firestore, DynamoDB, Redis and more ), API endpoints ( ToolJet supports OAuth2 authorization ) and external services ( Stripe, Slack, Google Sheets, airtable and more ). Once the data sources are connected, ToolJet can run queries on these data sources to fetch and update data. The data fetched from data sources can be visualised and modified using the UI widgets such as tables, charts, forms, etc. -ToolJet - introduction +ToolJet - introduction ## How ToolJet works @@ -30,6 +30,11 @@ The references for datasources and widgets: - **[Datasource Reference](/docs/data-sources/redis)** - **[Widget Reference](/docs/widgets/table)** +## Complete tutorials +- [Build a WhatsApp CRM](https://blog.tooljet.com/build-a-whatsapp-crm-using-tooljet-within-10-mins/) +- [Build a cryptocurrency dashboard](https://blog.tooljet.com/how-to-build-a-cryptocurrency-dashboard-in-10-minutes/) +- [Build a Redis GUI](https://blog.tooljet.com/building-a-redis-gui-using-tooljet-in-5-minutes/) + ## Help and Support We have extensively documented the features of ToolJet, but in case you are stuck, please feel to mail us: hello@tooljet.com. If you are using ToolJet cloud, click on the chat icon at the bottom-left corner for instant help. diff --git a/docs/docs/security.md b/docs/docs/security.md new file mode 100644 index 0000000000..ee47dd231d --- /dev/null +++ b/docs/docs/security.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 2 +sidebar_label: Security +--- + +# Security + +## Data storage + +ToolJet does not store data returned from your data sources. ToolJet server acts as a proxy and passes the data as it is to the ToolJet client. The credentials for the data sources are hanlded by the server and never exposed to the client. For example, if you are making an API request, the query is run from the server and not from the frontend. + +## Datasource credentials +All the datasource credentials are securely encrypted using `aes-256-gcm`. The credentials are never exposed to the frontend ( ToolJet client ). + +## Other security features +- **TLS**: If you are using ToolJet cloud, all connections are encrypted using TLS. We also have documentation for setting up TLS for self-hosted installations of ToolJet. +- **Audit logs**: Audit logs are available on the enterprise edition of ToolJet. Every user action is logged along with the IP addresses and user information. +- **Request logging**: All the requests to server are logged. If self-hosted, you can easily extend ToolJet to use your preferred logging service. ToolJet comes with built-in Sentry integration. +- **Whitelisted IPs**: If you are using ToolJet cloud, you can whitelist our IP address (3.129.198.40) so that your datasources are not exposed to the public. +- **Backups**: ToolJet cloud is hosted on AWS using EKS with autoscaling and regular backups. + +If you notice a security vulnerability, please let the team know by sending an email to `security@tooljet.com`. \ No newline at end of file diff --git a/docs/docs/sso/_category_.json b/docs/docs/sso/_category_.json index 0ee03805bf..109724a4fd 100644 --- a/docs/docs/sso/_category_.json +++ b/docs/docs/sso/_category_.json @@ -1,5 +1,5 @@ { "label": "Single Sign-on", - "position": 6, + "position": 10, "collapsed": true -} +} \ No newline at end of file diff --git a/docs/docs/tutorial/adding-widget.md b/docs/docs/tutorial/adding-widget.md index 47565d087b..2d4abfaf2e 100644 --- a/docs/docs/tutorial/adding-widget.md +++ b/docs/docs/tutorial/adding-widget.md @@ -15,6 +15,20 @@ To add a widget, drag and drop the widget to the canvas. The widgets can be resized and repositioned within the canvas. ToolJet - Table component +## Adding widgets to Modal +To add a widget to Modal, we need to trigger [Show modal action](/docs/tutorial/actions#available-actions) + +:::info +Before triggering `Show modal action` we need to add a modal widget to the canvas. +::: + +- Add a `modal widget` to the app +- Trigger the **Show modal action** +- Click on the canvas area for the `Widget manager` sidebar +- Navigate to the Widget manager on the right sidebar and Drag and drop a widget into the Modal + +ToolJet - Adding widget to Modal + ## Resize table columns We can resize the column width using the resize handle of the column. diff --git a/docs/docs/tutorial/creating-app.md b/docs/docs/tutorial/creating-app.md index 59652d2e43..4a98161264 100644 --- a/docs/docs/tutorial/creating-app.md +++ b/docs/docs/tutorial/creating-app.md @@ -14,7 +14,7 @@ This tutorial will walk you through building a simple app to fetch customer info To create a new ToolJet app, click on the **'Create App'** button on the ToolJet dashboard. -You will redirected to the visual app editor once the app has been created. The name of the app can be changed by clicking on the app name at top-left of the app builder. +You will be redirected to the visual app editor once the app has been created. The name of the app can be changed by clicking on the app name at top-left of the app builder. The main components of an app: diff --git a/docs/docs/widgets/_category_.json b/docs/docs/widgets/_category_.json index 0478fd4efb..28ad4a4735 100644 --- a/docs/docs/widgets/_category_.json +++ b/docs/docs/widgets/_category_.json @@ -1,5 +1,5 @@ { "label": "Widget Reference", - "position": 5, + "position": 6, "collapsed": true -} +} \ No newline at end of file diff --git a/docs/docs/widgets/container.md b/docs/docs/widgets/container.md new file mode 100644 index 0000000000..eb8bf934b1 --- /dev/null +++ b/docs/docs/widgets/container.md @@ -0,0 +1,20 @@ +# Container + +Containers are used to group widgets together. You can move the desired number of widgets inside a container to organize your app better. + +ToolJet - Widget Reference - Container + +#### Layout + +| Layout | description | +| ----------- | ----------- | +| Show on desktop | This property have toggle switch. If enabled, the Container widget will display in the desktop view else it will not appear. This is enabled by default.| +| Show on mobile | This property have toggle switch. If enabled, the Container wisget will display in the mobile view else it will not appear.| + +#### Styles + +| Style | Description | +| ----------- | ----------- | +| backgroundColor | You can change the background color of the Container by entering the Hex color code or choosing a color of your choice from the color picker. | +| Visibility | This is to control the visibility of the widget. If `{{false}}` the widget will not visible after the app is deployed. It can only have boolean values i.e. either `{{true}}` or `{{false}}`. By default, it's set to `{{true}}`. | +| Disable | This property only accepts boolean values. If set to `{{true}}`, the widget will be locked and becomes non-functional. By default, its value is set to `{{false}}`. | \ No newline at end of file diff --git a/docs/docs/widgets/modal.md b/docs/docs/widgets/modal.md index b301247ddc..9e91851417 100644 --- a/docs/docs/widgets/modal.md +++ b/docs/docs/widgets/modal.md @@ -5,6 +5,9 @@ Modal widget renders in front of a backdrop, and it blocks interaction with the ToolJet - Widget Reference - Modal +### Add widgets to Modal + +To add widgets to the Modals please refer to **[Tutorial - Adding a widget](/docs/tutorial/adding-widget#add-widgets-to-modal)** #### Properties diff --git a/docs/package-lock.json b/docs/docs/widgets/package-lock.json similarity index 100% rename from docs/package-lock.json rename to docs/docs/widgets/package-lock.json diff --git a/docs/docs/widgets/password-input.md b/docs/docs/widgets/password-input.md new file mode 100644 index 0000000000..b056eb8b7c --- /dev/null +++ b/docs/docs/widgets/password-input.md @@ -0,0 +1,34 @@ +# Password Input + +A Password Input widget provides a way for the users to securely enter a password. The Password Input is a one-line plain text editor in which the text is obscured so that it cannot be read, by replacing each character with an asterisk ("*") symbol. + +ToolJet - Widget Reference - Password Input + +#### Properties + +| properties | description | +| ----------- | ----------- | +| Placeholder | It specifies a hint that describes the expected value.| + +#### Validation + +| Validation | description | +| ----------- | ----------- | +| Regex | Use this field to enter a Regular Expression that will validate the password constraints. | +| Min length | Enter the number for a minimum length of password allowed.| +| Max length | Enter the number for the maximum length of password allowed. | +| Custom validation | If the condition is true, the validation passes, otherwise return a string that should be displayed as the error message. For example: `{{components.passwordInput1.value === 'something' ? true: 'value should be something'}}` | + +#### Layout + +| Layout | description | +| ----------- | ----------- | +| Show on desktop | If enabled, the Password Input widget will display in the desktop view else it will not appear. This is enabled by default.| +| Show on mobile | If enabled, the Password Input widget will display in the mobile view else it will not appear.| + +#### Styles + +| Style | Description | +| ----------- | ----------- | +| Visibility | This is to control the visibility of the widget. If `{{false}}` the widget will not visible after the app is deployed. It can only have boolean values i.e. either `{{true}}` or `{{false}}`. By default, it's set to `{{true}}`. | +| Disable | This property only accepts boolean values. If set to `{{true}}`, the widget will be locked and becomes non-functional. By default, its value is set to `{{false}}`. | \ No newline at end of file diff --git a/docs/docs/widgets/tabs.md b/docs/docs/widgets/tabs.md new file mode 100644 index 0000000000..ae812043e9 --- /dev/null +++ b/docs/docs/widgets/tabs.md @@ -0,0 +1,27 @@ +# Tabs + +A Tabs widget contains a number of defined containers that can be navigated through the tabs. Each tab acts as a container and can have different components or widgets. + +ToolJet - Widget Reference - Tabs + +#### Properties + +| properties | description | +| ----------- | ----------- | +| Tabs | This property lets you add and remove containers from the tabs widget. Each container in the tab has its unique `id` and `title` | +| Default tab | This property selects the container in the tab which matches the corresponding `id`. By default, the value is set to `0`| + +#### Layout + +| Layout | description | +| ----------- | ----------- | +| Show on desktop | This property have toggle switch. If enabled, the Tabs widget will display in the desktop view else it will not appear. This is enabled by default.| +| Show on mobile | This property have toggle switch. If enabled, the Tabs wisget will display in the mobile view else it will not appear.| + +#### Styles + +| Style | Description | +| ----------- | ----------- | +| Highlight Color | You can change the highlight color of the selected tab by entering the Hex color code or choosing a color of your choice from the color picker. | +| Visibility | This is to control the visibility of the widget. If `{{false}}` the widget will not visible after the app is deployed. It can only have boolean values i.e. either `{{true}}` or `{{false}}`. By default, it's set to `{{true}}`. | +| Disable | This property only accepts boolean values. If set to `{{true}}`, the widget will be locked and becomes non-functional. By default, its value is set to `{{false}}`. | diff --git a/docs/docs/widgets/text-input.md b/docs/docs/widgets/text-input.md index 1bdb0a0a77..9661e3c361 100644 --- a/docs/docs/widgets/text-input.md +++ b/docs/docs/widgets/text-input.md @@ -8,8 +8,13 @@ The Text Input should be preferred when user input is a single line of text. ToolJet - Widget Reference - Text input -#### Properties +### Properties | properties | description | | ----------- | ----------- | -| Placeholder | It specifies a hint that describes the expected value.| \ No newline at end of file +| Placeholder | It specifies a hint that describes the expected value.| + +### Events + +#### On change +This event is fired whenever the user types something on the text input. diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index d2c33763cf..09942f8f5f 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -13,7 +13,7 @@ module.exports = { announcementBar: { id: 'support_us', content: - '⭐️ If you like ToolJet, give it a star on GitHub GitHub and follow us on Twitter', + '⭐️ If you like ToolJet, give it a star on GitHub and follow us on Twitter', backgroundColor: '#4D72DA', textColor: '#ffffff', isCloseable: true, diff --git a/docs/static/img/datasource-reference/sendgrid/sendgrid-datasource.png b/docs/static/img/datasource-reference/sendgrid/sendgrid-datasource.png new file mode 100644 index 0000000000..a635a20dde Binary files /dev/null and b/docs/static/img/datasource-reference/sendgrid/sendgrid-datasource.png differ diff --git a/docs/static/img/datasource-reference/sendgrid/sendgrid-query.jpg b/docs/static/img/datasource-reference/sendgrid/sendgrid-query.jpg new file mode 100644 index 0000000000..57c742dc85 Binary files /dev/null and b/docs/static/img/datasource-reference/sendgrid/sendgrid-query.jpg differ diff --git a/docs/static/img/enterprise/audit_logs/audit_logs.gif b/docs/static/img/enterprise/audit_logs/audit_logs.gif new file mode 100644 index 0000000000..5831de536b Binary files /dev/null and b/docs/static/img/enterprise/audit_logs/audit_logs.gif differ diff --git a/docs/static/img/enterprise/audit_logs/reading_logs.png b/docs/static/img/enterprise/audit_logs/reading_logs.png new file mode 100644 index 0000000000..ef456f4bd3 Binary files /dev/null and b/docs/static/img/enterprise/audit_logs/reading_logs.png differ diff --git a/docs/static/img/tutorial/adding-widget/adding-widget-to-modal.gif b/docs/static/img/tutorial/adding-widget/adding-widget-to-modal.gif new file mode 100644 index 0000000000..c8f9656d23 Binary files /dev/null and b/docs/static/img/tutorial/adding-widget/adding-widget-to-modal.gif differ diff --git a/docs/static/img/widgets/Container/container.gif b/docs/static/img/widgets/Container/container.gif new file mode 100644 index 0000000000..430b88fc68 Binary files /dev/null and b/docs/static/img/widgets/Container/container.gif differ diff --git a/docs/static/img/widgets/password-input/password-input.gif b/docs/static/img/widgets/password-input/password-input.gif new file mode 100644 index 0000000000..1a566f306a Binary files /dev/null and b/docs/static/img/widgets/password-input/password-input.gif differ diff --git a/docs/static/img/widgets/tabs/tabs.gif b/docs/static/img/widgets/tabs/tabs.gif new file mode 100644 index 0000000000..8ec34a7b5e Binary files /dev/null and b/docs/static/img/widgets/tabs/tabs.gif differ diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index b705cb1a22..9542aee69e 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -36,7 +36,10 @@ ], "react/prop-types": 0, "react/display-name": "off", - "no-unused-vars": [2, { "args": "after-used", "argsIgnorePattern": "reject" }], + "no-unused-vars": ["warn", { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + }], "react/no-deprecated": 0, "no-prototype-builtins": 0 }, @@ -53,4 +56,3 @@ "__dirname": true } } - \ No newline at end of file diff --git a/frontend/assets/images/icons/editor/datasources/sendgrid.svg b/frontend/assets/images/icons/editor/datasources/sendgrid.svg new file mode 100644 index 0000000000..f84825fcb2 --- /dev/null +++ b/frontend/assets/images/icons/editor/datasources/sendgrid.svg @@ -0,0 +1,70 @@ + +image/svg+xml diff --git a/frontend/assets/images/icons/editor/datasources/twilio.svg b/frontend/assets/images/icons/editor/datasources/twilio.svg new file mode 100644 index 0000000000..75602ede8a --- /dev/null +++ b/frontend/assets/images/icons/editor/datasources/twilio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/assets/images/icons/editor/datasources/typesense.svg b/frontend/assets/images/icons/editor/datasources/typesense.svg new file mode 100644 index 0000000000..e9f46540b8 --- /dev/null +++ b/frontend/assets/images/icons/editor/datasources/typesense.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/assets/images/icons/portal-close.svg b/frontend/assets/images/icons/portal-close.svg new file mode 100644 index 0000000000..cf6d2a465e --- /dev/null +++ b/frontend/assets/images/icons/portal-close.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/frontend/assets/images/icons/portal-open.svg b/frontend/assets/images/icons/portal-open.svg new file mode 100644 index 0000000000..3a13b0397e --- /dev/null +++ b/frontend/assets/images/icons/portal-open.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 61cbb9f565..53e44c7882 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "frontend", "version": "0.1.0", "dependencies": { "@babel/core": "^7.4.3", @@ -33,6 +34,7 @@ "fuse.js": "^6.4.6", "history": "^4.9.0", "html-webpack-plugin": "^5.3.2", + "immer": "^9.0.6", "immutability-helper": "^3.1.1", "lodash": "^4.17.21", "moment": "^2.29.1", @@ -54,6 +56,7 @@ "react-easy-sort": "^0.2.1", "react-google-login": "^5.2.2", "react-hot-toast": "^2.1.1", + "react-hotkeys-hook": "^3.4.4", "react-json-view": "^1.21.3", "react-lazyload": "^3.2.0", "react-loading-skeleton": "^2.2.0", @@ -11215,6 +11218,11 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, + "node_modules/hotkeys-js": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz", + "integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg==" + }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -11627,9 +11635,13 @@ } }, "node_modules/immer": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", - "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", + "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } }, "node_modules/immutability-helper": { "version": "3.1.1", @@ -17683,6 +17695,11 @@ "node": ">=8" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", + "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" + }, "node_modules/react-dev-utils/node_modules/inquirer": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", @@ -17993,6 +18010,18 @@ "csstype": "^2.6.2" } }, + "node_modules/react-hotkeys-hook": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.4.tgz", + "integrity": "sha512-vaORq07rWgmuF3owWRhgFV/3VL8/l2q9lz0WyVEddJnWTtKW+AOgU5YgYKuwN6h6h7bCcLG3MFsJIjCrM/5DvQ==", + "dependencies": { + "hotkeys-js": "3.8.7" + }, + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -33209,6 +33238,11 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, + "hotkeys-js": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz", + "integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg==" + }, "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -33525,9 +33559,9 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, "immer": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", - "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", + "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==" }, "immutability-helper": { "version": "3.1.1", @@ -38358,6 +38392,11 @@ "path-exists": "^4.0.0" } }, + "immer": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", + "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" + }, "inquirer": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", @@ -38585,6 +38624,14 @@ } } }, + "react-hotkeys-hook": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.4.tgz", + "integrity": "sha512-vaORq07rWgmuF3owWRhgFV/3VL8/l2q9lz0WyVEddJnWTtKW+AOgU5YgYKuwN6h6h7bCcLG3MFsJIjCrM/5DvQ==", + "requires": { + "hotkeys-js": "3.8.7" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index bffee674b7..70e38a5396 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "fuse.js": "^6.4.6", "history": "^4.9.0", "html-webpack-plugin": "^5.3.2", + "immer": "^9.0.6", "immutability-helper": "^3.1.1", "lodash": "^4.17.21", "moment": "^2.29.1", @@ -50,6 +51,7 @@ "react-easy-sort": "^0.2.1", "react-google-login": "^5.2.2", "react-hot-toast": "^2.1.1", + "react-hotkeys-hook": "^3.4.4", "react-json-view": "^1.21.3", "react-lazyload": "^3.2.0", "react-loading-skeleton": "^2.2.0", diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index 787c98fe92..1ce1d9365c 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -53,6 +53,17 @@ class App extends React.Component { render() { const { currentUser, fetchedMetadata, updateAvailable, onboarded, darkMode } = this.state; + let toastOptions = {}; + + if (darkMode) { + toastOptions = { + style: { + borderRadius: '10px', + background: '#333', + color: '#fff', + }, + }; + } if (currentUser && fetchedMetadata === false) { tooljetService.fetchMetaData().then((data) => { @@ -175,15 +186,7 @@ class App extends React.Component { /> - + ); } diff --git a/frontend/src/Editor/Box.jsx b/frontend/src/Editor/Box.jsx index 0f198a1abe..d28833c69f 100644 --- a/frontend/src/Editor/Box.jsx +++ b/frontend/src/Editor/Box.jsx @@ -32,6 +32,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import '@/_styles/custom.scss'; import { resolveProperties, resolveStyles } from './component-properties-resolution'; import { validateWidget } from '@/_helpers/utils'; +import ErrorBoundary from './ErrorBoundary'; const AllComponents = { Button, @@ -122,53 +123,55 @@ export const Box = function Box({ trigger={!inCanvas ? ['hover', 'focus'] : null} overlay={(props) => renderTooltip({ props, text: `${component.description}` })} > -
- {inCanvas ? ( - onComponentOptionChanged(component, variable, value)} - fireEvent={fireEvent} - validate={validate} - > - ) : ( -
-
-
-
-
- {component.displayName} + +
+ {inCanvas ? ( + onComponentOptionChanged(component, variable, value)} + fireEvent={fireEvent} + validate={validate} + > + ) : ( +
+
+
+
+
+ {component.displayName} +
-
- )} -
+ )} +
+ ); }; diff --git a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx index fa302678e9..fa69747aeb 100644 --- a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx +++ b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useSpring, config, animated } from 'react-spring'; - +import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; +import Tooltip from 'react-bootstrap/Tooltip'; import CodeMirror from '@uiw/react-codemirror'; import 'codemirror/mode/handlebars/handlebars'; import 'codemirror/mode/javascript/javascript'; @@ -15,6 +16,7 @@ import 'codemirror/theme/monokai.css'; import { getSuggestionKeys, onBeforeChange, handleChange } from './utils'; import { resolveReferences } from '@/_helpers/utils'; import useHeight from '@/_hooks/use-height-transition'; +import usePortal from '@/_hooks/use-portal'; export function CodeHinter({ initialValue, @@ -30,10 +32,13 @@ export function CodeHinter({ height, minHeight, lineWrapping, + componentName = null, + usePortalEditor = true, }) { + const darkMode = localStorage.getItem('darkMode') === 'true'; const options = { - lineNumbers: lineNumbers, - lineWrapping: lineWrapping, + lineNumbers: lineNumbers ?? false, + lineWrapping: lineWrapping ?? true, singleLine: true, mode: mode || 'handlebars', tabSize: 2, @@ -83,10 +88,11 @@ export function CodeHinter({ const getPreview = () => { const [preview, error] = resolveReferences(currentValue, realState, null, {}, true); + const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1'; if (error) { return ( - +
@@ -103,7 +109,7 @@ export function CodeHinter({ const content = getPreviewContent(preview, previewType); return ( - +
@@ -116,30 +122,105 @@ export function CodeHinter({ ); }; enablePreview = enablePreview ?? true; + + const [isOpen, setIsOpen] = React.useState(false); + + const handleToggle = () => { + if (!isOpen) { + setIsOpen(true); + } + + return new Promise((resolve) => { + const element = document.getElementsByClassName('portal-container'); + if (element) { + const checkPortalExits = element[0]?.classList.contains(componentName); + + if (checkPortalExits === false) { + const parent = element[0].parentNode; + parent.removeChild(element[0]); + } + + setIsOpen(false); + resolve(); + } + }).then(() => { + setIsOpen(true); + forceUpdate(); + }); + }; + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + return ( -
+
- setFocused(true)} - onBlur={(editor) => { - const value = editor.getValue(); - onChange(value); - setFocused(false); - }} - onChange={(editor) => valueChanged(editor, onChange, suggestions, ignoreBraces)} - onBeforeChange={(editor, change) => onBeforeChange(editor, change, ignoreBraces)} - options={options} - /> + {usePortalEditor && } + + setFocused(true)} + onBlur={(editor) => { + const value = editor.getValue(); + onChange(value); + setFocused(false); + }} + onChange={(editor) => valueChanged(editor, onChange, suggestions, ignoreBraces)} + onBeforeChange={(editor, change) => onBeforeChange(editor, change, ignoreBraces)} + options={options} + viewportMargin={Infinity} + /> +
- {enablePreview && getPreview()} + {enablePreview && !isOpen && getPreview()}
); } + +const PopupIcon = ({ callback }) => { + return ( +
+ {'Pop out code editor into a new window'}} + > + { + e.stopPropagation(); + callback(); + }} + /> + +
+ ); +}; + +const Portal = ({ children, ...restProps }) => { + const renderPortal = usePortal({ children, ...restProps }); + + return {renderPortal}; +}; + +CodeHinter.PopupIcon = PopupIcon; +CodeHinter.Portal = Portal; diff --git a/frontend/src/Editor/Comment/CommentFooter.jsx b/frontend/src/Editor/Comment/CommentFooter.jsx index 63a3bf8fde..23415c16bd 100644 --- a/frontend/src/Editor/Comment/CommentFooter.jsx +++ b/frontend/src/Editor/Comment/CommentFooter.jsx @@ -4,8 +4,8 @@ import { Picker } from 'emoji-mart'; import TextareaMentions from '@/_ui/Mentions'; import Button from '@/_ui/Button'; -import useShortcuts from '@/_hooks/use-shortcuts'; import usePopover from '@/_hooks/use-popover'; +import { useHotkeys } from 'react-hotkeys-hook'; function CommentFooter({ users, editComment = '', editCommentId, handleSubmit }) { const [comment, setComment] = React.useState(editComment); @@ -28,7 +28,7 @@ function CommentFooter({ users, editComment = '', editCommentId, handleSubmit }) setOpen(false); }; - useShortcuts(['Meta', 'Enter'], () => handleClick(), [comment]); + useHotkeys('⌘+enter, control+enter', () => handleClick()); return ( <> diff --git a/frontend/src/Editor/CommentNotifications/Content.jsx b/frontend/src/Editor/CommentNotifications/Content.jsx index 9d6c9e8fc6..e00ed90005 100644 --- a/frontend/src/Editor/CommentNotifications/Content.jsx +++ b/frontend/src/Editor/CommentNotifications/Content.jsx @@ -4,7 +4,7 @@ import { isEmpty } from 'lodash'; import { pluralize } from '@/_helpers/utils'; import moment from 'moment'; import usePopover from '@/_hooks/use-popover'; -import { useSpring, animated } from 'react-spring'; +import { useSpring } from 'react-spring'; import useRouter from '@/_hooks/use-router'; import Spinner from '@/_ui/Spinner'; @@ -12,8 +12,8 @@ import Spinner from '@/_ui/Spinner'; const Content = ({ notifications, loading }) => { const router = useRouter(); const [selectedCommentId, setSelectedCommentId] = React.useState(router.query.commentId); - const [open, trigger, content] = usePopover(false); - const popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 }); + const [open, _trigger, _content] = usePopover(false); + const _popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 }); React.useEffect(() => { if (router.query?.commentId) setSelectedCommentId(router.query?.commentId); diff --git a/frontend/src/Editor/Components/Button.jsx b/frontend/src/Editor/Components/Button.jsx index 9ba7046997..0a9841a403 100644 --- a/frontend/src/Editor/Components/Button.jsx +++ b/frontend/src/Editor/Components/Button.jsx @@ -1,50 +1,23 @@ -import React, { useState, useEffect } from 'react'; -import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; +import React from 'react'; var tinycolor = require('tinycolor2'); -export const Button = function Button({ width, height, component, currentState, fireEvent }) { - const [loadingState, setLoadingState] = useState(false); - - useEffect(() => { - const loadingStateProperty = component.definition.properties.loadingState; - if (loadingStateProperty && currentState) { - const newState = resolveReferences(loadingStateProperty.value, currentState, false); - setLoadingState(newState); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentState]); - - const text = component.definition.properties.text.value; - const backgroundColor = component.definition.styles.backgroundColor.value; - const color = component.definition.styles.textColor.value; - const borderRadius = component.definition.styles.borderRadius?.value ?? 3; // using 2 for backward compatibility - const widgetVisibility = component.definition.styles?.visibility?.value ?? true; - const disabledState = component.definition.styles?.disabledState?.value ?? false; - - const parsedDisabledState = - typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; - const parsedBorderRadius = typeof borderRadius !== 'number' ? resolveWidgetFieldValue(borderRadius, currentState) : borderRadius; - let parsedWidgetVisibility = widgetVisibility; - - try { - parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); - } catch (err) { - console.log(err); - } +export const Button = function Button({ height, properties, styles, fireEvent }) { + const { loadingState, text } = properties; + const { backgroundColor, textColor, borderRadius, visibility, disabledState } = styles; const computedStyles = { backgroundColor, - color, + color: textColor, width: '100%', - borderRadius: `${parsedBorderRadius}px`, + borderRadius: `${borderRadius}px`, height, - display: parsedWidgetVisibility ? '' : 'none', + display: visibility ? '' : 'none', '--tblr-btn-color-darker': tinycolor(backgroundColor).darken(8).toString(), }; return ( ); }; diff --git a/frontend/src/Editor/Components/Chart.jsx b/frontend/src/Editor/Components/Chart.jsx index 2dbd3c17b9..81df3624dd 100644 --- a/frontend/src/Editor/Components/Chart.jsx +++ b/frontend/src/Editor/Components/Chart.jsx @@ -1,58 +1,34 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; // Use plotly basic bundle import Plotly from 'plotly.js-basic-dist-min'; import createPlotlyComponent from 'react-plotly.js/factory'; const Plot = createPlotlyComponent(Plotly); -export const Chart = function Chart({ id, width, height, component, onComponentClick, currentState, darkMode }) { +export const Chart = function Chart({ width, height, darkMode, properties, styles }) { const [loadingState, setLoadingState] = useState(false); - const widgetVisibility = component.definition.styles?.visibility?.value ?? true; - const disabledState = component.definition.styles?.disabledState?.value ?? false; - - let parsedWidgetVisibility = widgetVisibility; - const parsedDisabledState = - typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; - - try { - parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); - } catch (err) { - console.log(err); - } + const { visibility, disabledState } = styles; + const { title, markerColor, showGridLines, type, data } = properties; useEffect(() => { - const loadingStateProperty = component.definition.properties.loadingState; - if (loadingStateProperty && currentState) { - const newState = resolveReferences(loadingStateProperty.value, currentState, false); - setLoadingState(newState); + const loadingStateProperty = properties.loadingState; + if (loadingStateProperty != undefined) { + setLoadingState(loadingStateProperty); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentState]); + }, [properties.loadingState]); const computedStyles = { width: width - 4, height, - display: parsedWidgetVisibility ? '' : 'none', + display: visibility ? '' : 'none', background: darkMode ? '#1f2936' : 'white', }; - // darkMode ? '#1f2936' : 'white' - const dataProperty = component.definition.properties.data; - const dataString = dataProperty ? dataProperty.value : []; + const dataString = data ?? []; - const titleProperty = component.definition.properties.title; - const title = titleProperty.value; + const chartType = type; - const typeProperty = component.definition.properties.type; - const chartType = typeProperty.value; - - const markerColorProperty = component.definition.properties.markerColor; - const markerColor = markerColorProperty ? markerColorProperty.value : 'red'; - - const gridLinesProperty = component.definition.properties.showGridLines; - const showGridLines = gridLinesProperty ? gridLinesProperty.value : true; const fontColor = darkMode ? '#c3c3c3' : null; const layout = { @@ -84,8 +60,6 @@ export const Chart = function Chart({ id, width, height, component, onComponentC }, }; - const data = resolveReferences(dataString, currentState, []); - const computeChartData = (data, dataString) => { let rawData = data; if (typeof rawData === 'string') { @@ -128,14 +102,7 @@ export const Chart = function Chart({ id, width, height, component, onComponentC const memoizedChartData = useMemo(() => computeChartData(data, dataString), [data, dataString]); return ( -
{ - event.stopPropagation(); - onComponentClick(id, component, event); - }} - > +
{loadingState === true ? (
diff --git a/frontend/src/Editor/Components/Checkbox.jsx b/frontend/src/Editor/Components/Checkbox.jsx index 72bed7e748..fd326a0b1d 100644 --- a/frontend/src/Editor/Components/Checkbox.jsx +++ b/frontend/src/Editor/Components/Checkbox.jsx @@ -1,56 +1,23 @@ import React from 'react'; -import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; -export const Checkbox = function Checkbox({ - id, - height, - component, - onComponentClick, - currentState, - onComponentOptionChanged, - onEvent, -}) { +export const Checkbox = function Checkbox({ height, properties, styles, fireEvent, setExposedVariable }) { const [checked, setChecked] = React.useState(false); - const label = component.definition.properties.label.value; - const textColorProperty = component.definition.styles.textColor; - const textColor = textColorProperty ? textColorProperty.value : '#000'; - const checkboxColorProperty = component.definition.styles.checkboxColor; - const checkboxColor = checkboxColorProperty ? checkboxColorProperty.value : '#3c92dc'; - const widgetVisibility = component.definition.styles?.visibility?.value ?? true; - const disabledState = component.definition.styles?.disabledState?.value ?? false; - - const parsedDisabledState = - typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; - - let parsedWidgetVisibility = widgetVisibility; - - try { - parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); - } catch (err) { - console.log(err); - } + const { label } = properties; + const { visibility, disabledState, checkboxColor, textColor } = styles; function toggleValue(e) { const isChecked = e.target.checked; setChecked(isChecked); - onComponentOptionChanged(component, 'value', isChecked); + setExposedVariable('value', isChecked); if (isChecked) { - onEvent('onCheck', { component }); + fireEvent('onCheck'); } else { - onEvent('onUnCheck', { component }); + fireEvent('onUnCheck'); } } return ( -
{ - event.stopPropagation(); - onComponentClick(id, component, event); - }} - > +