Merge branch 'release/v0.12.0'

This commit is contained in:
navaneeth 2021-12-22 17:27:39 +05:30
commit 5fbfcbe10a
193 changed files with 3789 additions and 1676 deletions

156
.github/workflows/ci.yml vendored Normal file
View file

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

View file

@ -1 +1 @@
0.11.1 0.12.0

View file

@ -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/)<br> [GitHub contributor leaderboard using ToolJet](https://blog.tooljet.io/building-a-github-contributor-leaderboard-using-tooljet/)<br>
[Cryptocurrency dashboard using ToolJet](https://blog.tooljet.com/how-to-build-a-cryptocurrency-dashboard-in-10-minutes/)<br> [Cryptocurrency dashboard using ToolJet](https://blog.tooljet.com/how-to-build-a-cryptocurrency-dashboard-in-10-minutes/)<br>
[WhatsApp CRM using ToolJet](https://blog.tooljet.com/build-a-whatsapp-crm-using-tooljet-within-10-mins/)<br>
## Documentation ## Documentation
Documentation is available at https://docs.tooljet.com. Documentation is available at https://docs.tooljet.com.

View file

@ -40,6 +40,10 @@
"SSO_DISABLE_SIGNUP": { "SSO_DISABLE_SIGNUP": {
"description": "Disable sign-up via SSO", "description": "Disable sign-up via SSO",
"value": "" "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": { "formation": {
@ -63,4 +67,4 @@
} }
} }
} }
} }

View file

@ -0,0 +1,5 @@
{
"label": "Enterprise",
"position": 8,
"collapsed": true
}

View file

@ -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.
<img class="screenshot-full" src="/img/Enterprise/audit_logs/audit_logs.gif" alt="ToolJet - Enterprise - Audit logs" height="420"/>
### 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
<img class="screenshot-full" src="/img/Enterprise/audit_logs/reading_logs.png" alt="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. |

View file

@ -1,5 +1,5 @@
{ {
"label": "Actions Reference", "label": "Actions Reference",
"position": 6, "position": 7,
"collapsed": true "collapsed": true
} }

View file

@ -1,5 +1,5 @@
{ {
"label": "Contributing Guide", "label": "Contributing Guide",
"position": 6, "position": 9,
"collapsed": true "collapsed": true
} }

View file

@ -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 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).
<img class="screenshot-full" src="/img/datasource-reference/airtable/airtable-intro.gif" alt="ToolJet - ToolJet - Datasource Airtable" height="420" /> <img class="screenshot-full" src="/img/datasource-reference/airtable/airtable-intro.gif" alt="ToolJet - Datasource Airtable" height="420" />
:::info :::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 ). 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 ).

View file

@ -0,0 +1,58 @@
---
sidebar_position: 11
---
# SendGrid
ToolJet can connect to your SendGrid account to send emails.
<img class="screenshot-full" src="/img/datasource-reference/sendgrid/sendgrid-datasource.png" alt="ToolJet - Datasource SendGrid" height="420" />
:::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
<img class="screenshot-full" src="/img/datasource-reference/sendgrid/sendgrid-query.jpg" alt="ToolJet - Query SendGrid" height="420"/>
:::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 <b>Multiple recipients</b> 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.
:::

View file

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

View file

@ -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. 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 ) #### 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. By default, the `SERVE_CLIENT` variable will be set to `false` and the server won't serve the client at its `/` end-point.

View file

@ -4,9 +4,9 @@ sidebar_position: 1
# Introduction # 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.
<img class="screenshot-full" src="/img/introduction.gif" alt="ToolJet - introduction" height="420"/> <img class="screenshot-full" src="https://user-images.githubusercontent.com/7828962/144586771-c6d6cba5-8f79-4e0c-80b4-aa1a38657229.png" alt="ToolJet - introduction" />
## How ToolJet works ## How ToolJet works
@ -30,6 +30,11 @@ The references for datasources and widgets:
- **[Datasource Reference](/docs/data-sources/redis)** - **[Datasource Reference](/docs/data-sources/redis)**
- **[Widget Reference](/docs/widgets/table)** - **[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 ## 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. 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. If you are using ToolJet cloud, click on the chat icon at the bottom-left corner for instant help.

22
docs/docs/security.md Normal file
View file

@ -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`.

View file

@ -1,5 +1,5 @@
{ {
"label": "Single Sign-on", "label": "Single Sign-on",
"position": 6, "position": 10,
"collapsed": true "collapsed": true
} }

View file

@ -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. The widgets can be resized and repositioned within the canvas.
<img class="screenshot-full" src="/img/tutorial/adding-widget/resize-table.gif" alt="ToolJet - Table component" height="420"/> <img class="screenshot-full" src="/img/tutorial/adding-widget/resize-table.gif" alt="ToolJet - Table component" height="420"/>
## 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
<img class="screenshot-full" src="/img/tutorial/adding-widget/adding-widget-to-modal.gif" alt="ToolJet - Adding widget to Modal" height="420"/>
## Resize table columns ## Resize table columns
We can resize the column width using the resize handle of the column. We can resize the column width using the resize handle of the column.

View file

@ -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. 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: The main components of an app:

View file

@ -1,5 +1,5 @@
{ {
"label": "Widget Reference", "label": "Widget Reference",
"position": 5, "position": 6,
"collapsed": true "collapsed": true
} }

View file

@ -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.
<img class="screenshot-full" src="/img/widgets/container/container.gif" alt="ToolJet - Widget Reference - Container" height="420"/>
#### 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}}`. |

View file

@ -5,6 +5,9 @@ Modal widget renders in front of a backdrop, and it blocks interaction with the
<img class="screenshot-full" src="/img/widgets/modal/modal.gif" alt="ToolJet - Widget Reference - Modal" height="420"/> <img class="screenshot-full" src="/img/widgets/modal/modal.gif" alt="ToolJet - Widget Reference - Modal" height="420"/>
### 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 #### Properties

View file

@ -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.
<img class="screenshot-full" src="/img/widgets/password-input/password-input.gif" alt="ToolJet - Widget Reference - Password Input" height="420"/>
#### 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}}`. |

27
docs/docs/widgets/tabs.md Normal file
View file

@ -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.
<img class="screenshot-full" src="/img/widgets/tabs/tabs.gif" alt="ToolJet - Widget Reference - Tabs" height="420"/>
#### 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}}`. |

View file

@ -8,8 +8,13 @@ The Text Input should be preferred when user input is a single line of text.
<img class="screenshot-full" src="/img/widgets/text-input/textinput.gif" alt="ToolJet - Widget Reference - Text input" height="420"/> <img class="screenshot-full" src="/img/widgets/text-input/textinput.gif" alt="ToolJet - Widget Reference - Text input" height="420"/>
#### Properties ### Properties
| properties | description | | properties | description |
| ----------- | ----------- | | ----------- | ----------- |
| Placeholder | It specifies a hint that describes the expected value.| | 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.

View file

@ -13,7 +13,7 @@ module.exports = {
announcementBar: { announcementBar: {
id: 'support_us', id: 'support_us',
content: content:
'⭐️ If you like ToolJet, give it a star on GitHub <a target="_blank" rel="noopener noreferrer" href="https://github.com/ToolJet/ToolJet">GitHub</a> and follow us on <a target="_blank" rel="noopener noreferrer" href="https://twitter.com/ToolJet">Twitter</a>', '⭐️ If you like ToolJet, give it a star on <a target="_blank" rel="noopener noreferrer" href="https://github.com/ToolJet/ToolJet">GitHub</a> and follow us on <a target="_blank" rel="noopener noreferrer" href="https://twitter.com/ToolJet">Twitter</a>',
backgroundColor: '#4D72DA', backgroundColor: '#4D72DA',
textColor: '#ffffff', textColor: '#ffffff',
isCloseable: true, isCloseable: true,

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

BIN
docs/static/img/widgets/tabs/tabs.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

View file

@ -36,7 +36,10 @@
], ],
"react/prop-types": 0, "react/prop-types": 0,
"react/display-name": "off", "react/display-name": "off",
"no-unused-vars": [2, { "args": "after-used", "argsIgnorePattern": "reject" }], "no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
"react/no-deprecated": 0, "react/no-deprecated": 0,
"no-prototype-builtins": 0 "no-prototype-builtins": 0
}, },
@ -53,4 +56,3 @@
"__dirname": true "__dirname": true
} }
} }

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
xml:space="preserve"
width="246.66667"
height="231.54668"
viewBox="0 0 246.66667 231.54668"
sodipodi:docname="SendGrid-Logomark.eps"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview4" /><g
id="g10"
inkscape:groupmode="layer"
inkscape:label="ink_ext_XXXXXX"
transform="matrix(1.3333333,0,0,-1.3333333,0,231.54667)"><g
id="g12"
transform="scale(0.1)"><path
d="M 1561.5,1504.82 H 712.832 V 1080.49 H 288.496 V 231.82 h 848.674 v 424.328 h 424.33 v 848.672"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path14" /><path
d="m 288.492,656.148 v 0 424.342 h 424.34 v 0 H 288.496 V 656.148"
style="fill:#aee5f5;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path16" /><path
d="M 1137.17,231.82 H 712.836 V 656.148 H 288.492 l 0.004,424.342 h 424.336 0.004 V 656.148 H 1137.17 V 231.82"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path18" /><path
d="M 1137.17,231.82 H 712.836 V 656.148 H 288.492 l 0.004,424.342 h 424.336 0.004 V 656.148 H 1137.17 V 231.82"
style="fill:#aee5f5;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path20" /><path
d="M 288.496,231.82 H 712.832 V 656.148 H 288.496 Z"
style="fill:#457dc8;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path22" /><path
d="m 1561.51,656.148 v 0 l -0.01,424.342 h 0.01 V 656.148 m -424.34,848.672 h -424.338 0.004 424.334"
style="fill:#34bde9;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path24" /><path
d="M 1561.51,656.148 H 1137.17 V 1080.49 H 712.836 v 424.33 h 424.334 v -424.33 h 424.34 l -0.01,-424.342"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path26" /><path
d="M 1561.51,656.148 H 1137.17 V 1080.49 H 712.836 v 424.33 h 424.334 v -424.33 h 424.34 l -0.01,-424.342"
style="fill:#34bde9;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path28" /><path
d="m 712.836,656.148 h 424.336 v 424.34 H 712.836 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path30" /><path
d="m 712.836,656.148 h 424.336 v 424.34 H 712.836 Z"
style="fill:#34bde9;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path32" /><path
d="m 1561.5,1080.49 h -424.33 v 424.33 h 424.33 v -424.33"
style="fill:#457dc8;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path34" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-label="Twilio" viewBox="0 0 512 512"><rect width="512" height="512" fill="#fff" rx="15%"/><g fill="#f22f46"><circle cx="256" cy="256" r="256"/><circle cx="256" cy="256" r="188" fill="#fff"/><circle id="a" cx="193" cy="193" r="53"/><use x="126" xlink:href="#a"/><use y="126" xlink:href="#a"/><use x="126" y="126" xlink:href="#a"/></g></svg>

After

Width:  |  Height:  |  Size: 427 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="480.221px" height="480.221px" viewBox="0 0 480.221 480.221" style="enable-background:new 0 0 480.221 480.221;"
xml:space="preserve">
<g>
<path d="M480.158,260.878v166.979c0,28.874-23.501,52.363-52.381,52.363H52.453c-28.889,0-52.39-23.489-52.39-52.363V52.938
c0-28.874,23.501-52.369,52.39-52.369h167.434c-9.011,9.244-15.004,21.45-16.316,35.003H52.447
c-9.582,0-17.378,7.791-17.378,17.366v374.92c0,9.569,7.796,17.36,17.378,17.36h375.325c9.581,0,17.372-7.791,17.372-17.36V277.169
C458.33,275.904,470.56,270.236,480.158,260.878z M399.287,230.096H284.831L470.099,44.829c10.249-10.261,10.249-26.882,0-37.131
c-10.256-10.261-26.883-10.261-37.132-0.012L247.7,192.958V78.497c0-14.499-11.757-26.262-26.259-26.262
c-7.25,0-13.816,2.932-18.569,7.689c-4.752,4.765-7.693,11.325-7.693,18.572v177.854c0,14.499,11.754,26.256,26.256,26.256h177.852
c14.505,0,26.256-11.751,26.256-26.256S413.792,230.096,399.287,230.096z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="483.252px" height="483.252px" viewBox="0 0 483.252 483.252" style="enable-background:new 0 0 483.252 483.252;"
xml:space="preserve">
<g>
<path d="M481.354,263.904v166.979c0,28.88-23.507,52.369-52.387,52.369H53.646c-28.889,0-52.393-23.489-52.393-52.369V55.969
c0-28.877,23.504-52.372,52.393-52.372h167.428c-9.014,9.247-15.004,21.45-16.319,35.007H53.64c-9.582,0-17.377,7.79-17.377,17.365
v374.914c0,9.575,7.796,17.366,17.377,17.366h375.322c9.581,0,17.378-7.791,17.378-17.366V280.199
C459.515,278.935,471.744,273.267,481.354,263.904z M277.895,52.52h114.456L207.086,237.79c-10.255,10.249-10.255,26.882,0,37.132
c10.252,10.255,26.879,10.255,37.131,0.006L429.482,89.657v114.462c0,14.502,11.756,26.256,26.261,26.256
c7.247,0,13.813-2.929,18.566-7.687c4.752-4.764,7.689-11.319,7.689-18.569V26.256C481.999,11.754,470.249,0,455.743,0H277.895
c-14.499,0-26.256,11.754-26.256,26.262C251.633,40.764,263.396,52.52,277.895,52.52z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -5,6 +5,7 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@babel/core": "^7.4.3", "@babel/core": "^7.4.3",
@ -33,6 +34,7 @@
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",
"history": "^4.9.0", "history": "^4.9.0",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "^5.3.2",
"immer": "^9.0.6",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.1", "moment": "^2.29.1",
@ -54,6 +56,7 @@
"react-easy-sort": "^0.2.1", "react-easy-sort": "^0.2.1",
"react-google-login": "^5.2.2", "react-google-login": "^5.2.2",
"react-hot-toast": "^2.1.1", "react-hot-toast": "^2.1.1",
"react-hotkeys-hook": "^3.4.4",
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0", "react-lazyload": "^3.2.0",
"react-loading-skeleton": "^2.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", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" "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": { "node_modules/hpack.js": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@ -11627,9 +11635,13 @@
} }
}, },
"node_modules/immer": { "node_modules/immer": {
"version": "1.10.0", "version": "9.0.6",
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz",
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
}, },
"node_modules/immutability-helper": { "node_modules/immutability-helper": {
"version": "3.1.1", "version": "3.1.1",
@ -17683,6 +17695,11 @@
"node": ">=8" "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": { "node_modules/react-dev-utils/node_modules/inquirer": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
@ -17993,6 +18010,18 @@
"csstype": "^2.6.2" "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": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "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", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" "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": { "hpack.js": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "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==" "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
}, },
"immer": { "immer": {
"version": "1.10.0", "version": "9.0.6",
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz",
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ=="
}, },
"immutability-helper": { "immutability-helper": {
"version": "3.1.1", "version": "3.1.1",
@ -38358,6 +38392,11 @@
"path-exists": "^4.0.0" "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": { "inquirer": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", "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": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View file

@ -29,6 +29,7 @@
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",
"history": "^4.9.0", "history": "^4.9.0",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "^5.3.2",
"immer": "^9.0.6",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.1", "moment": "^2.29.1",
@ -50,6 +51,7 @@
"react-easy-sort": "^0.2.1", "react-easy-sort": "^0.2.1",
"react-google-login": "^5.2.2", "react-google-login": "^5.2.2",
"react-hot-toast": "^2.1.1", "react-hot-toast": "^2.1.1",
"react-hotkeys-hook": "^3.4.4",
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0", "react-lazyload": "^3.2.0",
"react-loading-skeleton": "^2.2.0", "react-loading-skeleton": "^2.2.0",

View file

@ -53,6 +53,17 @@ class App extends React.Component {
render() { render() {
const { currentUser, fetchedMetadata, updateAvailable, onboarded, darkMode } = this.state; const { currentUser, fetchedMetadata, updateAvailable, onboarded, darkMode } = this.state;
let toastOptions = {};
if (darkMode) {
toastOptions = {
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
},
};
}
if (currentUser && fetchedMetadata === false) { if (currentUser && fetchedMetadata === false) {
tooljetService.fetchMetaData().then((data) => { tooljetService.fetchMetaData().then((data) => {
@ -175,15 +186,7 @@ class App extends React.Component {
/> />
</div> </div>
</Router> </Router>
<Toaster <Toaster toastOptions={toastOptions} />
toastOptions={{
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
},
}}
/>
</> </>
); );
} }

View file

@ -32,6 +32,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import '@/_styles/custom.scss'; import '@/_styles/custom.scss';
import { resolveProperties, resolveStyles } from './component-properties-resolution'; import { resolveProperties, resolveStyles } from './component-properties-resolution';
import { validateWidget } from '@/_helpers/utils'; import { validateWidget } from '@/_helpers/utils';
import ErrorBoundary from './ErrorBoundary';
const AllComponents = { const AllComponents = {
Button, Button,
@ -122,53 +123,55 @@ export const Box = function Box({
trigger={!inCanvas ? ['hover', 'focus'] : null} trigger={!inCanvas ? ['hover', 'focus'] : null}
overlay={(props) => renderTooltip({ props, text: `${component.description}` })} overlay={(props) => renderTooltip({ props, text: `${component.description}` })}
> >
<div style={{ ...styles, backgroundColor }} role={preview ? 'BoxPreview' : 'Box'}> <ErrorBoundary showFallback={mode === 'edit'}>
{inCanvas ? ( <div style={{ ...styles, backgroundColor }} role={preview ? 'BoxPreview' : 'Box'}>
<ComponentToRender {inCanvas ? (
onComponentClick={onComponentClick} <ComponentToRender
onComponentOptionChanged={onComponentOptionChanged} onComponentClick={onComponentClick}
currentState={currentState} onComponentOptionChanged={onComponentOptionChanged}
onEvent={onEvent} currentState={currentState}
id={id} onEvent={onEvent}
paramUpdated={paramUpdated} id={id}
width={width} paramUpdated={paramUpdated}
changeCanDrag={changeCanDrag} width={width}
onComponentOptionsChanged={onComponentOptionsChanged} changeCanDrag={changeCanDrag}
height={height} onComponentOptionsChanged={onComponentOptionsChanged}
component={component} height={height}
containerProps={containerProps} component={component}
darkMode={darkMode} containerProps={containerProps}
removeComponent={removeComponent} darkMode={darkMode}
canvasWidth={canvasWidth} removeComponent={removeComponent}
properties={resolvedProperties} canvasWidth={canvasWidth}
exposedVariables={exposedVariables} properties={resolvedProperties}
styles={resolvedStyles} exposedVariables={exposedVariables}
setExposedVariable={(variable, value) => onComponentOptionChanged(component, variable, value)} styles={resolvedStyles}
fireEvent={fireEvent} setExposedVariable={(variable, value) => onComponentOptionChanged(component, variable, value)}
validate={validate} fireEvent={fireEvent}
></ComponentToRender> validate={validate}
) : ( ></ComponentToRender>
<div className="m-1" style={{ height: '100%' }}> ) : (
<div <div className="m-1" style={{ height: '100%' }}>
className="component-image-holder p-2 d-flex flex-column justify-content-center" <div
style={{ height: '100%' }} className="component-image-holder p-2 d-flex flex-column justify-content-center"
> style={{ height: '100%' }}
<center> >
<div <center>
style={{ <div
width: '20px', style={{
height: '20px', width: '20px',
backgroundSize: 'contain', height: '20px',
backgroundImage: `url(/assets/images/icons/widgets/${component.name.toLowerCase()}.svg)`, backgroundSize: 'contain',
backgroundRepeat: 'no-repeat', backgroundImage: `url(/assets/images/icons/widgets/${component.name.toLowerCase()}.svg)`,
}} backgroundRepeat: 'no-repeat',
></div> }}
</center> ></div>
<span className="component-title">{component.displayName}</span> </center>
<span className="component-title">{component.displayName}</span>
</div>
</div> </div>
</div> )}
)} </div>
</div> </ErrorBoundary>
</OverlayTrigger> </OverlayTrigger>
); );
}; };

View file

@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useSpring, config, animated } from 'react-spring'; 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 from '@uiw/react-codemirror';
import 'codemirror/mode/handlebars/handlebars'; import 'codemirror/mode/handlebars/handlebars';
import 'codemirror/mode/javascript/javascript'; import 'codemirror/mode/javascript/javascript';
@ -15,6 +16,7 @@ import 'codemirror/theme/monokai.css';
import { getSuggestionKeys, onBeforeChange, handleChange } from './utils'; import { getSuggestionKeys, onBeforeChange, handleChange } from './utils';
import { resolveReferences } from '@/_helpers/utils'; import { resolveReferences } from '@/_helpers/utils';
import useHeight from '@/_hooks/use-height-transition'; import useHeight from '@/_hooks/use-height-transition';
import usePortal from '@/_hooks/use-portal';
export function CodeHinter({ export function CodeHinter({
initialValue, initialValue,
@ -30,10 +32,13 @@ export function CodeHinter({
height, height,
minHeight, minHeight,
lineWrapping, lineWrapping,
componentName = null,
usePortalEditor = true,
}) { }) {
const darkMode = localStorage.getItem('darkMode') === 'true';
const options = { const options = {
lineNumbers: lineNumbers, lineNumbers: lineNumbers ?? false,
lineWrapping: lineWrapping, lineWrapping: lineWrapping ?? true,
singleLine: true, singleLine: true,
mode: mode || 'handlebars', mode: mode || 'handlebars',
tabSize: 2, tabSize: 2,
@ -83,10 +88,11 @@ export function CodeHinter({
const getPreview = () => { const getPreview = () => {
const [preview, error] = resolveReferences(currentValue, realState, null, {}, true); const [preview, error] = resolveReferences(currentValue, realState, null, {}, true);
const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1';
if (error) { if (error) {
return ( return (
<animated.div style={{ ...slideInStyles, overflow: 'hidden' }}> <animated.div className={isOpen ? themeCls : null} style={{ ...slideInStyles, overflow: 'hidden' }}>
<div ref={heightRef} className="dynamic-variable-preview bg-red-lt px-1 py-1"> <div ref={heightRef} className="dynamic-variable-preview bg-red-lt px-1 py-1">
<div> <div>
<div className="heading my-1"> <div className="heading my-1">
@ -103,7 +109,7 @@ export function CodeHinter({
const content = getPreviewContent(preview, previewType); const content = getPreviewContent(preview, previewType);
return ( return (
<animated.div style={{ ...slideInStyles, overflow: 'hidden' }}> <animated.div className={isOpen ? themeCls : null} style={{ ...slideInStyles, overflow: 'hidden' }}>
<div ref={heightRef} className="dynamic-variable-preview bg-green-lt px-1 py-1"> <div ref={heightRef} className="dynamic-variable-preview bg-green-lt px-1 py-1">
<div> <div>
<div className="heading my-1"> <div className="heading my-1">
@ -116,30 +122,105 @@ export function CodeHinter({
); );
}; };
enablePreview = enablePreview ?? true; 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 ( return (
<div style={{ width: '100%' }}> <div className="code-hinter-wrapper" style={{ width: '100%' }}>
<div <div
className={`code-hinter ${className || 'codehinter-default-input'}`} className={`code-hinter ${className || 'codehinter-default-input'}`}
key={suggestions.length} key={suggestions.length}
style={{ height: height || 'auto', minHeight, maxHeight: '320px', overflow: 'auto' }} style={{ height: height || 'auto', minHeight, maxHeight: '320px', overflow: 'auto' }}
> >
<CodeMirror {usePortalEditor && <CodeHinter.PopupIcon callback={handleToggle} />}
value={initialValue} <CodeHinter.Portal
realState={realState} isOpen={isOpen}
scrollbarStyle={null} callback={setIsOpen}
height={height} componentName={componentName}
onFocus={() => setFocused(true)} key={suggestions.length}
onBlur={(editor) => { customComponent={getPreview}
const value = editor.getValue(); forceUpdate={forceUpdate}
onChange(value); optionalProps={{ height: 300 }}
setFocused(false); darkMode={darkMode}
}} selectors={{ className: 'preview-block-portal' }}
onChange={(editor) => valueChanged(editor, onChange, suggestions, ignoreBraces)} >
onBeforeChange={(editor, change) => onBeforeChange(editor, change, ignoreBraces)} <CodeMirror
options={options} value={initialValue}
/> realState={realState}
scrollbarStyle={null}
height={height || 'auto'}
onFocus={() => 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}
/>
</CodeHinter.Portal>
</div> </div>
{enablePreview && getPreview()} {enablePreview && !isOpen && getPreview()}
</div> </div>
); );
} }
const PopupIcon = ({ callback }) => {
return (
<div className="d-flex justify-content-end" style={{ position: 'relative' }}>
<OverlayTrigger
trigger={['hover', 'focus']}
placement="top"
delay={{ show: 800, hide: 100 }}
overlay={<Tooltip id="button-tooltip">{'Pop out code editor into a new window'}</Tooltip>}
>
<img
className="svg-icon m-2 popup-btn"
src="/assets/images/icons/portal-open.svg"
width="12"
height="12"
onClick={(e) => {
e.stopPropagation();
callback();
}}
/>
</OverlayTrigger>
</div>
);
};
const Portal = ({ children, ...restProps }) => {
const renderPortal = usePortal({ children, ...restProps });
return <React.Fragment>{renderPortal}</React.Fragment>;
};
CodeHinter.PopupIcon = PopupIcon;
CodeHinter.Portal = Portal;

View file

@ -4,8 +4,8 @@ import { Picker } from 'emoji-mart';
import TextareaMentions from '@/_ui/Mentions'; import TextareaMentions from '@/_ui/Mentions';
import Button from '@/_ui/Button'; import Button from '@/_ui/Button';
import useShortcuts from '@/_hooks/use-shortcuts';
import usePopover from '@/_hooks/use-popover'; import usePopover from '@/_hooks/use-popover';
import { useHotkeys } from 'react-hotkeys-hook';
function CommentFooter({ users, editComment = '', editCommentId, handleSubmit }) { function CommentFooter({ users, editComment = '', editCommentId, handleSubmit }) {
const [comment, setComment] = React.useState(editComment); const [comment, setComment] = React.useState(editComment);
@ -28,7 +28,7 @@ function CommentFooter({ users, editComment = '', editCommentId, handleSubmit })
setOpen(false); setOpen(false);
}; };
useShortcuts(['Meta', 'Enter'], () => handleClick(), [comment]); useHotkeys('⌘+enter, control+enter', () => handleClick());
return ( return (
<> <>

View file

@ -4,7 +4,7 @@ import { isEmpty } from 'lodash';
import { pluralize } from '@/_helpers/utils'; import { pluralize } from '@/_helpers/utils';
import moment from 'moment'; import moment from 'moment';
import usePopover from '@/_hooks/use-popover'; import usePopover from '@/_hooks/use-popover';
import { useSpring, animated } from 'react-spring'; import { useSpring } from 'react-spring';
import useRouter from '@/_hooks/use-router'; import useRouter from '@/_hooks/use-router';
import Spinner from '@/_ui/Spinner'; import Spinner from '@/_ui/Spinner';
@ -12,8 +12,8 @@ import Spinner from '@/_ui/Spinner';
const Content = ({ notifications, loading }) => { const Content = ({ notifications, loading }) => {
const router = useRouter(); const router = useRouter();
const [selectedCommentId, setSelectedCommentId] = React.useState(router.query.commentId); const [selectedCommentId, setSelectedCommentId] = React.useState(router.query.commentId);
const [open, trigger, content] = usePopover(false); const [open, _trigger, _content] = usePopover(false);
const popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 }); const _popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 });
React.useEffect(() => { React.useEffect(() => {
if (router.query?.commentId) setSelectedCommentId(router.query?.commentId); if (router.query?.commentId) setSelectedCommentId(router.query?.commentId);

View file

@ -1,50 +1,23 @@
import React, { useState, useEffect } from 'react'; import React from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
var tinycolor = require('tinycolor2'); var tinycolor = require('tinycolor2');
export const Button = function Button({ width, height, component, currentState, fireEvent }) { export const Button = function Button({ height, properties, styles, fireEvent }) {
const [loadingState, setLoadingState] = useState(false); const { loadingState, text } = properties;
const { backgroundColor, textColor, borderRadius, visibility, disabledState } = styles;
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);
}
const computedStyles = { const computedStyles = {
backgroundColor, backgroundColor,
color, color: textColor,
width: '100%', width: '100%',
borderRadius: `${parsedBorderRadius}px`, borderRadius: `${borderRadius}px`,
height, height,
display: parsedWidgetVisibility ? '' : 'none', display: visibility ? '' : 'none',
'--tblr-btn-color-darker': tinycolor(backgroundColor).darken(8).toString(), '--tblr-btn-color-darker': tinycolor(backgroundColor).darken(8).toString(),
}; };
return ( return (
<button <button
disabled={parsedDisabledState} disabled={disabledState}
className={`jet-button btn btn-primary p-1 ${loadingState === true ? ' btn-loading' : ''}`} className={`jet-button btn btn-primary p-1 ${loadingState === true ? ' btn-loading' : ''}`}
style={computedStyles} style={computedStyles}
onClick={(event) => { onClick={(event) => {
@ -52,7 +25,7 @@ export const Button = function Button({ width, height, component, currentState,
fireEvent('onClick'); fireEvent('onClick');
}} }}
> >
{resolveReferences(text, currentState)} {text}
</button> </button>
); );
}; };

View file

@ -1,58 +1,34 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
// Use plotly basic bundle // Use plotly basic bundle
import Plotly from 'plotly.js-basic-dist-min'; import Plotly from 'plotly.js-basic-dist-min';
import createPlotlyComponent from 'react-plotly.js/factory'; import createPlotlyComponent from 'react-plotly.js/factory';
const Plot = createPlotlyComponent(Plotly); 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 [loadingState, setLoadingState] = useState(false);
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const { visibility, disabledState } = styles;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const { title, markerColor, showGridLines, type, data } = properties;
let parsedWidgetVisibility = widgetVisibility;
const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
useEffect(() => { useEffect(() => {
const loadingStateProperty = component.definition.properties.loadingState; const loadingStateProperty = properties.loadingState;
if (loadingStateProperty && currentState) { if (loadingStateProperty != undefined) {
const newState = resolveReferences(loadingStateProperty.value, currentState, false); setLoadingState(loadingStateProperty);
setLoadingState(newState);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [properties.loadingState]);
}, [currentState]);
const computedStyles = { const computedStyles = {
width: width - 4, width: width - 4,
height, height,
display: parsedWidgetVisibility ? '' : 'none', display: visibility ? '' : 'none',
background: darkMode ? '#1f2936' : 'white', background: darkMode ? '#1f2936' : 'white',
}; };
// darkMode ? '#1f2936' : 'white' const dataString = data ?? [];
const dataProperty = component.definition.properties.data;
const dataString = dataProperty ? dataProperty.value : [];
const titleProperty = component.definition.properties.title; const chartType = type;
const title = titleProperty.value;
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 fontColor = darkMode ? '#c3c3c3' : null;
const layout = { 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) => { const computeChartData = (data, dataString) => {
let rawData = data; let rawData = data;
if (typeof rawData === 'string') { 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]); const memoizedChartData = useMemo(() => computeChartData(data, dataString), [data, dataString]);
return ( return (
<div <div data-disabled={disabledState} style={computedStyles}>
data-disabled={parsedDisabledState}
style={computedStyles}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
{loadingState === true ? ( {loadingState === true ? (
<div style={{ width }} className="p-2"> <div style={{ width }} className="p-2">
<center> <center>

View file

@ -1,56 +1,23 @@
import React from 'react'; import React from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const Checkbox = function Checkbox({ export const Checkbox = function Checkbox({ height, properties, styles, fireEvent, setExposedVariable }) {
id,
height,
component,
onComponentClick,
currentState,
onComponentOptionChanged,
onEvent,
}) {
const [checked, setChecked] = React.useState(false); const [checked, setChecked] = React.useState(false);
const label = component.definition.properties.label.value; const { label } = properties;
const textColorProperty = component.definition.styles.textColor; const { visibility, disabledState, checkboxColor, textColor } = styles;
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);
}
function toggleValue(e) { function toggleValue(e) {
const isChecked = e.target.checked; const isChecked = e.target.checked;
setChecked(isChecked); setChecked(isChecked);
onComponentOptionChanged(component, 'value', isChecked); setExposedVariable('value', isChecked);
if (isChecked) { if (isChecked) {
onEvent('onCheck', { component }); fireEvent('onCheck');
} else { } else {
onEvent('onUnCheck', { component }); fireEvent('onUnCheck');
} }
} }
return ( return (
<div <div data-disabled={disabledState} className="row py-1" style={{ height, display: visibility ? '' : 'none' }}>
data-disabled={parsedDisabledState}
className="row py-1"
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
<div className="col px-1 py-0 mt-0"> <div className="col px-1 py-0 mt-0">
<label className="mx-1 form-check form-check-inline"> <label className="mx-1 form-check form-check-inline">
<input <input

View file

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import CodeMirror from '@uiw/react-codemirror'; import CodeMirror from '@uiw/react-codemirror';
import 'codemirror/addon/comment/comment'; import 'codemirror/addon/comment/comment';
import 'codemirror/addon/hint/show-hint'; import 'codemirror/addon/hint/show-hint';
@ -11,41 +10,23 @@ import 'codemirror/theme/duotone-light.css';
import 'codemirror/theme/monokai.css'; import 'codemirror/theme/monokai.css';
import { onBeforeChange, handleChange } from '../CodeBuilder/utils'; import { onBeforeChange, handleChange } from '../CodeBuilder/utils';
export const CodeEditor = ({ width, height, component, currentState, onComponentOptionChanged, darkMode }) => { export const CodeEditor = ({ height, darkMode, properties, styles, exposedVariables, setExposedVariable }) => {
const enableLineNumber = component.definition.properties?.enableLineNumber?.value ?? true; const { enableLineNumber, mode, placeholder } = properties;
const languageMode = component.definition.properties.mode.value; const { visibility, disabledState } = styles;
const placeholder = component.definition.properties.placeholder.value;
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 parsedWidgetVisibility =
typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility, currentState) : widgetVisibility;
const parsedEnableLineNumber =
typeof enableLineNumber !== 'boolean' ? resolveWidgetFieldValue(enableLineNumber, currentState) : enableLineNumber;
const value = currentState?.components[component?.name]?.value;
const [editorValue, setEditorValue] = useState(value);
const [realState, setRealState] = useState(currentState);
function codeChanged(code) { function codeChanged(code) {
setEditorValue(code); setExposedVariable('value', code);
onComponentOptionChanged(component, 'value', code);
} }
const styles = { const editorStyles = {
height: height, height: height,
display: !parsedWidgetVisibility ? 'none' : 'block', display: !visibility ? 'none' : 'block',
}; };
const options = { const options = {
lineNumbers: parsedEnableLineNumber, lineNumbers: enableLineNumber,
lineWrapping: true, lineWrapping: true,
singleLine: true, singleLine: true,
mode: languageMode, mode: mode,
tabSize: 2, tabSize: 2,
theme: darkMode ? 'monokai' : 'duotone-light', theme: darkMode ? 'monokai' : 'duotone-light',
readOnly: false, readOnly: false,
@ -55,23 +36,21 @@ export const CodeEditor = ({ width, height, component, currentState, onComponent
function valueChanged(editor, onChange, ignoreBraces = false) { function valueChanged(editor, onChange, ignoreBraces = false) {
handleChange(editor, onChange, [], ignoreBraces); handleChange(editor, onChange, [], ignoreBraces);
setEditorValue(editor.getValue());
} }
useEffect(() => {
setRealState(currentState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState.components]);
return ( return (
<div data-disabled={parsedDisabledState} style={styles}> <div data-disabled={disabledState} style={editorStyles}>
<div <div
className={`code-hinter codehinter-default-input code-editor-widget`} className={`code-hinter codehinter-default-input code-editor-widget`}
style={{ height: height || 'auto', minHeight: height - 1, maxHeight: '320px', overflow: 'auto' }} style={{
height: height || 'auto',
minHeight: height - 1,
maxHeight: '320px',
overflow: 'auto',
}}
> >
<CodeMirror <CodeMirror
value={editorValue} value={exposedVariables.value}
realState={realState}
scrollbarStyle={null} scrollbarStyle={null}
height={height - 1} height={height - 1}
onBlur={(editor) => { onBlur={(editor) => {

View file

@ -1,43 +1,42 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { SubCustomDragLayer } from '../SubCustomDragLayer'; import { SubCustomDragLayer } from '../SubCustomDragLayer';
import { SubContainer } from '../SubContainer'; import { SubContainer } from '../SubContainer';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const Container = function Container({ id, component, width, height, containerProps, currentState, removeComponent }) { export const Container = function Container({ id, component, width, height, containerProps, removeComponent, styles }) {
const backgroundColor = component.definition.styles.backgroundColor.value; const { backgroundColor, visibility, disabledState } = styles;
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 computedStyles = { const computedStyles = {
backgroundColor, backgroundColor,
height, height,
display: parsedWidgetVisibility ? 'flex' : 'none', display: visibility ? 'flex' : 'none',
}; };
const parentRef = useRef(null); const parentRef = useRef(null);
return ( return (
<div <div
data-disabled={parsedDisabledState} data-disabled={disabledState}
className="jet-container" className="jet-container"
id={id} id={id}
ref={parentRef} ref={parentRef}
onClick={(e) => { if (e.target.className === 'real-canvas') containerProps.onComponentClick(id, component) }} //Hack, should find a better solution - to prevent losing z index when comtainer element is clicked
style={computedStyles} style={computedStyles}
onClick={(e) => {
if (e.target.className === 'real-canvas') containerProps.onComponentClick(id, component);
}} //Hack, should find a better solution - to prevent losing z index when comtainer element is clicked
> >
<SubContainer containerCanvasWidth={width} parent={id} {...containerProps} parentRef={parentRef} removeComponent={removeComponent} /> <SubContainer
<SubCustomDragLayer containerCanvasWidth={width} parent={id} parentRef={parentRef} currentLayout={containerProps.currentLayout} /> containerCanvasWidth={width}
parent={id}
{...containerProps}
parentRef={parentRef}
removeComponent={removeComponent}
/>
<SubCustomDragLayer
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
currentLayout={containerProps.currentLayout}
/>
</div> </div>
); );
}; };

View file

@ -56,7 +56,9 @@ export const Datepicker = function Datepicker({
dateFormat={isDateFormat} dateFormat={isDateFormat}
placeholderText={defaultValue} placeholderText={defaultValue}
inputProps={{ placeholder: defaultValue }} inputProps={{ placeholder: defaultValue }}
onOpen={(event) => { onComponentClick(id, component, event) }} onOpen={(event) => {
onComponentClick(id, component, event);
}}
renderInput={(props) => { renderInput={(props) => {
return ( return (
<input <input

View file

@ -3,48 +3,34 @@ import 'react-datetime/css/react-datetime.css';
import { DateRangePicker } from 'react-dates'; import { DateRangePicker } from 'react-dates';
import 'react-dates/lib/css/_datepicker.css'; import 'react-dates/lib/css/_datepicker.css';
import 'react-dates/initialize'; import 'react-dates/initialize';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const DaterangePicker = function DaterangePicker({ export const DaterangePicker = function DaterangePicker({
id,
height, height,
component, properties,
onComponentClick, styles,
currentState, exposedVariables,
onComponentOptionChanged, setExposedVariable,
}) { }) {
console.log('currentState', currentState); const { visibility, disabledState } = styles;
const startDateProp = component.definition.properties.startDate; const startDateProp = exposedVariables.startDate;
const endDateProp = component.definition.properties.endDate; const endDateProp = exposedVariables.endDate;
const formatProp = component.definition.properties.format; const formatProp = properties.format;
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 [focusedInput, setFocusedInput] = useState(null); const [focusedInput, setFocusedInput] = useState(null);
const [startDate, setStartDate] = useState(startDateProp ? startDateProp.value : null); const [startDate, setStartDate] = useState(startDateProp ?? null);
const [endDate, setEndDate] = useState(endDateProp ? endDateProp.value : null); const [endDate, setEndDate] = useState(endDateProp ?? null);
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
function onDateChange(dates) { function onDateChange(dates) {
const start = dates.startDate; const start = dates.startDate;
const end = dates.endDate; const end = dates.endDate;
if (start) { if (start) {
onComponentOptionChanged(component, 'startDate', start.format(formatProp.value)); setExposedVariable('startDate', start.format(formatProp.value));
} }
if (end) { if (end) {
onComponentOptionChanged(component, 'endDate', end.format(formatProp.value)); setExposedVariable('endDate', end.format(formatProp.value));
} }
setStartDate(start); setStartDate(start);
@ -56,16 +42,9 @@ export const DaterangePicker = function DaterangePicker({
} }
return ( return (
<div <div className="daterange-picker-widget p-0" style={{ height, display: visibility ? '' : 'none' }}>
className="daterange-picker-widget p-0"
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
<DateRangePicker <DateRangePicker
disabled={parsedDisabledState} disabled={disabledState}
startDate={startDate} startDate={startDate}
startDateId="startDate" startDateId="startDate"
isOutsideRange={() => false} isOutsideRange={() => false}

View file

@ -1,23 +1,8 @@
import React from 'react'; import React from 'react';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
export const Divider = function Divider({ id, component, onComponentClick, currentState }) { export const Divider = function Divider({ styles }) {
const dividerColorProperty = component.definition.styles.dividerColor; const { visibility, dividerColor } = styles;
const color = dividerColorProperty ? dividerColorProperty.value : '#E7E8EA'; const color = dividerColor ?? '#E7E8EA';
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
let parsedWidgetVisibility = widgetVisibility; return <div className="hr mt-1" style={{ display: visibility ? '' : 'none', color: color, opacity: '1' }}></div>;
parsedWidgetVisibility = resolveWidgetFieldValue(parsedWidgetVisibility, currentState);
return (
<div
className="hr mt-1"
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
style={{ display: parsedWidgetVisibility ? '' : 'none', color: color, opacity: '1' }}
></div>
);
}; };

View file

@ -53,7 +53,7 @@ const HEADINGS = [
{ label: 'H4', style: 'header-four' }, { label: 'H4', style: 'header-four' },
{ label: 'H5', style: 'header-five' }, { label: 'H5', style: 'header-five' },
{ label: 'H6', style: 'header-six' }, { label: 'H6', style: 'header-six' },
] ];
const BLOCK_TYPES = [ const BLOCK_TYPES = [
{ {
@ -86,19 +86,17 @@ const BlockStyleControls = (props) => {
Heading Heading
</button> </button>
<div className="dropdown-content bg-white"> <div className="dropdown-content bg-white">
{ {HEADINGS.map((type) => (
HEADINGS.map((type) => ( <a className="dropitem m-0 p-0" href="#" key={type.label}>
<a className="dropitem m-0 p-0" href="#" key={type.label}> <StyleButton
<StyleButton key={type.label}
key={type.label} active={type.style === blockType}
active={type.style === blockType} label={type.label}
label={type.label} onToggle={props.onToggle}
onToggle={props.onToggle} style={type.style}
style={type.style} />
/> </a>
</a> ))}
))
}
</div> </div>
</div> </div>
{BLOCK_TYPES.map((type) => ( {BLOCK_TYPES.map((type) => (
@ -150,7 +148,9 @@ const InlineStyleControls = (props) => {
class DraftEditor extends React.Component { class DraftEditor extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { editorState: EditorState.createWithContent(ContentState.createFromText(this.props.defaultValue)) }; this.state = {
editorState: EditorState.createWithContent(ContentState.createFromText(this.props.defaultValue)),
};
this.focus = () => this.refs.editor.focus(); this.focus = () => this.refs.editor.focus();
this.onChange = (editorState) => { this.onChange = (editorState) => {
@ -224,7 +224,7 @@ class DraftEditor extends React.Component {
<BlockStyleControls editorState={editorState} onToggle={this.toggleBlockType} /> <BlockStyleControls editorState={editorState} onToggle={this.toggleBlockType} />
<InlineStyleControls editorState={editorState} onToggle={this.toggleInlineStyle} /> <InlineStyleControls editorState={editorState} onToggle={this.toggleInlineStyle} />
</div> </div>
<div className={className} style={{height: `${this.props.height-60}px`}} onClick={this.focus}> <div className={className} style={{ height: `${this.props.height - 60}px` }} onClick={this.focus}>
<Editor <Editor
blockStyleFn={getBlockStyle} blockStyleFn={getBlockStyle}
customStyleMap={styleMap} customStyleMap={styleMap}

View file

@ -1,134 +1,70 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue, validateWidget } from '@/_helpers/utils';
import SelectSearch, { fuzzySearch } from 'react-select-search'; import SelectSearch, { fuzzySearch } from 'react-select-search';
export const DropDown = function DropDown({ export const DropDown = function DropDown({ height, validate, properties, styles, setExposedVariable, fireEvent }) {
id, const { label, value, display_values, values } = properties;
height, const { visibility, disabledState } = styles;
component, const [currentValue, setCurrentValue] = useState(() => value);
onComponentClick,
currentState,
onComponentOptionChanged,
onEvent,
}) {
console.log('currentState', currentState);
const label = component.definition.properties.label.value;
const values = component.definition.properties.values.value;
const displayValues = component.definition.properties.display_values.value;
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 parsedValues = values;
try {
parsedValues = resolveReferences(values, currentState, []);
} catch (err) {
console.log(err);
}
let parsedDisplayValues = displayValues;
try {
parsedDisplayValues = resolveReferences(displayValues, currentState, []);
} catch (err) {
console.log(err);
}
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
let selectOptions = []; let selectOptions = [];
try { try {
selectOptions = [ selectOptions = [
...parsedValues.map((value, index) => { ...values.map((value, index) => {
return { name: parsedDisplayValues[index], value: value }; return { name: display_values[index], value: value };
}), }),
]; ];
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
const currentValueProperty = component.definition.properties.value; const validationData = validate(value);
const value = currentValueProperty ? currentValueProperty.value : '';
const [currentValue, setCurrentValue] = useState(() =>
resolveReferences(currentValueProperty.value, currentState, '')
);
let newValue = value;
if (currentValueProperty && currentState) {
newValue = resolveReferences(currentValueProperty.value, currentState, '');
}
const validationData = validateWidget({
validationObject: component.definition.validation,
widgetValue: currentValue,
currentState,
});
const { isValid, validationError } = validationData; const { isValid, validationError } = validationData;
const currentValidState = currentState?.components[component?.name]?.isValid; useEffect(() => {
setExposedVariable('isValid', isValid);
if (currentValidState !== isValid) { // eslint-disable-next-line react-hooks/exhaustive-deps
onComponentOptionChanged(component, 'isValid', isValid); }, [isValid]);
}
useEffect(() => { useEffect(() => {
setCurrentValue(value);
setExposedVariable('value', value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
useEffect(() => {
let newValue = undefined;
if (values?.includes(value)) newValue = value;
setCurrentValue(newValue); setCurrentValue(newValue);
}, [newValue]); setExposedVariable('value', newValue);
useEffect(() => {
onComponentOptionChanged(component, 'value', currentValue).then(() => onEvent('onSelect', { component }));
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentValue]); }, [JSON.stringify(values)]);
useEffect(() => {
if (selectOptions.some((e) => e.value === newValue)) {
setCurrentValue(newValue);
} else {
setCurrentValue(undefined);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [values]);
return ( return (
<div <>
className="dropdown-widget row g-0" <div className="dropdown-widget row g-0" style={{ height, display: visibility ? '' : 'none' }}>
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }} <div className="col-auto my-auto">
onClick={(event) => { <label style={{ marginRight: label !== '' ? '1rem' : '0.001rem' }} className="form-label py-1">
event.stopPropagation(); {label}
onComponentClick(id, component, event); </label>
}} </div>
> <div className="col px-0 h-100">
<div className="col-auto my-auto"> <SelectSearch
<label style={{ marginRight: label !== '' ? '1rem' : '0.001rem' }} className="form-label py-1"> disabled={disabledState}
{label} options={selectOptions}
</label> value={currentValue}
</div> search={true}
<div className="col px-0 h-100"> onChange={(newVal) => {
<SelectSearch setCurrentValue(newVal);
disabled={parsedDisabledState} setExposedVariable('value', newVal).then(() => fireEvent('onSelect'));
options={selectOptions} }}
value={currentValue} filterOptions={fuzzySearch}
search={true} placeholder="Select.."
onChange={(newVal) => { />
setCurrentValue(newVal); </div>
}}
filterOptions={fuzzySearch}
placeholder="Select.."
/>
</div> </div>
<div className={`invalid-feedback ${isValid ? '' : 'd-flex'}`}>{validationError}</div> <div className={`invalid-feedback ${isValid ? '' : 'd-flex'}`}>{validationError}</div>
</div> </>
); );
}; };

View file

@ -1,7 +1,7 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { resolveWidgetFieldValue } from '@/_helpers/utils'; import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { toast } from 'react-toastify'; import toast from 'react-hot-toast';
export const FilePicker = ({ width, height, component, currentState, onComponentOptionChanged, onEvent, darkMode }) => { export const FilePicker = ({ width, height, component, currentState, onComponentOptionChanged, onEvent, darkMode }) => {
//* properties definitions //* properties definitions
@ -110,7 +110,7 @@ export const FilePicker = ({ width, height, component, currentState, onComponent
reader.onerror = (error) => { reader.onerror = (error) => {
reject(error); reject(error);
if (error.name == 'NotReadableError') { if (error.name == 'NotReadableError') {
toast.error(error.message, { hideProgressBar: true, autoClose: 3000 }); toast.error(error.message);
} }
}; };
}).then((result) => { }).then((result) => {
@ -166,9 +166,7 @@ export const FilePicker = ({ width, height, component, currentState, onComponent
} }
if (fileRejections.length > 0) { if (fileRejections.length > 0) {
fileRejections.map((rejectedFile) => fileRejections.map((rejectedFile) => toast.error(rejectedFile.errors[0].message));
toast.error(rejectedFile.errors[0].message, { hideProgressBar: true, autoClose: 3000 })
);
} }
return () => { return () => {

View file

@ -1,41 +1,18 @@
import React from 'react'; import React from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import LazyLoad from 'react-lazyload'; import LazyLoad from 'react-lazyload';
export const Image = function Image({ id, height, component, onComponentClick, currentState }) { export const Image = function Image({ height, properties, styles }) {
const source = component.definition.properties.source.value; const source = properties.source;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = styles.visibility ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let data = resolveReferences(source, currentState, null);
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
if (data === '') data = null;
function Placeholder() { function Placeholder() {
return <div className="skeleton-image" style={{ objectFit: 'contain', height }}></div>; return <div className="skeleton-image" style={{ objectFit: 'contain', height }}></div>;
} }
return ( return (
<div <div data-disabled={styles.disabledState} style={{ display: widgetVisibility ? '' : 'none' }}>
data-disabled={parsedDisabledState}
style={{ display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
<LazyLoad height={height} placeholder={<Placeholder />} debounce={500}> <LazyLoad height={height} placeholder={<Placeholder />} debounce={500}>
<img style={{ objectFit: 'contain' }} src={data} height={height} /> <img src={source} height={height} />
</LazyLoad> </LazyLoad>
</div> </div>
); );

View file

@ -14,7 +14,7 @@ export const Map = function Map({
onComponentOptionChanged, onComponentOptionChanged,
onComponentOptionsChanged, onComponentOptionsChanged,
onEvent, onEvent,
canvasWidth canvasWidth,
}) { }) {
const center = component.definition.properties.initialLocation.value; const center = component.definition.properties.initialLocation.value;
const defaultMarkerValue = component.definition.properties.defaultMarkers.value; const defaultMarkerValue = component.definition.properties.defaultMarkers.value;

View file

@ -4,53 +4,55 @@ import Button from 'react-bootstrap/Button';
import { SubCustomDragLayer } from '../SubCustomDragLayer'; import { SubCustomDragLayer } from '../SubCustomDragLayer';
import { SubContainer } from '../SubContainer'; import { SubContainer } from '../SubContainer';
import { ConfigHandle } from '../ConfigHandle'; import { ConfigHandle } from '../ConfigHandle';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
export const Modal = function Modal({ id, component, height, containerProps, currentState, darkMode }) { export const Modal = function Modal({
const [show, showModal] = useState(false); id,
component,
height,
containerProps,
darkMode,
properties,
styles,
exposedVariables,
setExposedVariable,
}) {
const [showModal, setShowModal] = useState(false);
const parentRef = useRef(null); const parentRef = useRef(null);
const titleProp = component.definition.properties.title; const title = properties.title ?? '';
const title = titleProp ? titleProp.value : ''; const size = properties.size ?? 'lg';
const sizeProp = component.definition.properties.size; const { disabledState } = styles;
const size = sizeProp ? sizeProp.value : 'lg';
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
useEffect(() => { useEffect(() => {
const componentState = containerProps.currentState.components[component.name]; const canShowModal = exposedVariables.show ?? false;
const canShowModel = componentState ? componentState.show : false; setShowModal(canShowModal);
showModal(canShowModel);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerProps.currentState.components[component.name]]); }, [exposedVariables.show]);
function hideModal() { function hideModal() {
containerProps.onComponentOptionChanged(component, 'show', false); setExposedVariable('show', false);
showModal(false); setShowModal(false);
} }
return ( return (
<div data-disabled={parsedDisabledState}> <div data-disabled={disabledState}>
<BootstrapModal <BootstrapModal
contentClassName="modal-component" contentClassName="modal-component"
show={show} show={showModal}
container={document.getElementsByClassName('canvas-area')[0]} container={document.getElementsByClassName('canvas-area')[0]}
size={size} size={size}
backdrop={true} backdrop={true}
keyboard={true} keyboard={true}
enforceFocus={false} enforceFocus={false}
animation={false} animation={false}
onEscapeKeyDown={() => showModal(false)} onEscapeKeyDown={() => setShowModal(false)}
> >
{containerProps.mode === 'edit' && ( {containerProps.mode === 'edit' && (
<ConfigHandle id={id} component={component} configHandleClicked={containerProps.onComponentClick} /> <ConfigHandle id={id} component={component} configHandleClicked={containerProps.onComponentClick} />
)} )}
<BootstrapModal.Header> <BootstrapModal.Header>
<BootstrapModal.Title>{resolveWidgetFieldValue(title, currentState)}</BootstrapModal.Title> <BootstrapModal.Title>{title}</BootstrapModal.Title>
<div> <div>
<Button variant={darkMode ? 'secondary' : 'light'} size="sm" onClick={hideModal}> <Button variant={darkMode ? 'secondary' : 'light'} size="sm" onClick={hideModal}>
x x

View file

@ -1,66 +1,65 @@
import _ from 'lodash';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import SelectSearch, { fuzzySearch } from 'react-select-search'; import SelectSearch, { fuzzySearch } from 'react-select-search';
export const Multiselect = function Multiselect({ export const Multiselect = function Multiselect({
id,
width,
height, height,
component,
onComponentClick, properties,
currentState, styles,
onComponentOptionChanged, exposedVariables,
setExposedVariable,
fireEvent,
}) { }) {
console.log('currentState', currentState); const { label, value, values, display_values } = properties;
const { visibility, disabledState } = styles;
const label = component.definition.properties.label.value; useEffect(() => {
const values = component.definition.properties.option_values.value; let newValues = [];
const displayValues = component.definition.properties.display_values.value;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = if (_.intersection(values, value)?.length === value?.length) newValues = value;
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
const parsedValues = JSON.parse(values); setExposedVariable('values', newValues);
const parsedDisplayValues = JSON.parse(displayValues); setCurrentValue(newValues);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(values)]);
const selectOptions = [ useEffect(() => {
...parsedValues.map((value, index) => { setExposedVariable('values', value);
return { name: parsedDisplayValues[index], value: value }; setCurrentValue(value);
}), // eslint-disable-next-line react-hooks/exhaustive-deps
]; }, [JSON.stringify(value)]);
const currentValueProperty = component.definition.properties.values;
const value = currentValueProperty ? currentValueProperty.value : '';
const [currentValue, setCurrentValue] = useState(value);
let newValue = value;
if (currentValueProperty && currentState) {
newValue = resolveReferences(currentValueProperty.value, currentState, '');
}
let parsedWidgetVisibility = widgetVisibility;
const [currentValue, setCurrentValue] = useState(() => value);
let selectOptions = [];
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); selectOptions = [
...values.map((value, index) => {
return { name: display_values[index], value: value };
}),
];
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
useEffect(() => { useEffect(() => {
setCurrentValue(newValue); if (value && !currentValue) {
}, [newValue]); setCurrentValue(properties.value);
}
if (JSON.stringify(exposedVariables.values) === '{}') {
setCurrentValue(properties.value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const handleChange = (value) => {
setCurrentValue(value);
setExposedVariable('values', value).then(() => fireEvent('onSelect'));
};
return ( return (
<div <div className="multiselect-widget row g-0" style={{ height, display: visibility ? '' : 'none' }}>
className="multiselect-widget row g-0"
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
<div className="col-auto my-auto"> <div className="col-auto my-auto">
<label style={{ marginRight: '1rem' }} className="form-label py-1"> <label style={{ marginRight: '1rem' }} className="form-label py-1">
{label} {label}
@ -68,14 +67,14 @@ export const Multiselect = function Multiselect({
</div> </div>
<div className="col px-0 h-100"> <div className="col px-0 h-100">
<SelectSearch <SelectSearch
disabled={parsedDisabledState} disabled={disabledState}
options={selectOptions} options={selectOptions}
value={currentValue} value={currentValue}
search={true} search={true}
multiple={true} multiple={true}
printOptions="on-focus" printOptions="on-focus"
onChange={(newValues) => { onChange={(newValues) => {
onComponentOptionChanged(component, 'values', newValues); handleChange(newValues);
}} }}
filterOptions={fuzzySearch} filterOptions={fuzzySearch}
placeholder="Select.." placeholder="Select.."

View file

@ -1,60 +1,22 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const NumberInput = function NumberInput({
id,
height,
component,
onComponentClick,
currentState,
onComponentOptionChanged,
}) {
const value = component.definition.properties.value ? component.definition.properties.value.value : '';
const [number, setNumber] = useState(value);
const numberInputProperty = component.definition.properties.value;
let newNumber = value;
if (numberInputProperty && currentState) {
newNumber = resolveReferences(numberInputProperty.value, currentState, '');
}
export const NumberInput = function NumberInput({ height, properties, exposedVariables, styles, setExposedVariable }) {
useEffect(() => { useEffect(() => {
setNumber(parseInt(newNumber)); setExposedVariable('value', properties.value);
onComponentOptionChanged(component, 'value', parseInt(newNumber));
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [newNumber]); }, [properties.value]);
const placeholder = component.definition.properties.placeholder.value;
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);
}
return ( return (
<input <input
disabled={parsedDisabledState} disabled={styles.disabledState}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
onChange={(e) => { onChange={(e) => {
setNumber(parseInt(e.target.value)); setExposedVariable('value', parseInt(e.target.value));
onComponentOptionChanged(component, 'value', parseInt(e.target.value));
}} }}
type="number" type="number"
className="form-control rounded-0" className="form-control rounded-0"
placeholder={placeholder} placeholder={properties.placeholder}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }} style={{ height, display: styles.visibility ? '' : 'none' }}
value={number} value={exposedVariables.value}
/> />
); );
}; };

View file

@ -1,56 +1,40 @@
import React, { useState } from 'react'; import React from 'react';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
export const PasswordInput = ({ export const PasswordInput = ({
id,
width, width,
height, height,
component,
onComponentClick,
currentState,
onComponentOptionChanged,
validate, validate,
properties,
styles,
exposedVariables,
setExposedVariable,
}) => { }) => {
const value = currentState?.components[component?.name]?.value; const value = exposedVariables.value;
const [text, setText] = useState(() => value ?? ''); const { visibility, disabledState } = styles;
const placeholder = properties.placeholder;
const placeholder = component.definition.properties.placeholder.value; const currentValidState = exposedVariables.isValid;
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 parsedWidgetVisibility =
typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility, currentState) : widgetVisibility;
const currentValidState = currentState?.components[component?.name]?.isValid;
const validationData = validate(value); const validationData = validate(value);
const { isValid, validationError } = validationData; const { isValid, validationError } = validationData;
if (currentValidState !== isValid) { if (currentValidState !== isValid) {
onComponentOptionChanged(component, 'isValid', isValid); setExposedVariable('isValid', isValid);
} }
return ( return (
<div> <div>
<input <input
disabled={parsedDisabledState} disabled={disabledState}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
onChange={(e) => { onChange={(e) => {
setText(e.target.value); setExposedVariable('value', e.target.value);
onComponentOptionChanged(component, 'value', e.target.value);
}} }}
type={'password'} type={'password'}
className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon rounded-0`} className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon rounded-0`}
placeholder={placeholder} placeholder={placeholder}
value={text} value={exposedVariables.value}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }} style={{ height, display: visibility ? '' : 'none' }}
/> />
<div className="invalid-feedback">{validationError}</div> <div className="invalid-feedback">{validationError}</div>

View file

@ -1,9 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import QrReader from 'react-qr-reader'; import QrReader from 'react-qr-reader';
import ErrorModal from './ErrorModal'; import ErrorModal from './ErrorModal';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const QrScanner = function QrScanner({ component, onEvent, onComponentOptionChanged, currentState }) { export const QrScanner = function QrScanner({ styles, fireEvent, setExposedVariable }) {
const handleError = async (errorMessage) => { const handleError = async (errorMessage) => {
console.log(errorMessage); console.log(errorMessage);
await setErrorOccured(true); await setErrorOccured(true);
@ -11,29 +10,17 @@ export const QrScanner = function QrScanner({ component, onEvent, onComponentOpt
const handleScan = async (data) => { const handleScan = async (data) => {
if (data !== null || data !== undefined) { if (data !== null || data !== undefined) {
await onEvent('onDetect', { component, data: data }); await fireEvent('onDetect');
await onComponentOptionChanged(component, 'lastDetectedValue', data); await setExposedVariable('lastDetectedValue', data);
} }
}; };
let [errorOccured, setErrorOccured] = useState(false); const [errorOccured, setErrorOccured] = useState(false);
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const { visibility, disabledState } = styles;
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);
}
return ( return (
<div data-disabled={parsedDisabledState} style={{ display: parsedWidgetVisibility ? '' : 'none' }}> <div data-disabled={disabledState} style={{ display: visibility ? '' : 'none' }}>
{errorOccured ? <ErrorModal /> : <QrReader onError={handleError} onScan={handleScan} />} {errorOccured ? <ErrorModal /> : <QrReader onError={handleError} onScan={handleScan} />}
</div> </div>
); );

View file

@ -1,94 +1,41 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const RadioButton = function RadioButton({ export const RadioButton = function RadioButton({
id, id,
height, height,
component, properties,
onComponentClick, styles,
currentState, fireEvent,
onComponentOptionChanged, exposedVariables,
onEvent, setExposedVariable,
}) { }) {
const label = component.definition.properties.label.value; const { label, value, values, display_values } = properties;
const textColorProperty = component.definition.styles.textColor; const { visibility, disabledState, textColor } = styles;
const textColor = textColorProperty ? textColorProperty.value : '#000';
const defaultValue = component.definition.properties.value.value;
const values = component.definition.properties.values.value;
const displayValues = component.definition.properties.display_values.value;
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 parsedValues = values;
try {
parsedValues = resolveReferences(values, currentState, []);
} catch (err) {
console.log(err);
}
let parsedDisplayValues = displayValues;
try {
parsedDisplayValues = resolveReferences(displayValues, currentState, []);
} catch (err) {
console.log(err);
}
let parsedDefaultValue = defaultValue;
try {
parsedDefaultValue = resolveReferences(defaultValue, currentState, []);
} catch (err) {
console.log(err);
}
const value = currentState?.components[component?.name]?.value ?? parsedDefaultValue;
let selectOptions = []; let selectOptions = [];
try { try {
selectOptions = [ selectOptions = [
...parsedValues.map((value, index) => { ...values.map((value, index) => {
return { name: parsedDisplayValues[index], value: value }; return { name: display_values[index], value: value };
}), }),
]; ];
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
function onSelect(selection) { function onSelect(selection) {
onComponentOptionChanged(component, 'value', selection); setExposedVariable('value', selection);
onEvent('onSelectionChange', { component }); fireEvent('onSelectionChange');
} }
useEffect(() => { useEffect(() => {
onComponentOptionChanged(component, 'value', parsedDefaultValue); setExposedVariable('value', value);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [parsedDefaultValue]); }, [value]);
return ( return (
<div <div data-disabled={disabledState} className="row py-1" style={{ height, display: visibility ? '' : 'none' }}>
data-disabled={parsedDisabledState}
className="row py-1"
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
<span className="form-check-label col-auto py-0" style={{ color: textColor }}> <span className="form-check-label col-auto py-0" style={{ color: textColor }}>
{label} {label}
</span> </span>
@ -98,7 +45,7 @@ export const RadioButton = function RadioButton({
<input <input
style={{ marginTop: '1px' }} style={{ marginTop: '1px' }}
className="form-check-input" className="form-check-input"
checked={value === option.value} checked={exposedVariables.value === option.value}
type="radio" type="radio"
value={option.value} value={option.value}
name={`${id}-radio-options`} name={`${id}-radio-options`}

View file

@ -1,46 +1,24 @@
import React from 'react'; import React, { useEffect } from 'react';
import 'draft-js/dist/Draft.css'; import 'draft-js/dist/Draft.css';
import { DraftEditor } from './DraftEditor'; import { DraftEditor } from './DraftEditor';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const RichTextEditor = function RichTextEditor({ export const RichTextEditor = function RichTextEditor({ width, height, properties, styles, setExposedVariable }) {
id, const { visibility, disabledState } = styles;
width, const placeholder = properties.placeholder;
height, const defaultValue = properties?.defaultValue ?? '';
component,
onComponentClick,
currentState,
onComponentOptionChanged,
}) {
const placeholder = component.definition.properties.placeholder.value;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const defaultValue = component.definition.properties?.defaultValue?.value ?? '';
const parsedDisabledState = // exposing the default value at first
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; useEffect(() => {
setExposedVariable('value', defaultValue);
let parsedWidgetVisibility = widgetVisibility; // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
function handleChange(html) { function handleChange(html) {
onComponentOptionChanged(component, 'value', html); setExposedVariable('value', html);
} }
return ( return (
<div <div data-disabled={disabledState} style={{ height: `${height}px`, display: visibility ? '' : 'none' }}>
data-disabled={parsedDisabledState}
style={{ height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
<DraftEditor <DraftEditor
handleChange={handleChange} handleChange={handleChange}
height={height} height={height}

View file

@ -2,42 +2,18 @@ import '@/_styles/widgets/star-rating.scss';
import React from 'react'; import React from 'react';
import { useTrail } from 'react-spring'; import { useTrail } from 'react-spring';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import Star from './star'; import Star from './star';
export const StarRating = function StarRating({ export const StarRating = function StarRating({ properties, styles, fireEvent, setExposedVariable }) {
id, const label = properties.label;
component, const defaultSelected = properties.defaultSelected ?? 5;
onComponentClick, const maxRating = properties.maxRating ?? 5;
onComponentOptionChanged, const allowHalfStar = properties.allowHalfStar ?? false;
currentState, const tooltips = properties.tooltips;
onEvent,
}) {
const label = component.definition.properties.label.value;
const defaultSelected = +component.definition.properties.defaultSelected.value ?? 5;
const maxRating = +component.definition.properties.maxRating.value ?? 5;
const allowHalfStar = component.definition.properties.allowHalfStar.value ?? false;
const textColorProperty = component.definition.styles.textColor;
const color = textColorProperty ? textColorProperty.value : '#ffb400';
const labelColorProperty = component.definition.styles.labelColor;
const labelColor = labelColorProperty ? labelColorProperty.value : '#333';
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = const { visibility, disabledState, textColorProperty, labelColor } = styles;
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const color = textColorProperty ?? '#ffb400';
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
const tooltips = component.definition.properties.tooltips.value ?? [];
const _tooltips = resolveReferences(tooltips, currentState, []) ?? [];
const animatedStars = useTrail(maxRating, { const animatedStars = useTrail(maxRating, {
config: { config: {
@ -58,21 +34,21 @@ export const StarRating = function StarRating({
React.useEffect(() => { React.useEffect(() => {
setRatingIndex(defaultSelected - 1); setRatingIndex(defaultSelected - 1);
onComponentOptionChanged(component, 'value', defaultSelected); setExposedVariable('value', defaultSelected);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultSelected]); }, [defaultSelected]);
React.useEffect(() => { React.useEffect(() => {
setTimeout(() => { setTimeout(() => {
onComponentOptionChanged(component, 'value', defaultSelected); setExposedVariable('value', defaultSelected);
}, 1000); }, 1000);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
function handleClick(idx) { function handleClick(idx) {
// +1 cos code is considering index from 0,1,2..... // +1 cos code is considering index from 0,1,2.....
onComponentOptionChanged(component, 'value', idx + 1); setExposedVariable('value', idx + 1);
onEvent('onChange', { component }); fireEvent('onChange');
} }
const getActive = (index) => { const getActive = (index) => {
@ -86,20 +62,12 @@ export const StarRating = function StarRating({
}; };
const getTooltip = (index) => { const getTooltip = (index) => {
if (_tooltips && Array.isArray(_tooltips) && _tooltips.length > 0) return _tooltips[index]; if (tooltips && Array.isArray(tooltips) && tooltips.length > 0) return tooltips[index];
return ''; return '';
}; };
return ( return (
<div <div data-disabled={disabledState} className="star-rating" style={{ display: visibility ? '' : 'none' }}>
data-disabled={parsedDisabledState}
className="star-rating"
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
style={{ display: parsedWidgetVisibility ? '' : 'none' }}
>
{/* TODO: Add label color defination property instead of hardcoded color*/} {/* TODO: Add label color defination property instead of hardcoded color*/}
<span className="label form-check-label col-auto" style={{ color: labelColor }}> <span className="label form-check-label col-auto" style={{ color: labelColor }}>
{label} {label}

View file

@ -1,66 +1,31 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
export const Text = function Text({ id, height, component, onComponentClick, currentState }) { export const Text = function Text({ height, properties, styles }) {
const text = component.definition.properties.text.value;
const color = component.definition.styles.textColor.value;
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 [loadingState, setLoadingState] = useState(false); const [loadingState, setLoadingState] = useState(false);
const { textColor, visibility, disabledState } = styles;
const text = properties.text ?? '';
const color = textColor;
useEffect(() => { useEffect(() => {
const loadingStateProperty = component.definition.properties.loadingState; const loadingStateProperty = properties.loadingState;
if (loadingStateProperty && currentState) { if (loadingStateProperty) {
const newState = resolveReferences(loadingStateProperty.value, currentState, false); setLoadingState(loadingStateProperty);
setLoadingState(newState);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState]); }, [properties.loadingState]);
let data = text;
if (currentState) {
const matchedParams = text.match(/\{\{(.*?)\}\}/g);
if (matchedParams) {
for (const param of matchedParams) {
const resolvedParam = resolveReferences(param, currentState, '');
console.log('resolved param', param, resolvedParam);
data = data.replace(param, resolvedParam);
}
}
}
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
const computedStyles = { const computedStyles = {
color, color,
height, height,
display: parsedWidgetVisibility ? 'flex' : 'none', display: visibility ? 'flex' : 'none',
alignItems: 'center', alignItems: 'center',
}; };
return ( return (
<div <div data-disabled={disabledState} className="text-widget" style={computedStyles}>
data-disabled={parsedDisabledState} {!loadingState && <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text) }} />}
className="text-widget"
style={computedStyles}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
{!loadingState && <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) }} />}
{loadingState === true && ( {loadingState === true && (
<div> <div>
<div className="skeleton-line w-10"></div> <div className="skeleton-line w-10"></div>

View file

@ -15,7 +15,11 @@ export const TextArea = function TextArea({ width, height, properties, exposedVa
type="text" type="text"
className="form-control" className="form-control"
placeholder={properties.placeholder} placeholder={properties.placeholder}
style={{ height, resize: 'none', display: styles.visibility ? '' : 'none' }} style={{
height,
resize: 'none',
display: styles.visibility ? '' : 'none',
}}
value={exposedVariables.value} value={exposedVariables.value}
></textarea> ></textarea>
); );

View file

@ -1,71 +1,34 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const TextInput = function TextInput({ export const TextInput = function TextInput({ height, validate, properties, styles, setExposedVariable, fireEvent }) {
id, const [value, setValue] = useState(properties.value);
height, const { isValid, validationError } = validate(value);
component,
onComponentClick,
currentState,
onComponentOptionChanged,
validate,
}) {
const placeholder = component.definition.properties.placeholder.value;
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;
const value = currentState?.components[component?.name]?.value;
const currentValidState = currentState?.components[component?.name]?.isValid;
const [text, setText] = useState(value);
const textProperty = component.definition.properties.value;
let newText = value;
if (textProperty && currentState) {
newText = resolveReferences(textProperty.value, currentState, '');
}
useEffect(() => { useEffect(() => {
setText(newText); setExposedVariable('isValid', isValid);
onComponentOptionChanged(component, 'value', newText);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [newText]); }, [isValid]);
const validationData = validate(value); useEffect(() => {
setValue(properties.value);
const { isValid, validationError } = validationData; setExposedVariable('value', properties.value);
// eslint-disable-next-line react-hooks/exhaustive-deps
if (currentValidState !== isValid) { }, [properties.value]);
onComponentOptionChanged(component, 'isValid', isValid);
}
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
return ( return (
<div> <div>
<input <input
disabled={parsedDisabledState} disabled={styles.disabledState}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
onChange={(e) => { onChange={(e) => {
setText(e.target.value); setValue(e.target.value);
onComponentOptionChanged(component, 'value', e.target.value); setExposedVariable('value', e.target.value);
fireEvent('onChange');
}} }}
type="text" type="text"
className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon`} className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon`}
placeholder={placeholder} placeholder={properties.placeholder}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }} style={{ height, display: styles.visibility ? '' : 'none' }}
value={text} value={value}
/> />
<div className="invalid-feedback">{validationError}</div> <div className="invalid-feedback">{validationError}</div>
</div> </div>

View file

@ -1,5 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
class Switch extends React.Component { class Switch extends React.Component {
render() { render() {
@ -8,7 +7,10 @@ class Switch extends React.Component {
return ( return (
<label className="form-switch form-check-inline"> <label className="form-switch form-check-inline">
<input <input
style={{ backgroundColor: on ? `${color}` : 'white', marginTop: '0px' }} style={{
backgroundColor: on ? `${color}` : 'white',
marginTop: '0px',
}}
disabled={disabledState} disabled={disabledState}
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
@ -21,58 +23,35 @@ class Switch extends React.Component {
} }
} }
export const ToggleSwitch = ({ export const ToggleSwitch = ({ height, properties, styles, fireEvent, setExposedVariable }) => {
id,
height,
component,
onComponentClick,
currentState,
onComponentOptionChanged,
onEvent,
}) => {
const [on, setOn] = React.useState(false); const [on, setOn] = React.useState(false);
const label = component.definition.properties.label.value; const label = properties.label;
const textColorProperty = component.definition.styles.textColor;
const toggleSwitchColorProperty = component.definition.styles.toggleSwitchColor;
const toggleSwitchColor = toggleSwitchColorProperty ? toggleSwitchColorProperty.value : '#3c92dc';
const textColor = textColorProperty ? textColorProperty.value : '#000';
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; const { visibility, disabledState, toggleSwitchColor, textColor } = styles;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
function toggleValue(e) { function toggleValue(e) {
const toggled = e.target.checked; const toggled = e.target.checked;
onComponentOptionChanged(component, 'value', toggled); setExposedVariable('value', toggled);
onEvent('onChange', { component }); fireEvent('onChange');
} }
// Exposing the initially set false value once on load
useEffect(() => {
console.log('shashi');
setExposedVariable('value', false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const toggle = () => setOn(!on); const toggle = () => setOn(!on);
return ( return (
<div <div className="row py-1" style={{ height, display: visibility ? '' : 'none' }}>
className="row py-1"
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
<span className="form-check-label form-check-label col-auto my-auto" style={{ color: textColor }}> <span className="form-check-label form-check-label col-auto my-auto" style={{ color: textColor }}>
{label} {label}
</span> </span>
<div className="col px-1 py-0 mt-0"> <div className="col px-1 py-0 mt-0">
<Switch <Switch
disabledState={parsedDisabledState} disabledState={disabledState}
on={on} on={on}
onClick={toggle} onClick={toggle}
onChange={toggleValue} onChange={toggleValue}

View file

@ -75,7 +75,7 @@ export const componentTypes = [
properties: { properties: {
title: { value: 'Table' }, title: { value: 'Table' },
visible: { value: true }, visible: { value: true },
loadingState: { value: false }, loadingState: { value: '{{false}}' },
data: { data: {
value: value:
"{{ [ \n\t\t{ id: 1, name: 'Sarah', email: 'sarah@example.com'}, \n\t\t{ id: 2, name: 'Lisa', email: 'lisa@example.com'}, \n\t\t{ id: 3, name: 'Sam', email: 'sam@example.com'}, \n\t\t{ id: 4, name: 'Jon', email: 'jon@example.com'} \n] }}", "{{ [ \n\t\t{ id: 1, name: 'Sarah', email: 'sarah@example.com'}, \n\t\t{ id: 2, name: 'Lisa', email: 'lisa@example.com'}, \n\t\t{ id: 3, name: 'Sam', email: 'sam@example.com'}, \n\t\t{ id: 4, name: 'Jon', email: 'jon@example.com'} \n] }}",
@ -286,7 +286,9 @@ export const componentTypes = [
maxLength: { type: 'code', displayName: 'Max length' }, maxLength: { type: 'code', displayName: 'Max length' },
customRule: { type: 'code', displayName: 'Custom validation' }, customRule: { type: 'code', displayName: 'Custom validation' },
}, },
events: {}, events: {
onChange: { displayName: 'On change' },
},
styles: { styles: {
visibility: { type: 'code', displayName: 'Visibility' }, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: { type: 'code', displayName: 'Disable' }, disabledState: { type: 'code', displayName: 'Disable' },
@ -576,7 +578,9 @@ export const componentTypes = [
visibility: { type: 'code', displayName: 'Visibility' }, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: { type: 'code', displayName: 'Disable' }, disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: {}, exposedVariables: {
value: false,
},
definition: { definition: {
others: { others: {
showOnDesktop: { value: true }, showOnDesktop: { value: true },
@ -644,7 +648,7 @@ export const componentTypes = [
description: 'Select a date range', description: 'Select a date range',
component: 'DaterangePicker', component: 'DaterangePicker',
defaultSize: { defaultSize: {
width: 8, width: 10,
height: 30, height: 30,
}, },
others: { others: {
@ -887,9 +891,9 @@ export const componentTypes = [
}, },
properties: { properties: {
label: { value: 'Select' }, label: { value: 'Select' },
values: { value: '[]' }, value: { value: '{{[2,3]}}' },
option_values: { value: '[1,2,3]' }, values: { value: '{{[1,2,3]}}' },
display_values: { value: '["one", "two", "three"]' }, display_values: { value: '{{["one", "two", "three"]}}' },
visible: { value: true }, visible: { value: true },
}, },
events: [], events: [],

View file

@ -12,6 +12,7 @@ import Comments from './Comments';
import { commentsService } from '@/_services'; import { commentsService } from '@/_services';
import config from 'config'; import config from 'config';
import Spinner from '@/_ui/Spinner'; import Spinner from '@/_ui/Spinner';
import { useHotkeys } from 'react-hotkeys-hook';
function uuidv4() { function uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
@ -41,6 +42,8 @@ export const Container = ({
showComments, showComments,
appVersionsId, appVersionsId,
socket, socket,
handleUndo,
handleRedo,
}) => { }) => {
const styles = { const styles = {
width: currentLayout === 'mobile' ? deviceWindowWidth : '100%', width: currentLayout === 'mobile' ? deviceWindowWidth : '100%',
@ -59,6 +62,9 @@ export const Container = ({
const [newThread, addNewThread] = useState({}); const [newThread, addNewThread] = useState({});
const router = useRouter(); const router = useRouter();
useHotkeys('⌘+z, control+z', () => handleUndo());
useHotkeys('⌘+shift+z, control+shift+z', () => handleRedo());
useEffect(() => { useEffect(() => {
setBoxes(components); setBoxes(components);
}, [components]); }, [components]);
@ -303,7 +309,7 @@ export const Container = ({
const handleAddThread = async (e) => { const handleAddThread = async (e) => {
e.stopPropogation && e.stopPropogation(); e.stopPropogation && e.stopPropogation();
const x = (e.nativeEvent.offsetX) * 100 / canvasWidth; const x = (e.nativeEvent.offsetX * 100) / canvasWidth;
const elementIndex = commentsPreviewList.length; const elementIndex = commentsPreviewList.length;
setCommentsPreviewList([ setCommentsPreviewList([
@ -405,7 +411,7 @@ export const Container = ({
<div <div
key={index} key={index}
style={{ style={{
transform: `translate(${previewComment.x * canvasWidth / 100}px, ${previewComment.y}px)`, transform: `translate(${(previewComment.x * canvasWidth) / 100}px, ${previewComment.y}px)`,
}} }}
> >
<label className="form-selectgroup-item comment-preview-bubble"> <label className="form-selectgroup-item comment-preview-bubble">

View file

@ -31,7 +31,7 @@ function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout,
if (id) { if (id) {
// Dragging within the canvas // Dragging within the canvas
x = Math.round((item.layouts[currentLayout].left * canvasWidth / 100) + delta.x); x = Math.round((item.layouts[currentLayout].left * canvasWidth) / 100 + delta.x);
y = Math.round(item.layouts[currentLayout].top + delta.y); y = Math.round(item.layouts[currentLayout].top + delta.y);
} else { } else {
// New component being dragged from components sidebar // New component being dragged from components sidebar
@ -47,7 +47,7 @@ function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout,
x += realCanvasDelta; x += realCanvasDelta;
console.log('cvv', canvasWidth, x) console.log('cvv', canvasWidth, x);
// x = (x * canvasWidth) / 100; // x = (x * canvasWidth) / 100;
@ -83,7 +83,9 @@ export const CustomDragLayer = ({ canvasWidth, currentLayout }) => {
return ( return (
<div style={layerStyles}> <div style={layerStyles}>
<div style={getItemStyles(delta, item, initialOffset, currentOffset, currentLayout, canvasWidth)}>{renderItem()}</div> <div style={getItemStyles(delta, item, initialOffset, currentOffset, currentLayout, canvasWidth)}>
{renderItem()}
</div>
</div> </div>
); );
}; };

View file

@ -0,0 +1,33 @@
{
"$schema": "https://json-schema.org/",
"$id": "https://tooljet.io/Sendgrid.schema.json",
"title": "Sendgrid datasource",
"description": "A schema defining SendGrid datasource",
"type": "object",
"source": {
"name": "SendGrid",
"kind": "sendgrid",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"api_key": { "type": "string", "encrypted": true }
},
"customTesting": true
},
"defaults": {
"api_key": { "value": "" }
},
"properties": {
"api_key": {
"$label": "API key",
"$key": "api_key",
"type": "password",
"description": "Api key for SendGrid",
"helpText": "For generating API key, visit: <a href='https://app.sendgrid.com/settings/api_keys' target='_blank' rel='noreferrer'>SendGrid Account</a>"
}
},
"required": ["api_key"]
}

View file

@ -0,0 +1,59 @@
{
"$schema": "https://json-schema.org/",
"$id": "https://tooljet.io/Twilio.schema.json",
"title": "Twilio datasource",
"description": "A schema defining Twilio datasource",
"type": "object",
"source": {
"name": "Twilio",
"kind": "twilio",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"accountSid": {
"type": "string"
},
"authToken": {
"type": "string",
"encrypted": true
}
},
"customTesting": true
},
"defaults": {
"authToken": {
"value": ""
}
},
"properties": {
"authToken": {
"$label": "Auth Token",
"$key": "authToken",
"type": "password",
"description": "Auth Token for Twilio",
"helpText": "For generating Auth Token, visit: <a href='https://console.twilio.com/' target='_blank' rel='noreferrer'>Twilio Console</a>"
},
"accountSid": {
"$label": "Account SID",
"$key": "accountSid",
"type": "text",
"description": "Account SID for Twilio",
"helpText": "For generating Account SID, visit: <a href='https://console.twilio.com/' target='_blank' rel='noreferrer'>Twilio Console</a>"
},
"messagingServiceSid": {
"$label": "Messaging Service SID",
"$key": "messagingServiceSid",
"type": "text",
"description": "Messaging Service SID for Twilio",
"helpText": "For generating Messaging Service SID, visit: <a href='https://console.twilio.com/' target='_blank' rel='noreferrer'>Twilio Console</a>"
}
},
"required": [
"authToken",
"accountSid",
"messagingServiceSid"
]
}

View file

@ -0,0 +1,60 @@
{
"$schema": "https://json-schema.org/",
"$id": "https://tooljet.io/TypeSense.schema.json",
"title": "TypeSense datasource",
"description": "A schema defining TypeSense datasource",
"type": "object",
"source": {
"name": "TypeSense",
"kind": "typesense",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"host": { "type": "string" },
"port": { "type": "string" },
"api_key": { "type": "string ", "encrypted": true },
"protocol": { "type": "string"}
}
},
"defaults": {
"scheme": { "value": "https" },
"host": { "value": "localhost" },
"port": { "value": 8108 },
"protocol": { "value": "http"}
},
"properties": {
"host": {
"$label": "Host",
"$key": "host",
"type": "text",
"description": "Enter host"
},
"port": {
"$label": "Port",
"$key": "port",
"type": "text",
"description": "Enter port"
},
"api_key": {
"$label": "API Key",
"$key": "api_key",
"type": "text",
"description": "Enter API key"
},
"protocol": {
"$label": "Protocol",
"$key": "protocol",
"type": "dropdown",
"$options": [
{ "name": "HTTP", "value": "http" },
{ "name": "HTTPS", "value": "https" }
],
"description": "Enter protocol"
}
},
"required": ["host", "port", "api_key", "protocol"]
}

View file

@ -9,10 +9,13 @@ import GraphqlSchema from './Api/Graphql.schema.json';
import StripeSchema from './Api/Stripe.schema.json'; import StripeSchema from './Api/Stripe.schema.json';
import GooglesheetSchema from './Api/Googlesheets.schema.json'; import GooglesheetSchema from './Api/Googlesheets.schema.json';
import SlackSchema from './Api/Slack.schema.json'; import SlackSchema from './Api/Slack.schema.json';
import TwilioSchema from './Api/Twilio.schema.json';
import SendgridSchema from './Api/Sendgrid.schema.json';
// Database sources // Database sources
import DynamodbSchema from './Database/Dynamodb.schema.json'; import DynamodbSchema from './Database/Dynamodb.schema.json';
import ElasticsearchSchema from './Database/Elasticsearch.schema.json'; import ElasticsearchSchema from './Database/Elasticsearch.schema.json';
import TypeSenseSchema from './Database/TypeSense.schema.json';
import RedisSchema from './Database/Redis.schema.json'; import RedisSchema from './Database/Redis.schema.json';
import FirestoreSchema from './Database/Firestore.schema.json'; import FirestoreSchema from './Database/Firestore.schema.json';
import MongodbSchema from './Database/Mongodb.schema.json'; import MongodbSchema from './Database/Mongodb.schema.json';
@ -35,6 +38,7 @@ const Googlesheets = ({ ...rest }) => <DynamicForm schema={GooglesheetSchema} {.
const Slack = ({ ...rest }) => <DynamicForm schema={SlackSchema} {...rest} />; const Slack = ({ ...rest }) => <DynamicForm schema={SlackSchema} {...rest} />;
const Dynamodb = ({ ...rest }) => <DynamicForm schema={DynamodbSchema} {...rest} />; const Dynamodb = ({ ...rest }) => <DynamicForm schema={DynamodbSchema} {...rest} />;
const Elasticsearch = ({ ...rest }) => <DynamicForm schema={ElasticsearchSchema} {...rest} />; const Elasticsearch = ({ ...rest }) => <DynamicForm schema={ElasticsearchSchema} {...rest} />;
const Typesense = ({ ...rest }) => <DynamicForm schema={TypeSenseSchema} {...rest} />;
const Redis = ({ ...rest }) => <DynamicForm schema={RedisSchema} {...rest} />; const Redis = ({ ...rest }) => <DynamicForm schema={RedisSchema} {...rest} />;
const Firestore = ({ ...rest }) => <DynamicForm schema={FirestoreSchema} {...rest} />; const Firestore = ({ ...rest }) => <DynamicForm schema={FirestoreSchema} {...rest} />;
const Mongodb = ({ ...rest }) => <DynamicForm schema={MongodbSchema} {...rest} />; const Mongodb = ({ ...rest }) => <DynamicForm schema={MongodbSchema} {...rest} />;
@ -43,10 +47,13 @@ const Mysql = ({ ...rest }) => <DynamicForm schema={MysqlSchema} {...rest} />;
const Mssql = ({ ...rest }) => <DynamicForm schema={MssqlSchema} {...rest} />; const Mssql = ({ ...rest }) => <DynamicForm schema={MssqlSchema} {...rest} />;
const S3 = ({ ...rest }) => <DynamicForm schema={S3Schema} {...rest} />; const S3 = ({ ...rest }) => <DynamicForm schema={S3Schema} {...rest} />;
const Gcs = ({ ...rest }) => <DynamicForm schema={GcsSchema} {...rest} />; const Gcs = ({ ...rest }) => <DynamicForm schema={GcsSchema} {...rest} />;
const Twilio = ({ ...rest }) => <DynamicForm schema={TwilioSchema} {...rest} />;
const Sendgrid = ({ ...rest }) => <DynamicForm schema={SendgridSchema} {...rest} />;
export const DataBaseSources = [ export const DataBaseSources = [
DynamodbSchema.source, DynamodbSchema.source,
ElasticsearchSchema.source, ElasticsearchSchema.source,
TypeSenseSchema.source,
RedisSchema.source, RedisSchema.source,
FirestoreSchema.source, FirestoreSchema.source,
MongodbSchema.source, MongodbSchema.source,
@ -61,6 +68,8 @@ export const ApiSources = [
StripeSchema.source, StripeSchema.source,
GooglesheetSchema.source, GooglesheetSchema.source,
SlackSchema.source, SlackSchema.source,
TwilioSchema.source,
SendgridSchema.source,
]; ];
export const OtherSources = [RunjsSchema.source]; export const OtherSources = [RunjsSchema.source];
@ -69,6 +78,7 @@ export const DataSourceTypes = [...DataBaseSources, ...ApiSources, ...CloudStora
export const SourceComponents = { export const SourceComponents = {
Elasticsearch, Elasticsearch,
Typesense,
Redis, Redis,
Postgresql, Postgresql,
Stripe, Stripe,
@ -84,4 +94,6 @@ export const SourceComponents = {
Mssql, Mssql,
S3, S3,
Gcs, Gcs,
Twilio,
Sendgrid,
}; };

View file

@ -59,8 +59,8 @@ export const DraggableBox = function DraggableBox({
id, id,
mode, mode,
title, title,
left, _left,
top, _top,
parent, parent,
component, component,
index, index,
@ -80,7 +80,7 @@ export const DraggableBox = function DraggableBox({
removeComponent, removeComponent,
currentLayout, currentLayout,
layouts, layouts,
deviceWindowWidth, _deviceWindowWidth,
isSelectedComponent, isSelectedComponent,
draggingStatusChanged, draggingStatusChanged,
darkMode, darkMode,
@ -134,10 +134,10 @@ export const DraggableBox = function DraggableBox({
padding: '0px', padding: '0px',
}; };
let refProps = {}; let _refProps = {};
if (mode === 'edit' && canDrag) { if (mode === 'edit' && canDrag) {
refProps = { _refProps = {
ref: drag, ref: drag,
}; };
} }
@ -162,7 +162,7 @@ export const DraggableBox = function DraggableBox({
}, [layoutData.height, layoutData.width, layoutData.left, layoutData.top, currentLayout]); }, [layoutData.height, layoutData.width, layoutData.left, layoutData.top, currentLayout]);
const gridWidth = canvasWidth / 43; const gridWidth = canvasWidth / 43;
const width = (canvasWidth * currentLayoutOptions.width) / 43 const width = (canvasWidth * currentLayoutOptions.width) / 43;
return ( return (
<div <div
@ -194,14 +194,14 @@ export const DraggableBox = function DraggableBox({
onDrag={(e) => { onDrag={(e) => {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
setDragging(true) setDragging(true);
}} }}
resizeHandleClasses={isSelectedComponent || mouseOver ? resizerClasses : {}} resizeHandleClasses={isSelectedComponent || mouseOver ? resizerClasses : {}}
resizeHandleStyles={resizerStyles} resizeHandleStyles={resizerStyles}
disableDragging={mode !== 'edit'} disableDragging={mode !== 'edit'}
onDragStop={(e, direction) => { onDragStop={(e, direction) => {
setDragging(false) setDragging(false);
onDragStop(e, id, direction, currentLayout, currentLayoutOptions) onDragStop(e, id, direction, currentLayout, currentLayoutOptions);
}} }}
cancel={`div.table-responsive.jet-data-table, div.calendar-widget`} cancel={`div.table-responsive.jet-data-table, div.calendar-widget`}
onDragStart={(e) => e.stopPropagation()} onDragStart={(e) => e.stopPropagation()}

View file

@ -2,7 +2,7 @@ import React, { createRef } from 'react';
import { datasourceService, dataqueryService, appService, authenticationService } from '@/_services'; import { datasourceService, dataqueryService, appService, authenticationService } from '@/_services';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import { defaults } from 'lodash'; import { defaults, cloneDeep, isEqual, isEmpty } from 'lodash';
import { Container } from './Container'; import { Container } from './Container';
import { CustomDragLayer } from './CustomDragLayer'; import { CustomDragLayer } from './CustomDragLayer';
import { LeftSidebar } from './LeftSidebar'; import { LeftSidebar } from './LeftSidebar';
@ -10,7 +10,6 @@ import { componentTypes } from './Components/components';
import { Inspector } from './Inspector/Inspector'; import { Inspector } from './Inspector/Inspector';
import { DataSourceTypes } from './DataSourceManager/SourceComponents'; import { DataSourceTypes } from './DataSourceManager/SourceComponents';
import { QueryManager } from './QueryManager'; import { QueryManager } from './QueryManager';
import { toast } from 'react-toastify';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ManageAppUsers } from './ManageAppUsers'; import { ManageAppUsers } from './ManageAppUsers';
import { SaveAndPreview } from './SaveAndPreview'; import { SaveAndPreview } from './SaveAndPreview';
@ -31,11 +30,15 @@ import { WidgetManager } from './WidgetManager';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import config from 'config'; import config from 'config';
import queryString from 'query-string'; import queryString from 'query-string';
import toast from 'react-hot-toast';
import produce, { enablePatches, setAutoFreeze, applyPatches } from 'immer';
import Logo from './Icons/logo.svg'; import Logo from './Icons/logo.svg';
import EditIcon from './Icons/edit.svg'; import EditIcon from './Icons/edit.svg';
import MobileSelectedIcon from './Icons/mobile-selected.svg'; import MobileSelectedIcon from './Icons/mobile-selected.svg';
import DesktopSelectedIcon from './Icons/desktop-selected.svg'; import DesktopSelectedIcon from './Icons/desktop-selected.svg';
setAutoFreeze(false);
enablePatches();
class Editor extends React.Component { class Editor extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -66,7 +69,8 @@ class Editor extends React.Component {
currentUser: authenticationService.currentUserValue, currentUser: authenticationService.currentUserValue,
app: {}, app: {},
allComponentTypes: componentTypes, allComponentTypes: componentTypes,
queryPaneHeight: '30%', isQueryPaneDragging: false,
queryPaneHeight: 70,
isLoading: true, isLoading: true,
users: null, users: null,
appId, appId,
@ -104,6 +108,8 @@ class Editor extends React.Component {
this.fetchApp(); this.fetchApp();
this.fetchDataSources(); this.fetchDataSources();
this.fetchDataQueries(); this.fetchDataQueries();
this.initComponentVersioning();
this.initEventListeners();
config.COMMENT_FEATURE_ENABLE && this.initWebSocket(); config.COMMENT_FEATURE_ENABLE && this.initWebSocket();
this.setState({ this.setState({
currentSidebarTab: 2, currentSidebarTab: 2,
@ -111,7 +117,39 @@ class Editor extends React.Component {
}); });
} }
onMouseMove = (e) => {
if (this.state.isQueryPaneDragging) {
let queryPaneHeight = (e.clientY / window.screen.height) * 100;
if (queryPaneHeight > 95) queryPaneHeight = 100;
if (queryPaneHeight < 4.5) queryPaneHeight = 4.5;
this.setState({
queryPaneHeight,
});
}
};
onMouseDown = () => {
this.setState({
isQueryPaneDragging: true,
});
};
onMouseUp = () => {
this.setState({
isQueryPaneDragging: false,
});
};
initEventListeners() {
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
}
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
if (this.state.socket) { if (this.state.socket) {
this.state.socket?.close(); this.state.socket?.close();
} }
@ -164,6 +202,17 @@ class Editor extends React.Component {
}); });
}; };
// 1. When we receive an undoable action we can always undo but cannot redo anymore.
// 2. Whenever you perform an undo you can always redo and keep doing undo as long as we have a patch for it.
// 3. Whenever you redo you can always undo and keep doing redo as long as we have a patch for it.
initComponentVersioning = () => {
this.currentVersion = -1;
this.currentVersionChanges = {};
this.noOfVersionsSupported = 100;
this.canUndo = false;
this.canRedo = false;
};
fetchDataSources = () => { fetchDataSources = () => {
this.setState( this.setState(
{ {
@ -305,13 +354,70 @@ class Editor extends React.Component {
this.setState({ componentTypes: filteredComponents }); this.setState({ componentTypes: filteredComponents });
}; };
handleAddPatch = (patches, inversePatches) => {
if (isEmpty(patches) && isEmpty(inversePatches)) return;
if (isEqual(patches, inversePatches)) return;
this.currentVersion++;
this.currentVersionChanges[this.currentVersion] = {
redo: patches,
undo: inversePatches,
};
this.canUndo = this.currentVersionChanges.hasOwnProperty(this.currentVersion);
this.canRedo = this.currentVersionChanges.hasOwnProperty(this.currentVersion + 1);
delete this.currentVersionChanges[this.currentVersion + 1];
delete this.currentVersionChanges[this.currentVersion - this.noOfVersionsSupported];
};
handleUndo = () => {
if (this.canUndo) {
const appDefinition = applyPatches(
this.state.appDefinition,
this.currentVersionChanges[this.currentVersion--].undo
);
this.canUndo = this.currentVersionChanges.hasOwnProperty(this.currentVersion);
this.canRedo = true;
if (!appDefinition) return;
this.setState({
appDefinition,
});
}
};
handleRedo = () => {
if (this.canRedo) {
const appDefinition = applyPatches(
this.state.appDefinition,
this.currentVersionChanges[++this.currentVersion].redo
);
this.canUndo = true;
this.canRedo = this.currentVersionChanges.hasOwnProperty(this.currentVersion + 1);
if (!appDefinition) return;
this.setState({
appDefinition,
});
}
};
appDefinitionChanged = (newDefinition) => { appDefinitionChanged = (newDefinition) => {
produce(
this.state.appDefinition,
(draft) => {
draft.components = newDefinition.components;
},
this.handleAddPatch
);
this.setState({ appDefinition: newDefinition }); this.setState({ appDefinition: newDefinition });
computeComponentState(this, newDefinition.components); computeComponentState(this, newDefinition.components);
}; };
handleInspectorView = (component) => { handleInspectorView = (component) => {
if (this.state.selectedComponent.hasOwnProperty('component')) { if (this.state.selectedComponent?.hasOwnProperty('component')) {
const { id: selectedComponentId } = this.state.selectedComponent; const { id: selectedComponentId } = this.state.selectedComponent;
if (selectedComponentId === component.id) { if (selectedComponentId === component.id) {
this.setState({ selectedComponent: null }); this.setState({ selectedComponent: null });
@ -325,8 +431,7 @@ class Editor extends React.Component {
}; };
removeComponent = (component) => { removeComponent = (component) => {
let newDefinition = this.state.appDefinition; let newDefinition = cloneDeep(this.state.appDefinition);
// Delete child components when parent is deleted // Delete child components when parent is deleted
const childComponents = Object.keys(newDefinition.components).filter( const childComponents = Object.keys(newDefinition.components).filter(
(key) => newDefinition.components[key].parent === component.id (key) => newDefinition.components[key].parent === component.id
@ -336,25 +441,31 @@ class Editor extends React.Component {
}); });
delete newDefinition.components[component.id]; delete newDefinition.components[component.id];
toast('Component deleted! (⌘Z to undo)', {
icon: '🗑️',
});
this.appDefinitionChanged(newDefinition); this.appDefinitionChanged(newDefinition);
this.handleInspectorView(component); this.handleInspectorView(component);
}; };
componentDefinitionChanged = (newDefinition) => { componentDefinitionChanged = (componentDefinition) => {
let _self = this; let _self = this;
return setStateAsync(_self, { const newDefinition = {
appDefinition: { appDefinition: produce(this.state.appDefinition, (draft) => {
...this.state.appDefinition, draft.components[componentDefinition.id].component = componentDefinition.component;
components: { }),
...this.state.appDefinition.components, };
[newDefinition.id]: {
...this.state.appDefinition.components[newDefinition.id], produce(
component: newDefinition.component, this.state.appDefinition,
}, (draft) => {
}, draft.components[componentDefinition.id].component = componentDefinition.component;
}, },
}); this.handleAddPatch
);
return setStateAsync(_self, newDefinition);
}; };
componentChanged = (newComponent) => { componentChanged = (newComponent) => {
@ -384,16 +495,15 @@ class Editor extends React.Component {
saveApp = (id, attributes, notify = false) => { saveApp = (id, attributes, notify = false) => {
appService.saveApp(id, attributes).then(() => { appService.saveApp(id, attributes).then(() => {
if (notify) { if (notify) {
toast.success('App saved sucessfully', { hideProgressBar: true, position: 'top-center' }); toast.success('App saved sucessfully');
} }
}); });
}; };
saveAppName = (id, name, notify = false) => { saveAppName = (id, name, notify = false) => {
if (!name.trim()) { if (!name.trim()) {
toast.warn("App name can't be empty or whitespace", { toast("App name can't be empty or whitespace", {
hideProgressBar: true, icon: '🚨',
position: 'top-center',
}); });
this.setState({ this.setState({
@ -440,13 +550,13 @@ class Editor extends React.Component {
dataqueryService dataqueryService
.del(this.state.selectedQuery.id) .del(this.state.selectedQuery.id)
.then(() => { .then(() => {
toast.success('Query Deleted', { hideProgressBar: true, position: 'bottom-center' }); toast.success('Query Deleted');
this.setState({ isDeletingDataQuery: false }); this.setState({ isDeletingDataQuery: false });
this.dataQueriesChanged(); this.dataQueriesChanged();
}) })
.catch(({ error }) => { .catch(({ error }) => {
this.setState({ isDeletingDataQuery: false }); this.setState({ isDeletingDataQuery: false });
toast.error(error, { hideProgressBar: true, position: 'bottom-center' }); toast.error(error);
}); });
}; };
@ -511,9 +621,8 @@ class Editor extends React.Component {
className="btn badge bg-azure-lt" className="btn badge bg-azure-lt"
onClick={() => { onClick={() => {
runQuery(this, dataQuery.id, dataQuery.name).then(() => { runQuery(this, dataQuery.id, dataQuery.name).then(() => {
toast.info(`Query (${dataQuery.name}) completed.`, { toast(`Query (${dataQuery.name}) completed.`, {
hideProgressBar: true, icon: '🚀',
position: 'bottom-center',
}); });
}); });
}} }}
@ -534,20 +643,11 @@ class Editor extends React.Component {
}); });
}; };
toggleQueryPaneHeight = () => {
this.setState({
queryPaneHeight: this.state.queryPaneHeight === '30%' ? '80%' : '30%',
});
};
toggleQueryEditor = () => { toggleQueryEditor = () => {
this.setState((prev) => ({ showQueryEditor: !prev.showQueryEditor })); this.setState((prev) => ({
this.toolTipRefHide.current.style.display = this.state.showQueryEditor ? 'none' : 'flex'; showQueryEditor: !prev.showQueryEditor,
this.toolTipRefShow.current.style.display = this.state.showQueryEditor ? 'flex' : 'none'; queryPaneHeight: this.state.queryPaneHeight === 100 ? 30 : 100,
}; }));
toggleLeftSidebar = () => {
this.setState({ showLeftSidebar: !this.state.showLeftSidebar });
}; };
toggleComments = () => { toggleComments = () => {
@ -565,7 +665,7 @@ class Editor extends React.Component {
const results = fuse.search(value); const results = fuse.search(value);
this.setState({ this.setState({
dataQueries: results.map((result) => result.item), dataQueries: results.map((result) => result.item),
dataQueriesDefaultText: results.length || 'No Queries found.', dataQueriesDefaultText: results.length ?? 'No Queries found.',
}); });
} else { } else {
this.fetchDataQueries(); this.fetchDataQueries();
@ -591,8 +691,7 @@ class Editor extends React.Component {
}); });
}; };
toolTipRefHide = createRef(); queryPaneRef = createRef();
toolTipRefShow = createRef();
getCanvasWidth = () => { getCanvasWidth = () => {
const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0].getBoundingClientRect(); const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0].getBoundingClientRect();
@ -685,8 +784,9 @@ class Editor extends React.Component {
<Logo /> <Logo />
</Link> </Link>
</h1> </h1>
{this.state.app && ( {this.state.app && (
<div className="app-name input-icon"> <div className={`app-name input-icon ${this.props.darkMode ? 'dark' : ''}`}>
<input <input
type="text" type="text"
onFocus={(e) => this.setState({ oldName: e.target.value })} onFocus={(e) => this.setState({ oldName: e.target.value })}
@ -703,23 +803,6 @@ class Editor extends React.Component {
{this.state.editingVersion && ( {this.state.editingVersion && (
<small className="app-version-name">{`App version: ${this.state.editingVersion.name}`}</small> <small className="app-version-name">{`App version: ${this.state.editingVersion.name}`}</small>
)} )}
<div className="editor-buttons">
<span
className={`btn btn-light mx-2`}
onClick={this.toggleQueryEditor}
data-tip="Show query editor"
data-class="py-1 px-2"
ref={this.toolTipRefShow}
style={{ display: 'none', opacity: 0.5 }}
>
<img
style={{ transform: 'rotate(-90deg)' }}
src="/assets/images/icons/editor/sidebar-toggle.svg"
width="12"
height="12"
/>
</span>
</div>
<div className="layout-buttons cursor-pointer"> <div className="layout-buttons cursor-pointer">
{this.renderLayoutIcon(currentLayout === 'desktop')} {this.renderLayoutIcon(currentLayout === 'desktop')}
</div> </div>
@ -824,6 +907,8 @@ class Editor extends React.Component {
} }
currentState={this.state.currentState} currentState={this.state.currentState}
configHandleClicked={this.configHandleClicked} configHandleClicked={this.configHandleClicked}
handleUndo={this.handleUndo}
handleRedo={this.handleRedo}
removeComponent={this.removeComponent} removeComponent={this.removeComponent}
onComponentClick={(id, component) => { onComponentClick={(id, component) => {
this.setState({ selectedComponent: { id, component } }); this.setState({ selectedComponent: { id, component } });
@ -842,7 +927,7 @@ class Editor extends React.Component {
<div <div
className="query-pane" className="query-pane"
style={{ style={{
height: showQueryEditor ? 0 : 40, height: 40,
background: '#fff', background: '#fff',
padding: '8px 16px', padding: '8px 16px',
display: 'flex', display: 'flex',
@ -851,14 +936,9 @@ class Editor extends React.Component {
}} }}
> >
<h5 className="mb-0">QUERIES</h5> <h5 className="mb-0">QUERIES</h5>
<span <span onClick={this.toggleQueryEditor} className="cursor-pointer m-1" data-tip="Show query editor">
onClick={this.props.toggleQueryEditor}
className="cursor-pointer m-1"
data-tip="Show query editor"
>
<svg <svg
style={{ transform: 'rotate(180deg)' }} style={{ transform: 'rotate(180deg)' }}
onClick={this.toggleQueryEditor}
width="18" width="18"
height="10" height="10"
viewBox="0 0 18 10" viewBox="0 0 18 10"
@ -876,11 +956,24 @@ class Editor extends React.Component {
</span> </span>
</div> </div>
<div <div
ref={this.queryPaneRef}
onTouchEnd={this.onMouseUp}
onMouseDown={this.onMouseDown}
className="query-pane" className="query-pane"
style={{ style={{
height: showQueryEditor ? this.state.queryPaneHeight : 0, height: `calc(100% - ${this.state.queryPaneHeight - 1}%)`,
background: 'transparent',
border: 0,
cursor: 'row-resize',
}}
></div>
<div
className="query-pane"
style={{
height: `calc(100% - ${this.state.queryPaneHeight}%)`,
width: !showLeftSidebar ? '85%' : '', width: !showLeftSidebar ? '85%' : '',
left: !showLeftSidebar ? '0' : '', left: !showLeftSidebar ? '0' : '',
cursor: this.state.isQueryPaneDragging ? 'row-resize' : 'default',
}} }}
> >
<div className="row main-row"> <div className="row main-row">
@ -970,7 +1063,6 @@ class Editor extends React.Component {
<QueryManager <QueryManager
toggleQueryEditor={this.toggleQueryEditor} toggleQueryEditor={this.toggleQueryEditor}
dataSources={dataSources} dataSources={dataSources}
toggleQueryPaneHeight={this.toggleQueryPaneHeight}
dataQueries={dataQueries} dataQueries={dataQueries}
mode={editingQuery ? 'edit' : 'create'} mode={editingQuery ? 'edit' : 'create'}
selectedQuery={selectedQuery} selectedQuery={selectedQuery}
@ -999,7 +1091,9 @@ class Editor extends React.Component {
{currentSidebarTab === 1 && ( {currentSidebarTab === 1 && (
<div className="pages-container"> <div className="pages-container">
{selectedComponent ? ( {selectedComponent &&
!isEmpty(appDefinition.components) &&
!isEmpty(appDefinition.components[selectedComponent.id]) ? (
<Inspector <Inspector
componentDefinitionChanged={this.componentDefinitionChanged} componentDefinitionChanged={this.componentDefinitionChanged}
dataQueries={dataQueries} dataQueries={dataQueries}
@ -1007,7 +1101,7 @@ class Editor extends React.Component {
removeComponent={this.removeComponent} removeComponent={this.removeComponent}
selectedComponentId={selectedComponent.id} selectedComponentId={selectedComponent.id}
currentState={currentState} currentState={currentState}
allComponents={appDefinition.components} allComponents={cloneDeep(appDefinition.components)}
key={selectedComponent.id} key={selectedComponent.id}
switchSidebarTab={this.switchSidebarTab} switchSidebarTab={this.switchSidebarTab}
apps={apps} apps={apps}

View file

@ -0,0 +1,29 @@
import React, { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
console.log(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.showFallback ? <h2>Something went wrong.</h2> : <div></div>;
}
return this.props.children;
}
}
export default ErrorBoundary;

View file

@ -1,6 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16 1C16 0.734784 15.8946 0.48043 15.7071 0.292893C15.5196 0.105357 15.2652 0 15 0H10C9.73478 0 9.48043 0.105357 9.29289 0.292893C9.10536 0.48043 9 0.734784 9 1C9 1.26522 9.10536 1.51957 9.29289 1.70711C9.48043 1.89464 9.73478 2 10 2H12.57L9.29 5.29C9.19627 5.38296 9.12188 5.49356 9.07111 5.61542C9.02034 5.73728 8.9942 5.86799 8.9942 6C8.9942 6.13201 9.02034 6.26272 9.07111 6.38458C9.12188 6.50644 9.19627 6.61704 9.29 6.71C9.38296 6.80373 9.49356 6.87812 9.61542 6.92889C9.73728 6.97966 9.86799 7.0058 10 7.0058C10.132 7.0058 10.2627 6.97966 10.3846 6.92889C10.5064 6.87812 10.617 6.80373 10.71 6.71L14 3.42V6C14 6.26522 14.1054 6.51957 14.2929 6.70711C14.4804 6.89464 14.7348 7 15 7C15.2652 7 15.5196 6.89464 15.7071 6.70711C15.8946 6.51957 16 6.26522 16 6V1ZM6.71 9.29C6.61704 9.19627 6.50644 9.12188 6.38458 9.07111C6.26272 9.02034 6.13201 8.9942 6 8.9942C5.86799 8.9942 5.73728 9.02034 5.61542 9.07111C5.49356 9.12188 5.38296 9.19627 5.29 9.29L2 12.57V10C2 9.73478 1.89464 9.48043 1.70711 9.29289C1.51957 9.10536 1.26522 9 1 9C0.734784 9 0.48043 9.10536 0.292893 9.29289C0.105357 9.48043 0 9.73478 0 10V15C0 15.2652 0.105357 15.5196 0.292893 15.7071C0.48043 15.8946 0.734784 16 1 16H6C6.26522 16 6.51957 15.8946 6.70711 15.7071C6.89464 15.5196 7 15.2652 7 15C7 14.7348 6.89464 14.4804 6.70711 14.2929C6.51957 14.1054 6.26522 14 6 14H3.42L6.71 10.71C6.80373 10.617 6.87812 10.5064 6.92889 10.3846C6.97966 10.2627 7.0058 10.132 7.0058 10C7.0058 9.86799 6.97966 9.73728 6.92889 9.61542C6.87812 9.49356 6.80373 9.38296 6.71 9.29Z"
fill="#61656F"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,10 +0,0 @@
<svg width="16" height="16" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="🔍-System-Icons" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="ic_fluent_arrow_minimize_28_filled" fill="#212121" fillRule="nonzero">
<path
d="M4,15 L12,15 C12.5128358,15 12.9355072,15.3860402 12.9932723,15.8833789 L13,16 L13,24 C13,24.5522847 12.5522847,25 12,25 C11.4871642,25 11.0644928,24.6139598 11.0067277,24.1166211 L11,24 L11,18.413 L3.70710678,25.7071068 C3.31658249,26.0976311 2.68341751,26.0976311 2.29289322,25.7071068 C1.93240926,25.3466228 1.90467972,24.7793918 2.20970461,24.3871006 L2.29289322,24.2928932 L9.585,17 L4,17 C3.48716416,17 3.06449284,16.6139598 3.00672773,16.1166211 L3,16 C3,15.4871642 3.38604019,15.0644928 3.88337887,15.0067277 L4,15 L12,15 L4,15 Z M25.7071068,2.29289322 C26.0675907,2.65337718 26.0953203,3.22060824 25.7902954,3.61289944 L25.7071068,3.70710678 L18.413,11 L24,11 C24.5128358,11 24.9355072,11.3860402 24.9932723,11.8833789 L25,12 C25,12.5128358 24.6139598,12.9355072 24.1166211,12.9932723 L24,13 L16,13 C15.4871642,13 15.0644928,12.6139598 15.0067277,12.1166211 L15,12 L15,4 C15,3.44771525 15.4477153,3 16,3 C16.5128358,3 16.9355072,3.38604019 16.9932723,3.88337887 L17,4 L17,9.585 L24.2928932,2.29289322 C24.6834175,1.90236893 25.3165825,1.90236893 25.7071068,2.29289322 Z"
id="🎨-Color"
></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -102,6 +102,7 @@ class Chart extends React.Component {
lineNumbers={false} lineNumbers={false}
className="chart-input pr-2" className="chart-input pr-2"
onChange={(value) => this.props.paramUpdated({ name: 'data' }, 'value', value, 'properties')} onChange={(value) => this.props.paramUpdated({ name: 'data' }, 'value', value, 'properties')}
componentName={`widget/${this.props.component.component.name}::${chartType}`}
/> />
), ),
}); });

View file

@ -174,6 +174,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={column.name} placeholder={column.name}
onChange={(value) => this.onColumnItemChange(index, 'key', value)} onChange={(value) => this.onColumnItemChange(index, 'key', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'key')}
/> />
</div> </div>
@ -189,6 +190,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={'Text color of the cell'} placeholder={'Text color of the cell'}
onChange={(value) => this.onColumnItemChange(index, 'textColor', value)} onChange={(value) => this.onColumnItemChange(index, 'textColor', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'textColor')}
/> />
</div> </div>
{column.isEditable && ( {column.isEditable && (
@ -204,6 +206,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={''} placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'regex', value)} onChange={(value) => this.onColumnItemChange(index, 'regex', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'regex')}
/> />
</div> </div>
<div className="field mb-2"> <div className="field mb-2">
@ -216,6 +219,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={''} placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'minLength', value)} onChange={(value) => this.onColumnItemChange(index, 'minLength', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'minLength')}
/> />
</div> </div>
<div className="field mb-2"> <div className="field mb-2">
@ -228,6 +232,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={''} placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'maxLength', value)} onChange={(value) => this.onColumnItemChange(index, 'maxLength', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'maxLength')}
/> />
</div> </div>
<div className="field mb-2"> <div className="field mb-2">
@ -240,6 +245,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={''} placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'customRule', value)} onChange={(value) => this.onColumnItemChange(index, 'customRule', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'customRule')}
/> />
</div> </div>
</div> </div>
@ -295,6 +301,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={'{{[1, 2, 3]}}'} placeholder={'{{[1, 2, 3]}}'}
onChange={(value) => this.onColumnItemChange(index, 'values', value)} onChange={(value) => this.onColumnItemChange(index, 'values', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'values')}
/> />
</div> </div>
<div className="field mb-2"> <div className="field mb-2">
@ -307,6 +314,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={'{{["one", "two", "three"]}}'} placeholder={'{{["one", "two", "three"]}}'}
onChange={(value) => this.onColumnItemChange(index, 'labels', value)} onChange={(value) => this.onColumnItemChange(index, 'labels', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'labels')}
/> />
</div> </div>
</div> </div>
@ -327,6 +335,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={''} placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'customRule', value)} onChange={(value) => this.onColumnItemChange(index, 'customRule', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'customRule')}
/> />
</div> </div>
</div> </div>
@ -346,6 +355,7 @@ class Table extends React.Component {
lineNumbers={false} lineNumbers={false}
placeholder={'DD-MM-YYYY'} placeholder={'DD-MM-YYYY'}
onChange={(value) => this.onColumnItemChange(index, 'dateFormat', value)} onChange={(value) => this.onColumnItemChange(index, 'dateFormat', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'dateFormat')}
/> />
</div> </div>
<label className="form-label">Date Parse Format</label> <label className="form-label">Date Parse Format</label>
@ -560,6 +570,9 @@ class Table extends React.Component {
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties'); this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties');
}; };
getPopoverFieldSource = (column, field) =>
`widget/${this.props.component.component.name}/${column ?? 'default'}::${field}`;
render() { render() {
const { dataQueries, component, paramUpdated, componentMeta, components, currentState, darkMode } = this.props; const { dataQueries, component, paramUpdated, componentMeta, components, currentState, darkMode } = this.props;

View file

@ -2,7 +2,16 @@ import React from 'react';
import { CodeHinter } from '../../CodeBuilder/CodeHinter'; import { CodeHinter } from '../../CodeBuilder/CodeHinter';
import { ToolTip } from './Components/ToolTip'; import { ToolTip } from './Components/ToolTip';
export const Code = ({ param, definition, onChange, paramType, componentMeta, currentState, darkMode }) => { export const Code = ({
param,
definition,
onChange,
paramType,
componentMeta,
currentState,
darkMode,
componentName,
}) => {
const initialValue = definition ? definition.value : ''; const initialValue = definition ? definition.value : '';
const paramMeta = componentMeta[paramType][param.name]; const paramMeta = componentMeta[paramType][param.name];
const displayName = paramMeta.displayName || param.name; const displayName = paramMeta.displayName || param.name;
@ -13,6 +22,10 @@ export const Code = ({ param, definition, onChange, paramType, componentMeta, cu
const options = paramMeta.options || {}; const options = paramMeta.options || {};
const getfieldName = React.useMemo(() => {
return param.name;
}, [param]);
return ( return (
<div className={`mb-2 field ${options.className}`}> <div className={`mb-2 field ${options.className}`}>
<ToolTip label={displayName} meta={paramMeta} /> <ToolTip label={displayName} meta={paramMeta} />
@ -25,6 +38,7 @@ export const Code = ({ param, definition, onChange, paramType, componentMeta, cu
lineWrapping={true} lineWrapping={true}
className={options.className} className={options.className}
onChange={(value) => handleCodeChanged(value)} onChange={(value) => handleCodeChanged(value)}
componentName={`widget/${componentName}::${getfieldName}`}
/> />
</div> </div>
); );

View file

@ -34,15 +34,13 @@ export const Color = ({ param, definition, onChange, paramType, componentMeta })
<div className="row mx-0 form-control color-picker-input" onClick={() => setShowPicker(true)}> <div className="row mx-0 form-control color-picker-input" onClick={() => setShowPicker(true)}>
<div <div
className="col-auto" className="col-auto"
style={ style={{
{ float: 'right',
float: 'right', width: '20px',
width: '20px', height: '20px',
height: '20px', backgroundColor: definition.value,
backgroundColor: definition.value, border: `0.25px solid ${['#ffffff', '#fff', '#1f2936'].includes(definition.value) && '#c5c8c9'}`,
border: `0.25px solid ${['#ffffff', '#fff', '#1f2936'].includes(definition.value) && '#c5c8c9'}` }}
}
}
></div> ></div>
<div className="col">{definition.value}</div> <div className="col">{definition.value}</div>
</div> </div>

View file

@ -109,6 +109,7 @@ export const EventManager = ({
eventId: Object.keys(componentMeta.events)[0], eventId: Object.keys(componentMeta.events)[0],
actionId: 'show-alert', actionId: 'show-alert',
message: 'Hello world!', message: 'Hello world!',
alertType: 'info',
}); });
eventsChanged(newEvents); eventsChanged(newEvents);
} }
@ -159,6 +160,7 @@ export const EventManager = ({
currentState={currentState} currentState={currentState}
initialValue={event.message} initialValue={event.message}
onChange={(value) => handlerChanged(index, 'message', value)} onChange={(value) => handlerChanged(index, 'message', value)}
usePortalEditor={false}
/> />
</div> </div>
</div> </div>
@ -185,6 +187,7 @@ export const EventManager = ({
currentState={currentState} currentState={currentState}
initialValue={event.url} initialValue={event.url}
onChange={(value) => handlerChanged(index, 'url', value)} onChange={(value) => handlerChanged(index, 'url', value)}
usePortalEditor={false}
/> />
</div> </div>
)} )}
@ -241,6 +244,7 @@ export const EventManager = ({
<CodeHinter <CodeHinter
currentState={currentState} currentState={currentState}
onChange={(value) => handlerChanged(index, 'contentToCopy', value)} onChange={(value) => handlerChanged(index, 'contentToCopy', value)}
usePortalEditor={false}
/> />
</div> </div>
)} )}
@ -277,6 +281,7 @@ export const EventManager = ({
initialValue={event.key} initialValue={event.key}
onChange={(value) => handlerChanged(index, 'key', value)} onChange={(value) => handlerChanged(index, 'key', value)}
enablePreview={true} enablePreview={true}
usePortalEditor={false}
/> />
</div> </div>
</div> </div>
@ -288,6 +293,7 @@ export const EventManager = ({
initialValue={event.value} initialValue={event.value}
onChange={(value) => handlerChanged(index, 'value', value)} onChange={(value) => handlerChanged(index, 'value', value)}
enablePreview={true} enablePreview={true}
usePortalEditor={false}
/> />
</div> </div>
</div> </div>

View file

@ -5,11 +5,11 @@ import { componentTypes } from '../Components/components';
import { Table } from './Components/Table'; import { Table } from './Components/Table';
import { Chart } from './Components/Chart'; import { Chart } from './Components/Chart';
import { renderElement } from './Utils'; import { renderElement } from './Utils';
import { toast } from 'react-toastify'; import toast from 'react-hot-toast';
import { validateQueryName, convertToKebabCase } from '@/_helpers/utils'; import { validateQueryName, convertToKebabCase } from '@/_helpers/utils';
import { EventManager } from './EventManager'; import { EventManager } from './EventManager';
import useShortcuts from '@/_hooks/use-shortcuts';
import { ConfirmDialog } from '@/_components'; import { ConfirmDialog } from '@/_components';
import { useHotkeys } from 'react-hotkeys-hook';
import Accordion from '@/_ui/Accordion'; import Accordion from '@/_ui/Accordion';
export const Inspector = ({ export const Inspector = ({
@ -34,13 +34,7 @@ export const Inspector = ({
const [components, setComponents] = useState(allComponents); const [components, setComponents] = useState(allComponents);
const [key, setKey] = React.useState('properties'); const [key, setKey] = React.useState('properties');
useShortcuts( useHotkeys('backspace', () => setWidgetDeleteConfirmation(true));
['Backspace'],
() => {
setWidgetDeleteConfirmation(true);
},
[]
);
const componentMeta = componentTypes.find((comp) => component.component.component === comp.component); const componentMeta = componentTypes.find((comp) => component.component.component === comp.component);
@ -60,9 +54,7 @@ export const Inspector = ({
setComponent(newComponent); setComponent(newComponent);
componentChanged(newComponent); componentChanged(newComponent);
} else { } else {
toast.error('Invalid query name. Should be unique and only include letters, numbers and underscore.', { toast.error('Invalid query name. Should be unique and only include letters, numbers and underscore.');
hideProgressBar: true,
});
} }
} }

View file

@ -62,6 +62,7 @@ export function renderElement(
componentMeta={componentMeta} componentMeta={componentMeta}
currentState={currentState} currentState={currentState}
darkMode={darkMode} darkMode={darkMode}
componentName={component.component.name || null}
/> />
); );
} }

View file

@ -1,5 +1,5 @@
export const ItemTypes = { export const ItemTypes = {
BOX: 'box', BOX: 'box',
COMMENT: 'comment', COMMENT: 'comment',
NEW_COMMENT: 'new_comment' NEW_COMMENT: 'new_comment',
}; };

View file

@ -2,9 +2,8 @@ import React from 'react';
import { appService, organizationService } from '@/_services'; import { appService, organizationService } from '@/_services';
import Modal from 'react-bootstrap/Modal'; import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button'; import Button from 'react-bootstrap/Button';
import { toast } from 'react-toastify'; import toast from 'react-hot-toast';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import 'react-toastify/dist/ReactToastify.css';
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import Textarea from '@/_ui/Textarea'; import Textarea from '@/_ui/Textarea';
@ -65,12 +64,12 @@ class ManageAppUsers extends React.Component {
.createAppUser(this.state.app.id, organizationUserId, role) .createAppUser(this.state.app.id, organizationUserId, role)
.then(() => { .then(() => {
this.setState({ addingUser: false, newUser: {} }); this.setState({ addingUser: false, newUser: {} });
toast.success('Added user successfully', { hideProgressBar: true, position: 'top-center' }); toast.success('Added user successfully');
this.fetchAppUsers(); this.fetchAppUsers();
}) })
.catch(({ error }) => { .catch(({ error }) => {
this.setState({ addingUser: false }); this.setState({ addingUser: false });
toast.error(error, { hideProgressBar: true, position: 'top-center' }); toast.error(error);
}); });
}; };
@ -91,15 +90,9 @@ class ManageAppUsers extends React.Component {
}); });
if (newState) { if (newState) {
toast.success('Application is now public.', { toast.success('Application is now public.');
hideProgressBar: true,
position: 'top-center',
});
} else { } else {
toast.success('Application visibility set to private', { toast.success('Application visibility set to private');
hideProgressBar: true,
position: 'top-center',
});
} }
}); });
}; };
@ -206,15 +199,7 @@ class ManageAppUsers extends React.Component {
)} )}
</div> </div>
<span className="input-group-text"> <span className="input-group-text">
<CopyToClipboard <CopyToClipboard text={shareableLink} onCopy={() => toast.success('Link copied to clipboard')}>
text={shareableLink}
onCopy={() =>
toast.success('Link copied to clipboard', {
hideProgressBar: true,
position: 'bottom-center',
})
}
>
<button className="btn btn-secondary btn-sm">Copy</button> <button className="btn btn-secondary btn-sm">Copy</button>
</CopyToClipboard> </CopyToClipboard>
</span> </span>

View file

@ -103,11 +103,10 @@
"description": "Enter spreadsheet_id" "description": "Enter spreadsheet_id"
}, },
"sheet": { "sheet": {
"$label": "Sheet", "$label": "GID",
"$key": "sheet", "$key": "sheet",
"type": "codehinter", "type": "codehinter",
"lineNumbers": false, "lineNumbers": false,
"placeholder": "Leave blank to use first sheet",
"description": "Enter sheet" "description": "Enter sheet"
}, },
"row_index": { "row_index": {

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { CodeHinter } from '../../../CodeBuilder/CodeHinter'; import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode }) => { export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode, componentName }) => {
return ( return (
<> <>
<div className={`row py-2 border-bottom mb-1 mx-0 ${!darkMode && 'bg-light'}`}> <div className={`row py-2 border-bottom mb-1 mx-0 ${!darkMode && 'bg-light'}`}>
@ -29,6 +29,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
placeholder="key" placeholder="key"
className="form-control codehinter-query-editor-input" className="form-control codehinter-query-editor-input"
onChange={onChange('body', 0, index)} onChange={onChange('body', 0, index)}
componentName={`${componentName}/body::key::${index}`}
/> />
</div> </div>
@ -40,6 +41,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
theme={theme} theme={theme}
placeholder="value" placeholder="value"
onChange={onChange('body', 1, index)} onChange={onChange('body', 1, index)}
componentName={`${componentName}/body::value::${index}`}
/> />
</div> </div>
{index > 0 && ( {index > 0 && (

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { CodeHinter } from '../../../CodeBuilder/CodeHinter'; import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode }) => { export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode, componentName }) => {
return ( return (
<> <>
<div className={`row py-2 border-bottom mb-1 mx-0 ${!darkMode && 'bg-light'}`}> <div className={`row py-2 border-bottom mb-1 mx-0 ${!darkMode && 'bg-light'}`}>
@ -29,6 +29,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
placeholder="key" placeholder="key"
className="form-control codehinter-query-editor-input" className="form-control codehinter-query-editor-input"
onChange={onChange('headers', 0, index)} onChange={onChange('headers', 0, index)}
componentName={`${componentName}/headers::key::${index}`}
/> />
</div> </div>
<div className="col-6 field"> <div className="col-6 field">
@ -39,6 +40,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
theme={theme} theme={theme}
placeholder="value" placeholder="value"
onChange={onChange('headers', 1, index)} onChange={onChange('headers', 1, index)}
componentName={`${componentName}/headers::value::${index}`}
/> />
</div> </div>
{index > 0 && ( {index > 0 && (

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { CodeHinter } from '../../../CodeBuilder/CodeHinter'; import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode }) => { export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode, componentName }) => {
return ( return (
<> <>
<div className={`row py-2 border-bottom mb-1 mx-0 ${!darkMode && 'bg-light'}`}> <div className={`row py-2 border-bottom mb-1 mx-0 ${!darkMode && 'bg-light'}`}>
@ -29,6 +29,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
placeholder="key" placeholder="key"
className="form-control codehinter-query-editor-input" className="form-control codehinter-query-editor-input"
onChange={onChange('url_params', 0, index)} onChange={onChange('url_params', 0, index)}
componentName={`${componentName}/params::key::${index}`}
/> />
</div> </div>
<div className="col-6 field"> <div className="col-6 field">
@ -39,6 +40,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
theme={theme} theme={theme}
placeholder="value" placeholder="value"
onChange={onChange('url_params', 1, index)} onChange={onChange('url_params', 1, index)}
componentName={`${componentName}/params::value::${index}`}
/> />
</div> </div>
{index > 0 && ( {index > 0 && (

View file

@ -6,7 +6,7 @@ import Headers from './TabHeaders';
import Params from './TabParams'; import Params from './TabParams';
import Body from './TabBody'; import Body from './TabBody';
function ControlledTabs({ options, currentState, theme, onChange, removeKeyValuePair, darkMode }) { function ControlledTabs({ options, currentState, theme, onChange, removeKeyValuePair, darkMode, componentName }) {
const [key, setKey] = React.useState('headers'); const [key, setKey] = React.useState('headers');
return ( return (
@ -19,6 +19,7 @@ function ControlledTabs({ options, currentState, theme, onChange, removeKeyValue
currentState={currentState} currentState={currentState}
theme={theme} theme={theme}
darkMode={darkMode} darkMode={darkMode}
componentName={componentName}
/> />
</Tab> </Tab>
<Tab eventKey="params" title="Params"> <Tab eventKey="params" title="Params">
@ -29,6 +30,7 @@ function ControlledTabs({ options, currentState, theme, onChange, removeKeyValue
currentState={currentState} currentState={currentState}
theme={theme} theme={theme}
darkMode={darkMode} darkMode={darkMode}
componentName={componentName}
/> />
</Tab> </Tab>
<Tab eventKey="body" title="Body"> <Tab eventKey="body" title="Body">
@ -39,6 +41,7 @@ function ControlledTabs({ options, currentState, theme, onChange, removeKeyValue
currentState={currentState} currentState={currentState}
theme={theme} theme={theme}
darkMode={darkMode} darkMode={darkMode}
componentName={componentName}
/> />
</Tab> </Tab>
</Tabs> </Tabs>

View file

@ -74,6 +74,8 @@ class Restapi extends React.Component {
render() { render() {
const { options } = this.state; const { options } = this.state;
const dataSourceURL = this.props.selectedDataSource?.options?.url?.value; const dataSourceURL = this.props.selectedDataSource?.options?.url?.value;
const queryName = this.props.queryName;
return ( return (
<div> <div>
<div className="mb-3 mt-2"> <div className="mb-3 mt-2">
@ -113,6 +115,7 @@ class Restapi extends React.Component {
changeOption(this, 'url', value); changeOption(this, 'url', value);
}} }}
placeholder="Enter request URL" placeholder="Enter request URL"
componentName={`${queryName}::url`}
/> />
</div> </div>
</div> </div>
@ -124,6 +127,7 @@ class Restapi extends React.Component {
onChange={this.handleChange} onChange={this.handleChange}
removeKeyValuePair={this.removeKeyValuePair} removeKeyValuePair={this.removeKeyValuePair}
darkMode={this.props.darkMode} darkMode={this.props.darkMode}
componentName={queryName}
/> />
</div> </div>
</div> </div>

View file

@ -12,8 +12,7 @@ class Runjs extends React.Component {
}; };
} }
componentDidMount() { componentDidMount() {}
}
render() { render() {
return ( return (

View file

@ -73,6 +73,13 @@
"type": "codehinter", "type": "codehinter",
"lineNumbers": false, "lineNumbers": false,
"description": "Enter bucket" "description": "Enter bucket"
},
"prefix": {
"$label": "Prefix",
"$key": "prefix",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter prefix"
} }
}, },
"signed_url_for_get": { "signed_url_for_get": {

Some files were not shown because too many files have changed in this diff Show more