Merge branch 'release/v0.12.0'
156
.github/workflows/ci.yml
vendored
Normal 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
|
||||
2
.version
|
|
@ -1 +1 @@
|
|||
0.11.1
|
||||
0.12.0
|
||||
|
|
@ -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>
|
||||
[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 is available at https://docs.tooljet.com.
|
||||
|
|
|
|||
6
app.json
|
|
@ -40,6 +40,10 @@
|
|||
"SSO_DISABLE_SIGNUP": {
|
||||
"description": "Disable sign-up via SSO",
|
||||
"value": ""
|
||||
},
|
||||
"DISABLE_PASSWORD_LOGIN": {
|
||||
"description": "Disable logging in with username and password. (Do not turn this on unless you've configured SSO and additional admins)",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
|
@ -63,4 +67,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
docs/docs/Enterprise/_category_.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"label": "Enterprise",
|
||||
"position": 8,
|
||||
"collapsed": true
|
||||
}
|
||||
69
docs/docs/Enterprise/audit_logs.md
Normal 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. |
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"label": "Actions Reference",
|
||||
"position": 6,
|
||||
"position": 7,
|
||||
"collapsed": true
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"label": "Contributing Guide",
|
||||
"position": 6,
|
||||
"position": 9,
|
||||
"collapsed": true
|
||||
}
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
||||
<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
|
||||
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 ).
|
||||
|
|
|
|||
58
docs/docs/data-sources/sendgrid.md
Normal 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.
|
||||
:::
|
||||
28
docs/docs/data-sources/typesense.md
Normal 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
|
||||
|
|
@ -48,6 +48,14 @@ If you want to restrict the signups and allow new users only by invitations, set
|
|||
You will still be able to see the signup page but won't be able to successfully submit the form.
|
||||
:::
|
||||
|
||||
#### Disable login and signup using username and password
|
||||
|
||||
:::info
|
||||
Use this feature only if you have configured other methods of authentication, such as SSO.
|
||||
:::
|
||||
|
||||
If you want to restrict users from logging in using regular username and password, set the environment variable `DISABLE_PASSWORD_LOGIN` to `true`.
|
||||
|
||||
#### Serve client as a server end-point ( optional )
|
||||
|
||||
By default, the `SERVE_CLIENT` variable will be set to `false` and the server won't serve the client at its `/` end-point.
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ sidebar_position: 1
|
|||
|
||||
# Introduction
|
||||
|
||||
ToolJet is an **open-source no-code framework** to build and deploy custom internal tools. ToolJet can connect to your data sources such as databases ( PostgreSQL, MongoDB, MySQL, Elasticsearch, Firestore, DynamoDB, Redis and more ), API endpoints ( ToolJet supports OAuth2 authorization ) and external services ( Stripe, Slack, Google Sheets, airtable and more ). Once the data sources are connected, ToolJet can run queries on these data sources to fetch and update data. The data fetched from data sources can be visualised and modified using the UI widgets such as tables, charts, forms, etc.
|
||||
ToolJet is an **open-source low-code framework** to build and deploy custom internal tools. ToolJet can connect to your data sources such as databases ( PostgreSQL, MongoDB, MySQL, Elasticsearch, Firestore, DynamoDB, Redis and more ), API endpoints ( ToolJet supports OAuth2 authorization ) and external services ( Stripe, Slack, Google Sheets, airtable and more ). Once the data sources are connected, ToolJet can run queries on these data sources to fetch and update data. The data fetched from data sources can be visualised and modified using the UI widgets such as tables, charts, forms, etc.
|
||||
|
||||
<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
|
||||
|
||||
|
|
@ -30,6 +30,11 @@ The references for datasources and widgets:
|
|||
- **[Datasource Reference](/docs/data-sources/redis)**
|
||||
- **[Widget Reference](/docs/widgets/table)**
|
||||
|
||||
## Complete tutorials
|
||||
- [Build a WhatsApp CRM](https://blog.tooljet.com/build-a-whatsapp-crm-using-tooljet-within-10-mins/)
|
||||
- [Build a cryptocurrency dashboard](https://blog.tooljet.com/how-to-build-a-cryptocurrency-dashboard-in-10-minutes/)
|
||||
- [Build a Redis GUI](https://blog.tooljet.com/building-a-redis-gui-using-tooljet-in-5-minutes/)
|
||||
|
||||
## Help and Support
|
||||
We have extensively documented the features of ToolJet, but in case you are stuck, please feel to mail us: hello@tooljet.com.
|
||||
If you are using ToolJet cloud, click on the chat icon at the bottom-left corner for instant help.
|
||||
|
|
|
|||
22
docs/docs/security.md
Normal 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`.
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"label": "Single Sign-on",
|
||||
"position": 6,
|
||||
"position": 10,
|
||||
"collapsed": true
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
<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
|
||||
We can resize the column width using the resize handle of the column.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ This tutorial will walk you through building a simple app to fetch customer info
|
|||
To create a new ToolJet app, click on the **'Create App'** button on the ToolJet dashboard.
|
||||
|
||||
|
||||
You will redirected to the visual app editor once the app has been created. The name of the app can be changed by clicking on the app name at top-left of the app builder.
|
||||
You will be redirected to the visual app editor once the app has been created. The name of the app can be changed by clicking on the app name at top-left of the app builder.
|
||||
|
||||
The main components of an app:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"label": "Widget Reference",
|
||||
"position": 5,
|
||||
"position": 6,
|
||||
"collapsed": true
|
||||
}
|
||||
}
|
||||
20
docs/docs/widgets/container.md
Normal 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}}`. |
|
||||
|
|
@ -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"/>
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
|||
34
docs/docs/widgets/password-input.md
Normal 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
|
|
@ -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}}`. |
|
||||
|
|
@ -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"/>
|
||||
|
||||
#### Properties
|
||||
### Properties
|
||||
|
||||
| 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.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module.exports = {
|
|||
announcementBar: {
|
||||
id: 'support_us',
|
||||
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',
|
||||
textColor: '#ffffff',
|
||||
isCloseable: true,
|
||||
|
|
|
|||
BIN
docs/static/img/datasource-reference/sendgrid/sendgrid-datasource.png
vendored
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/static/img/datasource-reference/sendgrid/sendgrid-query.jpg
vendored
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
docs/static/img/enterprise/audit_logs/audit_logs.gif
vendored
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
docs/static/img/enterprise/audit_logs/reading_logs.png
vendored
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/static/img/tutorial/adding-widget/adding-widget-to-modal.gif
vendored
Normal file
|
After Width: | Height: | Size: 11 MiB |
BIN
docs/static/img/widgets/Container/container.gif
vendored
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/static/img/widgets/password-input/password-input.gif
vendored
Normal file
|
After Width: | Height: | Size: 388 KiB |
BIN
docs/static/img/widgets/tabs/tabs.gif
vendored
Normal file
|
After Width: | Height: | Size: 533 KiB |
|
|
@ -36,7 +36,10 @@
|
|||
],
|
||||
"react/prop-types": 0,
|
||||
"react/display-name": "off",
|
||||
"no-unused-vars": [2, { "args": "after-used", "argsIgnorePattern": "reject" }],
|
||||
"no-unused-vars": ["warn", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}],
|
||||
"react/no-deprecated": 0,
|
||||
"no-prototype-builtins": 0
|
||||
},
|
||||
|
|
@ -53,4 +56,3 @@
|
|||
"__dirname": true
|
||||
}
|
||||
}
|
||||
|
||||
70
frontend/assets/images/icons/editor/datasources/sendgrid.svg
Normal 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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 7 KiB |
14
frontend/assets/images/icons/portal-close.svg
Normal 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 |
14
frontend/assets/images/icons/portal-open.svg
Normal 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 |
59
frontend/package-lock.json
generated
|
|
@ -5,6 +5,7 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.4.3",
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
"fuse.js": "^6.4.6",
|
||||
"history": "^4.9.0",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"immer": "^9.0.6",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
|
|
@ -54,6 +56,7 @@
|
|||
"react-easy-sort": "^0.2.1",
|
||||
"react-google-login": "^5.2.2",
|
||||
"react-hot-toast": "^2.1.1",
|
||||
"react-hotkeys-hook": "^3.4.4",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-lazyload": "^3.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
|
|
@ -11215,6 +11218,11 @@
|
|||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
||||
},
|
||||
"node_modules/hotkeys-js": {
|
||||
"version": "3.8.7",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz",
|
||||
"integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg=="
|
||||
},
|
||||
"node_modules/hpack.js": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
|
||||
|
|
@ -11627,9 +11635,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
|
||||
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz",
|
||||
"integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/immutability-helper": {
|
||||
"version": "3.1.1",
|
||||
|
|
@ -17683,6 +17695,11 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dev-utils/node_modules/immer": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
|
||||
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
|
||||
},
|
||||
"node_modules/react-dev-utils/node_modules/inquirer": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
|
||||
|
|
@ -17993,6 +18010,18 @@
|
|||
"csstype": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hotkeys-hook": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.4.tgz",
|
||||
"integrity": "sha512-vaORq07rWgmuF3owWRhgFV/3VL8/l2q9lz0WyVEddJnWTtKW+AOgU5YgYKuwN6h6h7bCcLG3MFsJIjCrM/5DvQ==",
|
||||
"dependencies": {
|
||||
"hotkeys-js": "3.8.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.1",
|
||||
"react-dom": ">=16.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
|
@ -33209,6 +33238,11 @@
|
|||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
||||
},
|
||||
"hotkeys-js": {
|
||||
"version": "3.8.7",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz",
|
||||
"integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg=="
|
||||
},
|
||||
"hpack.js": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
|
||||
|
|
@ -33525,9 +33559,9 @@
|
|||
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
|
||||
},
|
||||
"immer": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
|
||||
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz",
|
||||
"integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ=="
|
||||
},
|
||||
"immutability-helper": {
|
||||
"version": "3.1.1",
|
||||
|
|
@ -38358,6 +38392,11 @@
|
|||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"immer": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
|
||||
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
|
||||
},
|
||||
"inquirer": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
|
||||
|
|
@ -38585,6 +38624,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-hotkeys-hook": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.4.tgz",
|
||||
"integrity": "sha512-vaORq07rWgmuF3owWRhgFV/3VL8/l2q9lz0WyVEddJnWTtKW+AOgU5YgYKuwN6h6h7bCcLG3MFsJIjCrM/5DvQ==",
|
||||
"requires": {
|
||||
"hotkeys-js": "3.8.7"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"fuse.js": "^6.4.6",
|
||||
"history": "^4.9.0",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"immer": "^9.0.6",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
|
|
@ -50,6 +51,7 @@
|
|||
"react-easy-sort": "^0.2.1",
|
||||
"react-google-login": "^5.2.2",
|
||||
"react-hot-toast": "^2.1.1",
|
||||
"react-hotkeys-hook": "^3.4.4",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-lazyload": "^3.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,17 @@ class App extends React.Component {
|
|||
|
||||
render() {
|
||||
const { currentUser, fetchedMetadata, updateAvailable, onboarded, darkMode } = this.state;
|
||||
let toastOptions = {};
|
||||
|
||||
if (darkMode) {
|
||||
toastOptions = {
|
||||
style: {
|
||||
borderRadius: '10px',
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (currentUser && fetchedMetadata === false) {
|
||||
tooljetService.fetchMetaData().then((data) => {
|
||||
|
|
@ -175,15 +186,7 @@ class App extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
</Router>
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
style: {
|
||||
borderRadius: '10px',
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Toaster toastOptions={toastOptions} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|||
import '@/_styles/custom.scss';
|
||||
import { resolveProperties, resolveStyles } from './component-properties-resolution';
|
||||
import { validateWidget } from '@/_helpers/utils';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
const AllComponents = {
|
||||
Button,
|
||||
|
|
@ -122,53 +123,55 @@ export const Box = function Box({
|
|||
trigger={!inCanvas ? ['hover', 'focus'] : null}
|
||||
overlay={(props) => renderTooltip({ props, text: `${component.description}` })}
|
||||
>
|
||||
<div style={{ ...styles, backgroundColor }} role={preview ? 'BoxPreview' : 'Box'}>
|
||||
{inCanvas ? (
|
||||
<ComponentToRender
|
||||
onComponentClick={onComponentClick}
|
||||
onComponentOptionChanged={onComponentOptionChanged}
|
||||
currentState={currentState}
|
||||
onEvent={onEvent}
|
||||
id={id}
|
||||
paramUpdated={paramUpdated}
|
||||
width={width}
|
||||
changeCanDrag={changeCanDrag}
|
||||
onComponentOptionsChanged={onComponentOptionsChanged}
|
||||
height={height}
|
||||
component={component}
|
||||
containerProps={containerProps}
|
||||
darkMode={darkMode}
|
||||
removeComponent={removeComponent}
|
||||
canvasWidth={canvasWidth}
|
||||
properties={resolvedProperties}
|
||||
exposedVariables={exposedVariables}
|
||||
styles={resolvedStyles}
|
||||
setExposedVariable={(variable, value) => onComponentOptionChanged(component, variable, value)}
|
||||
fireEvent={fireEvent}
|
||||
validate={validate}
|
||||
></ComponentToRender>
|
||||
) : (
|
||||
<div className="m-1" style={{ height: '100%' }}>
|
||||
<div
|
||||
className="component-image-holder p-2 d-flex flex-column justify-content-center"
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<center>
|
||||
<div
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundSize: 'contain',
|
||||
backgroundImage: `url(/assets/images/icons/widgets/${component.name.toLowerCase()}.svg)`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
></div>
|
||||
</center>
|
||||
<span className="component-title">{component.displayName}</span>
|
||||
<ErrorBoundary showFallback={mode === 'edit'}>
|
||||
<div style={{ ...styles, backgroundColor }} role={preview ? 'BoxPreview' : 'Box'}>
|
||||
{inCanvas ? (
|
||||
<ComponentToRender
|
||||
onComponentClick={onComponentClick}
|
||||
onComponentOptionChanged={onComponentOptionChanged}
|
||||
currentState={currentState}
|
||||
onEvent={onEvent}
|
||||
id={id}
|
||||
paramUpdated={paramUpdated}
|
||||
width={width}
|
||||
changeCanDrag={changeCanDrag}
|
||||
onComponentOptionsChanged={onComponentOptionsChanged}
|
||||
height={height}
|
||||
component={component}
|
||||
containerProps={containerProps}
|
||||
darkMode={darkMode}
|
||||
removeComponent={removeComponent}
|
||||
canvasWidth={canvasWidth}
|
||||
properties={resolvedProperties}
|
||||
exposedVariables={exposedVariables}
|
||||
styles={resolvedStyles}
|
||||
setExposedVariable={(variable, value) => onComponentOptionChanged(component, variable, value)}
|
||||
fireEvent={fireEvent}
|
||||
validate={validate}
|
||||
></ComponentToRender>
|
||||
) : (
|
||||
<div className="m-1" style={{ height: '100%' }}>
|
||||
<div
|
||||
className="component-image-holder p-2 d-flex flex-column justify-content-center"
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<center>
|
||||
<div
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundSize: 'contain',
|
||||
backgroundImage: `url(/assets/images/icons/widgets/${component.name.toLowerCase()}.svg)`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
></div>
|
||||
</center>
|
||||
<span className="component-title">{component.displayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useSpring, config, animated } from 'react-spring';
|
||||
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/Tooltip';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import 'codemirror/mode/handlebars/handlebars';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
|
|
@ -15,6 +16,7 @@ import 'codemirror/theme/monokai.css';
|
|||
import { getSuggestionKeys, onBeforeChange, handleChange } from './utils';
|
||||
import { resolveReferences } from '@/_helpers/utils';
|
||||
import useHeight from '@/_hooks/use-height-transition';
|
||||
import usePortal from '@/_hooks/use-portal';
|
||||
|
||||
export function CodeHinter({
|
||||
initialValue,
|
||||
|
|
@ -30,10 +32,13 @@ export function CodeHinter({
|
|||
height,
|
||||
minHeight,
|
||||
lineWrapping,
|
||||
componentName = null,
|
||||
usePortalEditor = true,
|
||||
}) {
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const options = {
|
||||
lineNumbers: lineNumbers,
|
||||
lineWrapping: lineWrapping,
|
||||
lineNumbers: lineNumbers ?? false,
|
||||
lineWrapping: lineWrapping ?? true,
|
||||
singleLine: true,
|
||||
mode: mode || 'handlebars',
|
||||
tabSize: 2,
|
||||
|
|
@ -83,10 +88,11 @@ export function CodeHinter({
|
|||
|
||||
const getPreview = () => {
|
||||
const [preview, error] = resolveReferences(currentValue, realState, null, {}, true);
|
||||
const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1';
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<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>
|
||||
<div className="heading my-1">
|
||||
|
|
@ -103,7 +109,7 @@ export function CodeHinter({
|
|||
const content = getPreviewContent(preview, previewType);
|
||||
|
||||
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>
|
||||
<div className="heading my-1">
|
||||
|
|
@ -116,30 +122,105 @@ export function CodeHinter({
|
|||
);
|
||||
};
|
||||
enablePreview = enablePreview ?? true;
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const element = document.getElementsByClassName('portal-container');
|
||||
if (element) {
|
||||
const checkPortalExits = element[0]?.classList.contains(componentName);
|
||||
|
||||
if (checkPortalExits === false) {
|
||||
const parent = element[0].parentNode;
|
||||
parent.removeChild(element[0]);
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
resolve();
|
||||
}
|
||||
}).then(() => {
|
||||
setIsOpen(true);
|
||||
forceUpdate();
|
||||
});
|
||||
};
|
||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div className="code-hinter-wrapper" style={{ width: '100%' }}>
|
||||
<div
|
||||
className={`code-hinter ${className || 'codehinter-default-input'}`}
|
||||
key={suggestions.length}
|
||||
style={{ height: height || 'auto', minHeight, maxHeight: '320px', overflow: 'auto' }}
|
||||
>
|
||||
<CodeMirror
|
||||
value={initialValue}
|
||||
realState={realState}
|
||||
scrollbarStyle={null}
|
||||
height={height}
|
||||
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}
|
||||
/>
|
||||
{usePortalEditor && <CodeHinter.PopupIcon callback={handleToggle} />}
|
||||
<CodeHinter.Portal
|
||||
isOpen={isOpen}
|
||||
callback={setIsOpen}
|
||||
componentName={componentName}
|
||||
key={suggestions.length}
|
||||
customComponent={getPreview}
|
||||
forceUpdate={forceUpdate}
|
||||
optionalProps={{ height: 300 }}
|
||||
darkMode={darkMode}
|
||||
selectors={{ className: 'preview-block-portal' }}
|
||||
>
|
||||
<CodeMirror
|
||||
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>
|
||||
{enablePreview && getPreview()}
|
||||
{enablePreview && !isOpen && getPreview()}
|
||||
</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;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { Picker } from 'emoji-mart';
|
|||
|
||||
import TextareaMentions from '@/_ui/Mentions';
|
||||
import Button from '@/_ui/Button';
|
||||
import useShortcuts from '@/_hooks/use-shortcuts';
|
||||
import usePopover from '@/_hooks/use-popover';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
function CommentFooter({ users, editComment = '', editCommentId, handleSubmit }) {
|
||||
const [comment, setComment] = React.useState(editComment);
|
||||
|
|
@ -28,7 +28,7 @@ function CommentFooter({ users, editComment = '', editCommentId, handleSubmit })
|
|||
setOpen(false);
|
||||
};
|
||||
|
||||
useShortcuts(['Meta', 'Enter'], () => handleClick(), [comment]);
|
||||
useHotkeys('⌘+enter, control+enter', () => handleClick());
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { isEmpty } from 'lodash';
|
|||
import { pluralize } from '@/_helpers/utils';
|
||||
import moment from 'moment';
|
||||
import usePopover from '@/_hooks/use-popover';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import { useSpring } from 'react-spring';
|
||||
import useRouter from '@/_hooks/use-router';
|
||||
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
|
|
@ -12,8 +12,8 @@ import Spinner from '@/_ui/Spinner';
|
|||
const Content = ({ notifications, loading }) => {
|
||||
const router = useRouter();
|
||||
const [selectedCommentId, setSelectedCommentId] = React.useState(router.query.commentId);
|
||||
const [open, trigger, content] = usePopover(false);
|
||||
const popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 });
|
||||
const [open, _trigger, _content] = usePopover(false);
|
||||
const _popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 });
|
||||
|
||||
React.useEffect(() => {
|
||||
if (router.query?.commentId) setSelectedCommentId(router.query?.commentId);
|
||||
|
|
|
|||
|
|
@ -1,50 +1,23 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import React from 'react';
|
||||
var tinycolor = require('tinycolor2');
|
||||
|
||||
export const Button = function Button({ width, height, component, currentState, fireEvent }) {
|
||||
const [loadingState, setLoadingState] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadingStateProperty = component.definition.properties.loadingState;
|
||||
if (loadingStateProperty && currentState) {
|
||||
const newState = resolveReferences(loadingStateProperty.value, currentState, false);
|
||||
setLoadingState(newState);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentState]);
|
||||
|
||||
const text = component.definition.properties.text.value;
|
||||
const backgroundColor = component.definition.styles.backgroundColor.value;
|
||||
const color = component.definition.styles.textColor.value;
|
||||
const borderRadius = component.definition.styles.borderRadius?.value ?? 3; // using 2 for backward compatibility
|
||||
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
||||
|
||||
const parsedDisabledState =
|
||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||
const parsedBorderRadius = typeof borderRadius !== 'number' ? resolveWidgetFieldValue(borderRadius, currentState) : borderRadius;
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
export const Button = function Button({ height, properties, styles, fireEvent }) {
|
||||
const { loadingState, text } = properties;
|
||||
const { backgroundColor, textColor, borderRadius, visibility, disabledState } = styles;
|
||||
|
||||
const computedStyles = {
|
||||
backgroundColor,
|
||||
color,
|
||||
color: textColor,
|
||||
width: '100%',
|
||||
borderRadius: `${parsedBorderRadius}px`,
|
||||
borderRadius: `${borderRadius}px`,
|
||||
height,
|
||||
display: parsedWidgetVisibility ? '' : 'none',
|
||||
display: visibility ? '' : 'none',
|
||||
'--tblr-btn-color-darker': tinycolor(backgroundColor).darken(8).toString(),
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
disabled={parsedDisabledState}
|
||||
disabled={disabledState}
|
||||
className={`jet-button btn btn-primary p-1 ${loadingState === true ? ' btn-loading' : ''}`}
|
||||
style={computedStyles}
|
||||
onClick={(event) => {
|
||||
|
|
@ -52,7 +25,7 @@ export const Button = function Button({ width, height, component, currentState,
|
|||
fireEvent('onClick');
|
||||
}}
|
||||
>
|
||||
{resolveReferences(text, currentState)}
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,58 +1,34 @@
|
|||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
// Use plotly basic bundle
|
||||
import Plotly from 'plotly.js-basic-dist-min';
|
||||
import createPlotlyComponent from 'react-plotly.js/factory';
|
||||
const Plot = createPlotlyComponent(Plotly);
|
||||
|
||||
export const Chart = function Chart({ id, width, height, component, onComponentClick, currentState, darkMode }) {
|
||||
export const Chart = function Chart({ width, height, darkMode, properties, styles }) {
|
||||
const [loadingState, setLoadingState] = useState(false);
|
||||
|
||||
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
const parsedDisabledState =
|
||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
const { visibility, disabledState } = styles;
|
||||
const { title, markerColor, showGridLines, type, data } = properties;
|
||||
|
||||
useEffect(() => {
|
||||
const loadingStateProperty = component.definition.properties.loadingState;
|
||||
if (loadingStateProperty && currentState) {
|
||||
const newState = resolveReferences(loadingStateProperty.value, currentState, false);
|
||||
setLoadingState(newState);
|
||||
const loadingStateProperty = properties.loadingState;
|
||||
if (loadingStateProperty != undefined) {
|
||||
setLoadingState(loadingStateProperty);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentState]);
|
||||
}, [properties.loadingState]);
|
||||
|
||||
const computedStyles = {
|
||||
width: width - 4,
|
||||
height,
|
||||
display: parsedWidgetVisibility ? '' : 'none',
|
||||
display: visibility ? '' : 'none',
|
||||
background: darkMode ? '#1f2936' : 'white',
|
||||
};
|
||||
|
||||
// darkMode ? '#1f2936' : 'white'
|
||||
const dataProperty = component.definition.properties.data;
|
||||
const dataString = dataProperty ? dataProperty.value : [];
|
||||
const dataString = data ?? [];
|
||||
|
||||
const titleProperty = component.definition.properties.title;
|
||||
const title = titleProperty.value;
|
||||
const chartType = type;
|
||||
|
||||
const typeProperty = component.definition.properties.type;
|
||||
const chartType = typeProperty.value;
|
||||
|
||||
const markerColorProperty = component.definition.properties.markerColor;
|
||||
const markerColor = markerColorProperty ? markerColorProperty.value : 'red';
|
||||
|
||||
const gridLinesProperty = component.definition.properties.showGridLines;
|
||||
const showGridLines = gridLinesProperty ? gridLinesProperty.value : true;
|
||||
const fontColor = darkMode ? '#c3c3c3' : null;
|
||||
|
||||
const layout = {
|
||||
|
|
@ -84,8 +60,6 @@ export const Chart = function Chart({ id, width, height, component, onComponentC
|
|||
},
|
||||
};
|
||||
|
||||
const data = resolveReferences(dataString, currentState, []);
|
||||
|
||||
const computeChartData = (data, dataString) => {
|
||||
let rawData = data;
|
||||
if (typeof rawData === 'string') {
|
||||
|
|
@ -128,14 +102,7 @@ export const Chart = function Chart({ id, width, height, component, onComponentC
|
|||
const memoizedChartData = useMemo(() => computeChartData(data, dataString), [data, dataString]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
style={computedStyles}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div data-disabled={disabledState} style={computedStyles}>
|
||||
{loadingState === true ? (
|
||||
<div style={{ width }} className="p-2">
|
||||
<center>
|
||||
|
|
|
|||
|
|
@ -1,56 +1,23 @@
|
|||
import React from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const Checkbox = function Checkbox({
|
||||
id,
|
||||
height,
|
||||
component,
|
||||
onComponentClick,
|
||||
currentState,
|
||||
onComponentOptionChanged,
|
||||
onEvent,
|
||||
}) {
|
||||
export const Checkbox = function Checkbox({ height, properties, styles, fireEvent, setExposedVariable }) {
|
||||
const [checked, setChecked] = React.useState(false);
|
||||
const label = component.definition.properties.label.value;
|
||||
const textColorProperty = component.definition.styles.textColor;
|
||||
const textColor = textColorProperty ? textColorProperty.value : '#000';
|
||||
const checkboxColorProperty = component.definition.styles.checkboxColor;
|
||||
const checkboxColor = checkboxColorProperty ? checkboxColorProperty.value : '#3c92dc';
|
||||
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
||||
|
||||
const parsedDisabledState =
|
||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
const { label } = properties;
|
||||
const { visibility, disabledState, checkboxColor, textColor } = styles;
|
||||
|
||||
function toggleValue(e) {
|
||||
const isChecked = e.target.checked;
|
||||
setChecked(isChecked);
|
||||
onComponentOptionChanged(component, 'value', isChecked);
|
||||
setExposedVariable('value', isChecked);
|
||||
if (isChecked) {
|
||||
onEvent('onCheck', { component });
|
||||
fireEvent('onCheck');
|
||||
} else {
|
||||
onEvent('onUnCheck', { component });
|
||||
fireEvent('onUnCheck');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
className="row py-1"
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div data-disabled={disabledState} className="row py-1" style={{ height, display: visibility ? '' : 'none' }}>
|
||||
<div className="col px-1 py-0 mt-0">
|
||||
<label className="mx-1 form-check form-check-inline">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import React from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import 'codemirror/addon/comment/comment';
|
||||
import 'codemirror/addon/hint/show-hint';
|
||||
|
|
@ -11,41 +10,23 @@ import 'codemirror/theme/duotone-light.css';
|
|||
import 'codemirror/theme/monokai.css';
|
||||
import { onBeforeChange, handleChange } from '../CodeBuilder/utils';
|
||||
|
||||
export const CodeEditor = ({ width, height, component, currentState, onComponentOptionChanged, darkMode }) => {
|
||||
const enableLineNumber = component.definition.properties?.enableLineNumber?.value ?? true;
|
||||
const languageMode = component.definition.properties.mode.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;
|
||||
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);
|
||||
export const CodeEditor = ({ height, darkMode, properties, styles, exposedVariables, setExposedVariable }) => {
|
||||
const { enableLineNumber, mode, placeholder } = properties;
|
||||
const { visibility, disabledState } = styles;
|
||||
|
||||
function codeChanged(code) {
|
||||
setEditorValue(code);
|
||||
onComponentOptionChanged(component, 'value', code);
|
||||
setExposedVariable('value', code);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
const editorStyles = {
|
||||
height: height,
|
||||
display: !parsedWidgetVisibility ? 'none' : 'block',
|
||||
display: !visibility ? 'none' : 'block',
|
||||
};
|
||||
const options = {
|
||||
lineNumbers: parsedEnableLineNumber,
|
||||
lineNumbers: enableLineNumber,
|
||||
lineWrapping: true,
|
||||
singleLine: true,
|
||||
mode: languageMode,
|
||||
mode: mode,
|
||||
tabSize: 2,
|
||||
theme: darkMode ? 'monokai' : 'duotone-light',
|
||||
readOnly: false,
|
||||
|
|
@ -55,23 +36,21 @@ export const CodeEditor = ({ width, height, component, currentState, onComponent
|
|||
|
||||
function valueChanged(editor, onChange, ignoreBraces = false) {
|
||||
handleChange(editor, onChange, [], ignoreBraces);
|
||||
setEditorValue(editor.getValue());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setRealState(currentState);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentState.components]);
|
||||
|
||||
return (
|
||||
<div data-disabled={parsedDisabledState} style={styles}>
|
||||
<div data-disabled={disabledState} style={editorStyles}>
|
||||
<div
|
||||
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
|
||||
value={editorValue}
|
||||
realState={realState}
|
||||
value={exposedVariables.value}
|
||||
scrollbarStyle={null}
|
||||
height={height - 1}
|
||||
onBlur={(editor) => {
|
||||
|
|
|
|||
|
|
@ -1,43 +1,42 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { SubCustomDragLayer } from '../SubCustomDragLayer';
|
||||
import { SubContainer } from '../SubContainer';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const Container = function Container({ id, component, width, height, containerProps, currentState, removeComponent }) {
|
||||
const backgroundColor = component.definition.styles.backgroundColor.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);
|
||||
}
|
||||
export const Container = function Container({ id, component, width, height, containerProps, removeComponent, styles }) {
|
||||
const { backgroundColor, visibility, disabledState } = styles;
|
||||
|
||||
const computedStyles = {
|
||||
backgroundColor,
|
||||
height,
|
||||
display: parsedWidgetVisibility ? 'flex' : 'none',
|
||||
display: visibility ? 'flex' : 'none',
|
||||
};
|
||||
|
||||
const parentRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
data-disabled={disabledState}
|
||||
className="jet-container"
|
||||
id={id}
|
||||
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}
|
||||
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} />
|
||||
<SubCustomDragLayer containerCanvasWidth={width} parent={id} parentRef={parentRef} currentLayout={containerProps.currentLayout} />
|
||||
<SubContainer
|
||||
containerCanvasWidth={width}
|
||||
parent={id}
|
||||
{...containerProps}
|
||||
parentRef={parentRef}
|
||||
removeComponent={removeComponent}
|
||||
/>
|
||||
<SubCustomDragLayer
|
||||
containerCanvasWidth={width}
|
||||
parent={id}
|
||||
parentRef={parentRef}
|
||||
currentLayout={containerProps.currentLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ export const Datepicker = function Datepicker({
|
|||
dateFormat={isDateFormat}
|
||||
placeholderText={defaultValue}
|
||||
inputProps={{ placeholder: defaultValue }}
|
||||
onOpen={(event) => { onComponentClick(id, component, event) }}
|
||||
onOpen={(event) => {
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
renderInput={(props) => {
|
||||
return (
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -3,48 +3,34 @@ import 'react-datetime/css/react-datetime.css';
|
|||
import { DateRangePicker } from 'react-dates';
|
||||
import 'react-dates/lib/css/_datepicker.css';
|
||||
import 'react-dates/initialize';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const DaterangePicker = function DaterangePicker({
|
||||
id,
|
||||
height,
|
||||
component,
|
||||
onComponentClick,
|
||||
currentState,
|
||||
onComponentOptionChanged,
|
||||
properties,
|
||||
styles,
|
||||
exposedVariables,
|
||||
setExposedVariable,
|
||||
}) {
|
||||
console.log('currentState', currentState);
|
||||
const { visibility, disabledState } = styles;
|
||||
|
||||
const startDateProp = component.definition.properties.startDate;
|
||||
const endDateProp = component.definition.properties.endDate;
|
||||
const formatProp = component.definition.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 startDateProp = exposedVariables.startDate;
|
||||
const endDateProp = exposedVariables.endDate;
|
||||
const formatProp = properties.format;
|
||||
|
||||
const [focusedInput, setFocusedInput] = useState(null);
|
||||
const [startDate, setStartDate] = useState(startDateProp ? startDateProp.value : null);
|
||||
const [endDate, setEndDate] = useState(endDateProp ? endDateProp.value : null);
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
const [startDate, setStartDate] = useState(startDateProp ?? null);
|
||||
const [endDate, setEndDate] = useState(endDateProp ?? null);
|
||||
|
||||
function onDateChange(dates) {
|
||||
const start = dates.startDate;
|
||||
const end = dates.endDate;
|
||||
|
||||
if (start) {
|
||||
onComponentOptionChanged(component, 'startDate', start.format(formatProp.value));
|
||||
setExposedVariable('startDate', start.format(formatProp.value));
|
||||
}
|
||||
|
||||
if (end) {
|
||||
onComponentOptionChanged(component, 'endDate', end.format(formatProp.value));
|
||||
setExposedVariable('endDate', end.format(formatProp.value));
|
||||
}
|
||||
|
||||
setStartDate(start);
|
||||
|
|
@ -56,16 +42,9 @@ export const DaterangePicker = function DaterangePicker({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="daterange-picker-widget p-0"
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div className="daterange-picker-widget p-0" style={{ height, display: visibility ? '' : 'none' }}>
|
||||
<DateRangePicker
|
||||
disabled={parsedDisabledState}
|
||||
disabled={disabledState}
|
||||
startDate={startDate}
|
||||
startDateId="startDate"
|
||||
isOutsideRange={() => false}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,8 @@
|
|||
import React from 'react';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const Divider = function Divider({ id, component, onComponentClick, currentState }) {
|
||||
const dividerColorProperty = component.definition.styles.dividerColor;
|
||||
const color = dividerColorProperty ? dividerColorProperty.value : '#E7E8EA';
|
||||
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||
export const Divider = function Divider({ styles }) {
|
||||
const { visibility, dividerColor } = styles;
|
||||
const color = dividerColor ?? '#E7E8EA';
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
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>
|
||||
);
|
||||
return <div className="hr mt-1" style={{ display: visibility ? '' : 'none', color: color, opacity: '1' }}></div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const HEADINGS = [
|
|||
{ label: 'H4', style: 'header-four' },
|
||||
{ label: 'H5', style: 'header-five' },
|
||||
{ label: 'H6', style: 'header-six' },
|
||||
]
|
||||
];
|
||||
|
||||
const BLOCK_TYPES = [
|
||||
{
|
||||
|
|
@ -86,19 +86,17 @@ const BlockStyleControls = (props) => {
|
|||
Heading
|
||||
</button>
|
||||
<div className="dropdown-content bg-white">
|
||||
{
|
||||
HEADINGS.map((type) => (
|
||||
<a className="dropitem m-0 p-0" href="#" key={type.label}>
|
||||
<StyleButton
|
||||
key={type.label}
|
||||
active={type.style === blockType}
|
||||
label={type.label}
|
||||
onToggle={props.onToggle}
|
||||
style={type.style}
|
||||
/>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
{HEADINGS.map((type) => (
|
||||
<a className="dropitem m-0 p-0" href="#" key={type.label}>
|
||||
<StyleButton
|
||||
key={type.label}
|
||||
active={type.style === blockType}
|
||||
label={type.label}
|
||||
onToggle={props.onToggle}
|
||||
style={type.style}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{BLOCK_TYPES.map((type) => (
|
||||
|
|
@ -150,7 +148,9 @@ const InlineStyleControls = (props) => {
|
|||
class DraftEditor extends React.Component {
|
||||
constructor(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.onChange = (editorState) => {
|
||||
|
|
@ -224,7 +224,7 @@ class DraftEditor extends React.Component {
|
|||
<BlockStyleControls editorState={editorState} onToggle={this.toggleBlockType} />
|
||||
<InlineStyleControls editorState={editorState} onToggle={this.toggleInlineStyle} />
|
||||
</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
|
||||
blockStyleFn={getBlockStyle}
|
||||
customStyleMap={styleMap}
|
||||
|
|
|
|||
|
|
@ -1,134 +1,70 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue, validateWidget } from '@/_helpers/utils';
|
||||
import SelectSearch, { fuzzySearch } from 'react-select-search';
|
||||
|
||||
export const DropDown = function DropDown({
|
||||
id,
|
||||
height,
|
||||
component,
|
||||
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);
|
||||
}
|
||||
export const DropDown = function DropDown({ height, validate, properties, styles, setExposedVariable, fireEvent }) {
|
||||
const { label, value, display_values, values } = properties;
|
||||
const { visibility, disabledState } = styles;
|
||||
const [currentValue, setCurrentValue] = useState(() => value);
|
||||
|
||||
let selectOptions = [];
|
||||
|
||||
try {
|
||||
selectOptions = [
|
||||
...parsedValues.map((value, index) => {
|
||||
return { name: parsedDisplayValues[index], value: value };
|
||||
...values.map((value, index) => {
|
||||
return { name: display_values[index], value: value };
|
||||
}),
|
||||
];
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
const currentValueProperty = component.definition.properties.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 validationData = validate(value);
|
||||
const { isValid, validationError } = validationData;
|
||||
|
||||
const currentValidState = currentState?.components[component?.name]?.isValid;
|
||||
|
||||
if (currentValidState !== isValid) {
|
||||
onComponentOptionChanged(component, 'isValid', isValid);
|
||||
}
|
||||
useEffect(() => {
|
||||
setExposedVariable('isValid', isValid);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isValid]);
|
||||
|
||||
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);
|
||||
}, [newValue]);
|
||||
|
||||
useEffect(() => {
|
||||
onComponentOptionChanged(component, 'value', currentValue).then(() => onEvent('onSelect', { component }));
|
||||
setExposedVariable('value', newValue);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectOptions.some((e) => e.value === newValue)) {
|
||||
setCurrentValue(newValue);
|
||||
} else {
|
||||
setCurrentValue(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [values]);
|
||||
}, [JSON.stringify(values)]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dropdown-widget row g-0"
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div className="col-auto my-auto">
|
||||
<label style={{ marginRight: label !== '' ? '1rem' : '0.001rem' }} className="form-label py-1">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col px-0 h-100">
|
||||
<SelectSearch
|
||||
disabled={parsedDisabledState}
|
||||
options={selectOptions}
|
||||
value={currentValue}
|
||||
search={true}
|
||||
onChange={(newVal) => {
|
||||
setCurrentValue(newVal);
|
||||
}}
|
||||
filterOptions={fuzzySearch}
|
||||
placeholder="Select.."
|
||||
/>
|
||||
<>
|
||||
<div className="dropdown-widget row g-0" style={{ height, display: visibility ? '' : 'none' }}>
|
||||
<div className="col-auto my-auto">
|
||||
<label style={{ marginRight: label !== '' ? '1rem' : '0.001rem' }} className="form-label py-1">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col px-0 h-100">
|
||||
<SelectSearch
|
||||
disabled={disabledState}
|
||||
options={selectOptions}
|
||||
value={currentValue}
|
||||
search={true}
|
||||
onChange={(newVal) => {
|
||||
setCurrentValue(newVal);
|
||||
setExposedVariable('value', newVal).then(() => fireEvent('onSelect'));
|
||||
}}
|
||||
filterOptions={fuzzySearch}
|
||||
placeholder="Select.."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`invalid-feedback ${isValid ? '' : 'd-flex'}`}>{validationError}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
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 }) => {
|
||||
//* properties definitions
|
||||
|
|
@ -110,7 +110,7 @@ export const FilePicker = ({ width, height, component, currentState, onComponent
|
|||
reader.onerror = (error) => {
|
||||
reject(error);
|
||||
if (error.name == 'NotReadableError') {
|
||||
toast.error(error.message, { hideProgressBar: true, autoClose: 3000 });
|
||||
toast.error(error.message);
|
||||
}
|
||||
};
|
||||
}).then((result) => {
|
||||
|
|
@ -166,9 +166,7 @@ export const FilePicker = ({ width, height, component, currentState, onComponent
|
|||
}
|
||||
|
||||
if (fileRejections.length > 0) {
|
||||
fileRejections.map((rejectedFile) =>
|
||||
toast.error(rejectedFile.errors[0].message, { hideProgressBar: true, autoClose: 3000 })
|
||||
);
|
||||
fileRejections.map((rejectedFile) => toast.error(rejectedFile.errors[0].message));
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -1,41 +1,18 @@
|
|||
import React from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
|
||||
export const Image = function Image({ id, height, component, onComponentClick, currentState }) {
|
||||
const source = component.definition.properties.source.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 data = resolveReferences(source, currentState, null);
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
if (data === '') data = null;
|
||||
export const Image = function Image({ height, properties, styles }) {
|
||||
const source = properties.source;
|
||||
const widgetVisibility = styles.visibility ?? true;
|
||||
|
||||
function Placeholder() {
|
||||
return <div className="skeleton-image" style={{ objectFit: 'contain', height }}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
style={{ display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div data-disabled={styles.disabledState} style={{ display: widgetVisibility ? '' : 'none' }}>
|
||||
<LazyLoad height={height} placeholder={<Placeholder />} debounce={500}>
|
||||
<img style={{ objectFit: 'contain' }} src={data} height={height} />
|
||||
<img src={source} height={height} />
|
||||
</LazyLoad>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const Map = function Map({
|
|||
onComponentOptionChanged,
|
||||
onComponentOptionsChanged,
|
||||
onEvent,
|
||||
canvasWidth
|
||||
canvasWidth,
|
||||
}) {
|
||||
const center = component.definition.properties.initialLocation.value;
|
||||
const defaultMarkerValue = component.definition.properties.defaultMarkers.value;
|
||||
|
|
|
|||
|
|
@ -4,53 +4,55 @@ import Button from 'react-bootstrap/Button';
|
|||
import { SubCustomDragLayer } from '../SubCustomDragLayer';
|
||||
import { SubContainer } from '../SubContainer';
|
||||
import { ConfigHandle } from '../ConfigHandle';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const Modal = function Modal({ id, component, height, containerProps, currentState, darkMode }) {
|
||||
const [show, showModal] = useState(false);
|
||||
export const Modal = function Modal({
|
||||
id,
|
||||
component,
|
||||
height,
|
||||
containerProps,
|
||||
darkMode,
|
||||
properties,
|
||||
styles,
|
||||
exposedVariables,
|
||||
setExposedVariable,
|
||||
}) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const parentRef = useRef(null);
|
||||
|
||||
const titleProp = component.definition.properties.title;
|
||||
const title = titleProp ? titleProp.value : '';
|
||||
const title = properties.title ?? '';
|
||||
const size = properties.size ?? 'lg';
|
||||
|
||||
const sizeProp = component.definition.properties.size;
|
||||
const size = sizeProp ? sizeProp.value : 'lg';
|
||||
|
||||
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
||||
|
||||
const parsedDisabledState =
|
||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||
const { disabledState } = styles;
|
||||
|
||||
useEffect(() => {
|
||||
const componentState = containerProps.currentState.components[component.name];
|
||||
const canShowModel = componentState ? componentState.show : false;
|
||||
showModal(canShowModel);
|
||||
const canShowModal = exposedVariables.show ?? false;
|
||||
setShowModal(canShowModal);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [containerProps.currentState.components[component.name]]);
|
||||
}, [exposedVariables.show]);
|
||||
|
||||
function hideModal() {
|
||||
containerProps.onComponentOptionChanged(component, 'show', false);
|
||||
showModal(false);
|
||||
setExposedVariable('show', false);
|
||||
setShowModal(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-disabled={parsedDisabledState}>
|
||||
<div data-disabled={disabledState}>
|
||||
<BootstrapModal
|
||||
contentClassName="modal-component"
|
||||
show={show}
|
||||
show={showModal}
|
||||
container={document.getElementsByClassName('canvas-area')[0]}
|
||||
size={size}
|
||||
backdrop={true}
|
||||
keyboard={true}
|
||||
enforceFocus={false}
|
||||
animation={false}
|
||||
onEscapeKeyDown={() => showModal(false)}
|
||||
onEscapeKeyDown={() => setShowModal(false)}
|
||||
>
|
||||
{containerProps.mode === 'edit' && (
|
||||
<ConfigHandle id={id} component={component} configHandleClicked={containerProps.onComponentClick} />
|
||||
)}
|
||||
<BootstrapModal.Header>
|
||||
<BootstrapModal.Title>{resolveWidgetFieldValue(title, currentState)}</BootstrapModal.Title>
|
||||
<BootstrapModal.Title>{title}</BootstrapModal.Title>
|
||||
<div>
|
||||
<Button variant={darkMode ? 'secondary' : 'light'} size="sm" onClick={hideModal}>
|
||||
x
|
||||
|
|
|
|||
|
|
@ -1,66 +1,65 @@
|
|||
import _ from 'lodash';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import SelectSearch, { fuzzySearch } from 'react-select-search';
|
||||
|
||||
export const Multiselect = function Multiselect({
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
component,
|
||||
onComponentClick,
|
||||
currentState,
|
||||
onComponentOptionChanged,
|
||||
|
||||
properties,
|
||||
styles,
|
||||
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;
|
||||
const values = component.definition.properties.option_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;
|
||||
useEffect(() => {
|
||||
let newValues = [];
|
||||
|
||||
const parsedDisabledState =
|
||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||
if (_.intersection(values, value)?.length === value?.length) newValues = value;
|
||||
|
||||
const parsedValues = JSON.parse(values);
|
||||
const parsedDisplayValues = JSON.parse(displayValues);
|
||||
setExposedVariable('values', newValues);
|
||||
setCurrentValue(newValues);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(values)]);
|
||||
|
||||
const selectOptions = [
|
||||
...parsedValues.map((value, index) => {
|
||||
return { name: parsedDisplayValues[index], value: 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;
|
||||
useEffect(() => {
|
||||
setExposedVariable('values', value);
|
||||
setCurrentValue(value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(value)]);
|
||||
|
||||
const [currentValue, setCurrentValue] = useState(() => value);
|
||||
let selectOptions = [];
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
selectOptions = [
|
||||
...values.map((value, index) => {
|
||||
return { name: display_values[index], value: value };
|
||||
}),
|
||||
];
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentValue(newValue);
|
||||
}, [newValue]);
|
||||
if (value && !currentValue) {
|
||||
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 (
|
||||
<div
|
||||
className="multiselect-widget row g-0"
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div className="multiselect-widget row g-0" style={{ height, display: visibility ? '' : 'none' }}>
|
||||
<div className="col-auto my-auto">
|
||||
<label style={{ marginRight: '1rem' }} className="form-label py-1">
|
||||
{label}
|
||||
|
|
@ -68,14 +67,14 @@ export const Multiselect = function Multiselect({
|
|||
</div>
|
||||
<div className="col px-0 h-100">
|
||||
<SelectSearch
|
||||
disabled={parsedDisabledState}
|
||||
disabled={disabledState}
|
||||
options={selectOptions}
|
||||
value={currentValue}
|
||||
search={true}
|
||||
multiple={true}
|
||||
printOptions="on-focus"
|
||||
onChange={(newValues) => {
|
||||
onComponentOptionChanged(component, 'values', newValues);
|
||||
handleChange(newValues);
|
||||
}}
|
||||
filterOptions={fuzzySearch}
|
||||
placeholder="Select.."
|
||||
|
|
|
|||
|
|
@ -1,60 +1,22 @@
|
|||
import React, { useEffect, useState } 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, '');
|
||||
}
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
export const NumberInput = function NumberInput({ height, properties, exposedVariables, styles, setExposedVariable }) {
|
||||
useEffect(() => {
|
||||
setNumber(parseInt(newNumber));
|
||||
onComponentOptionChanged(component, 'value', parseInt(newNumber));
|
||||
setExposedVariable('value', properties.value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [newNumber]);
|
||||
|
||||
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);
|
||||
}
|
||||
}, [properties.value]);
|
||||
|
||||
return (
|
||||
<input
|
||||
disabled={parsedDisabledState}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
disabled={styles.disabledState}
|
||||
onChange={(e) => {
|
||||
setNumber(parseInt(e.target.value));
|
||||
onComponentOptionChanged(component, 'value', parseInt(e.target.value));
|
||||
setExposedVariable('value', parseInt(e.target.value));
|
||||
}}
|
||||
type="number"
|
||||
className="form-control rounded-0"
|
||||
placeholder={placeholder}
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
value={number}
|
||||
placeholder={properties.placeholder}
|
||||
style={{ height, display: styles.visibility ? '' : 'none' }}
|
||||
value={exposedVariables.value}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,56 +1,40 @@
|
|||
import React, { useState } from 'react';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import React from 'react';
|
||||
|
||||
export const PasswordInput = ({
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
component,
|
||||
onComponentClick,
|
||||
currentState,
|
||||
onComponentOptionChanged,
|
||||
validate,
|
||||
properties,
|
||||
styles,
|
||||
exposedVariables,
|
||||
setExposedVariable,
|
||||
}) => {
|
||||
const value = currentState?.components[component?.name]?.value;
|
||||
const [text, setText] = useState(() => value ?? '');
|
||||
const value = exposedVariables.value;
|
||||
const { visibility, disabledState } = styles;
|
||||
const placeholder = properties.placeholder;
|
||||
|
||||
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 currentValidState = currentState?.components[component?.name]?.isValid;
|
||||
const currentValidState = exposedVariables.isValid;
|
||||
|
||||
const validationData = validate(value);
|
||||
|
||||
const { isValid, validationError } = validationData;
|
||||
|
||||
if (currentValidState !== isValid) {
|
||||
onComponentOptionChanged(component, 'isValid', isValid);
|
||||
setExposedVariable('isValid', isValid);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
disabled={parsedDisabledState}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component);
|
||||
}}
|
||||
disabled={disabledState}
|
||||
onChange={(e) => {
|
||||
setText(e.target.value);
|
||||
onComponentOptionChanged(component, 'value', e.target.value);
|
||||
setExposedVariable('value', e.target.value);
|
||||
}}
|
||||
type={'password'}
|
||||
className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon rounded-0`}
|
||||
placeholder={placeholder}
|
||||
value={text}
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
value={exposedVariables.value}
|
||||
style={{ height, display: visibility ? '' : 'none' }}
|
||||
/>
|
||||
|
||||
<div className="invalid-feedback">{validationError}</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import React, { useState } from 'react';
|
||||
import QrReader from 'react-qr-reader';
|
||||
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) => {
|
||||
console.log(errorMessage);
|
||||
await setErrorOccured(true);
|
||||
|
|
@ -11,29 +10,17 @@ export const QrScanner = function QrScanner({ component, onEvent, onComponentOpt
|
|||
|
||||
const handleScan = async (data) => {
|
||||
if (data !== null || data !== undefined) {
|
||||
await onEvent('onDetect', { component, data: data });
|
||||
await onComponentOptionChanged(component, 'lastDetectedValue', data);
|
||||
await fireEvent('onDetect');
|
||||
await setExposedVariable('lastDetectedValue', data);
|
||||
}
|
||||
};
|
||||
|
||||
let [errorOccured, setErrorOccured] = useState(false);
|
||||
const [errorOccured, setErrorOccured] = useState(false);
|
||||
|
||||
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 { visibility, disabledState } = styles;
|
||||
|
||||
return (
|
||||
<div data-disabled={parsedDisabledState} style={{ display: parsedWidgetVisibility ? '' : 'none' }}>
|
||||
<div data-disabled={disabledState} style={{ display: visibility ? '' : 'none' }}>
|
||||
{errorOccured ? <ErrorModal /> : <QrReader onError={handleError} onScan={handleScan} />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,94 +1,41 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const RadioButton = function RadioButton({
|
||||
id,
|
||||
height,
|
||||
component,
|
||||
onComponentClick,
|
||||
currentState,
|
||||
onComponentOptionChanged,
|
||||
onEvent,
|
||||
properties,
|
||||
styles,
|
||||
fireEvent,
|
||||
exposedVariables,
|
||||
setExposedVariable,
|
||||
}) {
|
||||
const label = component.definition.properties.label.value;
|
||||
const textColorProperty = component.definition.styles.textColor;
|
||||
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;
|
||||
const { label, value, values, display_values } = properties;
|
||||
const { visibility, disabledState, textColor } = styles;
|
||||
|
||||
let selectOptions = [];
|
||||
|
||||
try {
|
||||
selectOptions = [
|
||||
...parsedValues.map((value, index) => {
|
||||
return { name: parsedDisplayValues[index], value: value };
|
||||
...values.map((value, index) => {
|
||||
return { name: display_values[index], value: value };
|
||||
}),
|
||||
];
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
function onSelect(selection) {
|
||||
onComponentOptionChanged(component, 'value', selection);
|
||||
onEvent('onSelectionChange', { component });
|
||||
setExposedVariable('value', selection);
|
||||
fireEvent('onSelectionChange');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onComponentOptionChanged(component, 'value', parsedDefaultValue);
|
||||
setExposedVariable('value', value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [parsedDefaultValue]);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
className="row py-1"
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div data-disabled={disabledState} className="row py-1" style={{ height, display: visibility ? '' : 'none' }}>
|
||||
<span className="form-check-label col-auto py-0" style={{ color: textColor }}>
|
||||
{label}
|
||||
</span>
|
||||
|
|
@ -98,7 +45,7 @@ export const RadioButton = function RadioButton({
|
|||
<input
|
||||
style={{ marginTop: '1px' }}
|
||||
className="form-check-input"
|
||||
checked={value === option.value}
|
||||
checked={exposedVariables.value === option.value}
|
||||
type="radio"
|
||||
value={option.value}
|
||||
name={`${id}-radio-options`}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,24 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import 'draft-js/dist/Draft.css';
|
||||
import { DraftEditor } from './DraftEditor';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const RichTextEditor = function RichTextEditor({
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
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 ?? '';
|
||||
export const RichTextEditor = function RichTextEditor({ width, height, properties, styles, setExposedVariable }) {
|
||||
const { visibility, disabledState } = styles;
|
||||
const placeholder = properties.placeholder;
|
||||
const defaultValue = properties?.defaultValue ?? '';
|
||||
|
||||
const parsedDisabledState =
|
||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
// exposing the default value at first
|
||||
useEffect(() => {
|
||||
setExposedVariable('value', defaultValue);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function handleChange(html) {
|
||||
onComponentOptionChanged(component, 'value', html);
|
||||
setExposedVariable('value', html);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
style={{ height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div data-disabled={disabledState} style={{ height: `${height}px`, display: visibility ? '' : 'none' }}>
|
||||
<DraftEditor
|
||||
handleChange={handleChange}
|
||||
height={height}
|
||||
|
|
|
|||
|
|
@ -2,42 +2,18 @@ import '@/_styles/widgets/star-rating.scss';
|
|||
|
||||
import React from 'react';
|
||||
import { useTrail } from 'react-spring';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
import Star from './star';
|
||||
|
||||
export const StarRating = function StarRating({
|
||||
id,
|
||||
component,
|
||||
onComponentClick,
|
||||
onComponentOptionChanged,
|
||||
currentState,
|
||||
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;
|
||||
export const StarRating = function StarRating({ properties, styles, fireEvent, setExposedVariable }) {
|
||||
const label = properties.label;
|
||||
const defaultSelected = properties.defaultSelected ?? 5;
|
||||
const maxRating = properties.maxRating ?? 5;
|
||||
const allowHalfStar = properties.allowHalfStar ?? false;
|
||||
const tooltips = properties.tooltips;
|
||||
|
||||
const parsedDisabledState =
|
||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||
|
||||
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 { visibility, disabledState, textColorProperty, labelColor } = styles;
|
||||
const color = textColorProperty ?? '#ffb400';
|
||||
|
||||
const animatedStars = useTrail(maxRating, {
|
||||
config: {
|
||||
|
|
@ -58,21 +34,21 @@ export const StarRating = function StarRating({
|
|||
|
||||
React.useEffect(() => {
|
||||
setRatingIndex(defaultSelected - 1);
|
||||
onComponentOptionChanged(component, 'value', defaultSelected);
|
||||
setExposedVariable('value', defaultSelected);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultSelected]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
onComponentOptionChanged(component, 'value', defaultSelected);
|
||||
setExposedVariable('value', defaultSelected);
|
||||
}, 1000);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function handleClick(idx) {
|
||||
// +1 cos code is considering index from 0,1,2.....
|
||||
onComponentOptionChanged(component, 'value', idx + 1);
|
||||
onEvent('onChange', { component });
|
||||
setExposedVariable('value', idx + 1);
|
||||
fireEvent('onChange');
|
||||
}
|
||||
|
||||
const getActive = (index) => {
|
||||
|
|
@ -86,20 +62,12 @@ export const StarRating = function StarRating({
|
|||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
className="star-rating"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
style={{ display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
>
|
||||
<div data-disabled={disabledState} className="star-rating" style={{ display: visibility ? '' : 'none' }}>
|
||||
{/* TODO: Add label color defination property instead of hardcoded color*/}
|
||||
<span className="label form-check-label col-auto" style={{ color: labelColor }}>
|
||||
{label}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,31 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export const Text = function Text({ id, height, component, onComponentClick, currentState }) {
|
||||
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;
|
||||
|
||||
export const Text = function Text({ height, properties, styles }) {
|
||||
const [loadingState, setLoadingState] = useState(false);
|
||||
|
||||
const { textColor, visibility, disabledState } = styles;
|
||||
const text = properties.text ?? '';
|
||||
const color = textColor;
|
||||
|
||||
useEffect(() => {
|
||||
const loadingStateProperty = component.definition.properties.loadingState;
|
||||
if (loadingStateProperty && currentState) {
|
||||
const newState = resolveReferences(loadingStateProperty.value, currentState, false);
|
||||
setLoadingState(newState);
|
||||
const loadingStateProperty = properties.loadingState;
|
||||
if (loadingStateProperty) {
|
||||
setLoadingState(loadingStateProperty);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentState]);
|
||||
|
||||
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);
|
||||
}
|
||||
}, [properties.loadingState]);
|
||||
|
||||
const computedStyles = {
|
||||
color,
|
||||
height,
|
||||
display: parsedWidgetVisibility ? 'flex' : 'none',
|
||||
display: visibility ? 'flex' : 'none',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
className="text-widget"
|
||||
style={computedStyles}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
{!loadingState && <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) }} />}
|
||||
<div data-disabled={disabledState} className="text-widget" style={computedStyles}>
|
||||
{!loadingState && <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text) }} />}
|
||||
{loadingState === true && (
|
||||
<div>
|
||||
<div className="skeleton-line w-10"></div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ export const TextArea = function TextArea({ width, height, properties, exposedVa
|
|||
type="text"
|
||||
className="form-control"
|
||||
placeholder={properties.placeholder}
|
||||
style={{ height, resize: 'none', display: styles.visibility ? '' : 'none' }}
|
||||
style={{
|
||||
height,
|
||||
resize: 'none',
|
||||
display: styles.visibility ? '' : 'none',
|
||||
}}
|
||||
value={exposedVariables.value}
|
||||
></textarea>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,71 +1,34 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const TextInput = function TextInput({
|
||||
id,
|
||||
height,
|
||||
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, '');
|
||||
}
|
||||
export const TextInput = function TextInput({ height, validate, properties, styles, setExposedVariable, fireEvent }) {
|
||||
const [value, setValue] = useState(properties.value);
|
||||
const { isValid, validationError } = validate(value);
|
||||
|
||||
useEffect(() => {
|
||||
setText(newText);
|
||||
onComponentOptionChanged(component, 'value', newText);
|
||||
setExposedVariable('isValid', isValid);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [newText]);
|
||||
}, [isValid]);
|
||||
|
||||
const validationData = validate(value);
|
||||
|
||||
const { isValid, validationError } = validationData;
|
||||
|
||||
if (currentValidState !== isValid) {
|
||||
onComponentOptionChanged(component, 'isValid', isValid);
|
||||
}
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
useEffect(() => {
|
||||
setValue(properties.value);
|
||||
setExposedVariable('value', properties.value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [properties.value]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
disabled={parsedDisabledState}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
disabled={styles.disabledState}
|
||||
onChange={(e) => {
|
||||
setText(e.target.value);
|
||||
onComponentOptionChanged(component, 'value', e.target.value);
|
||||
setValue(e.target.value);
|
||||
setExposedVariable('value', e.target.value);
|
||||
fireEvent('onChange');
|
||||
}}
|
||||
type="text"
|
||||
className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon`}
|
||||
placeholder={placeholder}
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
value={text}
|
||||
placeholder={properties.placeholder}
|
||||
style={{ height, display: styles.visibility ? '' : 'none' }}
|
||||
value={value}
|
||||
/>
|
||||
<div className="invalid-feedback">{validationError}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
class Switch extends React.Component {
|
||||
render() {
|
||||
|
|
@ -8,7 +7,10 @@ class Switch extends React.Component {
|
|||
return (
|
||||
<label className="form-switch form-check-inline">
|
||||
<input
|
||||
style={{ backgroundColor: on ? `${color}` : 'white', marginTop: '0px' }}
|
||||
style={{
|
||||
backgroundColor: on ? `${color}` : 'white',
|
||||
marginTop: '0px',
|
||||
}}
|
||||
disabled={disabledState}
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
|
|
@ -21,58 +23,35 @@ class Switch extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export const ToggleSwitch = ({
|
||||
id,
|
||||
height,
|
||||
component,
|
||||
onComponentClick,
|
||||
currentState,
|
||||
onComponentOptionChanged,
|
||||
onEvent,
|
||||
}) => {
|
||||
export const ToggleSwitch = ({ height, properties, styles, fireEvent, setExposedVariable }) => {
|
||||
const [on, setOn] = React.useState(false);
|
||||
const label = component.definition.properties.label.value;
|
||||
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;
|
||||
const label = properties.label;
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
try {
|
||||
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
const { visibility, disabledState, toggleSwitchColor, textColor } = styles;
|
||||
|
||||
function toggleValue(e) {
|
||||
const toggled = e.target.checked;
|
||||
onComponentOptionChanged(component, 'value', toggled);
|
||||
onEvent('onChange', { component });
|
||||
setExposedVariable('value', toggled);
|
||||
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);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="row py-1"
|
||||
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component, event);
|
||||
}}
|
||||
>
|
||||
<div className="row py-1" style={{ height, display: visibility ? '' : 'none' }}>
|
||||
<span className="form-check-label form-check-label col-auto my-auto" style={{ color: textColor }}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="col px-1 py-0 mt-0">
|
||||
<Switch
|
||||
disabledState={parsedDisabledState}
|
||||
disabledState={disabledState}
|
||||
on={on}
|
||||
onClick={toggle}
|
||||
onChange={toggleValue}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export const componentTypes = [
|
|||
properties: {
|
||||
title: { value: 'Table' },
|
||||
visible: { value: true },
|
||||
loadingState: { value: false },
|
||||
loadingState: { value: '{{false}}' },
|
||||
data: {
|
||||
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] }}",
|
||||
|
|
@ -286,7 +286,9 @@ export const componentTypes = [
|
|||
maxLength: { type: 'code', displayName: 'Max length' },
|
||||
customRule: { type: 'code', displayName: 'Custom validation' },
|
||||
},
|
||||
events: {},
|
||||
events: {
|
||||
onChange: { displayName: 'On change' },
|
||||
},
|
||||
styles: {
|
||||
visibility: { type: 'code', displayName: 'Visibility' },
|
||||
disabledState: { type: 'code', displayName: 'Disable' },
|
||||
|
|
@ -576,7 +578,9 @@ export const componentTypes = [
|
|||
visibility: { type: 'code', displayName: 'Visibility' },
|
||||
disabledState: { type: 'code', displayName: 'Disable' },
|
||||
},
|
||||
exposedVariables: {},
|
||||
exposedVariables: {
|
||||
value: false,
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: true },
|
||||
|
|
@ -644,7 +648,7 @@ export const componentTypes = [
|
|||
description: 'Select a date range',
|
||||
component: 'DaterangePicker',
|
||||
defaultSize: {
|
||||
width: 8,
|
||||
width: 10,
|
||||
height: 30,
|
||||
},
|
||||
others: {
|
||||
|
|
@ -887,9 +891,9 @@ export const componentTypes = [
|
|||
},
|
||||
properties: {
|
||||
label: { value: 'Select' },
|
||||
values: { value: '[]' },
|
||||
option_values: { value: '[1,2,3]' },
|
||||
display_values: { value: '["one", "two", "three"]' },
|
||||
value: { value: '{{[2,3]}}' },
|
||||
values: { value: '{{[1,2,3]}}' },
|
||||
display_values: { value: '{{["one", "two", "three"]}}' },
|
||||
visible: { value: true },
|
||||
},
|
||||
events: [],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import Comments from './Comments';
|
|||
import { commentsService } from '@/_services';
|
||||
import config from 'config';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
function uuidv4() {
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||
|
|
@ -41,6 +42,8 @@ export const Container = ({
|
|||
showComments,
|
||||
appVersionsId,
|
||||
socket,
|
||||
handleUndo,
|
||||
handleRedo,
|
||||
}) => {
|
||||
const styles = {
|
||||
width: currentLayout === 'mobile' ? deviceWindowWidth : '100%',
|
||||
|
|
@ -59,6 +62,9 @@ export const Container = ({
|
|||
const [newThread, addNewThread] = useState({});
|
||||
const router = useRouter();
|
||||
|
||||
useHotkeys('⌘+z, control+z', () => handleUndo());
|
||||
useHotkeys('⌘+shift+z, control+shift+z', () => handleRedo());
|
||||
|
||||
useEffect(() => {
|
||||
setBoxes(components);
|
||||
}, [components]);
|
||||
|
|
@ -303,7 +309,7 @@ export const Container = ({
|
|||
const handleAddThread = async (e) => {
|
||||
e.stopPropogation && e.stopPropogation();
|
||||
|
||||
const x = (e.nativeEvent.offsetX) * 100 / canvasWidth;
|
||||
const x = (e.nativeEvent.offsetX * 100) / canvasWidth;
|
||||
|
||||
const elementIndex = commentsPreviewList.length;
|
||||
setCommentsPreviewList([
|
||||
|
|
@ -405,7 +411,7 @@ export const Container = ({
|
|||
<div
|
||||
key={index}
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout,
|
|||
if (id) {
|
||||
// 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);
|
||||
} else {
|
||||
// New component being dragged from components sidebar
|
||||
|
|
@ -47,7 +47,7 @@ function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout,
|
|||
|
||||
x += realCanvasDelta;
|
||||
|
||||
console.log('cvv', canvasWidth, x)
|
||||
console.log('cvv', canvasWidth, x);
|
||||
|
||||
// x = (x * canvasWidth) / 100;
|
||||
|
||||
|
|
@ -83,7 +83,9 @@ export const CustomDragLayer = ({ canvasWidth, currentLayout }) => {
|
|||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
||||
|
|
@ -9,10 +9,13 @@ import GraphqlSchema from './Api/Graphql.schema.json';
|
|||
import StripeSchema from './Api/Stripe.schema.json';
|
||||
import GooglesheetSchema from './Api/Googlesheets.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
|
||||
import DynamodbSchema from './Database/Dynamodb.schema.json';
|
||||
import ElasticsearchSchema from './Database/Elasticsearch.schema.json';
|
||||
import TypeSenseSchema from './Database/TypeSense.schema.json';
|
||||
import RedisSchema from './Database/Redis.schema.json';
|
||||
import FirestoreSchema from './Database/Firestore.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 Dynamodb = ({ ...rest }) => <DynamicForm schema={DynamodbSchema} {...rest} />;
|
||||
const Elasticsearch = ({ ...rest }) => <DynamicForm schema={ElasticsearchSchema} {...rest} />;
|
||||
const Typesense = ({ ...rest }) => <DynamicForm schema={TypeSenseSchema} {...rest} />;
|
||||
const Redis = ({ ...rest }) => <DynamicForm schema={RedisSchema} {...rest} />;
|
||||
const Firestore = ({ ...rest }) => <DynamicForm schema={FirestoreSchema} {...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 S3 = ({ ...rest }) => <DynamicForm schema={S3Schema} {...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 = [
|
||||
DynamodbSchema.source,
|
||||
ElasticsearchSchema.source,
|
||||
TypeSenseSchema.source,
|
||||
RedisSchema.source,
|
||||
FirestoreSchema.source,
|
||||
MongodbSchema.source,
|
||||
|
|
@ -61,6 +68,8 @@ export const ApiSources = [
|
|||
StripeSchema.source,
|
||||
GooglesheetSchema.source,
|
||||
SlackSchema.source,
|
||||
TwilioSchema.source,
|
||||
SendgridSchema.source,
|
||||
];
|
||||
|
||||
export const OtherSources = [RunjsSchema.source];
|
||||
|
|
@ -69,6 +78,7 @@ export const DataSourceTypes = [...DataBaseSources, ...ApiSources, ...CloudStora
|
|||
|
||||
export const SourceComponents = {
|
||||
Elasticsearch,
|
||||
Typesense,
|
||||
Redis,
|
||||
Postgresql,
|
||||
Stripe,
|
||||
|
|
@ -84,4 +94,6 @@ export const SourceComponents = {
|
|||
Mssql,
|
||||
S3,
|
||||
Gcs,
|
||||
Twilio,
|
||||
Sendgrid,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ export const DraggableBox = function DraggableBox({
|
|||
id,
|
||||
mode,
|
||||
title,
|
||||
left,
|
||||
top,
|
||||
_left,
|
||||
_top,
|
||||
parent,
|
||||
component,
|
||||
index,
|
||||
|
|
@ -80,7 +80,7 @@ export const DraggableBox = function DraggableBox({
|
|||
removeComponent,
|
||||
currentLayout,
|
||||
layouts,
|
||||
deviceWindowWidth,
|
||||
_deviceWindowWidth,
|
||||
isSelectedComponent,
|
||||
draggingStatusChanged,
|
||||
darkMode,
|
||||
|
|
@ -134,10 +134,10 @@ export const DraggableBox = function DraggableBox({
|
|||
padding: '0px',
|
||||
};
|
||||
|
||||
let refProps = {};
|
||||
let _refProps = {};
|
||||
|
||||
if (mode === 'edit' && canDrag) {
|
||||
refProps = {
|
||||
_refProps = {
|
||||
ref: drag,
|
||||
};
|
||||
}
|
||||
|
|
@ -162,7 +162,7 @@ export const DraggableBox = function DraggableBox({
|
|||
}, [layoutData.height, layoutData.width, layoutData.left, layoutData.top, currentLayout]);
|
||||
|
||||
const gridWidth = canvasWidth / 43;
|
||||
const width = (canvasWidth * currentLayoutOptions.width) / 43
|
||||
const width = (canvasWidth * currentLayoutOptions.width) / 43;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -194,14 +194,14 @@ export const DraggableBox = function DraggableBox({
|
|||
onDrag={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
setDragging(true)
|
||||
setDragging(true);
|
||||
}}
|
||||
resizeHandleClasses={isSelectedComponent || mouseOver ? resizerClasses : {}}
|
||||
resizeHandleStyles={resizerStyles}
|
||||
disableDragging={mode !== 'edit'}
|
||||
onDragStop={(e, direction) => {
|
||||
setDragging(false)
|
||||
onDragStop(e, id, direction, currentLayout, currentLayoutOptions)
|
||||
setDragging(false);
|
||||
onDragStop(e, id, direction, currentLayout, currentLayoutOptions);
|
||||
}}
|
||||
cancel={`div.table-responsive.jet-data-table, div.calendar-widget`}
|
||||
onDragStart={(e) => e.stopPropagation()}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { createRef } from 'react';
|
|||
import { datasourceService, dataqueryService, appService, authenticationService } from '@/_services';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { defaults } from 'lodash';
|
||||
import { defaults, cloneDeep, isEqual, isEmpty } from 'lodash';
|
||||
import { Container } from './Container';
|
||||
import { CustomDragLayer } from './CustomDragLayer';
|
||||
import { LeftSidebar } from './LeftSidebar';
|
||||
|
|
@ -10,7 +10,6 @@ import { componentTypes } from './Components/components';
|
|||
import { Inspector } from './Inspector/Inspector';
|
||||
import { DataSourceTypes } from './DataSourceManager/SourceComponents';
|
||||
import { QueryManager } from './QueryManager';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ManageAppUsers } from './ManageAppUsers';
|
||||
import { SaveAndPreview } from './SaveAndPreview';
|
||||
|
|
@ -31,11 +30,15 @@ import { WidgetManager } from './WidgetManager';
|
|||
import Fuse from 'fuse.js';
|
||||
import config from 'config';
|
||||
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 EditIcon from './Icons/edit.svg';
|
||||
import MobileSelectedIcon from './Icons/mobile-selected.svg';
|
||||
import DesktopSelectedIcon from './Icons/desktop-selected.svg';
|
||||
|
||||
setAutoFreeze(false);
|
||||
enablePatches();
|
||||
class Editor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
@ -66,7 +69,8 @@ class Editor extends React.Component {
|
|||
currentUser: authenticationService.currentUserValue,
|
||||
app: {},
|
||||
allComponentTypes: componentTypes,
|
||||
queryPaneHeight: '30%',
|
||||
isQueryPaneDragging: false,
|
||||
queryPaneHeight: 70,
|
||||
isLoading: true,
|
||||
users: null,
|
||||
appId,
|
||||
|
|
@ -104,6 +108,8 @@ class Editor extends React.Component {
|
|||
this.fetchApp();
|
||||
this.fetchDataSources();
|
||||
this.fetchDataQueries();
|
||||
this.initComponentVersioning();
|
||||
this.initEventListeners();
|
||||
config.COMMENT_FEATURE_ENABLE && this.initWebSocket();
|
||||
this.setState({
|
||||
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() {
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
if (this.state.socket) {
|
||||
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 = () => {
|
||||
this.setState(
|
||||
{
|
||||
|
|
@ -305,13 +354,70 @@ class Editor extends React.Component {
|
|||
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) => {
|
||||
produce(
|
||||
this.state.appDefinition,
|
||||
(draft) => {
|
||||
draft.components = newDefinition.components;
|
||||
},
|
||||
this.handleAddPatch
|
||||
);
|
||||
this.setState({ appDefinition: newDefinition });
|
||||
computeComponentState(this, newDefinition.components);
|
||||
};
|
||||
|
||||
handleInspectorView = (component) => {
|
||||
if (this.state.selectedComponent.hasOwnProperty('component')) {
|
||||
if (this.state.selectedComponent?.hasOwnProperty('component')) {
|
||||
const { id: selectedComponentId } = this.state.selectedComponent;
|
||||
if (selectedComponentId === component.id) {
|
||||
this.setState({ selectedComponent: null });
|
||||
|
|
@ -325,8 +431,7 @@ class Editor extends React.Component {
|
|||
};
|
||||
|
||||
removeComponent = (component) => {
|
||||
let newDefinition = this.state.appDefinition;
|
||||
|
||||
let newDefinition = cloneDeep(this.state.appDefinition);
|
||||
// Delete child components when parent is deleted
|
||||
const childComponents = Object.keys(newDefinition.components).filter(
|
||||
(key) => newDefinition.components[key].parent === component.id
|
||||
|
|
@ -336,25 +441,31 @@ class Editor extends React.Component {
|
|||
});
|
||||
|
||||
delete newDefinition.components[component.id];
|
||||
toast('Component deleted! (⌘Z to undo)', {
|
||||
icon: '🗑️',
|
||||
});
|
||||
this.appDefinitionChanged(newDefinition);
|
||||
this.handleInspectorView(component);
|
||||
};
|
||||
|
||||
componentDefinitionChanged = (newDefinition) => {
|
||||
componentDefinitionChanged = (componentDefinition) => {
|
||||
let _self = this;
|
||||
|
||||
return setStateAsync(_self, {
|
||||
appDefinition: {
|
||||
...this.state.appDefinition,
|
||||
components: {
|
||||
...this.state.appDefinition.components,
|
||||
[newDefinition.id]: {
|
||||
...this.state.appDefinition.components[newDefinition.id],
|
||||
component: newDefinition.component,
|
||||
},
|
||||
},
|
||||
const newDefinition = {
|
||||
appDefinition: produce(this.state.appDefinition, (draft) => {
|
||||
draft.components[componentDefinition.id].component = componentDefinition.component;
|
||||
}),
|
||||
};
|
||||
|
||||
produce(
|
||||
this.state.appDefinition,
|
||||
(draft) => {
|
||||
draft.components[componentDefinition.id].component = componentDefinition.component;
|
||||
},
|
||||
});
|
||||
this.handleAddPatch
|
||||
);
|
||||
|
||||
return setStateAsync(_self, newDefinition);
|
||||
};
|
||||
|
||||
componentChanged = (newComponent) => {
|
||||
|
|
@ -384,16 +495,15 @@ class Editor extends React.Component {
|
|||
saveApp = (id, attributes, notify = false) => {
|
||||
appService.saveApp(id, attributes).then(() => {
|
||||
if (notify) {
|
||||
toast.success('App saved sucessfully', { hideProgressBar: true, position: 'top-center' });
|
||||
toast.success('App saved sucessfully');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
saveAppName = (id, name, notify = false) => {
|
||||
if (!name.trim()) {
|
||||
toast.warn("App name can't be empty or whitespace", {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
toast("App name can't be empty or whitespace", {
|
||||
icon: '🚨',
|
||||
});
|
||||
|
||||
this.setState({
|
||||
|
|
@ -440,13 +550,13 @@ class Editor extends React.Component {
|
|||
dataqueryService
|
||||
.del(this.state.selectedQuery.id)
|
||||
.then(() => {
|
||||
toast.success('Query Deleted', { hideProgressBar: true, position: 'bottom-center' });
|
||||
toast.success('Query Deleted');
|
||||
this.setState({ isDeletingDataQuery: false });
|
||||
this.dataQueriesChanged();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
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"
|
||||
onClick={() => {
|
||||
runQuery(this, dataQuery.id, dataQuery.name).then(() => {
|
||||
toast.info(`Query (${dataQuery.name}) completed.`, {
|
||||
hideProgressBar: true,
|
||||
position: 'bottom-center',
|
||||
toast(`Query (${dataQuery.name}) completed.`, {
|
||||
icon: '🚀',
|
||||
});
|
||||
});
|
||||
}}
|
||||
|
|
@ -534,20 +643,11 @@ class Editor extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
toggleQueryPaneHeight = () => {
|
||||
this.setState({
|
||||
queryPaneHeight: this.state.queryPaneHeight === '30%' ? '80%' : '30%',
|
||||
});
|
||||
};
|
||||
|
||||
toggleQueryEditor = () => {
|
||||
this.setState((prev) => ({ showQueryEditor: !prev.showQueryEditor }));
|
||||
this.toolTipRefHide.current.style.display = this.state.showQueryEditor ? 'none' : 'flex';
|
||||
this.toolTipRefShow.current.style.display = this.state.showQueryEditor ? 'flex' : 'none';
|
||||
};
|
||||
|
||||
toggleLeftSidebar = () => {
|
||||
this.setState({ showLeftSidebar: !this.state.showLeftSidebar });
|
||||
this.setState((prev) => ({
|
||||
showQueryEditor: !prev.showQueryEditor,
|
||||
queryPaneHeight: this.state.queryPaneHeight === 100 ? 30 : 100,
|
||||
}));
|
||||
};
|
||||
|
||||
toggleComments = () => {
|
||||
|
|
@ -565,7 +665,7 @@ class Editor extends React.Component {
|
|||
const results = fuse.search(value);
|
||||
this.setState({
|
||||
dataQueries: results.map((result) => result.item),
|
||||
dataQueriesDefaultText: results.length || 'No Queries found.',
|
||||
dataQueriesDefaultText: results.length ?? 'No Queries found.',
|
||||
});
|
||||
} else {
|
||||
this.fetchDataQueries();
|
||||
|
|
@ -591,8 +691,7 @@ class Editor extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
toolTipRefHide = createRef();
|
||||
toolTipRefShow = createRef();
|
||||
queryPaneRef = createRef();
|
||||
|
||||
getCanvasWidth = () => {
|
||||
const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0].getBoundingClientRect();
|
||||
|
|
@ -685,8 +784,9 @@ class Editor extends React.Component {
|
|||
<Logo />
|
||||
</Link>
|
||||
</h1>
|
||||
|
||||
{this.state.app && (
|
||||
<div className="app-name input-icon">
|
||||
<div className={`app-name input-icon ${this.props.darkMode ? 'dark' : ''}`}>
|
||||
<input
|
||||
type="text"
|
||||
onFocus={(e) => this.setState({ oldName: e.target.value })}
|
||||
|
|
@ -703,23 +803,6 @@ class Editor extends React.Component {
|
|||
{this.state.editingVersion && (
|
||||
<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">
|
||||
{this.renderLayoutIcon(currentLayout === 'desktop')}
|
||||
</div>
|
||||
|
|
@ -824,6 +907,8 @@ class Editor extends React.Component {
|
|||
}
|
||||
currentState={this.state.currentState}
|
||||
configHandleClicked={this.configHandleClicked}
|
||||
handleUndo={this.handleUndo}
|
||||
handleRedo={this.handleRedo}
|
||||
removeComponent={this.removeComponent}
|
||||
onComponentClick={(id, component) => {
|
||||
this.setState({ selectedComponent: { id, component } });
|
||||
|
|
@ -842,7 +927,7 @@ class Editor extends React.Component {
|
|||
<div
|
||||
className="query-pane"
|
||||
style={{
|
||||
height: showQueryEditor ? 0 : 40,
|
||||
height: 40,
|
||||
background: '#fff',
|
||||
padding: '8px 16px',
|
||||
display: 'flex',
|
||||
|
|
@ -851,14 +936,9 @@ class Editor extends React.Component {
|
|||
}}
|
||||
>
|
||||
<h5 className="mb-0">QUERIES</h5>
|
||||
<span
|
||||
onClick={this.props.toggleQueryEditor}
|
||||
className="cursor-pointer m-1"
|
||||
data-tip="Show query editor"
|
||||
>
|
||||
<span onClick={this.toggleQueryEditor} className="cursor-pointer m-1" data-tip="Show query editor">
|
||||
<svg
|
||||
style={{ transform: 'rotate(180deg)' }}
|
||||
onClick={this.toggleQueryEditor}
|
||||
width="18"
|
||||
height="10"
|
||||
viewBox="0 0 18 10"
|
||||
|
|
@ -876,11 +956,24 @@ class Editor extends React.Component {
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
ref={this.queryPaneRef}
|
||||
onTouchEnd={this.onMouseUp}
|
||||
onMouseDown={this.onMouseDown}
|
||||
className="query-pane"
|
||||
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%' : '',
|
||||
left: !showLeftSidebar ? '0' : '',
|
||||
cursor: this.state.isQueryPaneDragging ? 'row-resize' : 'default',
|
||||
}}
|
||||
>
|
||||
<div className="row main-row">
|
||||
|
|
@ -970,7 +1063,6 @@ class Editor extends React.Component {
|
|||
<QueryManager
|
||||
toggleQueryEditor={this.toggleQueryEditor}
|
||||
dataSources={dataSources}
|
||||
toggleQueryPaneHeight={this.toggleQueryPaneHeight}
|
||||
dataQueries={dataQueries}
|
||||
mode={editingQuery ? 'edit' : 'create'}
|
||||
selectedQuery={selectedQuery}
|
||||
|
|
@ -999,7 +1091,9 @@ class Editor extends React.Component {
|
|||
|
||||
{currentSidebarTab === 1 && (
|
||||
<div className="pages-container">
|
||||
{selectedComponent ? (
|
||||
{selectedComponent &&
|
||||
!isEmpty(appDefinition.components) &&
|
||||
!isEmpty(appDefinition.components[selectedComponent.id]) ? (
|
||||
<Inspector
|
||||
componentDefinitionChanged={this.componentDefinitionChanged}
|
||||
dataQueries={dataQueries}
|
||||
|
|
@ -1007,7 +1101,7 @@ class Editor extends React.Component {
|
|||
removeComponent={this.removeComponent}
|
||||
selectedComponentId={selectedComponent.id}
|
||||
currentState={currentState}
|
||||
allComponents={appDefinition.components}
|
||||
allComponents={cloneDeep(appDefinition.components)}
|
||||
key={selectedComponent.id}
|
||||
switchSidebarTab={this.switchSidebarTab}
|
||||
apps={apps}
|
||||
|
|
|
|||
29
frontend/src/Editor/ErrorBoundary.jsx
Normal 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;
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -102,6 +102,7 @@ class Chart extends React.Component {
|
|||
lineNumbers={false}
|
||||
className="chart-input pr-2"
|
||||
onChange={(value) => this.props.paramUpdated({ name: 'data' }, 'value', value, 'properties')}
|
||||
componentName={`widget/${this.props.component.component.name}::${chartType}`}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={column.name}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'key', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'key')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -189,6 +190,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={'Text color of the cell'}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'textColor', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'textColor')}
|
||||
/>
|
||||
</div>
|
||||
{column.isEditable && (
|
||||
|
|
@ -204,6 +206,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={''}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'regex', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'regex')}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-2">
|
||||
|
|
@ -216,6 +219,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={''}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'minLength', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'minLength')}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-2">
|
||||
|
|
@ -228,6 +232,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={''}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'maxLength', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'maxLength')}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-2">
|
||||
|
|
@ -240,6 +245,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={''}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'customRule', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'customRule')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -295,6 +301,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={'{{[1, 2, 3]}}'}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'values', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'values')}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-2">
|
||||
|
|
@ -307,6 +314,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={'{{["one", "two", "three"]}}'}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'labels', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'labels')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -327,6 +335,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={''}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'customRule', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'customRule')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -346,6 +355,7 @@ class Table extends React.Component {
|
|||
lineNumbers={false}
|
||||
placeholder={'DD-MM-YYYY'}
|
||||
onChange={(value) => this.onColumnItemChange(index, 'dateFormat', value)}
|
||||
componentName={this.getPopoverFieldSource(column.columnType, 'dateFormat')}
|
||||
/>
|
||||
</div>
|
||||
<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');
|
||||
};
|
||||
|
||||
getPopoverFieldSource = (column, field) =>
|
||||
`widget/${this.props.component.component.name}/${column ?? 'default'}::${field}`;
|
||||
|
||||
render() {
|
||||
const { dataQueries, component, paramUpdated, componentMeta, components, currentState, darkMode } = this.props;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,16 @@ import React from 'react';
|
|||
import { CodeHinter } from '../../CodeBuilder/CodeHinter';
|
||||
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 paramMeta = componentMeta[paramType][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 getfieldName = React.useMemo(() => {
|
||||
return param.name;
|
||||
}, [param]);
|
||||
|
||||
return (
|
||||
<div className={`mb-2 field ${options.className}`}>
|
||||
<ToolTip label={displayName} meta={paramMeta} />
|
||||
|
|
@ -25,6 +38,7 @@ export const Code = ({ param, definition, onChange, paramType, componentMeta, cu
|
|||
lineWrapping={true}
|
||||
className={options.className}
|
||||
onChange={(value) => handleCodeChanged(value)}
|
||||
componentName={`widget/${componentName}::${getfieldName}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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="col-auto"
|
||||
style={
|
||||
{
|
||||
float: 'right',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundColor: definition.value,
|
||||
border: `0.25px solid ${['#ffffff', '#fff', '#1f2936'].includes(definition.value) && '#c5c8c9'}`
|
||||
}
|
||||
}
|
||||
style={{
|
||||
float: 'right',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundColor: definition.value,
|
||||
border: `0.25px solid ${['#ffffff', '#fff', '#1f2936'].includes(definition.value) && '#c5c8c9'}`,
|
||||
}}
|
||||
></div>
|
||||
<div className="col">{definition.value}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export const EventManager = ({
|
|||
eventId: Object.keys(componentMeta.events)[0],
|
||||
actionId: 'show-alert',
|
||||
message: 'Hello world!',
|
||||
alertType: 'info',
|
||||
});
|
||||
eventsChanged(newEvents);
|
||||
}
|
||||
|
|
@ -159,6 +160,7 @@ export const EventManager = ({
|
|||
currentState={currentState}
|
||||
initialValue={event.message}
|
||||
onChange={(value) => handlerChanged(index, 'message', value)}
|
||||
usePortalEditor={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -185,6 +187,7 @@ export const EventManager = ({
|
|||
currentState={currentState}
|
||||
initialValue={event.url}
|
||||
onChange={(value) => handlerChanged(index, 'url', value)}
|
||||
usePortalEditor={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -241,6 +244,7 @@ export const EventManager = ({
|
|||
<CodeHinter
|
||||
currentState={currentState}
|
||||
onChange={(value) => handlerChanged(index, 'contentToCopy', value)}
|
||||
usePortalEditor={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -277,6 +281,7 @@ export const EventManager = ({
|
|||
initialValue={event.key}
|
||||
onChange={(value) => handlerChanged(index, 'key', value)}
|
||||
enablePreview={true}
|
||||
usePortalEditor={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -288,6 +293,7 @@ export const EventManager = ({
|
|||
initialValue={event.value}
|
||||
onChange={(value) => handlerChanged(index, 'value', value)}
|
||||
enablePreview={true}
|
||||
usePortalEditor={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { componentTypes } from '../Components/components';
|
|||
import { Table } from './Components/Table';
|
||||
import { Chart } from './Components/Chart';
|
||||
import { renderElement } from './Utils';
|
||||
import { toast } from 'react-toastify';
|
||||
import toast from 'react-hot-toast';
|
||||
import { validateQueryName, convertToKebabCase } from '@/_helpers/utils';
|
||||
import { EventManager } from './EventManager';
|
||||
import useShortcuts from '@/_hooks/use-shortcuts';
|
||||
import { ConfirmDialog } from '@/_components';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import Accordion from '@/_ui/Accordion';
|
||||
|
||||
export const Inspector = ({
|
||||
|
|
@ -34,13 +34,7 @@ export const Inspector = ({
|
|||
const [components, setComponents] = useState(allComponents);
|
||||
const [key, setKey] = React.useState('properties');
|
||||
|
||||
useShortcuts(
|
||||
['Backspace'],
|
||||
() => {
|
||||
setWidgetDeleteConfirmation(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
useHotkeys('backspace', () => setWidgetDeleteConfirmation(true));
|
||||
|
||||
const componentMeta = componentTypes.find((comp) => component.component.component === comp.component);
|
||||
|
||||
|
|
@ -60,9 +54,7 @@ export const Inspector = ({
|
|||
setComponent(newComponent);
|
||||
componentChanged(newComponent);
|
||||
} else {
|
||||
toast.error('Invalid query name. Should be unique and only include letters, numbers and underscore.', {
|
||||
hideProgressBar: true,
|
||||
});
|
||||
toast.error('Invalid query name. Should be unique and only include letters, numbers and underscore.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export function renderElement(
|
|||
componentMeta={componentMeta}
|
||||
currentState={currentState}
|
||||
darkMode={darkMode}
|
||||
componentName={component.component.name || null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const ItemTypes = {
|
||||
BOX: 'box',
|
||||
COMMENT: 'comment',
|
||||
NEW_COMMENT: 'new_comment'
|
||||
NEW_COMMENT: 'new_comment',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import React from 'react';
|
|||
import { appService, organizationService } from '@/_services';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
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 'react-toastify/dist/ReactToastify.css';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { debounce } from 'lodash';
|
||||
import Textarea from '@/_ui/Textarea';
|
||||
|
|
@ -65,12 +64,12 @@ class ManageAppUsers extends React.Component {
|
|||
.createAppUser(this.state.app.id, organizationUserId, role)
|
||||
.then(() => {
|
||||
this.setState({ addingUser: false, newUser: {} });
|
||||
toast.success('Added user successfully', { hideProgressBar: true, position: 'top-center' });
|
||||
toast.success('Added user successfully');
|
||||
this.fetchAppUsers();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
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) {
|
||||
toast.success('Application is now public.', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
toast.success('Application is now public.');
|
||||
} else {
|
||||
toast.success('Application visibility set to private', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
toast.success('Application visibility set to private');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -206,15 +199,7 @@ class ManageAppUsers extends React.Component {
|
|||
)}
|
||||
</div>
|
||||
<span className="input-group-text">
|
||||
<CopyToClipboard
|
||||
text={shareableLink}
|
||||
onCopy={() =>
|
||||
toast.success('Link copied to clipboard', {
|
||||
hideProgressBar: true,
|
||||
position: 'bottom-center',
|
||||
})
|
||||
}
|
||||
>
|
||||
<CopyToClipboard text={shareableLink} onCopy={() => toast.success('Link copied to clipboard')}>
|
||||
<button className="btn btn-secondary btn-sm">Copy</button>
|
||||
</CopyToClipboard>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -103,11 +103,10 @@
|
|||
"description": "Enter spreadsheet_id"
|
||||
},
|
||||
"sheet": {
|
||||
"$label": "Sheet",
|
||||
"$label": "GID",
|
||||
"$key": "sheet",
|
||||
"type": "codehinter",
|
||||
"lineNumbers": false,
|
||||
"placeholder": "Leave blank to use first sheet",
|
||||
"description": "Enter sheet"
|
||||
},
|
||||
"row_index": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
|
||||
|
||||
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode }) => {
|
||||
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode, componentName }) => {
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
className="form-control codehinter-query-editor-input"
|
||||
onChange={onChange('body', 0, index)}
|
||||
componentName={`${componentName}/body::key::${index}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
|
|||
theme={theme}
|
||||
placeholder="value"
|
||||
onChange={onChange('body', 1, index)}
|
||||
componentName={`${componentName}/body::value::${index}`}
|
||||
/>
|
||||
</div>
|
||||
{index > 0 && (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
|
||||
|
||||
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode }) => {
|
||||
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode, componentName }) => {
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
className="form-control codehinter-query-editor-input"
|
||||
onChange={onChange('headers', 0, index)}
|
||||
componentName={`${componentName}/headers::key::${index}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 field">
|
||||
|
|
@ -39,6 +40,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
|
|||
theme={theme}
|
||||
placeholder="value"
|
||||
onChange={onChange('headers', 1, index)}
|
||||
componentName={`${componentName}/headers::value::${index}`}
|
||||
/>
|
||||
</div>
|
||||
{index > 0 && (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
|
||||
|
||||
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode }) => {
|
||||
export default ({ options = [], currentState, theme, removeKeyValuePair, onChange, darkMode, componentName }) => {
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
className="form-control codehinter-query-editor-input"
|
||||
onChange={onChange('url_params', 0, index)}
|
||||
componentName={`${componentName}/params::key::${index}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 field">
|
||||
|
|
@ -39,6 +40,7 @@ export default ({ options = [], currentState, theme, removeKeyValuePair, onChang
|
|||
theme={theme}
|
||||
placeholder="value"
|
||||
onChange={onChange('url_params', 1, index)}
|
||||
componentName={`${componentName}/params::value::${index}`}
|
||||
/>
|
||||
</div>
|
||||
{index > 0 && (
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Headers from './TabHeaders';
|
|||
import Params from './TabParams';
|
||||
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');
|
||||
|
||||
return (
|
||||
|
|
@ -19,6 +19,7 @@ function ControlledTabs({ options, currentState, theme, onChange, removeKeyValue
|
|||
currentState={currentState}
|
||||
theme={theme}
|
||||
darkMode={darkMode}
|
||||
componentName={componentName}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="params" title="Params">
|
||||
|
|
@ -29,6 +30,7 @@ function ControlledTabs({ options, currentState, theme, onChange, removeKeyValue
|
|||
currentState={currentState}
|
||||
theme={theme}
|
||||
darkMode={darkMode}
|
||||
componentName={componentName}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="body" title="Body">
|
||||
|
|
@ -39,6 +41,7 @@ function ControlledTabs({ options, currentState, theme, onChange, removeKeyValue
|
|||
currentState={currentState}
|
||||
theme={theme}
|
||||
darkMode={darkMode}
|
||||
componentName={componentName}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ class Restapi extends React.Component {
|
|||
render() {
|
||||
const { options } = this.state;
|
||||
const dataSourceURL = this.props.selectedDataSource?.options?.url?.value;
|
||||
const queryName = this.props.queryName;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 mt-2">
|
||||
|
|
@ -113,6 +115,7 @@ class Restapi extends React.Component {
|
|||
changeOption(this, 'url', value);
|
||||
}}
|
||||
placeholder="Enter request URL"
|
||||
componentName={`${queryName}::url`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -124,6 +127,7 @@ class Restapi extends React.Component {
|
|||
onChange={this.handleChange}
|
||||
removeKeyValuePair={this.removeKeyValuePair}
|
||||
darkMode={this.props.darkMode}
|
||||
componentName={queryName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ class Runjs extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
}
|
||||
componentDidMount() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -73,6 +73,13 @@
|
|||
"type": "codehinter",
|
||||
"lineNumbers": false,
|
||||
"description": "Enter bucket"
|
||||
},
|
||||
"prefix": {
|
||||
"$label": "Prefix",
|
||||
"$key": "prefix",
|
||||
"type": "codehinter",
|
||||
"lineNumbers": false,
|
||||
"description": "Enter prefix"
|
||||
}
|
||||
},
|
||||
"signed_url_for_get": {
|
||||
|
|
|
|||