Merge branch 'release/v0.9.1'

This commit is contained in:
navaneeth 2021-11-25 14:22:49 +05:30
commit 681bb7f77a
236 changed files with 25970 additions and 7109 deletions

View file

@ -1 +1 @@
0.9.0
0.9.1

View file

@ -74,4 +74,4 @@ Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand ab
</a>
## Licence
ToolJet © 2021, ToolJet Solutions Inc - Released under the GNU General Public License v3.0.
ToolJet © 2021, ToolJet Solutions Inc - Released under the GNU Affero General Public License v3.0.

View file

@ -5,7 +5,7 @@
"repository": "https://github.com/tooljet/tooljet",
"logo": "https://app.tooljet.io/assets/images/logo.svg",
"success_url": "/",
"scripts":{
"scripts": {
"predeploy": "npm install --prefix server && npm run build --prefix server"
},
"env": {
@ -28,6 +28,18 @@
"SECRET_KEY_BASE": {
"description": "Used by ToolJet server as the input secret to the application's key generator.",
"value": ""
},
"NODE_OPTIONS": {
"description": "Node options configured to increase node memory to support app build",
"value": "--max-old-space-size=4096"
},
"SSO_GOOGLE_OAUTH2_CLIENT_ID": {
"description": "Google OAuth2 client id to enable single sign-on",
"value": ""
},
"SSO_DISABLE_SIGNUP": {
"description": "Disable sign-up via SSO",
"value": ""
}
},
"formation": {
@ -36,7 +48,9 @@
}
},
"image": "heroku/nodejs",
"addons": ["heroku-postgresql"],
"addons": [
"heroku-postgresql"
],
"buildpacks": [
{
"url": "heroku/nodejs"

View file

@ -12,6 +12,7 @@ source "amazon-ebs" "ubuntu" {
instance_type = "${var.instance_type}"
region = "${var.ami_region}"
ami_regions = "${var.ami_regions}"
ami_groups = "${var.ami_groups}"
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"

View file

@ -12,6 +12,11 @@ variable "ami_region" {
default = "us-west-2"
}
variable "ami_groups" {
type = list(string)
default = ["all"]
}
variable "ami_regions" {
type = list(string)
default = ["us-west-1", "us-east-1", "us-east-2", "eu-west-2", "eu-central-1", "ap-northeast-1", "ap-southeast-1", "ap-northeast-3", "ap-south-1", "ap-northeast-2", "ap-southeast-2", "ca-central-1", "eu-west-1", "eu-north-1", "sa-east-1"]

View file

@ -9,7 +9,7 @@ WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
# Fix for heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=2048"
ENV NODE_OPTIONS="--max-old-space-size=4096"
# install app dependencies
COPY ./frontend/package.json ./frontend/package-lock.json ./

View file

@ -10,7 +10,7 @@ WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
# Fix for heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=2048"
ENV NODE_OPTIONS="--max-old-space-size=4096"
# install app dependencies

View file

@ -1,7 +1,7 @@
FROM node:14.17.3-buster
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=2048"
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt update && apt install -y \
build-essential \

View file

@ -3,7 +3,7 @@ FROM node:14.17.3-buster
ENV NODE_ENV=production
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=2048"
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt update && apt install -y \
build-essential \

View file

@ -0,0 +1,40 @@
---
sidebar_label: Generate file
---
# Generate file
This action allows you to construct files on the fly and let users download it.
Presently, the only file type supported is `CSV`.
## Options
| Option | Description |
|--------|-------------|
| Type | Type of file to be generated |
| File name | Name of the file to be generated |
| Data | Data that will be used to construct the file. Its format will depend on the file type, as specified in the following section |
### Data format for CSV
For `CSV` file type, the data field should be supplied with an array objects. ToolJet assumes that the keys of each of
these objects are the same and that they represent the column headers of the csv file.
Example:
```javascript
{{
[
{ name: 'John', email: 'john@tooljet.com' },
{ name: 'Sarah', email: 'sarah@tooljet.com' },
]
}}
```
Supplying the above snippet will generate a csv file which looks like this:
```csv
name,email
John,john@tooljet.com
Sarah,sarah@tooljet.com
```

View file

@ -1,5 +1,4 @@
---
sidebar_position: 1
sidebar_label: Set localStorage
---

View file

@ -0,0 +1,35 @@
---
sidebar_position: 12
---
# Amazon S3
ToolJet can connect to Amazon S3 buckets and perform various operation on them.
## Connection
To add a new S3 source, click on the Add or edit datasource icon on the left sidebar of the app editor and click on `Add datasource` button. Select AWS S3 from the modal that pops up.
ToolJet requires the following to connect to your DynamoDB.
- Region
- Access key
- Secret key
It is recommended to create a new IAM user for the database so that you can control the access levels of ToolJet.
<img src="/img/datasource-reference/aws-s3-connect.png" alt="ToolJet - AWS S3 connection" height="250"/>
Click on 'Test connection' button to verify if the credentials are correct and that the database is accessible to ToolJet server. Click on 'Save' button to save the datasource.
## Querying AWS S3
Click on + button of the query manager at the bottom panel of the editor and select the datasource added in the previous step as the datasource. Select the operation that you want to perform and click 'Save' to save the query.
<img src="/img/datasource-reference/aws-s3-query.png" alt="ToolJet - AWS S3 query" height="250"/>
Click on the 'run' button to run the query. NOTE: Query should be saved before running.
:::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: link
:::

View file

@ -0,0 +1,30 @@
---
sidebar_position: 13
---
# Google Cloud Storage
ToolJet can connect to GCS buckets and perform various operation on them.
## Connection
To add a new GCS source, click on the Add or edit datasource icon on the left sidebar of the app editor and click on `Add datasource` button. Select GCS from the modal that pops up.
ToolJet requires the json private key of a service account to be able to connect to GCS.
You can follow the [google documentation](https://cloud.google.com/docs/authentication/getting-started) to get started.
<img src="/img/datasource-reference/gcs-connect.png" alt="ToolJet - GCS connection" height="250"/>
Click on 'Test connection' button to verify if the credentials are correct and that the database is accessible to ToolJet server. Click on 'Save' button to save the datasource.
## Querying GCS
Click on + button of the query manager at the bottom panel of the editor and select the datasource added in the previous step as the datasource. Select the operation that you want to perform and click 'Save' to save the query.
<img src="/img/datasource-reference/gcs-query.png" alt="ToolJet - GCS query" height="250"/>
Click on the 'run' button to run the query. NOTE: Query should be saved before running.
:::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: link
:::

View file

@ -1,5 +1,5 @@
{
"label": "Deployment",
"position": 4,
"collapsed": false
"collapsed": true
}

View file

@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---
# Deploying ToolJet client
@ -11,7 +11,7 @@ ToolJet client is a standalone application and can be deployed on static website
:::tip
You should set the environment variable `TOOLJET_SERVER_URL` ( URL of the server ) while building the frontend.
For example: `TOOLJET_SERVER_URL=https://server.tooljet.io npm run build && firebase deploy`
For example: `NODE_ENV=production TOOLJET_SERVER_URL=https://server.tooljet.com npm run build && firebase deploy`
:::
1. Initialize firebase project
@ -24,6 +24,67 @@ For example: `TOOLJET_SERVER_URL=https://server.tooljet.io npm run build && fire
firebase deploy
```
## Deploying ToolJet client with Google Cloud Storage
:::tip
If you want to run ToolJet on your local machine, please checkout the setup section of the contributing guide: [link](/docs/contributing-guide/setup/docker)
You should set the environment variable `TOOLJET_SERVER_URL` ( URL of the server ) while building the frontend.
For example: `NODE_ENV=production TOOLJET_SERVER_URL=https://server.tooljet.io npm run build`
:::
#### Using Load balancer
Tooljet client can be hosted from Cloud Storage bucket just like hosting any other static website.
Follow the instructions from google documentation [here](https://cloud.google.com/storage/docs/hosting-static-website).
Summarising the steps below:
1. Create a bucket and upload files within the build folder such that the `index.html` is at the bucket root.
2. Edit permissions for the bucket to assign *New principal* as `allUsers` with role as `Storage Object Viewer` and permit for public access for the bucket.
3. Click on *Edit website configuration* from the [buckets browser](https://console.cloud.google.com/storage/browser?_ga=2.180838119.1530169400.1637242882-657891227.1637242882) and specify the main page as `index.html`
4. Follow the [instructions](https://cloud.google.com/storage/docs/hosting-static-website#lb-ssl) on creating a load balancer for hosting a static website.
5. Optionally, create Cloud CDN to use with the backend bucket assigned to the load balancer.
6. After the load balancer is created there will be an IP assigned to it. Try hitting it to check the website is being loaded.
7. Use the load balancer IP as the static IP for the A record of your domain.
#### Using Google App Engine
1. Upload the build folder onto a bucket
2. Upload `app.yaml` file onto bucket with the following config
```yaml
runtime: python27
api_version: 1
threadsafe: true
handlers:
- url: /
static_files: build/index.html
upload: build/index.html
- url: /(.*)
static_files: build/\1
upload: build/(.*)
```
3. Activate cloud shell on your browser and create build folder
```bash
mkdir tooljet-assets
```
4. Copy the uploaded files onto an assets folder which is to be served
```bash
gsutil rsync -r gs://your-bucket-name/path-to-assets ./tooljet-assets
```
5. Deploy static assets to be served
```bash
cd tooljet-assets && gcloud app deploy
```

View file

@ -16,7 +16,7 @@ Follow the steps below to deploy ToolJet on AWS EC2 instances.
3. Under the `Images` section, click on the `AMIs` button.
4. Now, from the AMI search page, select the search type as "Public Images" and input `AMI Name : tooljet_v0.5.11.ubuntu_bionic` in the search bar.
4. Find the [ToolJet version](https://github.com/ToolJet/ToolJet/releases) you want to deploy. Now, from the AMI search page, select the search type as "Public Images" and input the version you'd want `AMI Name : tooljet_vX.X.X.ubuntu_bionic` in the search bar.
5. Select ToolJet's AMI and bootup an EC2 instance.

View file

@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---
# Environment variables
@ -12,7 +12,7 @@ Both the ToolJet server and client requires some environment variables to start
| variable | description |
| ------------ | --------------------------------------------------------------- |
| TOOLJET_HOST | the public URL of ToolJet client ( eg: https://app.tooljet.io ) |
| TOOLJET_HOST | the public URL of ToolJet client ( eg: https://app.tooljet.com ) |
#### Database configuration ( required )
@ -114,7 +114,7 @@ This is used to set up for CSP headers and put trace info to be used with APM ve
| variable | description |
| ------------------ | ----------------------------------------------------------- |
| TOOLJET_SERVER_URL | the URL of ToolJet server ( eg: https://server.tooljet.io ) |
| TOOLJET_SERVER_URL | the URL of ToolJet server ( eg: https://server.tooljet.com ) |
#### RELEASE VERSION ( optional)
@ -128,7 +128,7 @@ This is required when client is built separately.
| variable | description |
| ------------------ | ----------------------------------------------------------- |
| TOOLJET_SERVER_URL | the URL of ToolJet server ( eg: https://server.tooljet.io ) |
| TOOLJET_SERVER_URL | the URL of ToolJet server ( eg: https://server.tooljet.com ) |
#### Asset path ( optionally required )
@ -138,4 +138,4 @@ This can be an absolute path, or relative to main HTML file.
| variable | description |
| ------------------ | ----------------------------------------------------------- |
| ASSET_PATH | the asset path for the website ( eg: https://app.tooljet.io/) |
| ASSET_PATH | the asset path for the website ( eg: https://app.tooljet.com/) |

View file

@ -0,0 +1,140 @@
---
sidebar_position: 7
sidebar_label: Google Cloud Run
---
# Deploying ToolJet on Google Cloud Run
:::info
You should setup a PostgreSQL database manually to be used by ToolJet.
:::
Follow the steps below to deploy ToolJet on Cloud run with `gcloud` CLI.
## Deploying ToolJet application
1. Cloud Run requires prebuilt image to be present within cloud registry. You can pull specific tooljet image from docker hub and then tag with your project to push it to cloud registry.
```bash
gcloud auth configure-docker
docker pull tooljet/tooljet-ce:latest
docker tag tooljet/tooljet-ce:latest gcr.io/<replace-your-project-id>/tooljet/tooljet-ce:latest
docker push gcr.io/<replace-your-project-id>/tooljet/tooljet-ce:latest
```
2. Deploy new cloud run service
:::note
This command takes the assumption that certain required environment has already been created in secrets. If you haven't created already then use the [secret manager](https://console.cloud.google.com/security/secret-manager).
:::
```bash
gcloud run deploy tooljet-ce --image gcr.io/<replace-your-project-id>/tooljet/tooljet-ce:latest \
--allow-unauthenticated \
--cpu 1 \
--memory 1Gi \
--min-instances 1 \
--set-env-vars "TOOLJET_HOST=https://<replace-your-public-host>.com" \
--set-secrets "PG_HOST=PG_HOST:latest" \
--set-secrets "PG_DB=PG_DB:latest" \
--set-secrets "PG_USER=PG_USER:latest" \
--set-secrets "PG_PASS=PG_PASS:latest" \
--set-secrets "PG_PORT=PG_PORT:latest" \
--set-secrets "LOCKBOX_MASTER_KEY=LOCKBOX_MASTER_KEY:latest" \
--set-secrets "SECRET_KEY_BASE=SECRET_KEY_BASE:latest" \
--args "npm,run,start:prod"
```
Update `TOOLJET_HOST` environment variable if you want to use the default url assigned with Cloud run after the initial deploy.
:::tip
If you are to use [Public IP](https://cloud.google.com/sql/docs/mysql/connect-run) for Cloud SQL, then database host connection needs to be done via unix socket. In that case you can set value for `PG_HOST` as `/cloudsql/<CLOUD_SQL_CONNECTION_NAME>`. Additionally you will also have to set these two flags with the above command:
```
--set-cloudsql-instances <replace-cloud-sql-connection-name> \
--set-secrets "CLOUD_SQL_CONNECTION_NAME=CLOUD_SQL_CONNECTION_NAME:latest" \
```
:::
3. Create default user (Optional)
Signing up requires [SMTP configuration](https://docs.tooljet.com/docs/deployment/env-vars#smtp-configuration--optional-) to be done, but if you want to start off with default user you can run the command by modifying the `args` flag for a one time usage.
```bash
gcloud run deploy <replace-service-name> \
--image gcr.io/<replace-your-project-id>/tooljet/tooljet-ce:latest \
--args "npm,run,--prefix,server,db:seed"
```
The deployment will fail as it runs a seed script. Check logs to see that default user was created. Now run the following command to have the app deployed.
```bash
gcloud run deploy <replace-service-name> \
--image gcr.io/<replace-your-project-id>/tooljet/tooljet-ce:latest \
--args "npm,run,start:prod"
```
The default username of the admin is `dev@tooljet.io` and the password is `password`.
## Deploying only ToolJet server
1. Cloud Run requires prebuilt image to be present within cloud registry. You can pull specific tooljet image from docker hub and then tag with you project to push it to cloud registry.
```bash
gcloud auth configure-docker
docker pull tooljet/tooljet-server-ce:latest
docker tag tooljet/tooljet-server-ce:latest gcr.io/<replace-your-project-id>/tooljet/tooljet-server-ce:latest
docker push gcr.io/<replace-your-project-id>/tooljet/tooljet-server-ce:latest
```
2. Deploy new cloud run service
:::note
This command takes the assumption that certain required environment has already been created in secrets. If you haven't created already then use the [secret manager](https://console.cloud.google.com/security/secret-manager).
:::
```bash
gcloud run deploy tooljet-server-ce --image gcr.io/<replace-your-project-id>/tooljet/tooljet-server-ce:latest \
--allow-unauthenticated \
--cpu 1 \
--memory 1Gi \
--min-instances 1 \
--set-env-vars "SERVE_CLIENT=false" \
--set-env-vars "TOOLJET_HOST=https://<replace-your-public-host>.com" \
--set-secrets "PG_HOST=PG_HOST:latest" \
--set-secrets "PG_DB=PG_DB:latest" \
--set-secrets "PG_USER=PG_USER:latest" \
--set-secrets "PG_PASS=PG_PASS:latest" \
--set-secrets "PG_PORT=PG_PORT:latest" \
--set-secrets "LOCKBOX_MASTER_KEY=LOCKBOX_MASTER_KEY:latest" \
--set-secrets "SECRET_KEY_BASE=SECRET_KEY_BASE:latest" \
--args "npm,run,start:prod"
```
:::tip
If you are to use [Public IP](https://cloud.google.com/sql/docs/mysql/connect-run) for Cloud SQL, then database host connection needs to be done via unix socket. In that case you can set value for `PG_HOST` as `/cloudsql/<CLOUD_SQL_CONNECTION_NAME>`. Additionally you will also have to set these two flags with the above command:
```
--set-cloudsql-instances <replace-cloud-sql-connection-name> \
--set-secrets "CLOUD_SQL_CONNECTION_NAME=CLOUD_SQL_CONNECTION_NAME:latest" \
```
:::
3. Create default user (Optional)
Signing up requires [SMTP configuration](https://docs.tooljet.com/docs/deployment/env-vars#smtp-configuration--optional-) to be done, but if you want to start off with default user you can run the command by modifying the `args` flag for a one time usage.
```bash
gcloud run deploy <replace-service-name> \
--image gcr.io/<replace-your-project-id>/tooljet/tooljet-server-ce:latest \
--args "npm,run,db:seed"
```
The deployment will fail as it runs a seed script. Check logs to see that default user was created. Now run the following command to have the app deployed.
```bash
gcloud run deploy <replace-service-name> \
--image gcr.io/<replace-your-project-id>/tooljet/tooljet-server-ce:latest \
--args "npm,run,start:prod"
```
The default username of the admin is `dev@tooljet.io` and the password is `password`.

View file

@ -31,7 +31,7 @@ Change the domain name to the domain/subdomain that you wish to use for ToolJet
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/deployment.yaml
```
Make sure to edit the environment variables in the `deployment.yaml`. You can check out the available options [here](https://docs.tooljet.io/docs/deployment/env-vars).
Make sure to edit the environment variables in the `deployment.yaml`. You can check out the available options [here](https://docs.tooljet.com/docs/deployment/env-vars).
4. Create k8s service

View file

@ -31,7 +31,7 @@ The references for datasources and widgets:
- **[Widget Reference](/docs/widgets/table)**
## Help and Support
We have extensively documented the features of ToolJet, but in case you are stuck, please feel to mail us: hello@tooljet.io.
We have extensively documented the features of ToolJet, but in case you are stuck, please feel to mail us: hello@tooljet.com.
If you are using ToolJet cloud, click on the chat icon at the bottom-left corner for instant help.
If you have found a bug, please create a GitHub issue for the same.
Also, feel free to join our highly active [slack community](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg).

View file

@ -5,10 +5,6 @@ sidebar_label: Google
# Google Single Sign-on
:::info
This feature is available only on the enterprise edition for ToolJet
:::
Goto [Google cloud console](https://console.cloud.google.com/) and create a project.
<img class="screenshot-full" src="/img/sso/google/create-project.png" alt="ToolJet - Google create project" height="420"/>

View file

@ -26,4 +26,5 @@ Some of the actions that ToolJet Support are
Show modal | Open any modal that you've added |
Close modal | Close any modal that you've added if its already open |
Copy to clipboard | Copy any available text that you see on the application to clipboard |
Set localStorage | Set a key and corresponding value to localStorage |
Set localStorage | Set a key and corresponding value to localStorage |
Generate file | Construct file using data available in your application and let the user download it |

View file

@ -18,6 +18,6 @@ You will redirected to the visual app editor once the app has been created. The
The main components of an app:
- **[Widgets](https://docs.tooljet.io/docs/tutorial/adding-widget)** - UI components such as tables, buttons, dropdowns.
- **[Data sources](https://docs.tooljet.io/docs/tutorial/adding-a-datasource)** - ToolJet can connect to databases, APIs and external services to fetch and modify data.
- **[Queries](https://docs.tooljet.io/docs/tutorial/building-queries)** - Queries are used to access the connected datasources.
- **[Widgets](https://docs.tooljet.com/docs/tutorial/adding-widget)** - UI components such as tables, buttons, dropdowns.
- **[Data sources](https://docs.tooljet.com/docs/tutorial/adding-a-datasource)** - ToolJet can connect to databases, APIs and external services to fetch and modify data.
- **[Queries](https://docs.tooljet.com/docs/tutorial/building-queries)** - Queries are used to access the connected datasources.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 1
---
# Button
Button widget can be used to take actions.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 22
---
# Calendar
Calendar widget comes with the following features:
- Day, month and week level views
@ -49,6 +45,7 @@ Assuming that you set the date format to `MM-DD-YYYY HH:mm:ss A Z`, setting the
| allDay | Optional. Qualifies the event as an 'All day event', which will pin it to date headers on `day` and `week` level views |
| tooltip | Tooltip which will be display when the user hovers over the event |
| color | Background color of the event, any css supported color name or hex code can be used |
| textColor | Color of the event title, any css supported color name or hex code can be used |
| textOrientation | Optional. If it is set to `vertical`, the title of the event will be oriented vertically. |
| resourceId | Applicable only if you're using resource scheduling. This is the id of the resource to which this event correspond to. |
@ -80,6 +77,8 @@ If we specify the `resourceId` of any of the events as `1`, then that event will
Determines whether the calendar would display a `day`, a `week` or a `month`.
Setting this property to anything other than these values will make the calendar default to `month` view.
The view that is currently selected will be exposed as the variable `currentView`.
#### Show toolbar
Determines whether the calendar toolbar should be displayed or not.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 2
---
# Chart
Chart widget takes the chart type, data and styles to draw charts using Plotly.js.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 3
---
# Checkbox
Checkbox widget can be used for allowing the users to make a binary choice, e.g,. unselected or selected.

View file

@ -0,0 +1,150 @@
# Code Editor
Code Editor widget is a versatile text editor for editing code and supports several languages.
:::info
<details>
<summary>Supporting all commonly used languages.</summary>
<ul>
<li>APL</li>
<li>ASN.1</li>
<li>Asterisk dialplan</li>
<li>Brainfuck</li>
<li>C, C++, C#</li>
<li>Ceylon</li>
<li>Clojure</li>
<li>Closure Stylesheets (GSS)</li>
<li>CMake</li>
<li>COBOL</li>
<li>CoffeeScript</li>
<li>Common Lisp</li>
<li>Crystal</li>
<li>CSS</li>
<li>Cypher</li>
<li>Cython</li>
<li>D</li>
<li>Dart</li>
<li>Django (templating language)</li>
<li>Dockerfile</li>
<li>diff</li>
<li>DTD</li>
<li>Dylan</li>
<li>EBNF</li>
<li>ECL</li>
<li>Eiffel</li>
<li>Elixir</li>
<li>Elm</li>
<li>Erlang</li>
<li>Factor</li>
<li>FCL</li>
<li>Forth</li>
<li>Fortran</li>
<li>F#</li>
<li>Gas (AT&amp;T-style assembly)</li>
<li>Gherkin</li>
<li>Go</li>
<li>Groovy</li>
<li>HAML</li>
<li>Handlebars</li>
<li>Haskell</li>
<li>Haxe</li>
<li>HTML embedded (JSP, ASP.NET)</li>
<li>HTML mixed-mode</li>
<li>HTTP</li>
<li>IDL</li>
<li>Java</li>
<li>JavaScript (JSX)</li>
<li>Jinja2</li>
<li>Julia</li>
<li>Kotlin</li>
<li>LESS</li>
<li>LiveScript</li>
<li>Lua</li>
<li>Markdown (GitHub-flavour)</li>
<li>Mathematica</li>
<li>mbox</li>
<li>mIRC</li>
<li>Modelica</li>
<li>MscGen</li>
<li>MUMPS</li>
<li>Nginx</li>
<li>NSIS</li>
<li>N-Triples/N-Quads</li>
<li>Objective C</li>
<li>OCaml</li>
<li>Octave (MATLAB)</li>
<li>Oz</li>
<li>Pascal</li>
<li>PEG.js</li>
<li>Perl</li>
<li>PGP (ASCII armor)</li>
<li>PHP</li>
<li>Pig Latin</li>
<li>PowerShell</li>
<li>Properties files</li>
<li>ProtoBuf</li>
<li>Pug</li>
<li>Puppet</li>
<li>Python</li>
<li>Q</li>
<li>R</li>
<li>RPM</li>
<li>reStructuredText</li>
<li>Ruby</li>
<li>Rust</li>
<li>SAS</li>
<li>Sass</li>
<li>Spreadsheet</li>
<li>Scala</li>
<li>Scheme</li>
<li>SCSS</li>
<li>Shell</li>
<li>Sieve</li>
<li>Slim</li>
<li>Smalltalk</li>
<li>Smarty</li>
<li>Solr</li>
<li>Soy</li>
<li>Stylus</li>
<li>SQL (several dialects)</li>
<li>SPARQL</li>
<li>Squirrel</li>
<li>Swift</li>
<li>sTeX, LaTeX</li>
<li>Tcl</li>
<li>Textile</li>
<li>Tiddlywiki</li>
<li>Tiki wiki</li>
<li>TOML</li>
<li>Tornado (templating language)</li>
<li>troff (for manpages)</li>
<li>TTCN</li>
<li>TTCN Configuration</li>
<li>Turtle</li>
<li>Twig</li>
<li>VB.NET</li>
<li>VBScript</li>
<li>Velocity</li>
<li>Verilog/SystemVerilog</li>
<li>VHDL</li>
<li>Vue.js app</li>
<li>Web IDL</li>
<li>WebAssembly Text Format</li>
<li>XML/HTML</li>
<li>XQuery</li>
<li>Yacas</li>
<li>YAML</li>
<li>YAML frontmatter</li>
<li>Z80</li>
</ul>
</details>
:::
<img class="screenshot-full" src="/img/widgets/code-editor/code-editor.png" alt="ToolJet - Widget Reference - Code Editor" height="420"/>
| properties | description |
| ----------- | ----------- |
| Placeholder | It specifies a hint that describes the expected value.|
| Mode | It is used to specify the language to be used for the code-editor.|
| Show Line Number | This property is used to show or hide line numbers to the left of the editor.|

View file

@ -1,7 +1,3 @@
---
sidebar_position: 4
---
# Date-range picker
The date-range picker widget allows users to select a range of dates.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 5
---
# Datepicker
The Datepicker widget allows users to select a single value for date and time from a pre-determined set.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 6
---
# Divider
Divider widget is used to add separator between components.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 7
---
# Dropdown
The Dropdown widget can be used to collect user input from a list of options.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 6
---
# Filepicker
Filepicker widget allows the user to drag and drop files or upload files by browsing the filesystem and selecting one or more files in a directory.

View file

@ -0,0 +1,10 @@
# Iframe
Iframe widget is used to embed another HTML page into the current one and display iframes in your app.
<img class="screenshot-full" src="/img/widgets/iframe/iframe.gif" alt="ToolJet - Widget Reference - Iframe " height="420"/>
#### Properties
| properties | description |
| ----------- | ----------- |
| URL | Sets the **URL** of the page to embed. |

View file

@ -1,7 +1,3 @@
---
sidebar_position: 8
---
# Image
Image widget is used to display images in your app.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 9
---
# Map
The map widget can be used to pick or select locations on the Google map with the location's coordinates.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 10
---
# Modal
Modal widget renders in front of a backdrop, and it blocks interaction with the rest of the application until the modal is closed. It can be used to add dialog boxes to your app for lightboxes, user notifications, forms, etc.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 11
---
# Multiselect
Multiselect widget can be used to collect multiple user inputs from a list of options.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 12
---
# Number Input
Number Input widget lets users enter and change numbers.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 13
---
# QR Scanner
Scan QR codes using device camera and hold the data they carry.
<img class="screenshot-full" src="/img/widgets/qr-scanner/qr-scanner.jpeg" alt="ToolJet - QR Scanner" height="420"/>

View file

@ -1,7 +1,3 @@
---
sidebar_position: 14
---
# Radio Button
Radio button widget can be used to select one option from a group of options.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 15
---
# Rich Text Editor
Rich Text Editor can be used to enter and edit the text in HTML format.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 16
---
# Star rating
Star rating widget can be used to display as well as input ratings. The widget supports half stars, and the number of stars can be set too.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 17
---
# Table
Tables can be used for both displaying and editing data.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 18
---
# Text Input
Text Input widget let users enter and edit text.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 19
---
# Text
Text widget can be used to display text.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 20
---
# Textarea
Textarea widgets let users enter and edit just text like [Text Input](/docs/widgets/text-input) widget.

View file

@ -1,7 +1,3 @@
---
sidebar_position: 21
---
# Toggle Switch
The toggle switch widget allows the user to change a setting between two states.

View file

@ -1,8 +1,8 @@
/** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = {
title: 'ToolJet - Documentation',
tagline: 'Build and deploy internal tools.',
url: 'https://docs.tooljet.io',
tagline: 'Low-code framework to Build internal tools and business apps.',
url: 'https://docs.tooljet.com',
baseUrl: '/',
onBrokenLinks: 'ignore',
onBrokenMarkdownLinks: 'warn',
@ -10,6 +10,19 @@ module.exports = {
organizationName: 'ToolJet', // Usually your GitHub org/user name.
projectName: 'ToolJet', // Usually your repo name.
themeConfig: {
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>',
backgroundColor: '#4D72DA',
textColor: '#ffffff',
isCloseable: true,
},
gtag: {
trackingID: process.env.GA_MID,
// Optional fields.
anonymizeIP: true, // Should IPs be anonymized?
},
colorMode: {
switchConfig: {
darkIcon: '\00a0 ',

18273
docs/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,8 +14,9 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "2.0.0-alpha.73",
"@docusaurus/preset-classic": "2.0.0-alpha.73",
"@docusaurus/core": "^2.0.0-beta.9",
"@docusaurus/plugin-google-gtag": "^2.0.0-alpha.73",
"@docusaurus/preset-classic": "^2.0.0-beta.9",
"@mdx-js/react": "^1.6.21",
"clsx": "^1.1.1",
"react": "^17.0.1",
@ -33,4 +34,4 @@
"last 1 safari version"
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View file

@ -2,7 +2,7 @@
<svg width="215px" height="190px" viewBox="0 0 215 190" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>logo</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="logo" fill="#3C92DC" fill-rule="nonzero">
<g id="logo" fill="#4D72DA" fill-rule="nonzero">
<path d="M214.605,188.381 C214.605,189.21 213.926,189.889 213.097,189.889 C213.082,189.889 213.082,189.889 213.068,189.889 C212.735,189.889 212.388,189.785 212.116,189.558 C195.325,175.876 160.494,166.296 121.197,164.532 C120.411,164.502 119.778,163.868 119.749,163.098 C116.369,87.596 110.274,65.933 107.515,65.043 C104.844,65.933 98.749,87.596 95.37,163.098 C95.34,163.868 94.706,164.501 93.922,164.532 C54.625,166.297 19.792,175.877 3.002,189.558 C2.443,190.01 1.629,189.996 1.086,189.528 C0.528,189.06 0.378,188.275 0.739,187.642 L106.247,0.824 C106.775,-0.127 108.344,-0.127 108.872,0.824 L214.243,187.4 C214.47,187.672 214.605,188.003 214.605,188.381 Z" id="Path"></path>
<path d="M115.715,166.383 C112.789,187.097 109.954,189.889 107.573,189.889 C105.191,189.889 102.358,187.097 99.433,166.383 C99.38,166.013 99.486,165.642 99.737,165.351 C99.975,165.06 100.333,164.888 100.703,164.888 C102.848,164.823 105.097,164.796 107.585,164.796 C110.06,164.796 112.31,164.822 114.44,164.888 C114.812,164.888 115.168,165.06 115.407,165.351 C115.662,165.642 115.768,166.013 115.715,166.383 Z" id="Path"></path>
</g>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-code" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<polyline points="7 8 3 12 7 16"></polyline>
<polyline points="17 8 21 12 17 16"></polyline>
<line x1="14" y1="4" x2="10" y2="20"></line>
</svg>

After

Width:  |  Height:  |  Size: 439 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-browser" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="4" y="4" width="16" height="16" rx="1" />
<line x1="4" y1="8" x2="20" y2="8" />
<line x1="8" y1="4" x2="8" y2="8" />
</svg>

After

Width:  |  Height:  |  Size: 422 B

View file

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-forms" width="24" height="24" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3a3 3 0 0 0 -3 3v12a3 3 0 0 0 3 3"></path>
<path d="M6 3a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3"></path>
<path d="M13 7h7a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-7"></path>
<path d="M5 7h-1a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h1"></path>
<path d="M17 12h.01"></path>
<path d="M13 12h.01"></path>
</svg>

Before

Width:  |  Height:  |  Size: 598 B

View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-forms" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3a3 3 0 0 0 -3 3v12a3 3 0 0 0 3 3" />
<path d="M6 3a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3" />
<path d="M13 7h7a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-7" />
<path d="M5 7h-1a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h1" />
<path d="M17 12h.01" />
<path d="M13 12h.01" />
</svg>

After

Width:  |  Height:  |  Size: 558 B

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill="#444" d="M14 4v-2h-14v12h16v-10h-2zM10 3h3v1h-3v-1zM6 3h3v1h-3v-1zM15 13h-14v-10h4v2h10v8z"></path>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="46px" height="46px" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
<title>btn_google_light_normal_ios</title>
<desc>Created with Sketch.</desc>
<defs>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0" in="shadowBlurOuter1" type="matrix" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0" in="shadowBlurOuter2" type="matrix" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2"></rect>
</defs>
<g id="Google-Button" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="9-PATCH" sketch:type="MSArtboardGroup" transform="translate(-608.000000, -160.000000)"></g>
<g id="btn_google_light_normal" sketch:type="MSArtboardGroup" transform="translate(-1.000000, -1.000000)">
<g id="button" sketch:type="MSLayerGroup" transform="translate(4.000000, 4.000000)" filter="url(#filter-1)">
<g id="button-bg">
<use fill="#FFFFFF" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-2"></use>
<use fill="none" xlink:href="#path-2"></use>
<use fill="none" xlink:href="#path-2"></use>
<use fill="none" xlink:href="#path-2"></use>
</g>
</g>
<g id="logo_googleg_48dp" sketch:type="MSLayerGroup" transform="translate(15.000000, 15.000000)">
<path d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z" id="Shape" fill="#4285F4" sketch:type="MSShapeGroup"></path>
<path d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z" id="Shape" fill="#34A853" sketch:type="MSShapeGroup"></path>
<path d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z" id="Shape" fill="#FBBC05" sketch:type="MSShapeGroup"></path>
<path d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z" id="Shape" fill="#EA4335" sketch:type="MSShapeGroup"></path>
<path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z" id="Shape" sketch:type="MSShapeGroup"></path>
</g>
<g id="handles_square" sketch:type="MSLayerGroup"></g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1,35 @@
import React from 'react';
import GoogleLogin from 'react-google-login';
import { authenticationService } from '@/_services';
export default function GoogleSSOLoginButton(props) {
const googleSSOSuccessHandler = (googleUser) => {
const idToken = googleUser.getAuthResponse().id_token;
authenticationService.signInViaOAuth(idToken).then(props.authSuccessHandler).catch(props.authFailureHandler);
};
return (
<div className="mt-2">
<GoogleLogin
clientId={window.public_config.SSO_GOOGLE_OAUTH2_CLIENT_ID}
buttonText="Login"
onSuccess={googleSSOSuccessHandler}
onFailure={props.authFailureHandler}
cookiePolicy={'single_host_origin'}
render={(renderProps) => (
<div>
<button {...renderProps} className="btn border-0 rounded-2">
<img
onClick={renderProps.onClick}
disabled={renderProps.disabled}
src="/assets/images/sso-buttons/google.svg"
className="h-4"
/>
<span className="px-1">Sign in with Google</span>
</button>
</div>
)}
/>
</div>
);
}

View file

@ -5,7 +5,6 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"version": "0.1.0",
"dependencies": {
"@babel/core": "^7.4.3",
@ -52,6 +51,7 @@
"react-dom": "^16.14.0",
"react-dropzone": "^11.4.2",
"react-easy-sort": "^0.2.1",
"react-google-login": "^5.2.2",
"react-hot-toast": "^2.1.1",
"react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0",
@ -17942,6 +17942,19 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
},
"node_modules/react-google-login": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/react-google-login/-/react-google-login-5.2.2.tgz",
"integrity": "sha512-JUngfvaSMcOuV0lFff7+SzJ2qviuNMQdqlsDJkUM145xkGPVIfqWXq9Ui+2Dr6jdJWH5KYdynz9+4CzKjI5u6g==",
"dependencies": {
"@types/react": "*",
"prop-types": "^15.6.0"
},
"peerDependencies": {
"react": "^16 || ^17",
"react-dom": "^16 || ^17"
}
},
"node_modules/react-hot-toast": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.1.1.tgz",
@ -21737,6 +21750,7 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
"integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==",
"deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.",
"dependencies": {
"chalk": "^2.4.1",
"coa": "^2.0.2",
@ -38523,6 +38537,15 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
},
"react-google-login": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/react-google-login/-/react-google-login-5.2.2.tgz",
"integrity": "sha512-JUngfvaSMcOuV0lFff7+SzJ2qviuNMQdqlsDJkUM145xkGPVIfqWXq9Ui+2Dr6jdJWH5KYdynz9+4CzKjI5u6g==",
"requires": {
"@types/react": "*",
"prop-types": "^15.6.0"
}
},
"react-hot-toast": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.1.1.tgz",

View file

@ -47,6 +47,7 @@
"react-dom": "^16.14.0",
"react-dropzone": "^11.4.2",
"react-easy-sort": "^0.2.1",
"react-google-login": "^5.2.2",
"react-hot-toast": "^2.1.1",
"react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0",

View file

@ -45,4 +45,13 @@ export const ActionTypes = [
{ name: 'value', type: 'code', default: '' },
],
},
{
name: 'Generate file',
id: 'generate-file',
options: [
{ name: 'fileType', type: 'text', default: '' },
{ name: 'fileName', type: 'text', default: '' },
{ name: 'data', type: 'code', default: '{{[]}}' },
],
},
];

View file

@ -7,6 +7,7 @@ import { TextInput } from './Components/TextInput';
import { NumberInput } from './Components/NumberInput';
import { TextArea } from './Components/TextArea';
import { Container } from './Components/Container';
import { Tabs } from './Components/Tabs';
import { RichTextEditor } from './Components/RichTextEditor';
import { DropDown } from './Components/DropDown';
import { Checkbox } from './Components/Checkbox';
@ -24,6 +25,8 @@ import { Divider } from './Components/Divider';
import { FilePicker } from './Components/FilePicker';
import { PasswordInput } from './Components/PasswordInput';
import { Calendar } from './Components/Calendar';
import { IFrame } from './Components/IFrame';
import { CodeEditor } from './Components/CodeEditor';
import { renderTooltip } from '../_helpers/appUtils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import '@/_styles/custom.scss';
@ -39,6 +42,7 @@ const AllComponents = {
Table,
TextArea,
Container,
Tabs,
RichTextEditor,
DropDown,
Checkbox,
@ -56,6 +60,8 @@ const AllComponents = {
FilePicker,
PasswordInput,
Calendar,
IFrame,
CodeEditor,
};
export const Box = function Box({
@ -76,6 +82,7 @@ export const Box = function Box({
containerProps,
darkMode,
removeComponent,
canvasWidth,
mode,
}) {
const backgroundColor = yellow ? 'yellow' : '';
@ -132,6 +139,7 @@ export const Box = function Box({
containerProps={containerProps}
darkMode={darkMode}
removeComponent={removeComponent}
canvasWidth={canvasWidth}
properties={resolvedProperties}
exposedVariables={exposedVariables}
styles={resolvedStyles}

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState, memo } from 'react';
export const BoxDragPreview = memo(function BoxDragPreview({ item, currentLayout }) {
export const BoxDragPreview = memo(function BoxDragPreview({ item, currentLayout, canvasWidth }) {
const [tickTock, setTickTock] = useState(false);
useEffect(
@ -22,7 +22,7 @@ export const BoxDragPreview = memo(function BoxDragPreview({ item, currentLayout
return (
<div
className="resizer-active draggable-box"
style={{ height, width, border: 'solid 1px rgb(70, 165, 253)', padding: '2px' }}
style={{ height, width: (width * canvasWidth) / 43, border: 'solid 1px rgb(70, 165, 253)', padding: '2px' }}
>
<div
style={{

View file

@ -115,9 +115,9 @@ export function CodeHinter({
</animated.div>
);
};
enablePreview = enablePreview ?? true;
return (
<>
<div style={{ width: '100%' }}>
<div
className={`code-hinter ${className || 'codehinter-default-input'}`}
key={suggestions.length}
@ -140,6 +140,6 @@ export function CodeHinter({
/>
</div>
{enablePreview && getPreview()}
</>
</div>
);
}

View file

@ -7,7 +7,7 @@ import Button from '@/_ui/Button';
import useShortcuts from '@/_hooks/use-shortcuts';
import usePopover from '@/_hooks/use-popover';
function CommentFooter({ editComment = '', editCommentId, handleSubmit }) {
function CommentFooter({ users, editComment = '', editCommentId, handleSubmit }) {
const [comment, setComment] = React.useState(editComment);
const [loading, setLoading] = React.useState(false);
const [open, trigger, content, setOpen] = usePopover(false);
@ -38,7 +38,12 @@ function CommentFooter({ editComment = '', editCommentId, handleSubmit }) {
<div className="card-footer">
<div className="row align-items-center">
<div className="col-8">
<TextareaMentions value={comment} setValue={setComment} placeholder="Type your comment here" />
<TextareaMentions
users={users}
value={comment}
setValue={setComment}
placeholder="Type your comment here"
/>
</div>
<div className="col-1 cursor-pointer">
<svg {...trigger} width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">

View file

@ -11,7 +11,18 @@ import usePopover from '@/_hooks/use-popover';
import { commentsService } from '@/_services';
import useRouter from '@/_hooks/use-router';
const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, appVersionsId }) => {
const Comment = ({
socket,
x,
y,
threadId,
user = {},
isResolved,
fetchThreads,
appVersionsId,
canvasWidth,
users,
}) => {
const [loading, setLoading] = React.useState(true);
const [editComment, setEditComment] = React.useState('');
const [editCommentId, setEditCommentId] = React.useState('');
@ -106,7 +117,7 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads,
id={`thread-${threadId}`}
className={cx('comments cursor-move', { open: open })}
style={{
transform: `translate(${x}px, ${y}px)`,
transform: `translate(${(x * canvasWidth) / 100}px, ${y}px)`,
...commentFadeStyle,
}}
onDragStart={() => setOpen(false)}
@ -156,6 +167,7 @@ const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads,
thread={thread}
/>
<CommentFooter
users={users}
editComment={editComment}
editCommentId={editCommentId}
handleSubmit={editCommentId ? handleEdit : handleSubmit}

View file

@ -1,17 +1,29 @@
import '@/_styles/editor/comments.scss';
import React from 'react';
import { isEmpty } from 'lodash';
import { isEmpty, capitalize } from 'lodash';
import Comment from './Comment';
import { commentsService } from '@/_services';
import { commentsService, organizationService } from '@/_services';
import useRouter from '@/_hooks/use-router';
const Comments = ({ newThread = {}, appVersionsId, socket }) => {
const Comments = ({ newThread = {}, appVersionsId, socket, canvasWidth }) => {
const [threads, setThreads] = React.useState([]);
const router = useRouter();
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
organizationService.getUsers(null).then((data) => {
const _users = data.users.map((u) => ({
id: u.id,
display: `${capitalize(u.first_name)} ${capitalize(u.last_name)}`,
}));
setUsers(_users);
});
}, []);
async function fetchData() {
const { data } = await commentsService.getThreads(router.query.id, appVersionsId);
setThreads(data);
@ -43,6 +55,8 @@ const Comments = ({ newThread = {}, appVersionsId, socket }) => {
fetchThreads={fetchData}
socket={socket}
threadId={id}
canvasWidth={canvasWidth}
users={users}
{...thread}
/>
);

View file

@ -35,8 +35,8 @@ export const Button = function Button({ width, height, component, currentState,
const computedStyles = {
backgroundColor,
color,
width: '100%',
borderRadius: `${parsedBorderRadius}px`,
width,
height,
display: parsedWidgetVisibility ? '' : 'none',
'--tblr-btn-color-darker': tinycolor(backgroundColor).darken(8).toString(),

View file

@ -1,7 +1,8 @@
import React from 'react';
import React, { useState } from 'react';
import { Calendar as ReactCalendar, momentLocalizer } from 'react-big-calendar';
import moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { CalendarEventPopover } from './CalendarPopover';
const localizer = momentLocalizer(moment);
@ -15,18 +16,31 @@ const parseDate = (date, dateFormat) => moment(date, dateFormat).toDate();
const allowedCalendarViews = ['month', 'week', 'day'];
export const Calendar = function ({ height, width, properties, styles, fireEvent, darkMode }) {
const style = { height, width };
export const Calendar = function ({
id,
height,
properties,
styles,
fireEvent,
darkMode,
containerProps,
removeComponent,
setExposedVariable,
}) {
const style = { height };
const resourcesParam = properties.resources?.length === 0 ? {} : { resources: properties.resources };
const events = properties.events ? properties.events.map((event) => prepareEvent(event, properties.dateFormat)) : [];
const defaultDate = parseDate(properties.defaultDate, properties.dateFormat);
const [eventPopoverOptions, setEventPopoverOptions] = useState({ show: false });
const eventPropGetter = (event) => {
const backgroundColor = event.color;
const textStyle =
event.textOrientation === 'vertical' ? { writingMode: 'vertical-rl', textOrientation: 'mixed' } : {};
const style = { backgroundColor, ...textStyle, padding: 3, paddingLeft: 5, paddingRight: 5 };
const color = event.textColor ?? 'white';
const style = { backgroundColor, ...textStyle, padding: 3, paddingLeft: 5, paddingRight: 5, color };
return { style };
};
@ -48,12 +62,26 @@ export const Calendar = function ({ height, width, properties, styles, fireEvent
fireEvent('onCalendarSlotSelect', { selectedSlots });
};
function popoverClosed() {
setEventPopoverOptions({
...eventPopoverOptions,
show: false,
});
}
const defaultView = allowedCalendarViews.includes(properties.defaultView)
? properties.defaultView
: allowedCalendarViews[0];
const components = {
timeGutterHeader: () => <div style={{ height: '100%', display: 'flex', alignItems: 'flex-end' }}>All day</div>,
week: {
header: (props) => <div>{moment(props.date).format(styles.displayDayNamesInWeekView ? 'ddd' : 'DD MMM')}</div>,
},
};
return (
<div>
<div id={id}>
<ReactCalendar
className={`calendar-widget
${darkMode ? 'dark-mode' : ''}
@ -68,16 +96,39 @@ export const Calendar = function ({ height, width, properties, styles, fireEvent
style={style}
views={allowedCalendarViews}
defaultView={defaultView}
onView={(view) => setExposedVariable('currentView', view)}
{...resourcesParam}
resourceIdAccessor="resourceId"
resourceTitleAccessor="title"
onSelectEvent={(calendarEvent) => fireEvent('onCalendarEventSelect', { calendarEvent })}
onSelectEvent={(calendarEvent, e) => {
fireEvent('onCalendarEventSelect', { calendarEvent });
if (properties.showPopOverOnEventClick)
setEventPopoverOptions({
...eventPopoverOptions,
show: true,
offset: {
left: e.target.getBoundingClientRect().x,
top: e.target.getBoundingClientRect().y,
width: e.target.getBoundingClientRect().width,
height: e.target.getBoundingClientRect().height,
},
});
}}
selectable={true}
onSelectSlot={slotSelectHandler}
toolbar={properties.displayToolbar}
eventPropGetter={eventPropGetter}
tooltipAccessor="tooltip"
popup={true}
components={components}
/>
<CalendarEventPopover
calenderWidgetId={id}
show={eventPopoverOptions.show}
offset={eventPopoverOptions.offset}
containerProps={containerProps}
removeComponent={removeComponent}
popoverClosed={popoverClosed}
/>
</div>
);

View file

@ -0,0 +1,98 @@
import React, { useEffect, useRef, useState } from 'react';
import { SubCustomDragLayer } from '../SubCustomDragLayer';
import { SubContainer } from '../SubContainer';
export const CalendarEventPopover = function ({
show,
offset,
calenderWidgetId,
containerProps,
removeComponent,
popoverClosed,
}) {
const parentRef = useRef(null);
const [showPopover, setShow] = useState(show);
const [top, setTop] = useState(0);
const [left, setLeft] = useState(0);
const minHeight = 400;
let calendarBounds;
let canvasBounds;
const calendarElement = document.getElementById(calenderWidgetId);
useEffect(() => {
setShow(show);
}, [show]);
useEffect(() => {
if (offset?.top && showPopover) {
const _left = offset.left - calendarBounds.x + offset.width;
const _top = ((offset.top - calendarBounds.y) * 100) / calendarBounds.height;
setTop(_top);
setLeft(_left);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [offset?.top, showPopover]);
if (calendarElement && showPopover) {
calendarBounds = calendarElement.getBoundingClientRect();
const canvasElement = document.getElementsByClassName('canvas-container')[0];
canvasBounds = canvasElement.getBoundingClientRect();
}
return (
<div>
{showPopover && (
<div
style={{
// backgroundColor: 'rgba(0, 0, 0, 0.6)', // This can be used for testing the overlay
top: -(calendarBounds.y + top),
left: -calendarBounds.x,
zIndex: 10,
position: 'fixed',
height: canvasBounds.height + top,
width: canvasBounds.width,
}}
onClick={() => popoverClosed()}
></div>
)}
<div
style={{
position: 'absolute',
zIndex: 100,
width: '300px',
maxWidth: '300px',
minHeight,
top: `${top}%`,
left,
display: showPopover ? 'block' : 'none',
}}
role="tooltip"
x-placement="left"
className="popover bs-popover-left shadow-lg"
ref={parentRef}
id={`${calenderWidgetId}-popover`}
>
{parentRef.current && showPopover && (
<div className="popover-body" style={{ padding: 'unset', width: '100%', height: '100%', zIndex: 11 }}>
<>
<SubContainer
containerCanvasWidth={300}
parent={`${calenderWidgetId}-popover`}
{...containerProps}
parentRef={parentRef}
removeComponent={removeComponent}
/>
<SubCustomDragLayer
parent={calenderWidgetId}
parentRef={parentRef}
currentLayout={containerProps.currentLayout}
/>
</>
</div>
)}
</div>
</div>
);
};

View file

@ -32,7 +32,7 @@ export const Chart = function Chart({ id, width, height, component, onComponentC
}, [currentState]);
const computedStyles = {
width,
width: width - 4,
height,
display: parsedWidgetVisibility ? '' : 'none',
background: darkMode ? '#1f2936' : 'white',
@ -56,7 +56,7 @@ export const Chart = function Chart({ id, width, height, component, onComponentC
const fontColor = darkMode ? '#c3c3c3' : null;
const layout = {
width,
width: width - 4,
height,
plot_bgcolor: darkMode ? '#1f2936' : null,
paper_bgcolor: darkMode ? '#1f2936' : null,
@ -137,7 +137,7 @@ export const Chart = function Chart({ id, width, height, component, onComponentC
}}
>
{loadingState === true ? (
<div style={{ width: '100%' }} className="p-2">
<div style={{ width }} className="p-2">
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>

View file

@ -3,7 +3,6 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const Checkbox = function Checkbox({
id,
width,
height,
component,
onComponentClick,
@ -46,7 +45,7 @@ export const Checkbox = function Checkbox({
<div
data-disabled={parsedDisabledState}
className="row py-1"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);

View file

@ -0,0 +1,89 @@
import React, { useEffect, useState } from 'react';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import CodeMirror from '@uiw/react-codemirror';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/search/match-highlighter';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/theme/base16-light.css';
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);
function codeChanged(code) {
setEditorValue(code);
onComponentOptionChanged(component, 'value', code);
}
const styles = {
width: width,
height: height,
display: !parsedWidgetVisibility ? 'none' : 'block',
};
const options = {
lineNumbers: parsedEnableLineNumber,
lineWrapping: true,
singleLine: true,
mode: languageMode,
tabSize: 2,
theme: darkMode ? 'monokai' : 'duotone-light',
readOnly: false,
highlightSelectionMatches: true,
placeholder,
};
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
className={`code-hinter codehinter-default-input code-editor-widget`}
style={{ height: height || 'auto', minHeight: height - 1, maxHeight: '320px', overflow: 'auto' }}
>
<CodeMirror
value={editorValue}
realState={realState}
scrollbarStyle={null}
height={height - 1}
onBlur={(editor) => {
const value = editor.getValue();
codeChanged(value);
}}
onChange={(editor) => valueChanged(editor, codeChanged)}
onBeforeChange={(editor, change) => onBeforeChange(editor, change)}
options={options}
/>
</div>
</div>
);
};

View file

@ -3,15 +3,7 @@ import { SubCustomDragLayer } from '../SubCustomDragLayer';
import { SubContainer } from '../SubContainer';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const Container = function Container({
id,
component,
height,
containerProps,
width,
currentState,
removeComponent,
}) {
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;
@ -29,7 +21,6 @@ export const Container = function Container({
const computedStyles = {
backgroundColor,
width,
height,
display: parsedWidgetVisibility ? 'flex' : 'none',
};
@ -40,12 +31,13 @@ export const Container = function Container({
<div
data-disabled={parsedDisabledState}
className="jet-container"
id={id}
ref={parentRef}
onClick={() => containerProps.onComponentClick(id, component)}
style={computedStyles}
>
<SubContainer parent={id} {...containerProps} parentRef={parentRef} removeComponent={removeComponent} />
<SubCustomDragLayer 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>
);
};

View file

@ -5,7 +5,6 @@ import { resolveReferences, resolveWidgetFieldValue, validateWidget } from '@/_h
export const Datepicker = function Datepicker({
id,
width,
height,
component,
onComponentClick,
@ -84,7 +83,7 @@ export const Datepicker = function Datepicker({
<div
data-disabled={parsedDisabledState}
className="datepicker-widget"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);

View file

@ -7,7 +7,6 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const DaterangePicker = function DaterangePicker({
id,
width,
height,
component,
onComponentClick,
@ -59,7 +58,7 @@ export const DaterangePicker = function DaterangePicker({
return (
<div
className="daterange-picker-widget p-0"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);

View file

@ -4,7 +4,6 @@ import SelectSearch, { fuzzySearch } from 'react-select-search';
export const DropDown = function DropDown({
id,
width,
height,
component,
onComponentClick,
@ -61,7 +60,9 @@ export const DropDown = function DropDown({
const currentValueProperty = component.definition.properties.value;
const value = currentValueProperty ? currentValueProperty.value : '';
const [currentValue, setCurrentValue] = useState('');
const [currentValue, setCurrentValue] = useState(() =>
resolveReferences(currentValueProperty.value, currentState, '')
);
let newValue = value;
if (currentValueProperty && currentState) {
@ -91,10 +92,19 @@ export const DropDown = function DropDown({
// 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]);
return (
<div
className="dropdown-widget row g-0"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);

View file

@ -17,6 +17,7 @@ export const FilePicker = ({ width, height, component, currentState, onComponent
typeof enableDropzone !== 'boolean' ? resolveWidgetFieldValue(enableDropzone, currentState) : true;
const parsedEnablePicker =
typeof enablePicker !== 'boolean' ? resolveWidgetFieldValue(enablePicker, currentState) : true;
const parsedMaxFileCount =
typeof maxFileCount !== 'number' ? resolveWidgetFieldValue(maxFileCount, currentState) : maxFileCount;
const parsedEnableMultiple =
@ -50,7 +51,6 @@ export const FilePicker = ({ width, height, component, currentState, onComponent
outline: 'none',
transition: 'border .24s ease-in-out',
display: parsedWidgetVisibility ? 'flex' : 'none',
width,
height,
backgroundColor: !parsedDisabledState && bgThemeColor,
};
@ -95,6 +95,47 @@ export const FilePicker = ({ width, height, component, currentState, onComponent
const [showSelectdFiles, setShowSelectedFiles] = React.useState(false);
const [selectedFiles, setSelectedFiles] = React.useState([]);
/**
* *getFileData()
* @param {*} file
* @param {*} method: readAsDataURL, readAsText
*/
const getFileData = (file = {}, method = 'readAsText') => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (result) => {
resolve([result, reader]);
};
reader[method](file);
reader.onerror = (error) => {
reject(error);
if (error.name == 'NotReadableError') {
toast.error(error.message, { hideProgressBar: true, autoClose: 3000 });
}
};
}).then((result) => {
if (method === 'readAsDataURL') {
return result[0].srcElement.result.split(',')[1];
}
return result[0].srcElement.result;
});
};
const fileReader = async (file) => {
// * readAsText
const readFileAsText = await getFileData(file);
// * readAsDataURL
const readFileAsDataURL = await getFileData(file, 'readAsDataURL');
return {
name: file.name,
type: file.type,
content: readFileAsText,
dataURL: readFileAsDataURL,
};
};
useEffect(() => {
if (acceptedFiles.length === 0) {
onComponentOptionChanged(component, 'file', []);
@ -103,23 +144,9 @@ export const FilePicker = ({ width, height, component, currentState, onComponent
if (acceptedFiles.length !== 0) {
const fileData = parsedEnableMultiple ? [...selectedFiles] : [];
acceptedFiles.map((acceptedFile) => {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = (result) => {
//* Resolve both the FileReader result and its original file.
resolve([result, acceptedFile]);
};
//* Reads contents of the file as a text string.
reader.readAsText(acceptedFile);
}).then((zippedResults) => {
//? Run the callback after all files have been read.
const fileSelected = {
name: zippedResults[1].name,
content: zippedResults[0].srcElement.result,
type: zippedResults[1].type,
};
fileData.push(fileSelected);
const acceptedFileData = fileReader(acceptedFile);
acceptedFileData.then((data) => {
fileData.push(data);
});
});

View file

@ -0,0 +1,40 @@
import React from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const IFrame = function IFrame({ id, width, 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 parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
return (
<div
data-disabled={parsedDisabledState}
style={{ display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
}}
>
<iframe
width={width}
height={height}
src={source}
title="IFrame Widget"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
);
};

View file

@ -2,7 +2,7 @@ import React from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import LazyLoad from 'react-lazyload';
export const Image = function Image({ id, width, height, component, onComponentClick, currentState }) {
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;
@ -22,7 +22,7 @@ export const Image = function Image({ id, width, height, component, onComponentC
if (data === '') data = null;
function Placeholder() {
return <div className="skeleton-image" style={{ objectFit: 'contain', width, height }}></div>;
return <div className="skeleton-image" style={{ objectFit: 'contain', height }}></div>;
}
return (
@ -34,8 +34,8 @@ export const Image = function Image({ id, width, height, component, onComponentC
onComponentClick(id, component, event);
}}
>
<LazyLoad width={width} height={height} placeholder={<Placeholder />} debounce={500}>
<img style={{ objectFit: 'contain' }} src={data} width={width} height={height} />
<LazyLoad height={height} placeholder={<Placeholder />} debounce={500}>
<img style={{ objectFit: 'contain' }} src={data} height={height} />
</LazyLoad>
</div>
);

View file

@ -14,6 +14,7 @@ export const Map = function Map({
onComponentOptionChanged,
onComponentOptionsChanged,
onEvent,
canvasWidth
}) {
const center = component.definition.properties.initialLocation.value;
const defaultMarkerValue = component.definition.properties.defaultMarkers.value;
@ -50,7 +51,7 @@ export const Map = function Map({
const [markers, setMarkers] = useState(resolveReferences(defaultMarkers, currentState));
const containerStyle = {
width,
width: '100%',
height,
};
@ -111,7 +112,7 @@ export const Map = function Map({
return (
<div
data-disabled={parsedDisabledState}
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);

View file

@ -58,7 +58,7 @@ export const Modal = function Modal({ id, component, height, containerProps, cur
</div>
</BootstrapModal.Header>
<BootstrapModal.Body style={{ height }} ref={parentRef}>
<BootstrapModal.Body style={{ height }} ref={parentRef} id={id}>
<SubContainer parent={id} {...containerProps} parentRef={parentRef} />
<SubCustomDragLayer
snapToGrid={true}

View file

@ -55,7 +55,7 @@ export const Multiselect = function Multiselect({
return (
<div
className="multiselect-widget row g-0"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);

View file

@ -3,7 +3,6 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const NumberInput = function NumberInput({
id,
width,
height,
component,
onComponentClick,
@ -54,7 +53,7 @@ export const NumberInput = function NumberInput({
type="number"
className="form-control rounded-0"
placeholder={placeholder}
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
value={number}
/>
);

View file

@ -3,7 +3,6 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const RadioButton = function RadioButton({
id,
width,
height,
component,
onComponentClick,
@ -72,9 +71,7 @@ export const RadioButton = function RadioButton({
function onSelect(selection) {
onComponentOptionChanged(component, 'value', selection);
if (selection) {
onEvent('onSelectionChange', { component });
}
onEvent('onSelectionChange', { component });
}
useEffect(() => {
@ -86,7 +83,7 @@ export const RadioButton = function RadioButton({
<div
data-disabled={parsedDisabledState}
className="row py-1"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);

View file

@ -35,7 +35,7 @@ export const RichTextEditor = function RichTextEditor({
return (
<div
data-disabled={parsedDisabledState}
style={{ width: `${width}px`, height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);

View file

@ -101,7 +101,7 @@ export const StarRating = function StarRating({
style={{ display: parsedWidgetVisibility ? '' : 'none' }}
>
{/* TODO: Add label color defination property instead of hardcoded color*/}
<span className="label form-check-label form-check-label col-auto" style={{ color: labelColor }}>
<span className="label form-check-label col-auto" style={{ color: labelColor }}>
{label}
</span>
<div className="col px-1 py-0 mt-0">

View file

@ -324,7 +324,7 @@ export function Table({
case 'string':
case undefined:
case 'default': {
const textColor = resolveReferences(column.textColor, currentState, { cellValue });
const textColor = resolveReferences(column.textColor, currentState, '', { cellValue });
const cellStyles = {
color: textColor ?? '',
@ -561,65 +561,65 @@ export function Table({
const leftActionsCellData =
leftActions().length > 0
? [
{
id: 'leftActions',
Header: 'Actions',
accessor: 'edit',
width: columnSizes.leftActions || defaultColumn.width,
Cell: (cell) => {
return leftActions().map((action) => (
<button
key={action.name}
className="btn btn-sm m-1 btn-light"
style={{ background: action.backgroundColor, color: action.textColor }}
onClick={(e) => {
e.stopPropagation();
onEvent('onTableActionButtonClicked', {
component,
data: cell.row.original,
rowId: cell.row.id,
action,
});
}}
>
{action.buttonText}
</button>
));
},
{
id: 'leftActions',
Header: 'Actions',
accessor: 'edit',
width: columnSizes.leftActions || defaultColumn.width,
Cell: (cell) => {
return leftActions().map((action) => (
<button
key={action.name}
className="btn btn-sm m-1 btn-light"
style={{ background: action.backgroundColor, color: action.textColor }}
onClick={(e) => {
e.stopPropagation();
onEvent('onTableActionButtonClicked', {
component,
data: cell.row.original,
rowId: cell.row.id,
action,
});
}}
>
{action.buttonText}
</button>
));
},
]
},
]
: [];
const rightActionsCellData =
rightActions().length > 0
? [
{
id: 'rightActions',
Header: 'Actions',
accessor: 'edit',
width: columnSizes.rightActions || defaultColumn.width,
Cell: (cell) => {
return rightActions().map((action) => (
<button
key={action.name}
className="btn btn-sm m-1 btn-light"
style={{ background: action.backgroundColor, color: action.textColor }}
onClick={(e) => {
e.stopPropagation();
onEvent('onTableActionButtonClicked', {
component,
data: cell.row.original,
rowId: cell.row.id,
action,
});
}}
>
{action.buttonText}
</button>
));
},
{
id: 'rightActions',
Header: 'Actions',
accessor: 'edit',
width: columnSizes.rightActions || defaultColumn.width,
Cell: (cell) => {
return rightActions().map((action) => (
<button
key={action.name}
className="btn btn-sm m-1 btn-light"
style={{ background: action.backgroundColor, color: action.textColor }}
onClick={(e) => {
e.stopPropagation();
onEvent('onTableActionButtonClicked', {
component,
data: cell.row.original,
rowId: cell.row.id,
action,
});
}}
>
{action.buttonText}
</button>
));
},
]
},
]
: [];
const IndeterminateCheckbox = React.forwardRef(({ indeterminate, ...rest }, ref) => {
@ -666,7 +666,7 @@ export function Table({
const data = useMemo(() => tableData, [tableData.length, componentState.changeSet]);
const computedStyles = {
width: `${width}px`,
// width: `${width}px`,
};
const {
@ -773,7 +773,7 @@ export function Table({
<div
data-disabled={parsedDisabledState}
className="card jet-table"
style={{ width: `${width}px`, height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ width: `100%`, height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component, event);
@ -835,9 +835,8 @@ export function Table({
return (
<tr
key={index}
className={`table-row ${
highlightSelectedRow && row.id === componentState.selectedRowId ? 'selected' : ''
}`}
className={`table-row ${highlightSelectedRow && row.id === componentState.selectedRowId ? 'selected' : ''
}`}
{...row.getRowProps()}
onClick={(e) => {
e.stopPropagation();
@ -899,62 +898,62 @@ export function Table({
Object.keys(componentState.changeSet || {}).length > 0 ||
showFilterButton ||
showDownloadButton) && (
<div className="card-footer d-flex align-items-center jet-table-footer">
<div className="table-footer row">
<div className="col">
{(clientSidePagination || serverSidePagination) && (
<Pagination
lastActivePageIndex={pageIndex}
serverSide={serverSidePagination}
autoGotoPage={gotoPage}
autoCanNextPage={canNextPage}
autoPageCount={pageCount}
autoPageOptions={pageOptions}
onPageIndexChanged={onPageIndexChanged}
/>
)}
</div>
{showBulkUpdateActions && Object.keys(componentState.changeSet || {}).length > 0 && (
<div className="card-footer d-flex align-items-center jet-table-footer">
<div className="table-footer row">
<div className="col">
<button
className={`btn btn-primary btn-sm ${componentState.isSavingChanges ? 'btn-loading' : ''}`}
onClick={() =>
onEvent('onBulkUpdate', { component }).then(() => {
handleChangesSaved();
})
}
>
Save Changes
</button>
<button className="btn btn-light btn-sm mx-2" onClick={() => handleChangesDiscarded()}>
Discard changes
</button>
{(clientSidePagination || serverSidePagination) && (
<Pagination
lastActivePageIndex={pageIndex}
serverSide={serverSidePagination}
autoGotoPage={gotoPage}
autoCanNextPage={canNextPage}
autoPageCount={pageCount}
autoPageOptions={pageOptions}
onPageIndexChanged={onPageIndexChanged}
/>
)}
</div>
)}
<div className="col-auto">
{showFilterButton && (
<span data-tip="Filter data" className="btn btn-light btn-sm p-1 mx-2" onClick={() => showFilters()}>
<img src="/assets/images/icons/filter.svg" width="13" height="13" />
{filters.length > 0 && (
<a className="badge bg-azure" style={{ width: '4px', height: '4px', marginTop: '5px' }}></a>
)}
</span>
)}
{showDownloadButton && (
<span
data-tip="Download as CSV"
className="btn btn-light btn-sm p-1"
onClick={() => exportData('csv', true)}
>
<img src="/assets/images/icons/download.svg" width="13" height="13" />
</span>
{showBulkUpdateActions && Object.keys(componentState.changeSet || {}).length > 0 && (
<div className="col">
<button
className={`btn btn-primary btn-sm ${componentState.isSavingChanges ? 'btn-loading' : ''}`}
onClick={() =>
onEvent('onBulkUpdate', { component }).then(() => {
handleChangesSaved();
})
}
>
Save Changes
</button>
<button className="btn btn-light btn-sm mx-2" onClick={() => handleChangesDiscarded()}>
Discard changes
</button>
</div>
)}
<div className="col-auto">
{showFilterButton && (
<span data-tip="Filter data" className="btn btn-light btn-sm p-1 mx-2" onClick={() => showFilters()}>
<img src="/assets/images/icons/filter.svg" width="13" height="13" />
{filters.length > 0 && (
<a className="badge bg-azure" style={{ width: '4px', height: '4px', marginTop: '5px' }}></a>
)}
</span>
)}
{showDownloadButton && (
<span
data-tip="Download as CSV"
className="btn btn-light btn-sm p-1"
onClick={() => exportData('csv', true)}
>
<img src="/assets/images/icons/download.svg" width="13" height="13" />
</span>
)}
</div>
</div>
</div>
</div>
)}
)}
{isFiltersVisible && (
<div className="table-filters card">
<div className="card-header row">

View file

@ -0,0 +1,92 @@
import React, { useRef, useState } from 'react';
import { SubCustomDragLayer } from '../SubCustomDragLayer';
import { SubContainer } from '../SubContainer';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const Tabs = function Tabs({ id, component, width, height, containerProps, currentState, removeComponent }) {
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const defaultTab = component.definition.properties.defaultTab.value;
// config for tabs. Includes title
const tabs = component.definition.properties?.tabs?.value ?? [];
let parsedTabs = tabs;
parsedTabs = resolveWidgetFieldValue(parsedTabs, currentState);
// set index as id if id is not provided
parsedTabs = parsedTabs.map((parsedTab, index) => ({ ...parsedTab, id: parsedTab.id ? parsedTab.id : index }));
// Highlight color - for active tab text and border
const highlightColor = component.definition.styles?.highlightColor?.value ?? '';
let parsedHighlightColor = highlightColor;
parsedHighlightColor = resolveWidgetFieldValue(highlightColor, currentState);
// Default tab
let parsedDefaultTab = defaultTab;
parsedDefaultTab = resolveWidgetFieldValue(parsedDefaultTab, currentState, 1);
const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
const computedStyles = {
height,
display: parsedWidgetVisibility ? 'flex' : 'none',
};
const parentRef = useRef(null);
const [currentTab, setCurrentTab] = useState(parsedDefaultTab);
return (
<div
data-disabled={parsedDisabledState}
className="jet-tabs card"
onClick={() => {
containerProps.onComponentClick(id, component);
}}
style={computedStyles}
>
<ul className="nav nav-tabs" data-bs-toggle="tabs">
{parsedTabs.map((tab) => (
<li className="nav-item" onClick={() => setCurrentTab(tab.id)} key={tab.id}>
<a
className={`nav-link ${currentTab === tab.id ? 'active' : ''}`}
style={
currentTab === tab.id
? { color: parsedHighlightColor, borderBottom: `1px solid ${parsedHighlightColor}` }
: {}
}
>
{tab.title}
</a>
</li>
))}
</ul>
<div className="tab-content" ref={parentRef} id={`${id}-${currentTab}`}>
<div className="tab-pane active show">
<SubContainer
parent={`${id}-${currentTab}`}
{...containerProps}
parentRef={parentRef}
removeComponent={removeComponent}
containerCanvasWidth={width}
/>
<SubCustomDragLayer
parent={id}
parentRef={parentRef}
currentLayout={containerProps.currentLayout}
containerCanvasWidth={width}
/>
</div>
</div>
</div>
);
};

View file

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import DOMPurify from 'dompurify';
export const Text = function Text({ id, width, height, component, onComponentClick, currentState }) {
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;
@ -45,7 +45,6 @@ export const Text = function Text({ id, width, height, component, onComponentCli
const computedStyles = {
color,
width,
height,
display: parsedWidgetVisibility ? 'flex' : 'none',
alignItems: 'center',

View file

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

View file

@ -3,7 +3,6 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const TextInput = function TextInput({
id,
width,
height,
component,
onComponentClick,
@ -65,7 +64,7 @@ export const TextInput = function TextInput({
type="text"
className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon`}
placeholder={placeholder}
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
style={{ height, display: parsedWidgetVisibility ? '' : 'none' }}
value={text}
/>
<div className="invalid-feedback">{validationError}</div>

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