Merge branch 'release/v0.7.4' into main

This commit is contained in:
navaneeth 2021-10-07 13:17:34 +05:30
commit baa577bc44
316 changed files with 22922 additions and 29070 deletions

View file

@ -1,2 +0,0 @@
frontend/node_modules/**
# **/*.js

View file

@ -1,8 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"printWidth": 120,
"singleQuote": true,
"arrowParens": "always",
"proseWrap": "preserve"
}

View file

@ -1 +1 @@
0.7.3 0.7.4

11
.vscode/extension.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"CoenraadS.bracket-pair-colorizer",
"mgmcdermott.vscode-language-babel",
"formulahendry.auto-rename-tag",
"xabikos.javascriptsnippets",
"streetsidesoftware.code-spell-checker",
"esbenp.prettier-vscode"
]
}

14
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,14 @@
{
"[javascript, typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"eslint.format.enable": true,
"editor.formatOnSave": true,
}

View file

@ -32,8 +32,8 @@ Pull requests are the best way to propose changes to the codebase (we use [Git-F
5. Make sure your code lints. 5. Make sure your code lints.
6. Issue that pull request! 6. Issue that pull request!
## Any contributions you make will be under the Apache-2.0 License ## Any contributions you make will be under the AGPL v3 License
In short, when you submit code changes, your submissions are understood to be under the same [Apache-2.0 License](https://www.apache.org/licenses/LICENSE-2.0) that covers the project. In short, when you submit code changes, your submissions are understood to be under the same [AGPL v3 License](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project.
## Report bugs using Github's [issues](https://github.com/ToolJet/ToolJet/issues) ## Report bugs using Github's [issues](https://github.com/ToolJet/ToolJet/issues)
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy! We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
@ -49,7 +49,7 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue]
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
## License ## License
By contributing, you agree that your contributions will be licensed under its Apache-2.0 License. By contributing, you agree that your contributions will be licensed under its AGPL v3 License.
## Questions? ## Questions?
Contact us on slack [Slack](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg) or mail us at [hello@tooljet.io](hello@tooljet) Contact us on slack [Slack](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg) or mail us at [hello@tooljet.io](hello@tooljet)

143
LICENSE
View file

@ -1,5 +1,5 @@
GNU GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
@ -7,17 +7,15 @@ GNU GENERAL PUBLIC LICENSE
Preamble Preamble
The GNU General Public License is a free, copyleft license for The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works. software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast, to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the software for all its users.
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you price. Our General Public Licenses are designed to make sure that you
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things. free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you Developers that use our General Public Licenses protect your rights
these rights or asking you to surrender the rights. Therefore, you have with two steps: (1) assert copyright on the software, and (2) offer
certain responsibilities if you distribute copies of the software, or if you this License which gives you legal permission to copy, distribute
you modify it: responsibilities to respect the freedom of others. and/or modify the software.
For example, if you distribute copies of such a program, whether A secondary benefit of defending all users' freedom is that
gratis or for a fee, you must pass on to the recipients the same improvements made in alternate versions of the program, if they
freedoms that you received. You must make sure that they, too, receive receive widespread use, become available for other developers to
or can get the source code. And you must show them these terms so they incorporate. Many developers of free software are heartened and
know their rights. encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
Developers that use the GNU GPL protect your rights with two steps: The GNU Affero General Public License is designed specifically to
(1) assert copyright on the software, and (2) offer you this License ensure that, in such cases, the modified source code becomes available
giving you legal permission to copy, distribute and/or modify it. to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
For the developers' and authors' protection, the GPL clearly explains An older license, called the Affero General Public License and
that there is no warranty for this free software. For both users' and published by Affero, was designed to accomplish similar goals. This is
authors' sake, the GPL requires that modified versions be marked as a different license, not a version of the Affero GPL, but Affero has
changed, so that their problems will not be attributed erroneously to released a new version of the Affero GPL which permits relicensing under
authors of previous versions. this license.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
@ -72,7 +60,7 @@ modification follow.
0. Definitions. 0. Definitions.
"This License" refers to version 3 of the GNU General Public License. "This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks. works, such as semiconductor masks.
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program. License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License. 13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work, License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License, but the work with which it is combined will remain governed by version
section 13, concerning interaction through a network will apply to the 3 of the GNU General Public License.
combination as such.
14. Revised Versions of this License. 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will the GNU Affero General Public License from time to time. Such new versions
be similar in spirit to the present version, but may differ in detail to will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns. address new problems or concerns.
Each version is given a distinguishing version number. If the Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation. by the Free Software Foundation.
If the Program specifies that a proxy can decide which future If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you public statement of acceptance of a version permanently authorizes you
to choose that version for the Program. to choose that version for the Program.
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author> Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU Affero General Public License as published
the Free Software Foundation, either version 3 of the License, or by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If your software can interact with users remotely through a computer
notice like this when it starts in an interactive mode: network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
<program> Copyright (C) <year> <name of author> interface could display a "Source" link that leads users to an archive
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. of the code. There are many ways you could offer source, and different
This is free software, and you are welcome to redistribute it solutions will be better for different programs; see section 13 for the
under certain conditions; type `show c' for details. specific requirements.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>. <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View file

@ -6,14 +6,21 @@
ToolJet is an **open-source no-code framework** to build and deploy internal tools quickly without much effort from the engineering teams. You can connect to your data sources such as databases ( like PostgreSQL, MongoDB, Elasticsearch, etc ), API endpoints ( ToolJet supports importing OpenAPI spec & OAuth2 authorization) and external services ( like Stripe, Slack, Google Sheets, Airtable ) and use our pre-built UI widgets to build internal tools. ToolJet is an **open-source no-code framework** to build and deploy internal tools quickly without much effort from the engineering teams. You can connect to your data sources such as databases ( like PostgreSQL, MongoDB, Elasticsearch, etc ), API endpoints ( ToolJet supports importing OpenAPI spec & OAuth2 authorization) and external services ( like Stripe, Slack, Google Sheets, Airtable ) and use our pre-built UI widgets to build internal tools.
![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/tooljet/tooljet-ce)
![GitHub contributors](https://img.shields.io/github/contributors/tooljet/tooljet)
[![GitHub issues](https://img.shields.io/github/issues/ToolJet/ToolJet)](https://github.com/ToolJet/ToolJet/issues) [![GitHub issues](https://img.shields.io/github/issues/ToolJet/ToolJet)](https://github.com/ToolJet/ToolJet/issues)
[![GitHub stars](https://img.shields.io/github/stars/ToolJet/ToolJet)](https://github.com/ToolJet/ToolJet/stargazers) [![GitHub stars](https://img.shields.io/github/stars/ToolJet/ToolJet)](https://github.com/ToolJet/ToolJet/stargazers)
![GitHub closed issues](https://img.shields.io/github/issues-closed/tooljet/tooljet)
![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/tooljet/tooljet)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/tooljet/tooljet)
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/tooljet/tooljet)
[![GitHub license](https://img.shields.io/github/license/ToolJet/ToolJet)](https://github.com/ToolJet/ToolJet) [![GitHub license](https://img.shields.io/github/license/ToolJet/ToolJet)](https://github.com/ToolJet/ToolJet)
<p align="center"> <p align="center">
<kbd> <kbd>
<img src="https://user-images.githubusercontent.com/7828962/130593222-d1268766-b776-4952-bd36-9caf9810d851.gif" /> <img src="https://user-images.githubusercontent.com/7828962/134216201-b2c65c48-547a-4e79-946c-60b7be54b70c.png" />
</kbd> </kbd>
</p> </p>
@ -29,7 +36,6 @@ ToolJet is an **open-source no-code framework** to build and deploy internal too
- Write JS code almost anywhere in the builder - Write JS code almost anywhere in the builder
- Query editors for all supported data sources - Query editors for all supported data sources
- Transform query results using JS code - Transform query results using JS code
- Import endpoints from OpenAPI specs
- All the credentials are securely encrypted using `aes-256-gcm`. - All the credentials are securely encrypted using `aes-256-gcm`.
- ToolJet acts only as a proxy and doesn't store any data. - ToolJet acts only as a proxy and doesn't store any data.
- Support for OAuth - Support for OAuth
@ -44,8 +50,6 @@ You can deploy ToolJet on Heroku for free using the one-click-deployment button
<a href="https://heroku.com/deploy?template=https://github.com/tooljet/tooljet/tree/main"><img src="https://www.herokucdn.com/deploy/button.svg" /></a> <a href="https://heroku.com/deploy?template=https://github.com/tooljet/tooljet/tree/main"><img src="https://www.herokucdn.com/deploy/button.svg" /></a>
</P> </P>
## Examples ## Examples
[Building a Github contributor leaderboard using ToolJet](https://blog.tooljet.io/building-a-github-contributor-leaderboard-using-tooljet/)<br> [Building a Github contributor leaderboard using ToolJet](https://blog.tooljet.io/building-a-github-contributor-leaderboard-using-tooljet/)<br>
@ -65,5 +69,10 @@ We use the git-flow branching model. The base branch is develop. If you are look
Read our contributing guide (CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to ToolJet. <br> Read our contributing guide (CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to ToolJet. <br>
[Contributing Guide](https://docs.tooljet.io/docs/contributing-guide/setup/docker) [Contributing Guide](https://docs.tooljet.io/docs/contributing-guide/setup/docker)
## Contributors
<a href="https://github.com/tooljet/tooljet/graphs/contributors">
<img src="https://contrib.rocks/image?repo=tooljet/tooljet" />
</a>
## Licence ## Licence
ToolJet © 2021, ToolJet Inc - Released under the GNU General Public License v3.0. ToolJet © 2021, ToolJet Solutions Inc - Released under the GNU General Public License v3.0.

View file

@ -0,0 +1,7 @@
apiVersion: networking.gke.io/v1beta1
kind: ManagedCertificate
metadata:
name: tj
spec:
domains:
- tooljet.your-domain.com

View file

@ -0,0 +1,58 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: tooljet-deployment
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
component: tooljet
template:
metadata:
labels:
component: tooljet
spec:
imagePullSecrets:
- name: docker-secret
containers:
- name: container
image: tooljet/tooljet-ce:latest
imagePullPolicy: Always
args: ["npm", "run", "start:prod"]
resources:
limits:
memory: "1000Mi"
cpu: "500m"
requests:
memory: "1000Mi"
cpu: "500m"
ports:
- containerPort: 3000
readinessProbe:
httpGet:
port: 3000
path: /api/health
successThreshold: 1
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 6
env:
- name: PG_HOST
value: ""
- name: PG_USER
value: "postgres"
- name: PG_PASS
value: ""
- name: PG_DB
value: ""
- name: LOCKBOX_MASTER_KEY
value: ""
- name: SECRET_KEY_BASE
value: ""
- name: TOOLJET_HOST
value: "https://tooljet.your-domain.com"

View file

@ -0,0 +1,19 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: backend-ingress
annotations:
kubernetes.io/ingress.global-static-ip-name: "tj-static-ip"
networking.gke.io/managed-certificates: "tj"
nginx.ingress.kubernetes.io/ssl-redirect: 'true'
spec:
rules:
- host: tooljet.your-domain.com
http:
paths:
- path: /*
backend:
serviceName: tj-service
servicePort: 8080

View file

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: tj-service
namespace: default
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 3000
selector:
component: tooljet
type: NodePort

View file

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

View file

@ -11,7 +11,7 @@ RUN apt update && apt install -y \
RUN mkdir -p /app RUN mkdir -p /app
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
COPY ./package.json ./package-lock.json . COPY ./package.json ./package-lock.json ./
# Building ToolJet client # Building ToolJet client
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/ COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/

View file

@ -27,5 +27,5 @@ Click on `+` button of the query manager at the bottom panel of the editor and s
Select the operation that you want to perform on Firestore and click 'Save' to save the query. Select the operation that you want to perform on Firestore and click 'Save' to save the query.
:::tip :::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations) Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)
::: :::

View file

@ -23,5 +23,5 @@ Click on `+` button of the query manager at the bottom panel of the editor and s
Select the operation that you want to perform on Firestore and click 'Save' to save the query. Select the operation that you want to perform on Firestore and click 'Save' to save the query.
:::tip :::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations) Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)
::: :::

View file

@ -36,5 +36,5 @@ Click on `+` button of the query manager at the bottom panel of the editor and s
Click on the 'run' button to run the query. NOTE: Query should be saved before running. Click on the 'run' button to run the query. NOTE: Query should be saved before running.
:::tip :::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations) Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)
::: :::

View file

@ -37,5 +37,5 @@ Click on the 'run' button to run the query. NOTE: Query should be saved before r
:::tip :::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations) Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)
::: :::

View file

@ -33,5 +33,5 @@ Click on `+` button of the query manager at the bottom panel of the editor.
Click on the 'run' button to run the query. NOTE: Query should be saved before running. Click on the 'run' button to run the query. NOTE: Query should be saved before running.
:::tip :::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations) Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)
::: :::

View file

@ -34,5 +34,5 @@ Click on '+' button of the query manager at the bottom panel of the editor and s
Click on the 'run' button to run the query. NOTE: Query should be saved before running. Click on the 'run' button to run the query. NOTE: Query should be saved before running.
:::tip :::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations) Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)
::: :::

View file

@ -41,5 +41,5 @@ NOTE: Query should be saved before running.
::: :::
:::tip :::tip
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations) Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)
::: :::

View file

@ -0,0 +1,62 @@
---
sidebar_position: 4
sidebar_label: Kubernetes (GKE)
---
# Deploying ToolJet on Kubernetes (GKE)
:::info
You should setup a PostgreSQL database manually to be used by ToolJet. We recommend using Cloud SQL since this guide is for deploying using GKE.
:::
Follow the steps below to deploy ToolJet on a GKE Kubernetes cluster.
1. Create an SSL certificate.
```bash
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/certificate.yaml
```
Change the domain name to the domain/subdomain that you wish to use for ToolJet installation.
2. Reserve a static IP address using `gcloud` cli
```bash
gcloud compute addresses create tj-static-ip --global
```
3. Create k8s deployment
```bash
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).
4. Create k8s service
```bash
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/service.yaml
```
5. Create k8s ingress
```bash
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/ingress.yaml
```
Change the domain name to the domain/subdomain that you wish to use for ToolJet installation.
6. Apply YAML configs
```bash
kubectl apply -f certificate.yaml, deployment.yaml, service.yaml, ingress.yaml
```
:::info
It might take a few minutes to provision the managed certificates. [Managed certificates documentation](https://cloud.google.com/kubernetes-engine/docs/how-to/managed-certs).
:::
You will be able to access your ToolJet installation once the pods, service and the ingress is running.
If you want to seed the database with a sample user, please SSH into a pod and run:
`npm run db:seed --prefix server`.
This seeds the database with a default user with the following credentials:
email: `dev@tooljet.io`
password: `password`

View file

@ -40,5 +40,5 @@ Application load balancing on Amazon EKS: https://docs.aws.amazon.com/eks/latest
GKE Ingress for HTTP(S) Load Balancing: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress GKE Ingress for HTTP(S) Load Balancing: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress
:::tip :::tip
If you want to serve ToolJet client from services such as Firebase or Netlify, please read the client deployment documentation [here](/docs/setup/client). If you want to serve ToolJet client from services such as Firebase or Netlify, please read the client deployment documentation [here](/docs/deployment/client).
::: :::

View file

@ -27,8 +27,8 @@ These resources will help you to quickly build and deploy apps using ToolJet:
The references for datasources and widgets: The references for datasources and widgets:
- [Datasource Reference](/docs/data-sources/redis) - **[Datasource Reference](/docs/data-sources/redis)**
- [Widget Reference](/docs/widgets/table) - **[Widget Reference](/docs/widgets/table)**
## Help and Support ## Help and Support
We have extensively documented the features of ToolJet, but in case you are stuck, please feel to mail us: hello@tooljet.io. We have extensively documented the features of ToolJet, but in case you are stuck, please feel to mail us: hello@tooljet.io.

View file

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

51
docs/docs/sso/google.md Normal file
View file

@ -0,0 +1,51 @@
---
sidebar_position: 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"/>
<br /><br /><br />
Goto [Google cloud console credentials page](https://console.cloud.google.com/apis/credentials), and create an OAuth client ID
<img class="screenshot-full" src="/img/sso/google/create-oauth.png" alt="ToolJet - Google create client id" height="420"/>
<br /><br /><br />
You'll be asked to select user type in consent screen. To allow only users within your organization, select 'Internal', otherwise,
select 'External'.
<img class="screenshot-full" src="/img/sso/google/oauth-type.png" alt="ToolJet - OAuth user type" height="420"/>
<br /><br /><br />
You'll be led to an app registration page where you can set OAuth scopes. Select 'Add or remove scopes' and add the scopes
userinfo.email and userinfo.profile as shown in the image. This will allow ToolJet to store the email and name of the
user who is signing in
<img class="screenshot-full" src="/img/sso/google/scope.png" alt="ToolJet - OAuth scope" height="420"/>
<br /><br /><br />
Set the domain on which ToolJet is hosted as an authorized domain
<img class="screenshot-full" src="/img/sso/google/authorized-urls.png" alt="ToolJet - Google authorized domain" height="420"/>
<br /><br /><br />
Lastly, supply the environment variable `SSO_GOOGLE_OAUTH2_CLIENT_ID` to your deployment. This value will be available from your [Google cloud console credentials page](https://console.cloud.google.com/apis/credentials)
:::info
### Restrict to your domain
Set the environment variable `RESTRICTED_DOMAIN` to ensure that ToolJet verifies the domain of the user who signs in via SSO, on the server side.
If you're setting this environment variable, please make sure that the value does not contain any protocols, sub domains or slashes. It should
simply be `yourdomain.com`.
:::
<br />
The Google sign-in button will now be available in your ToolJet login screen.

View file

@ -4,7 +4,7 @@ sidebar_position: 8
# Mobile layout # Mobile layout
Mobile layout is activated when the width of window is less than 600px. Mobile layout is activated when the width of window is less than 600px.
:::tip :::tip
Widgets can be shown on desktop, mobile, or both. Widgets can be shown on desktop, mobile, or both.
@ -12,11 +12,11 @@ Widgets can be shown on desktop, mobile, or both.
<img class="screenshot-full" src="/img/tutorial/mobile-layout/mobile-layout.gif" alt="ToolJet - widgets list" height="420"/> <img class="screenshot-full" src="/img/tutorial/mobile-layout/mobile-layout.gif" alt="ToolJet - widgets list" height="420"/>
## Adding existing widget to mobile layout ## Adding existing widget to mobile layout
Click on the widget to open inspector. Scroll down to the `layout` section and enable mobile layout. The width of the widget will be adjusted to fit the mobile layout. Click on the widget to open inspector. Scroll down to the `layout` section and enable mobile layout. The width of the widget will be adjusted to fit the mobile layout.
## Adding a new widget to mobile layout ## Adding a new widget to mobile layout
Switch the layout to mobile by clicking the button on the top navigation bar. Drag and drop a widget to the canvas. This widget will not be shown on desktop layout unless enabled from the widget inspector via the "Show on desktop?" button manually. Switch the layout to mobile by clicking the button on the top navigation bar. Drag and drop a widget to the canvas. This widget will not be shown on desktop layout unless enabled from the widget inspector via the "Show on desktop" button manually.
:::tip :::tip
Width of the widgets will be automatically adjusted to fit the screen while viewing the application in app viewer. Width of the widgets will be automatically adjusted to fit the screen while viewing the application in app viewer.

View file

@ -0,0 +1,19 @@
---
sidebar_position: 16
---
# Number Input
Number Input widget lets users enter and change numbers.
:::tip
Numbers can be changed by using the arrow keys.
:::
<img class="screenshot-full" src="/img/widgets/number-input/number-input.gif" alt="ToolJet - Widget Reference - Number Input" height="420"/>
#### Properties
| properties | description |
| ----------- | ----------- |
| Placeholder | It specifies a hint that describes the expected value.|

View file

@ -49,6 +49,14 @@ Client-side pagination is enabled by default. The number of records per page is
Server-side pagination can be used to run a query whenever the page is changed. Go to events section of the inspector and change the action for `on page changed` event. Number of records per page needs to be handled in your query. If server-side pagination is enabled, `pageIndex` property will be exposed on the table object, this property will have the current page index. `pageIndex` can be used to query the next set of results when page is changed. Server-side pagination can be used to run a query whenever the page is changed. Go to events section of the inspector and change the action for `on page changed` event. Number of records per page needs to be handled in your query. If server-side pagination is enabled, `pageIndex` property will be exposed on the table object, this property will have the current page index. `pageIndex` can be used to query the next set of results when page is changed.
## Bulk selection
To let the user select one or more rows from the current page of a table, enable 'Bulk selection' from the inspector. The values of selected rows will be exposed as `selectedRows`.
## Highlight selected row
Activate this option on the inspector to have the last selected(clicked on) row to be highlighted.
## Search ## Search
Client-side search is enabled by default and server-side search can be enabled from the events section of the inspector. Whenever the search text is changed, the `searchText` property of the table component is updated. If server-side search is enabled, `on search` event is fired after the content of `searchText` property is changed. `searchText` can be used to run a specific query to search for the records in your datasource. Client-side search is enabled by default and server-side search can be enabled from the events section of the inspector. Whenever the search text is changed, the `searchText` property of the table component is updated. If server-side search is enabled, `on search` event is fired after the content of `searchText` property is changed. `searchText` can be used to run a specific query to search for the records in your datasource.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
docs/static/img/sso/google/scope.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

View file

@ -1,37 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true
},
parser: 'babel-eslint',
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 12,
sourceType: 'module'
},
extends: [
'plugin:react/recommended',
'prettier',
'airbnb-base/legacy'
],
plugins: ['html', 'react', 'prettier', 'babel'],
rules: {
"react/prop-types": 0,
"no-underscore-dangle": ["error", { "allow": ["_self"] }],
"max-len": 0,
"no-bitwise": 0,
"no-use-before-define": ["error", { "variables": false, "functions": false }],
"no-nested-ternary": 0,
"no-loop-func": 0,
},
settings: {
react: {
version: "detect"
}
}
};

56
frontend/.eslintrc.json Normal file
View file

@ -0,0 +1,56 @@
{
"env": {
"browser": true,
"amd": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:prettier/recommended",
"plugin:cypress/recommended"
],
"parser": "babel-eslint",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["react", "prettier"],
"rules": {
"prettier/prettier": [
"error",
{
"semi": true,
"trailingComma": "es5",
"printWidth": 120,
"singleQuote": true,
"arrowParens": "always",
"proseWrap": "preserve"
}
],
"react/prop-types": 0,
"react/display-name": "off",
"no-unused-vars": [2, { "args": "after-used", "argsIgnorePattern": "reject" }],
"react/no-deprecated": 0,
"no-prototype-builtins": 0
},
"settings": {
"react": {
"version": "detect"
},
"import/resolver": "webpack"
},
"globals": {
"fetch": true,
"process": true,
"module": true,
"__dirname": true
}
}

View file

@ -1,16 +1,12 @@
const webpackPreprocessor = require('@cypress/webpack-preprocessor')
module.exports = (on, config) => { module.exports = (on, config) => {
if (config.testingType === 'component') { if (config.testingType === 'component') {
const { startDevServer } = require('@cypress/webpack-dev-server') const { startDevServer } = require('@cypress/webpack-dev-server');
// Your project's Webpack configuration // Your project's Webpack configuration
const webpackConfig = require('../../webpack.config.js') const webpackConfig = require('../../webpack.config.js');
on('dev-server:start', (options) => on('dev-server:start', (options) => startDevServer({ options, webpackConfig }));
startDevServer({ options, webpackConfig })
)
} }
return config return config;
} };

View file

@ -14,7 +14,7 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import './commands';
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,6 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"babel-plugin-import": "^1.13.3",
"@babel/core": "^7.4.3", "@babel/core": "^7.4.3",
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.4.3", "@babel/preset-env": "^7.4.3",
@ -18,13 +17,15 @@
"array-move": "^3.0.1", "array-move": "^3.0.1",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"babel-plugin-console-source": "^2.0.5", "babel-plugin-console-source": "^2.0.5",
"babel-plugin-import": "^1.13.3",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"classnames": "^2.3.1",
"dompurify": "^2.2.7", "dompurify": "^2.2.7",
"draft-js": "^0.11.7", "draft-js": "^0.11.7",
"html-webpack-plugin": "^5.3.2",
"draft-js-export-html": "^1.4.1", "draft-js-export-html": "^1.4.1",
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",
"history": "^4.9.0", "history": "^4.9.0",
"html-webpack-plugin": "^3.2.0",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.1", "moment": "^2.29.1",
@ -32,7 +33,6 @@
"papaparse": "^5.3.0", "papaparse": "^5.3.0",
"plotly.js-basic-dist-min": "^1.58.4", "plotly.js-basic-dist-min": "^1.58.4",
"query-string": "^6.13.6", "query-string": "^6.13.6",
"re-resizable": "^6.9.0",
"react": "^16.14.0", "react": "^16.14.0",
"react-bootstrap": "^1.5.2", "react-bootstrap": "^1.5.2",
"react-color": "^2.19.3", "react-color": "^2.19.3",
@ -48,7 +48,6 @@
"react-loading-skeleton": "^2.2.0", "react-loading-skeleton": "^2.2.0",
"react-plotly.js": "^2.5.1", "react-plotly.js": "^2.5.1",
"react-qr-reader": "^2.2.1", "react-qr-reader": "^2.2.1",
"react-resizable": "^1.11.1",
"react-rnd": "^10.3.0", "react-rnd": "^10.3.0",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.0.0",
"react-scripts": "3.4.3", "react-scripts": "3.4.3",
@ -62,8 +61,9 @@
"semver": "^5.7.1", "semver": "^5.7.1",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"uuid": "8.3.2", "uuid": "8.3.2",
"webpack-cli": "^3.3.0", "yup": "^0.27.0",
"yup": "^0.27.0" "webpack": "^5.55.1",
"webpack-cli": "^4.8.0"
}, },
"devDependencies": { "devDependencies": {
"@cypress/react": "^5.9.0", "@cypress/react": "^5.9.0",
@ -71,13 +71,23 @@
"@cypress/webpack-preprocessor": "^5.9.0", "@cypress/webpack-preprocessor": "^5.9.0",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "^5.5.0",
"cypress": "^7.4.0", "cypress": "^7.4.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-react": "^7.25.2",
"eslint-plugin-react-hooks": "^4.2.0",
"path": "^0.12.7", "path": "^0.12.7",
"webpack": "^4.29.6", "prettier": "^2.3.2",
"webpack-dev-server": "^3.11.2" "webpack-dev-server": "^3.11.2"
}, },
"scripts": { "scripts": {
"start": "webpack-dev-server --open --port 8082 --host 0.0.0.0", "start": "webpack serve --port 8082 --host 0.0.0.0",
"build": "webpack -p && cp -a ./assets/. ./build/assets/", "build": "webpack --mode production && cp -a ./assets/. ./build/assets/",
"lint": "eslint . '**/*.{js,jsx}'",
"format": "eslint . --fix '**/*.{js,jsx}'",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
@ -88,4 +98,4 @@
"production": [], "production": [],
"development": [] "development": []
} }
} }

View file

@ -15,7 +15,7 @@ import 'react-toastify/dist/ReactToastify.css';
import { ManageOrgUsers } from '@/ManageOrgUsers'; import { ManageOrgUsers } from '@/ManageOrgUsers';
import { SettingsPage } from '../SettingsPage/SettingsPage'; import { SettingsPage } from '../SettingsPage/SettingsPage';
import { OnboardingModal } from '@/Onboarding/OnboardingModal'; import { OnboardingModal } from '@/Onboarding/OnboardingModal';
import {ForgotPassword} from '@/ForgotPassword' import { ForgotPassword } from '@/ForgotPassword';
import { ResetPassword } from '@/ResetPassword'; import { ResetPassword } from '@/ResetPassword';
import { lt } from 'semver'; import { lt } from 'semver';
@ -27,7 +27,7 @@ class App extends React.Component {
currentUser: null, currentUser: null,
fetchedMetadata: false, fetchedMetadata: false,
onboarded: true, onboarded: true,
darkMode: localStorage.getItem('darkMode') === 'true' darkMode: localStorage.getItem('darkMode') === 'true',
}; };
} }
@ -40,56 +40,107 @@ class App extends React.Component {
logout = () => { logout = () => {
authenticationService.logout(); authenticationService.logout();
history.push('/login'); history.push('/login');
} };
switchDarkMode = (newMode) => { switchDarkMode = (newMode) => {
this.setState({ darkMode: newMode }); this.setState({ darkMode: newMode });
localStorage.setItem('darkMode', newMode); localStorage.setItem('darkMode', newMode);
} };
render() { render() {
const { currentUser, fetchedMetadata, updateAvailable, onboarded, darkMode } = this.state; const { currentUser, fetchedMetadata, updateAvailable, onboarded, darkMode } = this.state;
if(currentUser && fetchedMetadata === false) { if (currentUser && fetchedMetadata === false) {
tooljetService.fetchMetaData().then((data) => { tooljetService.fetchMetaData().then((data) => {
this.setState({ fetchedMetadata: true, onboarded: data.onboarded }); this.setState({ fetchedMetadata: true, onboarded: data.onboarded });
if(lt(data.installed_version, data.latest_version) && data.version_ignored === false) { if (lt(data.installed_version, data.latest_version) && data.version_ignored === false) {
this.setState({ updateAvailable: true }); this.setState({ updateAvailable: true });
} }
}) });
} }
return ( return (
<Router history={history}> <Router history={history}>
<div className={`main-wrapper ${darkMode ? 'theme-dark' : ''}`}> <div className={`main-wrapper ${darkMode ? 'theme-dark' : ''}`}>
{updateAvailable && <div className="alert alert-info alert-dismissible" role="alert"> {updateAvailable && (
<h3 className="mb-1">Update available</h3> <div className="alert alert-info alert-dismissible" role="alert">
<p>A new version of ToolJet has been released.</p> <h3 className="mb-1">Update available</h3>
<div className="btn-list"> <p>A new version of ToolJet has been released.</p>
<a href="https://docs.tooljet.io/docs/setup/updating" target="_blank" className="btn btn-info">Read release notes & update</a> <div className="btn-list">
<a onClick={() => { tooljetService.skipVersion(); this.setState({ updateAvailable: false }); }} className="btn">Skip this version</a> <a
href="https://docs.tooljet.io/docs/setup/updating"
target="_blank"
className="btn btn-info"
rel="noreferrer"
>
Read release notes & update
</a>
<a
onClick={() => {
tooljetService.skipVersion();
this.setState({ updateAvailable: false });
}}
className="btn"
>
Skip this version
</a>
</div>
</div> </div>
</div>} )}
{!onboarded && {!onboarded && <OnboardingModal />}
<OnboardingModal />
}
<ToastContainer /> <ToastContainer />
<PrivateRoute exact path="/" component={HomePage} switchDarkMode={this.switchDarkMode} darkMode={darkMode}/> <PrivateRoute exact path="/" component={HomePage} switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
<Route path="/login" component={LoginPage}/> <Route path="/login" component={LoginPage} />
<Route path="/signup" component={SignupPage} /> <Route path="/signup" component={SignupPage} />
<Route path = "/forgot-password" component ={ForgotPassword} /> <Route path="/forgot-password" component={ForgotPassword} />
<Route path = "/reset-password" component ={ResetPassword} /> <Route path="/reset-password" component={ResetPassword} />
<Route path="/invitations/:token" component={InvitationPage} /> <Route path="/invitations/:token" component={InvitationPage} />
<PrivateRoute exact path="/apps/:id" component={Editor} switchDarkMode={this.switchDarkMode} darkMode={darkMode} /> <PrivateRoute
<PrivateRoute exact path="/applications/:id/versions/:versionId" component={Viewer} switchDarkMode={this.switchDarkMode} darkMode={darkMode} /> exact
<PrivateRoute exact path="/applications/:slug" component={Viewer} switchDarkMode={this.switchDarkMode} darkMode={darkMode}/> path="/apps/:id"
<PrivateRoute exact path="/oauth2/authorize" component={Authorize} switchDarkMode={this.switchDarkMode} darkMode={darkMode} /> component={Editor}
<PrivateRoute exact path="/users" component={ManageOrgUsers} switchDarkMode={this.switchDarkMode} darkMode={darkMode} /> switchDarkMode={this.switchDarkMode}
<PrivateRoute exact path="/settings" component={SettingsPage} switchDarkMode={this.switchDarkMode} darkMode={darkMode} /> darkMode={darkMode}
/>
<PrivateRoute
exact
path="/applications/:id/versions/:versionId"
component={Viewer}
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}
/>
<PrivateRoute
exact
path="/applications/:slug"
component={Viewer}
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}
/>
<PrivateRoute
exact
path="/oauth2/authorize"
component={Authorize}
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}
/>
<PrivateRoute
exact
path="/users"
component={ManageOrgUsers}
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}
/>
<PrivateRoute
exact
path="/settings"
component={SettingsPage}
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}
/>
</div> </div>
</Router> </Router>
); );

View file

@ -2,51 +2,39 @@ export const ActionTypes = [
{ {
name: 'Show Alert', name: 'Show Alert',
id: 'show-alert', id: 'show-alert',
options: [ options: [{ name: 'message', type: 'text', default: 'Message !' }],
{ name: 'message', type: 'text', default: 'Message !' }
]
}, },
{ {
name: 'Run Query', name: 'Run Query',
id: 'run-query', id: 'run-query',
options: [ options: [{ queryId: '' }],
{ queryId: '' }
]
}, },
{ {
name: 'Open Webpage', name: 'Open Webpage',
id: 'open-webpage', id: 'open-webpage',
options: [ options: [{ name: 'url', type: 'text', default: 'https://example.com' }],
{ name: 'url', type: 'text', default: 'https://example.com' }
]
}, },
{ {
name: 'Go to app', name: 'Go to app',
id: 'go-to-app', id: 'go-to-app',
options: [ options: [
{ name: 'app', type: 'text', default: '' }, { name: 'app', type: 'text', default: '' },
{ name: 'queryParams', type: 'code', default: '[]' } { name: 'queryParams', type: 'code', default: '[]' },
] ],
}, },
{ {
name: 'Show Modal', name: 'Show Modal',
id: 'show-modal', id: 'show-modal',
options: [ options: [{ name: 'modal', type: 'text', default: '' }],
{ name: 'modal', type: 'text', default: '' }
]
}, },
{ {
name: 'Close Modal', name: 'Close Modal',
id: 'close-modal', id: 'close-modal',
options: [ options: [{ name: 'modal', type: 'text', default: '' }],
{ name: 'modal', type: 'text', default: '' }
]
}, },
{ {
name: 'Copy to clipboard', name: 'Copy to clipboard',
id: 'copy-to-clipboard', id: 'copy-to-clipboard',
options: [ options: [{ name: 'copy-to-clipboard', type: 'text', default: '' }],
{ name: 'copy-to-clipboard', type: 'text', default: '' } },
]
}
]; ];

View file

@ -17,9 +17,9 @@ import { Modal } from './Components/Modal';
import { Chart } from './Components/Chart'; import { Chart } from './Components/Chart';
import { Map } from './Components/Map/Map'; import { Map } from './Components/Map/Map';
import { QrScanner } from './Components/QrScanner/QrScanner'; import { QrScanner } from './Components/QrScanner/QrScanner';
import { ToggleSwitch } from './Components/Toggle' import { ToggleSwitch } from './Components/Toggle';
import { RadioButton } from './Components/RadioButton' import { RadioButton } from './Components/RadioButton';
import { StarRating } from './Components/StarRating' import { StarRating } from './Components/StarRating';
import { renderTooltip } from '../_helpers/appUtils'; import { renderTooltip } from '../_helpers/appUtils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import '@/_styles/custom.scss'; import '@/_styles/custom.scss';
@ -45,12 +45,11 @@ const AllComponents = {
QrScanner, QrScanner,
ToggleSwitch, ToggleSwitch,
RadioButton, RadioButton,
StarRating StarRating,
}; };
export const Box = function Box({ export const Box = function Box({
id, id,
mode,
width, width,
height, height,
yellow, yellow,
@ -66,12 +65,13 @@ export const Box = function Box({
changeCanDrag, changeCanDrag,
containerProps, containerProps,
darkMode, darkMode,
removeComponent removeComponent,
}) { }) {
const backgroundColor = yellow ? 'yellow' : ''; const backgroundColor = yellow ? 'yellow' : '';
let styles = { let styles = {
height: '100%', height: '100%',
padding: '1px',
}; };
if (inCanvas) { if (inCanvas) {
@ -86,49 +86,49 @@ export const Box = function Box({
<OverlayTrigger <OverlayTrigger
placement="top" placement="top"
delay={{ show: 500, hide: 0 }} delay={{ show: 500, hide: 0 }}
trigger={!inCanvas? ['hover', 'focus']: null} trigger={!inCanvas ? ['hover', 'focus'] : null}
overlay={(props) => renderTooltip({props, text: `${component.description}`})} overlay={(props) => renderTooltip({ props, text: `${component.description}` })}
> >
<div style={{ ...styles, backgroundColor }} role={preview ? 'BoxPreview' : 'Box'}> <div style={{ ...styles, backgroundColor }} role={preview ? 'BoxPreview' : 'Box'}>
{inCanvas ? ( {inCanvas ? (
<ComponentToRender <ComponentToRender
onComponentClick={onComponentClick} onComponentClick={onComponentClick}
onComponentOptionChanged={onComponentOptionChanged} onComponentOptionChanged={onComponentOptionChanged}
currentState={currentState} currentState={currentState}
onEvent={onEvent} onEvent={onEvent}
id={id} id={id}
paramUpdated={paramUpdated} paramUpdated={paramUpdated}
width={width} width={width}
changeCanDrag={changeCanDrag} changeCanDrag={changeCanDrag}
onComponentOptionsChanged={onComponentOptionsChanged} onComponentOptionsChanged={onComponentOptionsChanged}
height={height} height={height}
component={component} component={component}
containerProps={containerProps} containerProps={containerProps}
darkMode={darkMode} darkMode={darkMode}
removeComponent={removeComponent} removeComponent={removeComponent}
></ComponentToRender> ></ComponentToRender>
) : ( ) : (
<div className="m-1" style={{ height: '100%' }}> <div className="m-1" style={{ height: '100%' }}>
<div <div
className="component-image-holder p-2 d-flex flex-column justify-content-center" className="component-image-holder p-2 d-flex flex-column justify-content-center"
style={{ height: '100%' }} style={{ height: '100%' }}
> >
<center> <center>
<div <div
style={{ style={{
width: '20px', width: '20px',
height: '20px', height: '20px',
backgroundSize: 'contain', backgroundSize: 'contain',
backgroundImage: `url(/assets/images/icons/widgets/${component.name.toLowerCase()}.svg)`, backgroundImage: `url(/assets/images/icons/widgets/${component.name.toLowerCase()}.svg)`,
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
}} }}
></div> ></div>
</center> </center>
<span className="component-title">{component.displayName}</span> <span className="component-title">{component.displayName}</span>
</div>
</div> </div>
</div> )}
)} </div>
</div>
</OverlayTrigger> </OverlayTrigger>
); );
}; };

View file

@ -11,7 +11,7 @@ export const BoxDragPreview = memo(function BoxDragPreview({ item, currentLayout
[tickTock] [tickTock]
); );
const layouts = item.layouts; const layouts = item.layouts;
let { width, height } = layouts ? item.layouts[currentLayout] : {}; let { width, height } = layouts ? item.layouts[currentLayout] : {};
if (item.id === undefined) { if (item.id === undefined) {
@ -20,10 +20,18 @@ export const BoxDragPreview = memo(function BoxDragPreview({ item, currentLayout
} }
return ( return (
<div style={{ height, width, border: 'solid 1px rgb(70, 165, 253)' }}> <div
<div style={{ className="resizer-active draggable-box"
background: '#438fd7', opacity: '0.7', height: '100%', width: '100%' style={{ height, width, border: 'solid 1px rgb(70, 165, 253)', padding: '2px' }}
}}></div> >
<div
style={{
background: '#438fd7',
opacity: '0.7',
height: '100%',
width: '100%',
}}
></div>
</div> </div>
); );
}); });

View file

@ -2,13 +2,11 @@ import React, { useState } from 'react';
import CodeMirror from '@uiw/react-codemirror'; import CodeMirror from '@uiw/react-codemirror';
import 'codemirror/theme/duotone-light.css'; import 'codemirror/theme/duotone-light.css';
import { componentTypes } from '../Components/components'; import { componentTypes } from '../Components/components';
import { DataSourceTypes } from '../DataSourceManager/DataSourceTypes'; import { DataSourceTypes } from '../DataSourceManager/SourceComponents';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
export function CodeBuilder({ export function CodeBuilder({ initialValue, onChange, components, dataQueries }) {
initialValue, onChange, components, dataQueries
}) {
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const [cursorPosition, setCursorPosition] = useState(0); const [cursorPosition, setCursorPosition] = useState(0);
const [currentValue, setCurrentValue] = useState(initialValue); const [currentValue, setCurrentValue] = useState(initialValue);
@ -130,7 +128,7 @@ export function CodeBuilder({
mode: 'javascript', mode: 'javascript',
lineWrapping: true, lineWrapping: true,
scrollbarStyle: null, scrollbarStyle: null,
lineNumbers: false lineNumbers: false,
}} }}
/> />
{showDropdown && ( {showDropdown && (

View file

@ -57,10 +57,12 @@ export function CodeHinter({
}); });
useEffect(() => { useEffect(() => {
setRealState(currentState); setRealState(currentState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState.components]); }, [currentState.components]);
let suggestions = useMemo(() => { let suggestions = useMemo(() => {
return getSuggestionKeys(realState); return getSuggestionKeys(realState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [realState.components, realState.queries]); }, [realState.components, realState.queries]);
function valueChanged(editor, onChange, suggestions, ignoreBraces) { function valueChanged(editor, onChange, suggestions, ignoreBraces) {

View file

@ -1,33 +1,27 @@
import * as React from 'react' import * as React from 'react';
import { mount } from '@cypress/react' import { mount } from '@cypress/react';
import { CodeHinter } from './CodeHinter' import { CodeHinter } from './CodeHinter';
it('Codehinter', () => { it('Codehinter', () => {
mount(<CodeHinter mount(
currentState={{ <CodeHinter
queries: { currentState={{
postgres: { queries: {
data: [] postgres: {
} data: [],
}, },
components: { },
components: {},
globals: {},
}}
initialValue={''}
theme="duotone-light"
mode="javascript"
lineNumbers={true}
className="query-hinter"
onChange={(value) => console.log(value)}
/>
);
}, cy.get('.code-hinter').click().type('{{').contains('{{}}'); // autocomplete for dynamic variables
globals: { });
}
}}
initialValue={''}
theme="duotone-light"
mode="javascript"
lineNumbers={true}
className="query-hinter"
onChange={(value) => {}}
/>)
cy.get('.code-hinter')
.click()
.type('{{')
.contains('{{}}') // autocomplete for dynamic variables
})

View file

@ -6,16 +6,15 @@ export function getSuggestionKeys(currentState) {
_.keys(currentState).forEach((key) => { _.keys(currentState).forEach((key) => {
_.keys(currentState[key]).forEach((key2) => { _.keys(currentState[key]).forEach((key2) => {
_.keys(currentState[key][key2]).forEach((key3) => { _.keys(currentState[key][key2]).forEach((key3) => {
suggestions.push(`${key}.${key2}.${key3}`) suggestions.push(`${key}.${key2}.${key3}`);
}) });
}) });
}); });
return suggestions; return suggestions;
} }
export function generateHints(word, suggestions) { export function generateHints(word, suggestions) {
if (word === '') {
if(word === '') {
return suggestions; return suggestions;
} }
@ -38,25 +37,25 @@ export function computeCurrentWord(editor, _cursorPosition, ignoreBraces = false
export function makeOverlay(style) { export function makeOverlay(style) {
return { return {
// eslint-disable-next-line no-unused-vars
token: function (stream, state) { token: function (stream, state) {
var ch; var ch;
if (stream.match("{{")) { if (stream.match('{{')) {
while ((ch = stream.next()) != null) while ((ch = stream.next()) != null)
if (ch == "}" && stream.next() == "}") { if (ch == '}' && stream.next() == '}') {
stream.eat("}"); stream.eat('}');
return style; return style;
} }
} }
while (stream.next() != null && !stream.match("{{", false)) { } // eslint-disable-next-line no-empty
while (stream.next() != null && !stream.match('{{', false)) {}
return null; return null;
} },
} };
} }
export function onBeforeChange(editor, change, ignoreBraces = false) { export function onBeforeChange(editor, change, ignoreBraces = false) {
if (!ignoreBraces) {
if(!ignoreBraces) {
const cursor = editor.getCursor(); const cursor = editor.getCursor();
const line = cursor.line; const line = cursor.line;
const ch = cursor.ch; const ch = cursor.ch;
@ -64,33 +63,30 @@ export function onBeforeChange(editor, change, ignoreBraces = false) {
const isLastCharacterBrace = value.slice(ch - 1, value.length) === '{'; const isLastCharacterBrace = value.slice(ch - 1, value.length) === '{';
if (isLastCharacterBrace && change.origin === '+input' && change.text[0] === '{') { if (isLastCharacterBrace && change.origin === '+input' && change.text[0] === '{') {
change.text[0] = '{}}' change.text[0] = '{}}';
// editor.setCursor({ line: 0, ch: ch }) // editor.setCursor({ line: 0, ch: ch })
} }
} }
return change; return change;
} }
export function canShowHint(editor, ignoreBraces = false) { export function canShowHint(editor, ignoreBraces = false) {
if (!editor.hasFocus()) return false;
if(!editor.hasFocus()) return false;
const cursor = editor.getCursor(); const cursor = editor.getCursor();
const line = cursor.line; const line = cursor.line;
const ch = cursor.ch; const ch = cursor.ch;
const value = editor.getLine(line); const value = editor.getLine(line);
if(ignoreBraces && value.length > 0) return true; if (ignoreBraces && value.length > 0) return true;
return value.slice(ch, ch + 2) === '}}'; return value.slice(ch, ch + 2) === '}}';
} }
export function handleChange(editor, onChange, suggestions, ignoreBraces = false) { export function handleChange(editor, onChange, suggestions, ignoreBraces = false) {
let state = editor.state.matchHighlighter; let state = editor.state.matchHighlighter;
editor.addOverlay(state.overlay = makeOverlay(state.options.style)); editor.addOverlay((state.overlay = makeOverlay(state.options.style)));
const cursor = editor.getCursor(); const cursor = editor.getCursor();
const currentWord = computeCurrentWord(editor, cursor.ch, ignoreBraces); const currentWord = computeCurrentWord(editor, cursor.ch, ignoreBraces);
@ -103,11 +99,11 @@ export function handleChange(editor, onChange, suggestions, ignoreBraces = false
return { return {
from: { line: cursor.line, ch: cursor.ch - currentWord.length }, from: { line: cursor.line, ch: cursor.ch - currentWord.length },
to: cursor, to: cursor,
list: hints list: hints,
} };
} },
}; };
if (canShowHint(editor, ignoreBraces)) { if (canShowHint(editor, ignoreBraces)) {
editor.showHint(options); editor.showHint(options);
} }
}; }

View file

@ -1,10 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
var tinycolor = require("tinycolor2"); var tinycolor = require('tinycolor2');
export const Button = function Button({ export const Button = function Button({ id, width, height, component, onComponentClick, currentState }) {
id, width, height, component, onComponentClick, currentState
}) {
console.log('currentState', currentState); console.log('currentState', currentState);
const [loadingState, setLoadingState] = useState(false); const [loadingState, setLoadingState] = useState(false);
@ -15,6 +13,7 @@ export const Button = function Button({
const newState = resolveReferences(loadingStateProperty.value, currentState, false); const newState = resolveReferences(loadingStateProperty.value, currentState, false);
setLoadingState(newState); setLoadingState(newState);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState]); }, [currentState]);
const text = component.definition.properties.text.value; const text = component.definition.properties.text.value;
@ -23,12 +22,15 @@ export const Button = function Button({
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
const computedStyles = { const computedStyles = {
backgroundColor, backgroundColor,
@ -36,7 +38,7 @@ export const Button = function Button({
width, width,
height, height,
display: parsedWidgetVisibility ? '' : 'none', display: parsedWidgetVisibility ? '' : 'none',
'--tblr-btn-color-darker': tinycolor(backgroundColor).darken(8).toString() '--tblr-btn-color-darker': tinycolor(backgroundColor).darken(8).toString(),
}; };
return ( return (
@ -44,7 +46,10 @@ export const Button = function Button({
disabled={parsedDisabledState} disabled={parsedDisabledState}
className={`jet-button btn btn-primary p-1 ${loadingState === true ? ' btn-loading' : ''}`} className={`jet-button btn btn-primary p-1 ${loadingState === true ? ' btn-loading' : ''}`}
style={computedStyles} style={computedStyles}
onClick={(event) => {event.stopPropagation(); onComponentClick(id, component)}} onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
> >
{text} {text}
</button> </button>

View file

@ -4,14 +4,9 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
// Use plotly basic bundle // Use plotly basic bundle
import Plotly from 'plotly.js-basic-dist-min'; import Plotly from 'plotly.js-basic-dist-min';
import createPlotlyComponent from 'react-plotly.js/factory'; import createPlotlyComponent from 'react-plotly.js/factory';
const Plot = createPlotlyComponent(Plotly) const Plot = createPlotlyComponent(Plotly);
import Skeleton from 'react-loading-skeleton';
export const Chart = function Chart({
id, width, height, component, onComponentClick, currentState, darkMode
}) {
export const Chart = function Chart({ id, width, height, component, onComponentClick, currentState, darkMode }) {
const [loadingState, setLoadingState] = useState(false); const [loadingState, setLoadingState] = useState(false);
const [chartData, setChartData] = useState([]); const [chartData, setChartData] = useState([]);
@ -19,28 +14,31 @@ export const Chart = function Chart({
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
useEffect(() => { useEffect(() => {
const loadingStateProperty = component.definition.properties.loadingState; const loadingStateProperty = component.definition.properties.loadingState;
if (loadingStateProperty && currentState) { if (loadingStateProperty && currentState) {
const newState = resolveReferences(loadingStateProperty.value, currentState, false); const newState = resolveReferences(loadingStateProperty.value, currentState, false);
setLoadingState(newState); setLoadingState(newState);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState]); }, [currentState]);
const computedStyles = { const computedStyles = {
width, width,
height, height,
display: parsedWidgetVisibility ? '' : 'none', display: parsedWidgetVisibility ? '' : 'none',
background: darkMode ? '#1f2936' : 'white' background: darkMode ? '#1f2936' : 'white',
}; };
// darkMode ? '#1f2936' : 'white' // darkMode ? '#1f2936' : 'white'
const dataProperty = component.definition.properties.data; const dataProperty = component.definition.properties.data;
const dataString = dataProperty ? dataProperty.value : []; const dataString = dataProperty ? dataProperty.value : [];
@ -66,73 +64,84 @@ export const Chart = function Chart({
title: { title: {
text: title, text: title,
font: { font: {
color: fontColor color: fontColor,
} },
}, },
legend: { legend: {
text: title, text: title,
font: { font: {
color: fontColor color: fontColor,
} },
}, },
xaxis: { xaxis: {
showgrid: showGridLines, showgrid: showGridLines,
showline: true, showline: true,
color: fontColor color: fontColor,
}, },
yaxis: { yaxis: {
showgrid: showGridLines, showgrid: showGridLines,
showline: true, showline: true,
color: fontColor color: fontColor,
} },
} };
const data = resolveReferences(dataString, currentState, []); const data = resolveReferences(dataString, currentState, []);
useEffect(() => { useEffect(() => {
let rawData = data || []; let rawData = data || [];
if(typeof rawData === 'string') { if (typeof rawData === 'string') {
try { try {
rawData = JSON.parse(dataString); rawData = JSON.parse(dataString);
} catch (err) { rawData = []; } } catch (err) {
rawData = [];
}
} }
if(!Array.isArray(rawData)) { rawData = []; } if (!Array.isArray(rawData)) {
rawData = [];
}
let newData = []; let newData = [];
if(chartType === 'pie') { if (chartType === 'pie') {
newData = [{ newData = [
type: chartType, {
values: rawData.map((item) => item["value"]), type: chartType,
labels: rawData.map((item) => item["label"]), values: rawData.map((item) => item['value']),
}]; labels: rawData.map((item) => item['label']),
},
];
} else { } else {
newData = [{ newData = [
type: chartType || 'line', {
x: rawData.map((item) => item["x"]), type: chartType || 'line',
y: rawData.map((item) => item["y"]), x: rawData.map((item) => item['x']),
marker: { color: markerColor } y: rawData.map((item) => item['y']),
}]; marker: { color: markerColor },
},
];
} }
setChartData(newData); setChartData(newData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, chartType]); }, [data, chartType]);
return ( return (
<div <div
data-disabled={parsedDisabledState} data-disabled={parsedDisabledState}
style={computedStyles} style={computedStyles}
onClick={event => {event.stopPropagation(); onComponentClick(id, component)}} onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
> >
{loadingState === true ? {loadingState === true ? (
<div style={{ width: '100%' }} className="p-2"> <div style={{ width: '100%' }} className="p-2">
<center> <center>
<div className="spinner-border mt-5" role="status"></div> <div className="spinner-border mt-5" role="status"></div>
</center> </center>
</div> </div>
: ) : (
<Plot <Plot
data={chartData} data={chartData}
layout={layout} layout={layout}
@ -141,7 +150,7 @@ export const Chart = function Chart({
// staticPlot: true // staticPlot: true
}} }}
/> />
} )}
</div> </div>
); );
}; };

View file

@ -9,22 +9,24 @@ export const Checkbox = function Checkbox({
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged, onComponentOptionChanged,
onEvent onEvent,
}) { }) {
const label = component.definition.properties.label.value; const label = component.definition.properties.label.value;
const textColorProperty = component.definition.styles.textColor; const textColorProperty = component.definition.styles.textColor;
const textColor = textColorProperty ? textColorProperty.value : '#000'; const textColor = textColorProperty ? textColorProperty.value : '#000';
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
function toggleValue(e) { function toggleValue(e) {
const checked = e.target.checked; const checked = e.target.checked;
@ -37,7 +39,15 @@ export const Checkbox = function Checkbox({
} }
return ( return (
<div className="row" style={{ width, height, display:parsedWidgetVisibility ? '' : 'none' }} onClick={event => {event.stopPropagation(); onComponentClick(id, component)}}> <div
data-disabled={parsedDisabledState}
className="row py-1"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
>
<label className="my-auto mx-2 form-check form-check-inline"> <label className="my-auto mx-2 form-check form-check-inline">
<input <input
className="form-check-input" className="form-check-input"
@ -46,7 +56,9 @@ export const Checkbox = function Checkbox({
toggleValue(e); toggleValue(e);
}} }}
/> />
<span className="form-check-label" style={{color: textColor}}>{label}</span> <span className="form-check-label" style={{ color: textColor }}>
{label}
</span>
</label> </label>
</div> </div>
); );

View file

@ -10,43 +10,42 @@ export const Container = function Container({
containerProps, containerProps,
width, width,
currentState, currentState,
removeComponent removeComponent,
}) { }) {
const backgroundColor = component.definition.styles.backgroundColor.value; const backgroundColor = component.definition.styles.backgroundColor.value;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
const computedStyles = { const computedStyles = {
backgroundColor, backgroundColor,
width, width,
height, height,
display: parsedWidgetVisibility ? 'flex' : 'none' display: parsedWidgetVisibility ? 'flex' : 'none',
}; };
const parentRef = useRef(null); const parentRef = useRef(null);
return ( return (
<div data-disabled={parsedDisabledState} className="jet-container" ref={parentRef} onClick={() => containerProps.onComponentClick(id, component)} style={computedStyles}> <div
<SubContainer data-disabled={parsedDisabledState}
parent={id} className="jet-container"
{...containerProps} ref={parentRef}
parentRef={parentRef} onClick={() => containerProps.onComponentClick(id, component)}
removeComponent={removeComponent} style={computedStyles}
/> >
<SubCustomDragLayer <SubContainer parent={id} {...containerProps} parentRef={parentRef} removeComponent={removeComponent} />
parent={id} <SubCustomDragLayer parent={id} parentRef={parentRef} currentLayout={containerProps.currentLayout} />
parentRef={parentRef}
currentLayout={containerProps.currentLayout}
/>
</div> </div>
); );
}; };

View file

@ -1,8 +1,7 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import Datetime from 'react-datetime'; import Datetime from 'react-datetime';
import 'react-datetime/css/react-datetime.css'; import 'react-datetime/css/react-datetime.css';
import { resolveReferences, resolveWidgetFieldValue, validateWidget } from '@/_helpers/utils'; import { resolveReferences, resolveWidgetFieldValue, validateWidget } from '@/_helpers/utils';
import { useEffect, useState } from 'react';
export const Datepicker = function Datepicker({ export const Datepicker = function Datepicker({
id, id,
@ -11,7 +10,7 @@ export const Datepicker = function Datepicker({
component, component,
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged onComponentOptionChanged,
}) { }) {
console.log('currentState', currentState); console.log('currentState', currentState);
@ -22,71 +21,87 @@ export const Datepicker = function Datepicker({
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const defaultValue = component.definition.properties?.defaultValue?.value ?? ''; const defaultValue = component.definition.properties?.defaultValue?.value ?? '';
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
const enableTime = resolveReferences(enableTimeProp.value, currentState, false); const enableTime = resolveReferences(enableTimeProp.value, currentState, false);
let enableDate = true; let enableDate = true;
if (enableDateProp) { if (enableDateProp) {
// eslint-disable-next-line no-unused-vars
enableDate = resolveReferences(enableDateProp.value, currentState, true); enableDate = resolveReferences(enableDateProp.value, currentState, true);
} }
let dateFormat = formatProp let dateFormat = formatProp;
try { try {
dateFormat = resolveReferences(formatProp, currentState); dateFormat = resolveReferences(formatProp, currentState);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
function onDateChange(event) { function onDateChange(event) {
const value = event._isAMomentObject? event.format(dateFormat.value) : event; const value = event._isAMomentObject ? event.format(dateFormat.value) : event;
setDateText(value); setDateText(value);
onComponentOptionChanged(component, 'value', value); onComponentOptionChanged(component, 'value', value);
} }
let value = defaultValue; let value = defaultValue;
if (value && currentState) if (value && currentState) value = resolveReferences(value, currentState, '');
value = resolveReferences(value, currentState, '');
const [dateText, setDateText] = useState(value); const [dateText, setDateText] = useState(value);
useEffect(() => { useEffect(() => {
setDateText(value); setDateText(value);
onComponentOptionChanged(component, 'value', value); onComponentOptionChanged(component, 'value', value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]); }, [value]);
const validationData = validateWidget({ const validationData = validateWidget({
validationObject: component.definition.validation, validationObject: component.definition.validation,
widgetValue: value, widgetValue: value,
currentState currentState,
}) });
const { isValid, validationError } = validationData; const { isValid, validationError } = validationData;
const currentValidState = currentState?.components[component?.name]?.isValid; const currentValidState = currentState?.components[component?.name]?.isValid;
if(currentValidState !== isValid) { if (currentValidState !== isValid) {
onComponentOptionChanged(component, 'isValid', isValid); onComponentOptionChanged(component, 'isValid', isValid);
} }
return ( return (
<div data-disabled={parsedDisabledState} style={{ width, height, display:parsedWidgetVisibility ? '' : 'none'}} onClick={event => {event.stopPropagation(); onComponentClick(id, component)}}> <div
<Datetime data-disabled={parsedDisabledState}
onChange={onDateChange} className="datepicker-widget"
timeFormat={enableTime} style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
>
<Datetime
onChange={onDateChange}
timeFormat={enableTime}
closeOnSelect={true} closeOnSelect={true}
dateFormat={dateFormat.value} dateFormat={dateFormat.value}
value={dateText} value={dateText}
renderInput={(props) => { renderInput={(props) => {
return <input return (
{...props} <input
value={dateText} {...props}
className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon`} value={dateText}
/> className={`input-field form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon`}
/>
);
}} }}
/> />
<div className={`invalid-feedback ${isValid ? '' : 'd-flex'}`}>{validationError}</div> <div className={`invalid-feedback ${isValid ? '' : 'd-flex'}`}>{validationError}</div>

View file

@ -12,7 +12,7 @@ export const DaterangePicker = function DaterangePicker({
component, component,
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged onComponentOptionChanged,
}) { }) {
console.log('currentState', currentState); console.log('currentState', currentState);
@ -21,18 +21,20 @@ export const DaterangePicker = function DaterangePicker({
const formatProp = component.definition.properties.format; const formatProp = component.definition.properties.format;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
const [focusedInput, setFocusedInput] = useState(null); const [focusedInput, setFocusedInput] = useState(null);
const [startDate, setStartDate] = useState(startDateProp ? startDateProp.value : null); const [startDate, setStartDate] = useState(startDateProp ? startDateProp.value : null);
const [endDate, setEndDate] = useState(endDateProp ? endDateProp.value : null); const [endDate, setEndDate] = useState(endDateProp ? endDateProp.value : null);
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
function onDateChange(dates) { function onDateChange(dates) {
const start = dates.startDate; const start = dates.startDate;
@ -55,7 +57,14 @@ export const DaterangePicker = function DaterangePicker({
} }
return ( return (
<div style={{ width, height, display:parsedWidgetVisibility ? '' : 'none' }} onClick={event => {event.stopPropagation(); onComponentClick(id, component)}}> <div
className="daterange-picker-widget p-0"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
>
<DateRangePicker <DateRangePicker
disabled={parsedDisabledState} disabled={parsedDisabledState}
startDate={startDate} startDate={startDate}

View file

@ -1,3 +1,4 @@
/* eslint-disable react/no-string-refs */
import React from 'react'; import React from 'react';
import { Editor, EditorState, RichUtils, getDefaultKeyBinding } from 'draft-js'; import { Editor, EditorState, RichUtils, getDefaultKeyBinding } from 'draft-js';
import 'draft-js/dist/Draft.css'; import 'draft-js/dist/Draft.css';

View file

@ -10,7 +10,7 @@ export const DropDown = function DropDown({
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged, onComponentOptionChanged,
onEvent onEvent,
}) { }) {
console.log('currentState', currentState); console.log('currentState', currentState);
@ -20,25 +20,32 @@ export const DropDown = function DropDown({
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedValues = values; let parsedValues = values;
try { try {
parsedValues = resolveReferences(values, currentState, []); parsedValues = resolveReferences(values, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
let parsedDisplayValues = displayValues; let parsedDisplayValues = displayValues;
try { try {
parsedDisplayValues = resolveReferences(displayValues, currentState, []); parsedDisplayValues = resolveReferences(displayValues, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
let selectOptions = []; let selectOptions = [];
@ -46,14 +53,15 @@ export const DropDown = function DropDown({
selectOptions = [ selectOptions = [
...parsedValues.map((value, index) => { ...parsedValues.map((value, index) => {
return { name: parsedDisplayValues[index], value: value }; return { name: parsedDisplayValues[index], value: value };
}) }),
]; ];
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
const currentValueProperty = component.definition.properties.value; const currentValueProperty = component.definition.properties.value;
const value = currentValueProperty ? currentValueProperty.value : ''; const value = currentValueProperty ? currentValueProperty.value : '';
const [currentValue, setCurrentValue] = useState(''); const [currentValue, setCurrentValue] = useState('');
let newValue = value; let newValue = value;
if (currentValueProperty && currentState) { if (currentValueProperty && currentState) {
@ -63,14 +71,14 @@ export const DropDown = function DropDown({
const validationData = validateWidget({ const validationData = validateWidget({
validationObject: component.definition.validation, validationObject: component.definition.validation,
widgetValue: currentValue, widgetValue: currentValue,
currentState currentState,
}) });
const { isValid, validationError } = validationData; const { isValid, validationError } = validationData;
const currentValidState = currentState?.components[component?.name]?.isValid; const currentValidState = currentState?.components[component?.name]?.isValid;
if(currentValidState !== isValid) { if (currentValidState !== isValid) {
onComponentOptionChanged(component, 'isValid', isValid); onComponentOptionChanged(component, 'isValid', isValid);
} }
@ -79,13 +87,23 @@ export const DropDown = function DropDown({
}, [newValue]); }, [newValue]);
useEffect(() => { useEffect(() => {
onComponentOptionChanged(component, 'value', currentValue).then(() => onEvent('onSelect', { component }) ); onComponentOptionChanged(component, 'value', currentValue).then(() => onEvent('onSelect', { component }));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentValue]); }, [currentValue]);
return ( return (
<div className="dropdown-widget row g-0" style={{ width, height, display:parsedWidgetVisibility ? '' : 'none' }} onClick={event => {event.stopPropagation(); onComponentClick(id, component)}}> <div
className="dropdown-widget row g-0"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
>
<div className="col-auto"> <div className="col-auto">
<label style={{marginRight: label !== '' ? '1rem' : '0.001rem'}} className="form-label py-1">{label}</label> <label style={{ marginRight: label !== '' ? '1rem' : '0.001rem' }} className="form-label py-1">
{label}
</label>
</div> </div>
<div className="col px-0"> <div className="col px-0">
<SelectSearch <SelectSearch

View file

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

View file

@ -1,8 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { GoogleMap, LoadScript } from '@react-google-maps/api'; import { GoogleMap, LoadScript, Marker, Autocomplete } from '@react-google-maps/api';
import { Marker } from '@react-google-maps/api';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import { Autocomplete } from '@react-google-maps/api';
import { darkModeStyles } from './styles'; import { darkModeStyles } from './styles';
export const Map = function Map({ export const Map = function Map({
@ -88,6 +86,7 @@ export const Map = function Map({
]).then(() => onEvent('onBoundsChange', { component })); ]).then(() => onEvent('onBoundsChange', { component }));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
const onLoad = useCallback(function onLoad(mapInstance) { const onLoad = useCallback(function onLoad(mapInstance) {
setGmap(mapInstance); setGmap(mapInstance);
onComponentOptionsChanged(component, [['center', mapInstance.center?.toJSON()]]); onComponentOptionsChanged(component, [['center', mapInstance.center?.toJSON()]]);
@ -151,7 +150,7 @@ export const Map = function Map({
{Array.isArray(markers) && ( {Array.isArray(markers) && (
<> <>
{markers.map((marker, index) => ( {markers.map((marker, index) => (
<Marker position={marker} label={marker.label} onClick={(e) => handleMarkerClick(index)} /> <Marker key={index} position={marker} label={marker.label} onClick={() => handleMarkerClick(index)} />
))} ))}
</> </>
)} )}

View file

@ -1,137 +1,137 @@
export const darkModeStyles = [ export const darkModeStyles = [
{ {
"featureType": "all", featureType: 'all',
"elementType": "labels.text.fill", elementType: 'labels.text.fill',
"stylers": [ stylers: [
{ {
"color": "#ffffff" color: '#ffffff',
} },
] ],
}, },
{ {
"featureType": "all", featureType: 'all',
"elementType": "labels.text.stroke", elementType: 'labels.text.stroke',
"stylers": [ stylers: [
{ {
"color": "#000000" color: '#000000',
}, },
{ {
"lightness": 13 lightness: 13,
} },
] ],
}, },
{ {
"featureType": "administrative", featureType: 'administrative',
"elementType": "geometry.fill", elementType: 'geometry.fill',
"stylers": [ stylers: [
{ {
"color": "#000000" color: '#000000',
} },
] ],
}, },
{ {
"featureType": "administrative", featureType: 'administrative',
"elementType": "geometry.stroke", elementType: 'geometry.stroke',
"stylers": [ stylers: [
{ {
"color": "#144b53" color: '#144b53',
}, },
{ {
"lightness": 14 lightness: 14,
}, },
{ {
"weight": 1.4 weight: 1.4,
} },
] ],
}, },
{ {
"featureType": "landscape", featureType: 'landscape',
"elementType": "all", elementType: 'all',
"stylers": [ stylers: [
{ {
"color": "#08304b" color: '#08304b',
} },
] ],
}, },
{ {
"featureType": "poi", featureType: 'poi',
"elementType": "geometry", elementType: 'geometry',
"stylers": [ stylers: [
{ {
"color": "#0c4152" color: '#0c4152',
}, },
{ {
"lightness": 5 lightness: 5,
} },
] ],
}, },
{ {
"featureType": "road.highway", featureType: 'road.highway',
"elementType": "geometry.fill", elementType: 'geometry.fill',
"stylers": [ stylers: [
{ {
"color": "#000000" color: '#000000',
} },
] ],
}, },
{ {
"featureType": "road.highway", featureType: 'road.highway',
"elementType": "geometry.stroke", elementType: 'geometry.stroke',
"stylers": [ stylers: [
{ {
"color": "#0b434f" color: '#0b434f',
}, },
{ {
"lightness": 25 lightness: 25,
} },
] ],
}, },
{ {
"featureType": "road.arterial", featureType: 'road.arterial',
"elementType": "geometry.fill", elementType: 'geometry.fill',
"stylers": [ stylers: [
{ {
"color": "#000000" color: '#000000',
} },
] ],
}, },
{ {
"featureType": "road.arterial", featureType: 'road.arterial',
"elementType": "geometry.stroke", elementType: 'geometry.stroke',
"stylers": [ stylers: [
{ {
"color": "#0b3d51" color: '#0b3d51',
}, },
{ {
"lightness": 16 lightness: 16,
} },
] ],
}, },
{ {
"featureType": "road.local", featureType: 'road.local',
"elementType": "geometry", elementType: 'geometry',
"stylers": [ stylers: [
{ {
"color": "#000000" color: '#000000',
} },
] ],
}, },
{ {
"featureType": "transit", featureType: 'transit',
"elementType": "all", elementType: 'all',
"stylers": [ stylers: [
{ {
"color": "#146474" color: '#146474',
} },
] ],
}, },
{ {
"featureType": "water", featureType: 'water',
"elementType": "all", elementType: 'all',
"stylers": [ stylers: [
{ {
"color": "#021019" color: '#021019',
} },
] ],
} },
]; ];

View file

@ -4,16 +4,9 @@ import Button from 'react-bootstrap/Button';
import { SubCustomDragLayer } from '../SubCustomDragLayer'; import { SubCustomDragLayer } from '../SubCustomDragLayer';
import { SubContainer } from '../SubContainer'; import { SubContainer } from '../SubContainer';
import { ConfigHandle } from '../ConfigHandle'; import { ConfigHandle } from '../ConfigHandle';
import { resolveWidgetFieldValue, resolveReferences } from '../../_helpers/utils'; import { resolveWidgetFieldValue } from '../../_helpers/utils';
export const Modal = function Modal({ export const Modal = function Modal({ id, component, height, containerProps, currentState, darkMode }) {
id,
component,
height,
mode,
containerProps,
currentState
}) {
const [show, showModal] = useState(false); const [show, showModal] = useState(false);
const parentRef = useRef(null); const parentRef = useRef(null);
@ -25,12 +18,14 @@ export const Modal = function Modal({
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
useEffect(() => { useEffect(() => {
const componentState = containerProps.currentState.components[component.name]; const componentState = containerProps.currentState.components[component.name];
const canShowModel = componentState ? componentState.show : false; const canShowModel = componentState ? componentState.show : false;
showModal(canShowModel); showModal(canShowModel);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerProps.currentState.components[component.name]]); }, [containerProps.currentState.components[component.name]]);
function hideModal() { function hideModal() {
@ -51,36 +46,26 @@ export const Modal = function Modal({
animation={false} animation={false}
onEscapeKeyDown={() => showModal(false)} onEscapeKeyDown={() => showModal(false)}
> >
{containerProps.mode === 'edit' && {containerProps.mode === 'edit' && (
<ConfigHandle <ConfigHandle id={id} component={component} configHandleClicked={containerProps.onComponentClick} />
id={id} )}
component={component}
configHandleClicked={containerProps.onComponentClick}
/>
}
<BootstrapModal.Header> <BootstrapModal.Header>
<BootstrapModal.Title> <BootstrapModal.Title>{title}</BootstrapModal.Title>
{title}
</BootstrapModal.Title>
<div> <div>
<Button variant="light" size="sm" onClick={hideModal}> <Button variant={darkMode ? 'secondary' : 'light'} size="sm" onClick={hideModal}>
x x
</Button> </Button>
</div> </div>
</BootstrapModal.Header> </BootstrapModal.Header>
<BootstrapModal.Body style={{ height }} ref={parentRef}> <BootstrapModal.Body style={{ height }} ref={parentRef}>
<SubContainer <SubContainer parent={id} {...containerProps} parentRef={parentRef} />
parent={id} <SubCustomDragLayer
{...containerProps} snapToGrid={true}
parentRef={parentRef} parentRef={parentRef}
/> parent={id}
<SubCustomDragLayer currentLayout={containerProps.currentLayout}
snapToGrid={true} />
parentRef={parentRef}
parent={id}
currentLayout={containerProps.currentLayout}
/>
</BootstrapModal.Body> </BootstrapModal.Body>
</BootstrapModal> </BootstrapModal>
</div> </div>

View file

@ -9,7 +9,7 @@ export const Multiselect = function Multiselect({
component, component,
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged onComponentOptionChanged,
}) { }) {
console.log('currentState', currentState); console.log('currentState', currentState);
@ -19,7 +19,8 @@ export const Multiselect = function Multiselect({
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
const parsedValues = JSON.parse(values); const parsedValues = JSON.parse(values);
const parsedDisplayValues = JSON.parse(displayValues); const parsedDisplayValues = JSON.parse(displayValues);
@ -27,7 +28,7 @@ export const Multiselect = function Multiselect({
const selectOptions = [ const selectOptions = [
...parsedValues.map((value, index) => { ...parsedValues.map((value, index) => {
return { name: parsedDisplayValues[index], value: value }; return { name: parsedDisplayValues[index], value: value };
}) }),
]; ];
const currentValueProperty = component.definition.properties.values; const currentValueProperty = component.definition.properties.values;
@ -40,19 +41,30 @@ export const Multiselect = function Multiselect({
} }
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
useEffect(() => { useEffect(() => {
setCurrentValue(newValue); setCurrentValue(newValue);
}, [newValue]); }, [newValue]);
return ( return (
<div className="multiselect-widget row g-0" style={{ width, height, display:parsedWidgetVisibility ? '' : 'none' }} onClick={event => {event.stopPropagation(); onComponentClick(id, component)}}> <div
className="multiselect-widget row g-0"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
>
<div className="col-auto"> <div className="col-auto">
<label style={{marginRight: '1rem'}} className="form-label py-1">{label}</label> <label style={{ marginRight: '1rem' }} className="form-label py-1">
{label}
</label>
</div> </div>
<div className="col px-0"> <div className="col px-0">
<SelectSearch <SelectSearch

View file

@ -8,9 +8,8 @@ export const NumberInput = function NumberInput({
component, component,
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged onComponentOptionChanged,
}) { }) {
const value = component.definition.properties.value ? component.definition.properties.value.value : ''; const value = component.definition.properties.value ? component.definition.properties.value.value : '';
const [number, setNumber] = useState(value); const [number, setNumber] = useState(value);
@ -23,24 +22,31 @@ export const NumberInput = function NumberInput({
useEffect(() => { useEffect(() => {
setNumber(parseInt(newNumber)); setNumber(parseInt(newNumber));
onComponentOptionChanged(component, 'value', parseInt(newNumber)); onComponentOptionChanged(component, 'value', parseInt(newNumber));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newNumber]); }, [newNumber]);
const placeholder = component.definition.properties.placeholder.value; const placeholder = component.definition.properties.placeholder.value;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
return ( return (
<input <input
disabled={parsedDisabledState} disabled={parsedDisabledState}
onClick={event => {event.stopPropagation(); onComponentClick(id, component)}} onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
onChange={(e) => { onChange={(e) => {
setNumber(parseInt(e.target.value)); setNumber(parseInt(e.target.value));
onComponentOptionChanged(component, 'value', parseInt(e.target.value)); onComponentOptionChanged(component, 'value', parseInt(e.target.value));
@ -48,7 +54,7 @@ export const NumberInput = function NumberInput({
type="number" type="number"
className="form-control" className="form-control"
placeholder={placeholder} placeholder={placeholder}
style={{ width, height, display:parsedWidgetVisibility ? '' : 'none' }} style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
value={number} value={number}
/> />
); );

View file

@ -1,34 +1,41 @@
import React, { useState } from 'react'; import React from 'react';
export default function ErrorModal() { export default function ErrorModal() {
const [show, setShow] = React.useState(true);
const [show, setShow] = React.useState(true)
const close = () => { const close = () => {
setShow(false) setShow(false);
} };
return( return (
<div> <div>
{ {show ? (
show ? <div className="modal-dialog" role="document">
<div class="modal-dialog" role="document"> <div className="modal-content">
<div class="modal-content"> <div className="modal-header">
<div class="modal-header"> <h5 className="modal-title">QR Scanner is not working</h5>
<h5 class="modal-title">QR Scanner is not working</h5> <button
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onClick={close}></button> type="button"
</div> className="btn-close"
<div class="modal-body"> data-bs-dismiss="modal"
Please make sure a camera is available on your device. Try closing your browser and opening it again, if it doesn't work, please contact support. aria-label="Close"
</div> onClick={close}
<div class="modal-footer"> ></button>
<button type="button" class="btn" data-bs-dismiss="modal" onClick={close}>Close</button> </div>
</div> <div className="modal-body">
Please make sure a camera is available on your device. Try closing your browser and opening it again, if
it doesn&apos;t work, please contact support.
</div>
<div className="modal-footer">
<button type="button" className="btn" data-bs-dismiss="modal" onClick={close}>
Close
</button>
</div> </div>
</div> </div>
: </div>
'' ) : (
} ''
)}
</div> </div>
); );
}; }

View file

@ -3,10 +3,7 @@ import QrReader from 'react-qr-reader';
import ErrorModal from './ErrorModal'; import ErrorModal from './ErrorModal';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const QrScanner = function QrScanner({ export const QrScanner = function QrScanner({ component, onEvent, onComponentOptionChanged, currentState }) {
component, onEvent, onComponentOptionChanged, currentState
}) {
const handleError = async (errorMessage) => { const handleError = async (errorMessage) => {
console.log(errorMessage); console.log(errorMessage);
setErrorOccured(true); setErrorOccured(true);
@ -16,7 +13,7 @@ export const QrScanner = function QrScanner({
if (data != null) { if (data != null) {
onEvent('onDetect', { component, data: data }); onEvent('onDetect', { component, data: data });
onComponentOptionChanged(component, 'lastDetectedValue', data); onComponentOptionChanged(component, 'lastDetectedValue', data);
}; }
}; };
let [errorOccured, setErrorOccured] = useState(false); let [errorOccured, setErrorOccured] = useState(false);
@ -24,25 +21,20 @@ export const QrScanner = function QrScanner({
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
return ( return (
<div data-disabled={parsedDisabledState} style={{display:parsedWidgetVisibility ? '' : 'none'}}> <div data-disabled={parsedDisabledState} style={{ display: parsedWidgetVisibility ? '' : 'none' }}>
{ {errorOccured ? <ErrorModal /> : <QrReader onError={handleError} onScan={handleScan} />}
errorOccured ?
<ErrorModal />
:
<QrReader
onError={handleError}
onScan={handleScan}
/>
}
</div> </div>
); );
}; };

View file

@ -1,7 +1,6 @@
import React from 'react'; import React, { useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
export const RadioButton = function RadioButton({ export const RadioButton = function RadioButton({
id, id,
width, width,
@ -10,9 +9,8 @@ export const RadioButton = function RadioButton({
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged, onComponentOptionChanged,
onEvent onEvent,
}) { }) {
const label = component.definition.properties.label.value; const label = component.definition.properties.label.value;
const textColorProperty = component.definition.styles.textColor; const textColorProperty = component.definition.styles.textColor;
const textColor = textColorProperty ? textColorProperty.value : '#000'; const textColor = textColorProperty ? textColorProperty.value : '#000';
@ -23,25 +21,34 @@ export const RadioButton = function RadioButton({
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedValues = values; let parsedValues = values;
try { try {
parsedValues = resolveReferences(values, currentState, []); parsedValues = resolveReferences(values, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
let parsedDisplayValues = displayValues; let parsedDisplayValues = displayValues;
try { try {
parsedDisplayValues = resolveReferences(displayValues, currentState, []); parsedDisplayValues = resolveReferences(displayValues, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
let parsedDefaultValue = defaultValue; let parsedDefaultValue = defaultValue;
try { try {
parsedDefaultValue = resolveReferences(defaultValue, currentState, []); parsedDefaultValue = resolveReferences(defaultValue, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
const value = currentState?.components[component?.name]?.value ?? parsedDefaultValue;
let selectOptions = []; let selectOptions = [];
@ -49,33 +56,59 @@ export const RadioButton = function RadioButton({
selectOptions = [ selectOptions = [
...parsedValues.map((value, index) => { ...parsedValues.map((value, index) => {
return { name: parsedDisplayValues[index], value: value }; return { name: parsedDisplayValues[index], value: value };
}) }),
]; ];
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
function onSelect(selection) {
function onSelect(event) {
const selection = event.target.value
onComponentOptionChanged(component, 'value', selection); onComponentOptionChanged(component, 'value', selection);
if (selection) { if (selection) {
onEvent('onSelectionChange', { component }); onEvent('onSelectionChange', { component });
} }
} }
useEffect(() => {
onComponentOptionChanged(component, 'value', parsedDefaultValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [parsedDefaultValue]);
return ( return (
<div data-disabled={parsedDisabledState} className="row" style={{ width, height, display:parsedWidgetVisibility ? '' : 'none' }} onClick={event => {event.stopPropagation(); onComponentClick(id, component)}}> <div
<span className="form-check-label form-check-label col-auto py-1" style={{color: textColor}}>{label}</span> data-disabled={parsedDisabledState}
<div className="col py-1" onChange={(e) => onSelect(e)}> className="row"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
>
<span className="form-check-label col-auto py-1" style={{ color: textColor }}>
{label}
</span>
<div className="col py-1">
{selectOptions.map((option, index) => ( {selectOptions.map((option, index) => (
<label key={index} class="form-check form-check-inline"> <label key={index} className="form-check form-check-inline">
<input class="form-check-input" defaultChecked={parsedDefaultValue === option.value} type="radio" value={option.value} name="radio-options" /> <input
<span className="form-check-label" style={{color: textColor}}>{option.name}</span> className="form-check-input"
checked={value === option.value}
type="radio"
value={option.value}
name={`${id}-radio-options`}
onChange={() => onSelect(option.value)}
/>
<span className="form-check-label" style={{ color: textColor }}>
{option.name}
</span>
</label> </label>
))} ))}
</div> </div>

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Editor, EditorState } from "draft-js"; import 'draft-js/dist/Draft.css';
import "draft-js/dist/Draft.css";
import { DraftEditor } from './DraftEditor'; import { DraftEditor } from './DraftEditor';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
@ -11,33 +10,37 @@ export const RichTextEditor = function RichTextEditor({
component, component,
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged onComponentOptionChanged,
}) { }) {
const placeholder = component.definition.properties.placeholder.value; const placeholder = component.definition.properties.placeholder.value;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
function handleChange(html) { function handleChange(html) {
onComponentOptionChanged(component, 'value', html); onComponentOptionChanged(component, 'value', html);
} }
return ( return (
<div data-disabled={parsedDisabledState} style={{ width: `${width}px`, height: `${height}px`, display:parsedWidgetVisibility ? '' : 'none' }} onClick={event => {event.stopPropagation(); onComponentClick(id, component)}}> <div
<DraftEditor data-disabled={parsedDisabledState}
handleChange={handleChange} style={{ width: `${width}px`, height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }}
height={height} onClick={(event) => {
width={width} event.stopPropagation();
placeholder={placeholder} onComponentClick(id, component);
></DraftEditor> }}
>
<DraftEditor handleChange={handleChange} height={height} width={width} placeholder={placeholder}></DraftEditor>
</div> </div>
); );
}; };

View file

@ -1,47 +1,44 @@
import React from 'react' import React from 'react';
export default ({ fill = '#ffb400'}) => { export default ({ fill = '#ffb400' }) => {
return ( return (
<svg height="20" width="20" fill={fill} version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg
viewBox="0 0 630 630" xmlSpace="preserve"> height="20"
<path d="M610.089,233.999c-4.591-14.132-16.807-24.432-31.512-26.567l-164.157-23.856L341.006,34.827 width="20"
fill={fill}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 630 630"
xmlSpace="preserve"
>
<path
d="M610.089,233.999c-4.591-14.132-16.807-24.432-31.512-26.567l-164.157-23.856L341.006,34.827
c-6.577-13.325-20.147-21.76-35.005-21.76c-14.86,0-28.43,8.435-35.005,21.76l-73.416,148.749L33.424,207.432 c-6.577-13.325-20.147-21.76-35.005-21.76c-14.86,0-28.43,8.435-35.005,21.76l-73.416,148.749L33.424,207.432
c-14.705,2.136-26.921,12.436-31.512,26.567s-0.762,29.645,9.879,40.017l118.786,115.787l-28.043,163.492 c-14.705,2.136-26.921,12.436-31.512,26.567s-0.762,29.645,9.879,40.017l118.786,115.787l-28.043,163.492
c-2.513,14.646,3.507,29.446,15.529,38.18c12.021,8.734,27.958,9.885,41.112,2.972l146.825-77.192l146.825,77.192 c-2.513,14.646,3.507,29.446,15.529,38.18c12.021,8.734,27.958,9.885,41.112,2.972l146.825-77.192l146.825,77.192
c5.713,3.004,11.949,4.485,18.162,4.485c8.092,0,16.149-2.515,22.95-7.455c12.021-8.734,18.041-23.536,15.529-38.18 c5.713,3.004,11.949,4.485,18.162,4.485c8.092,0,16.149-2.515,22.95-7.455c12.021-8.734,18.041-23.536,15.529-38.18
l-28.041-163.494l118.784-115.789C610.851,263.643,614.68,248.131,610.089,233.999z M412.235,348.222 l-28.041-163.494l118.784-115.789C610.851,263.643,614.68,248.131,610.089,233.999z M412.235,348.222
c-9.202,8.967-13.399,21.889-11.227,34.552l18.139,105.762l-94.979-49.934c-3.162-1.664-6.498-2.847-9.909-3.584V157.04 c-9.202,8.967-13.399,21.889-11.227,34.552l18.139,105.762l-94.979-49.934c-3.162-1.664-6.498-2.847-9.909-3.584V157.04
l39.232,79.493c5.686,11.522,16.676,19.508,29.391,21.354l106.192,15.431L412.235,348.222z"/> l39.232,79.493c5.686,11.522,16.676,19.508,29.391,21.354l106.192,15.431L412.235,348.222z"
<g> />
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg> </svg>
) );
} };

View file

@ -1,44 +1,41 @@
import React from 'react' import React from 'react';
export default ({ fill = '#ffb400'}) => { export default ({ fill = '#ffb400' }) => {
return ( return (
<svg height="20" width="20" fill={fill} version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg
viewBox="0 0 500 500" xmlSpace="preserve"> height="20"
<path d="M471.563,173.778l-145.5-20.8l-64.4-132c-8-15.4-30-12.2-35.3,0l-64.4,132l-145.6,20.8c-16.4,1-21.6,20.9-10.4,33.2 width="20"
fill={fill}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 500 500"
xmlSpace="preserve"
>
<path
d="M471.563,173.778l-145.5-20.8l-64.4-132c-8-15.4-30-12.2-35.3,0l-64.4,132l-145.6,20.8c-16.4,1-21.6,20.9-10.4,33.2
l105,102.9l-25,144.5c-2.9,17.8,16.7,27.8,28.1,20.8l129.9-68.6l129.9,67.6c13.6,7,29.8-2.8,28.1-19.7l-25-144.6l105-102.9 l105,102.9l-25,144.5c-2.9,17.8,16.7,27.8,28.1,20.8l129.9-68.6l129.9,67.6c13.6,7,29.8-2.8,28.1-19.7l-25-144.6l105-102.9
C494.663,193.478,485.563,175.478,471.563,173.778z M342.663,288.078c-4.2,5.2-6.2,11.4-5.2,17.7l19.7,116.4l-103.9-55.1 C494.663,193.478,485.563,175.478,471.563,173.778z M342.663,288.078c-4.2,5.2-6.2,11.4-5.2,17.7l19.7,116.4l-103.9-55.1
c-6.7-2.8-13-2.8-18.7,0l-103.9,55.1l19.7-116.4c1-7.3-1-13.5-5.2-17.7l-84.1-82.1l116.4-16.6c6.2-1,11.4-4.2,14.6-10.4l52-105 c-6.7-2.8-13-2.8-18.7,0l-103.9,55.1l19.7-116.4c1-7.3-1-13.5-5.2-17.7l-84.1-82.1l116.4-16.6c6.2-1,11.4-4.2,14.6-10.4l52-105
l52,105c3.1,5.2,8.3,9.4,14.6,10.4l116.2,16.6L342.663,288.078z"/> l52,105c3.1,5.2,8.3,9.4,14.6,10.4l116.2,16.6L342.663,288.078z"
<g> />
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg> </svg>
) );
} };

View file

@ -1,44 +1,41 @@
import React from 'react' import React from 'react';
export default ({ fill = '#ffb400'}) => { export default ({ fill = '#ffb400' }) => {
return ( return (
<svg version="1.1" fill={fill} height="20" width="20" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg
viewBox="0 0 60 60" xmlSpace="preserve"> version="1.1"
<path d="M55.818,21.578c-0.118-0.362-0.431-0.626-0.808-0.681L36.92,18.268L28.83,1.876c-0.168-0.342-0.516-0.558-0.896-0.558 fill={fill}
height="20"
width="20"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 60 60"
xmlSpace="preserve"
>
<path
d="M55.818,21.578c-0.118-0.362-0.431-0.626-0.808-0.681L36.92,18.268L28.83,1.876c-0.168-0.342-0.516-0.558-0.896-0.558
s-0.729,0.216-0.896,0.558l-8.091,16.393l-18.09,2.629c-0.377,0.055-0.689,0.318-0.808,0.681c-0.117,0.361-0.02,0.759,0.253,1.024 s-0.729,0.216-0.896,0.558l-8.091,16.393l-18.09,2.629c-0.377,0.055-0.689,0.318-0.808,0.681c-0.117,0.361-0.02,0.759,0.253,1.024
l13.091,12.76l-3.091,18.018c-0.064,0.375,0.09,0.754,0.397,0.978c0.309,0.226,0.718,0.255,1.053,0.076l16.182-8.506l16.18,8.506 l13.091,12.76l-3.091,18.018c-0.064,0.375,0.09,0.754,0.397,0.978c0.309,0.226,0.718,0.255,1.053,0.076l16.182-8.506l16.18,8.506
c0.146,0.077,0.307,0.115,0.466,0.115c0.207,0,0.413-0.064,0.588-0.191c0.308-0.224,0.462-0.603,0.397-0.978l-3.09-18.017 c0.146,0.077,0.307,0.115,0.466,0.115c0.207,0,0.413-0.064,0.588-0.191c0.308-0.224,0.462-0.603,0.397-0.978l-3.09-18.017
l13.091-12.761C55.838,22.336,55.936,21.939,55.818,21.578z"/> l13.091-12.761C55.838,22.336,55.936,21.939,55.818,21.578z"
<g> />
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g> <g></g>
</g> <g></g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg> </svg>
) );
} };

View file

@ -20,16 +20,21 @@ export const StarRating = function StarRating({
const allowHalfStar = component.definition.properties.allowHalfStar.value ?? false; const allowHalfStar = component.definition.properties.allowHalfStar.value ?? false;
const textColorProperty = component.definition.styles.textColor; const textColorProperty = component.definition.styles.textColor;
const color = textColorProperty ? textColorProperty.value : '#ffb400'; 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 widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
const tooltips = component.definition.properties.tooltips.value ?? []; const tooltips = component.definition.properties.tooltips.value ?? [];
const _tooltips = resolveReferences(tooltips, currentState, []) ?? []; const _tooltips = resolveReferences(tooltips, currentState, []) ?? [];
@ -54,12 +59,14 @@ export const StarRating = function StarRating({
React.useEffect(() => { React.useEffect(() => {
setRatingIndex(defaultSelected - 1); setRatingIndex(defaultSelected - 1);
onComponentOptionChanged(component, 'value', defaultSelected); onComponentOptionChanged(component, 'value', defaultSelected);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultSelected]); }, [defaultSelected]);
React.useEffect(() => { React.useEffect(() => {
setTimeout(() => { setTimeout(() => {
onComponentOptionChanged(component, 'value', defaultSelected); onComponentOptionChanged(component, 'value', defaultSelected);
}, 1000) }, 1000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
function handleClick(idx) { function handleClick(idx) {
@ -84,9 +91,17 @@ export const StarRating = function StarRating({
}; };
return ( return (
<div data-disabled={parsedDisabledState} className="star-rating" onClick={event => {event.stopPropagation(); onComponentClick(id, component)}} style={{display:parsedWidgetVisibility ? '' : 'none'}}> <div
data-disabled={parsedDisabledState}
className="star-rating"
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
style={{ display: parsedWidgetVisibility ? '' : 'none' }}
>
{/* TODO: Add label color defination property instead of hardcoded color*/} {/* TODO: Add label color defination property instead of hardcoded color*/}
<span className="label form-check-label form-check-label col-auto" style={{ color: '#000' }}> <span className="label form-check-label form-check-label col-auto" style={{ color: labelColor }}>
{label} {label}
</span> </span>
{animatedStars.map((props, index) => ( {animatedStars.map((props, index) => (

View file

@ -37,6 +37,7 @@ const Star = ({
React.useEffect(() => { React.useEffect(() => {
setIcon(isHalfStar ? halfStar : star); setIcon(isHalfStar ? halfStar : star);
setOutlineIcon(isHalfStar ? halfStar : starOutline); setOutlineIcon(isHalfStar ? halfStar : starOutline);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [color]); }, [color]);
const ref = React.useRef(null); const ref = React.useRef(null);

View file

@ -1,26 +1,29 @@
import React from 'react'; import React from 'react';
import SelectSearch from 'react-select-search'; import SelectSearch from 'react-select-search';
export const CustomSelect = ({ options, value, multiple, disabled, onChange }) => { export const CustomSelect = ({ options, value, multiple, onChange }) => {
function renderValue(valueProps) {
function renderValue(valueProps) { if (valueProps) {
if(valueProps) { return valueProps.value.split(', ').map((value, index) => (
return valueProps.value.split(', ').map((value) => <span {...valueProps} className="badge bg-blue-lt p-2 mx-1">{value}</span>); <span key={index} {...valueProps} className="badge bg-blue-lt p-2 mx-1">
} {value}
</span>
));
} }
}
return ( return (
<div className="custom-select"> <div className="custom-select">
<SelectSearch <SelectSearch
options={options} options={options}
printOptions="on-focus" printOptions="on-focus"
value={value} value={value}
renderValue={renderValue} renderValue={renderValue}
search={false} search={false}
onChange={onChange} onChange={onChange}
multiple={multiple} multiple={multiple}
placeholder="Select.." placeholder="Select.."
/> />
</div> </div>
); );
}; };

View file

@ -1,81 +1,51 @@
import React from 'react'; import React from 'react';
import Datetime from 'react-datetime'; import Datetime from 'react-datetime';
import moment from 'moment';
import 'react-datetime/css/react-datetime.css'; import 'react-datetime/css/react-datetime.css';
import '@/_styles/custom.scss'; import '@/_styles/custom.scss';
import moment from 'moment'
export const Datepicker = function Datepicker({value, onChange, readOnly, isTimeChecked, dateFormat}) { export const Datepicker = function Datepicker({ value, onChange, readOnly, isTimeChecked, dateFormat }) {
const [date, setDate] = React.useState(value);
const [date, setDate] = React.useState(value)
const dateChange = (e) => { const dateChange = (e) => {
if(isTimeChecked) { if (isTimeChecked) {
setDate(e.format(`${dateFormat} LT`)) setDate(e.format(`${dateFormat} LT`));
} else { } else {
setDate(e.format(dateFormat)) setDate(e.format(dateFormat));
} }
};
React.useEffect(() => {
if (!isTimeChecked) {
setDate(moment(value, 'DD-MM-YYYY').format(dateFormat));
} }
React.useEffect(() => { if (isTimeChecked) {
const _date = isTimeChecked ? moment(value, `${dateFormat} LT`) : moment(value, dateFormat) setDate(moment(value, 'DD-MM-YYYY LT').format(`${dateFormat} LT`));
if(!isTimeChecked) {
setDate(moment(value, 'DD-MM-YYYY').format(dateFormat))
}
if(isTimeChecked) {
setDate(moment(value, 'DD-MM-YYYY LT' ).format(`${dateFormat} LT`))
}
},[isTimeChecked, readOnly, dateFormat])
let inputProps = {
disabled: !readOnly,
};
const [isDatepickerOpen, setIsDatepickerOpen] = React.useState(false)
const onDatepickerClose = () => {
onChange(date)
setIsDatepickerOpen((prev) => !prev)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTimeChecked, readOnly, dateFormat]);
React.useEffect(() => { let inputProps = {
const myElement = document.querySelector('.cell-type-datepicker') disabled: !readOnly,
};
myElement.parentNode.style.position = 'absolute' const onDatepickerClose = () => {
myElement.style.position = 'relative' onChange(date);
myElement.style.marginTop = '2px' };
myElement.style.left = '50%'
myElement.style.width = '250px'
myElement.style.transform = 'translate(-50%, -25%)'
return () => {
myElement.parentNode.style.position = ''
myElement.style.position = ''
myElement.style.marginTop = ''
myElement.style.left = ''
myElement.style.width = ''
myElement.style.transform = ''
}
},[isDatepickerOpen])
return (
<>
<Datetime
inputProps={inputProps}
timeFormat={isTimeChecked}
className='cell-type-datepicker'
dateFormat={dateFormat}
value={date}
onChange={dateChange}
onClose={onDatepickerClose}
/>
</>
)
return (
<>
<Datetime
inputProps={inputProps}
timeFormat={isTimeChecked}
className="cell-type-datepicker"
dateFormat={dateFormat}
value={date}
onChange={dateChange}
onClose={onDatepickerClose}
/>
</>
);
}; };

View file

@ -0,0 +1,53 @@
import React from 'react';
// Table Search
export const GlobalFilter = ({
preGlobalFilteredRows,
globalFilter,
useAsyncDebounce,
setGlobalFilter,
onComponentOptionChanged,
component,
serverSideSearch,
onEvent,
}) => {
const count = preGlobalFilteredRows.length;
const [value, setValue] = React.useState(globalFilter);
const onChange = useAsyncDebounce((filterValue) => {
setGlobalFilter(filterValue || undefined);
}, 200);
const handleSearchTextChange = (text) => {
setValue(text);
onChange(text);
onComponentOptionChanged(component, 'searchText', text).then(() => {
if (serverSideSearch === true) {
onEvent('onSearch', { component, data: {} });
}
});
};
return (
<div className="ms-2 d-inline-block">
Search:{' '}
<input
type="text"
className="global-search-field"
defaultValue={value || ''}
onBlur={(e) => {
handleSearchTextChange(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSearchTextChange(e.target.value);
}
}}
onChange={(e) => onChange(e.target.value)}
placeholder={`${count} records`}
style={{
border: '0',
}}
/>
</div>
);
};

View file

@ -7,7 +7,7 @@ export const Pagination = function Pagination({
autoCanNextPage, autoCanNextPage,
autoPageCount, autoPageCount,
autoPageOptions, autoPageOptions,
lastActivePageIndex lastActivePageIndex,
}) { }) {
const [pageIndex, setPageIndex] = useState(lastActivePageIndex ?? 1); const [pageIndex, setPageIndex] = useState(lastActivePageIndex ?? 1);
const [pageCount, setPageCount] = useState(autoPageCount); const [pageCount, setPageCount] = useState(autoPageCount);
@ -17,13 +17,12 @@ export const Pagination = function Pagination({
}, [autoPageCount]); }, [autoPageCount]);
useEffect(() => { useEffect(() => {
if (serverSide && lastActivePageIndex > 0) {
if(serverSide && lastActivePageIndex > 0) { setPageCount(lastActivePageIndex);
setPageCount(lastActivePageIndex) } else if (serverSide || lastActivePageIndex === 0) {
} else if(serverSide || lastActivePageIndex === 0) { setPageIndex(1);
setPageIndex(1)
} }
}, [serverSide, lastActivePageIndex ]) }, [serverSide, lastActivePageIndex]);
function gotoPage(page) { function gotoPage(page) {
setPageIndex(page); setPageIndex(page);
@ -42,48 +41,35 @@ export const Pagination = function Pagination({
} }
return ( return (
<div className="pagination"> <div className="pagination">
{!serverSide {!serverSide && (
&& <button className="btn btn-sm btn-light mx-2" onClick={() => gotoPage(1)}> <button className="btn btn-sm btn-light mx-2" onClick={() => gotoPage(1)}>
{'<<'} {'<<'}
</button> </button>
} )}
<button <button className="btn btn-light btn-sm" onClick={() => goToPreviousPage()} disabled={pageIndex === 1}>
className="btn btn-light btn-sm" {'<'}
onClick={() => goToPreviousPage()} </button>{' '}
disabled={pageIndex === 1} <small className="p-1 mx-2">
> {serverSide && <strong>{pageIndex}</strong>}
{'<'} {!serverSide && (
</button>{' '} <strong>
<small className="p-1 mx-2"> {pageIndex} of {autoPageOptions.length}
</strong>
{serverSide && )}
<strong> </small>
{pageIndex} <button
</strong> className="btn btn-light btn-sm"
} onClick={() => goToNextPage()}
{!serverSide && disabled={!autoCanNextPage && !serverSide}
<strong> >
{pageIndex} of {autoPageOptions.length} {'>'}
</strong> </button>{' '}
} {!serverSide && (
</small> <button className="btn btn-light btn-sm mx-2" onClick={() => gotoPage(pageCount)}>
<button {'>>'}
className="btn btn-light btn-sm" </button>
onClick={() => goToNextPage()} )}
disabled={!autoCanNextPage && !serverSide} </div>
>
{'>'}
</button>{' '}
{!serverSide
&& <button
className="btn btn-light btn-sm mx-2"
onClick={() => gotoPage(pageCount)}
>
{'>>'}
</button>
}
</div>
); );
}; };

View file

@ -1,20 +1,30 @@
import React, { useState } from 'react'; import React from 'react';
export const Radio = ({ options, value, onChange, readOnly }) => { export const Radio = ({ options, value, onChange, readOnly }) => {
value = value === undefined ? [] : value; value = value === undefined ? [] : value;
options = Array.isArray(options) ? options : []; options = Array.isArray(options) ? options : [];
return ( return (
<div className="radio row"> <div className="radio row">
<div> <div>
{options.map((option) => {options.map((option, index) => (
<label class="form-check form-check-inline" onClick={() => { if(!readOnly) onChange(option.value); } }> <label
<input class="form-check-input" type="radio" checked={option.value === value} disabled={readOnly && (option.value !== value)}/> key={index}
<span class ="form-check-label">{option.name}</span> className="form-check form-check-inline"
onClick={() => {
if (!readOnly) onChange(option.value);
}}
>
<input
className="form-check-input"
type="radio"
checked={option.value === value}
disabled={readOnly && option.value !== value}
/>
<span className="form-check-label">{option.name}</span>
</label> </label>
)} ))}
</div> </div>
</div> </div>
); );
}; };

File diff suppressed because it is too large Load diff

View file

@ -1,62 +1,69 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
export const Tags = ({ value, onChange }) => { export const Tags = ({ value, onChange, readOnly }) => {
const isValid = Array.isArray(value);
if (!isValid) console.warn('[Tags]: value provided is not an array');
value = isValid ? value : [];
value = value || []; const [showForm, setShowForm] = useState(false);
const [ showForm, setShowForm ] = useState(false); function addTag(text) {
if (text !== '') {
function addTag(text) { value.push(text);
if(text !== '') { onChange(value);
value.push(text); } else {
onChange(value); setShowForm(false);
} else {
setShowForm(false);
}
} }
}
function removeTag(text) { function removeTag(text) {
const newValue = value.filter(tag => tag !== text); const newValue = value.filter((tag) => tag !== text);
onChange(newValue); onChange(newValue);
setShowForm(false); setShowForm(false);
} }
function handleFormKeyDown(e) { function handleFormKeyDown(e) {
if(e.key === 'Enter') { if (e.key === 'Enter') {
addTag(e.target.value) addTag(e.target.value);
}
}
function renderTag(text) {
return <span className="col-auto badge bg-blue-lt p-2 mx-1 tag mb-2">
{text}
<span className="badge badge-pill bg-red-lt remove-tag-button" onClick={() => removeTag(text)}>
x
</span>
</span>;
} }
}
function renderTag(text) {
return ( return (
<div className="tags row"> <span className="col-auto badge bg-blue-lt p-2 mx-1 tag mb-2">
{value.map((item) => { {text}
return renderTag(item) {!readOnly && (
})} <span className="badge badge-pill bg-red-lt remove-tag-button" onClick={() => removeTag(text)}>
x
{!showForm && </span>
<span className="col-auto badge bg-green-lt mx-1 add-tag-button" onClick={() => setShowForm(true)}>{'+'}</span> )}
} </span>
{showForm &&
<span className="col-auto badge bg-green-lt mx-1">
<input
type="text"
autoFocus
className="form-control-plaintext"
onBlur={(e) => addTag(e.target.value)}
onKeyDown={handleFormKeyDown}
/>
</span>
}
</div>
); );
}; }
return (
<div className="tags row">
{value.map((item) => {
return renderTag(item);
})}
{!showForm && !readOnly && (
<span className="col-auto badge bg-green-lt mx-1 add-tag-button" onClick={() => setShowForm(true)}>
{'+'}
</span>
)}
{showForm && (
<span className="col-auto badge bg-green-lt mx-1">
<input
type="text"
autoFocus
className="form-control-plaintext"
onBlur={(e) => addTag(e.target.value)}
onKeyDown={handleFormKeyDown}
/>
</span>
)}
</div>
);
};

View file

@ -1,24 +1,26 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
export const Toggle = ({readOnly, value, onChange, activeColor, options }) => { export const Toggle = ({ readOnly, value, onChange, activeColor }) => {
const [on, setOn] = useState(() => value) const [on, setOn] = useState(() => value);
const toggle = () => { const toggle = () => {
setOn((prev) => !prev) setOn((prev) => !prev);
onChange(!on) onChange(!on);
} };
return ( return (
<div className="radio row g-0"> <div className="radio row g-0">
<label className="form-check form-switch form-check-inline"> <label className="form-check form-switch form-check-inline">
<input <input
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={on} checked={on}
style={ on ? { backgroundColor: activeColor} : {}} style={on ? { backgroundColor: activeColor } : {}}
onClick={() => {if(!readOnly) toggle()}} onClick={() => {
/> if (!readOnly) toggle();
</label> }}
/>
</label>
</div> </div>
); );
}; };

View file

@ -1,17 +1,15 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import Skeleton from 'react-loading-skeleton';
export const Text = function Text({ export const Text = function Text({ id, width, height, component, onComponentClick, currentState }) {
id, width, height, component, onComponentClick, currentState
}) {
const text = component.definition.properties.text.value; const text = component.definition.properties.text.value;
const color = component.definition.styles.textColor.value; const color = component.definition.styles.textColor.value;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
const [loadingState, setLoadingState] = useState(false); const [loadingState, setLoadingState] = useState(false);
@ -21,6 +19,7 @@ export const Text = function Text({
const newState = resolveReferences(loadingStateProperty.value, currentState, false); const newState = resolveReferences(loadingStateProperty.value, currentState, false);
setLoadingState(newState); setLoadingState(newState);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState]); }, [currentState]);
let data = text; let data = text;
@ -37,21 +36,31 @@ export const Text = function Text({
} }
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
const computedStyles = { const computedStyles = {
color, color,
width, width,
height, height,
display: parsedWidgetVisibility ? 'flex' : 'none', display: parsedWidgetVisibility ? 'flex' : 'none',
alignItems: 'center' alignItems: 'center',
}; };
return ( return (
<div data-disabled={parsedDisabledState} className="text-widget" style={computedStyles} onClick={event => {event.stopPropagation(); onComponentClick(id, component)}}> <div
data-disabled={parsedDisabledState}
className="text-widget"
style={computedStyles}
onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
>
{!loadingState && <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) }} />} {!loadingState && <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) }} />}
{loadingState === true && ( {loadingState === true && (
<div> <div>

View file

@ -8,9 +8,8 @@ export const TextArea = function TextArea({
component, component,
onComponentClick, onComponentClick,
currentState, currentState,
onComponentOptionChanged onComponentOptionChanged,
}) { }) {
const value = component.definition.properties.value ? component.definition.properties.value.value : ''; const value = component.definition.properties.value ? component.definition.properties.value.value : '';
const [text, setText] = useState(value); const [text, setText] = useState(value);
@ -23,24 +22,31 @@ export const TextArea = function TextArea({
useEffect(() => { useEffect(() => {
setText(newText); setText(newText);
onComponentOptionChanged(component, 'value', newText); onComponentOptionChanged(component, 'value', newText);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newText]); }, [newText]);
const placeholder = component.definition.properties.placeholder.value; const placeholder = component.definition.properties.placeholder.value;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
let parsedWidgetVisibility = widgetVisibility; let parsedWidgetVisibility = widgetVisibility;
try { try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) { console.log(err); } } catch (err) {
console.log(err);
}
return ( return (
<textarea <textarea
disabled={parsedDisabledState} disabled={parsedDisabledState}
onClick={event => {event.stopPropagation(); onComponentClick(id, component)}} onClick={(event) => {
event.stopPropagation();
onComponentClick(id, component);
}}
onChange={(e) => { onChange={(e) => {
setText(e.target.value); setText(e.target.value);
onComponentOptionChanged(component, 'value', e.target.value); onComponentOptionChanged(component, 'value', e.target.value);
@ -48,7 +54,7 @@ export const TextArea = function TextArea({
type="text" type="text"
className="form-control" className="form-control"
placeholder={placeholder} placeholder={placeholder}
style={{ width, height, display:parsedWidgetVisibility ? '' : 'none' }} style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
value={text} value={text}
></textarea> ></textarea>
); );

View file

@ -32,6 +32,7 @@ export const TextInput = function TextInput({
useEffect(() => { useEffect(() => {
setText(newText); setText(newText);
onComponentOptionChanged(component, 'value', newText); onComponentOptionChanged(component, 'value', newText);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newText]); }, [newText]);
const validationData = validateWidget({ const validationData = validateWidget({

View file

@ -3,10 +3,12 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
class Switch extends React.Component { class Switch extends React.Component {
render() { render() {
const { on, onClick, onChange, disabledState } = this.props; const { on, onClick, onChange, disabledState, color } = this.props;
return ( return (
<label className="form-check form-switch form-check-inline"> <label className="form-switch form-check-inline">
<input <input
style={{ backgroundColor: on ? `${color}` : 'white' }}
disabled={disabledState} disabled={disabledState}
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
@ -32,6 +34,8 @@ export const ToggleSwitch = ({
const [on, setOn] = React.useState(false); const [on, setOn] = React.useState(false);
const label = component.definition.properties.label.value; const label = component.definition.properties.label.value;
const textColorProperty = component.definition.styles.textColor; 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 textColor = textColorProperty ? textColorProperty.value : '#000';
const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false; const disabledState = component.definition.styles?.disabledState?.value ?? false;
@ -57,18 +61,24 @@ export const ToggleSwitch = ({
return ( return (
<div <div
className="row py-1" className="row"
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }} style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onComponentClick(id, component); onComponentClick(id, component);
}} }}
> >
<span className="form-check-label form-check-label col-auto" style={{ color: textColor }}> <span className="form-check-label form-check-label col-auto my-auto" style={{ color: textColor }}>
{label} {label}
</span> </span>
<div className="col"> <div className="col px-1 py-0 my-auto">
<Switch disabledState={parsedDisabledState} on={on} onClick={toggle} onChange={toggleValue} /> <Switch
disabledState={parsedDisabledState}
on={on}
onClick={toggle}
onChange={toggleValue}
color={toggleSwitchColor}
/>
</div> </div>
</div> </div>
); );

View file

@ -18,10 +18,12 @@ export const componentTypes = [
showDownloadButton: { type: 'toggle', displayName: 'Show download button' }, showDownloadButton: { type: 'toggle', displayName: 'Show download button' },
showFilterButton: { type: 'toggle', displayName: 'Show filter button' }, showFilterButton: { type: 'toggle', displayName: 'Show filter button' },
showBulkUpdateActions: { type: 'toggle', displayName: 'Show bulk update actions' }, showBulkUpdateActions: { type: 'toggle', displayName: 'Show bulk update actions' },
showBulkSelector: { type: 'toggle', displayName: 'Bulk selection' },
highlightSelectedRow: { type: 'toggle', displayName: 'Highlight selected row' },
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop ' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
defaultSize: { defaultSize: {
width: 810, width: 810,
@ -35,15 +37,19 @@ export const componentTypes = [
}, },
styles: { styles: {
textColor: { type: 'color', displayName: 'Text Color' }, textColor: { type: 'color', displayName: 'Text Color' },
tableType: { type: 'select', displayName: 'Table type', options: [ tableType: {
{ name: 'Bordered', value: '' }, type: 'select',
{ name: 'Borderless', value: 'table-borderless' }, displayName: 'Table type',
{ name: 'Classic', value: 'table-classic' }, options: [
{ name: 'Striped', value: 'table-striped' }, { name: 'Bordered', value: '' },
{ name: 'Striped & bordered', value: 'table-striped table-bordered' } { name: 'Borderless', value: 'table-borderless' },
] }, { name: 'Classic', value: 'table-classic' },
visibility: {type: 'code', displayName: 'Visibility'}, { name: 'Striped', value: 'table-striped' },
disabledState: {type: 'code', displayName: 'Disable'} { name: 'Striped & bordered', value: 'table-striped table-bordered' },
],
},
visibility: { type: 'code', displayName: 'Visibility' },
disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
selectedRow: {}, selectedRow: {},
@ -51,6 +57,7 @@ export const componentTypes = [
dataUpdates: [], dataUpdates: [],
pageIndex: 0, pageIndex: 0,
searchText: '', searchText: '',
selectedRows: [],
}, },
definition: { definition: {
others: { others: {
@ -78,12 +85,14 @@ export const componentTypes = [
], ],
}, },
showBulkUpdateActions: { value: true }, showBulkUpdateActions: { value: true },
showBulkSelector: { value: false },
highlightSelectedRow: { value: false },
}, },
events: [], events: [],
styles: { styles: {
textColor: { value: '' }, textColor: { value: undefined },
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -97,8 +106,8 @@ export const componentTypes = [
height: 30, height: 30,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
text: { type: 'code', displayName: 'Button Text' }, text: { type: 'code', displayName: 'Button Text' },
@ -110,8 +119,8 @@ export const componentTypes = [
styles: { styles: {
backgroundColor: { type: 'color', displayName: 'Background color' }, backgroundColor: { type: 'color', displayName: 'Background color' },
textColor: { type: 'color', displayName: 'Text color' }, textColor: { type: 'color', displayName: 'Text color' },
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: {}, exposedVariables: {},
definition: { definition: {
@ -128,8 +137,8 @@ export const componentTypes = [
styles: { styles: {
backgroundColor: { value: '#3c92dc' }, backgroundColor: { value: '#3c92dc' },
textColor: { value: '#fff' }, textColor: { value: '#fff' },
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -143,8 +152,8 @@ export const componentTypes = [
height: 400, height: 400,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
title: { type: 'string', displayName: 'Title' }, title: { type: 'string', displayName: 'Title' },
@ -164,8 +173,8 @@ export const componentTypes = [
}, },
events: {}, events: {},
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
show: null, show: null,
@ -183,16 +192,16 @@ export const componentTypes = [
type: { value: `line` }, type: { value: `line` },
data: { data: {
value: `[ value: `[
{ "x": 100, "y": "Jan"}, { "x": "Jan", "y": 100},
{ "x": 80, "y": "Feb"}, { "x": "Feb", "y": 80},
{ "x": 40, "y": "Mar"} { "x": "Mar", "y": 40}
]`, ]`,
}, },
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -206,8 +215,8 @@ export const componentTypes = [
height: 400, height: 400,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
title: { type: 'string', displayName: 'Title' }, title: { type: 'string', displayName: 'Title' },
@ -223,7 +232,7 @@ export const componentTypes = [
}, },
events: {}, events: {},
styles: { styles: {
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
show: null, show: null,
@ -239,7 +248,7 @@ export const componentTypes = [
}, },
events: [], events: [],
styles: { styles: {
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -249,37 +258,37 @@ export const componentTypes = [
description: 'Text field for forms', description: 'Text field for forms',
component: 'TextInput', component: 'TextInput',
defaultSize: { defaultSize: {
width: 200, width: 210,
height: 30, height: 30,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
value: { type: 'code', displayName: 'Default value' }, value: { type: 'code', displayName: 'Default value' },
placeholder: { type: 'code', displayName: 'Placeholder' } placeholder: { type: 'code', displayName: 'Placeholder' },
}, },
validation: { validation: {
regex: { type: 'code', displayName: 'Regex' }, regex: { type: 'code', displayName: 'Regex' },
minLength: { type: 'code', displayName: 'Min length' }, minLength: { type: 'code', displayName: 'Min length' },
maxLength: { type: 'code', displayName: 'Max length' }, maxLength: { type: 'code', displayName: 'Max length' },
customRule: { type: 'code', displayName: 'Custom validation' } customRule: { type: 'code', displayName: 'Custom validation' },
}, },
events: {}, events: {},
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
value: '', value: '',
}, },
definition: { definition: {
validation: { validation: {
regex: { value: '' }, regex: { value: '' },
minLength: { value: null }, minLength: { value: null },
maxLength: { value: null }, maxLength: { value: null },
customRule: { value: null } customRule: { value: null },
}, },
others: { others: {
showOnDesktop: { value: true }, showOnDesktop: { value: true },
@ -287,12 +296,12 @@ export const componentTypes = [
}, },
properties: { properties: {
value: { value: '' }, value: { value: '' },
placeholder: { value: 'Placeholder text' } placeholder: { value: 'Placeholder text' },
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -302,21 +311,21 @@ export const componentTypes = [
description: 'Number field for forms', description: 'Number field for forms',
component: 'NumberInput', component: 'NumberInput',
defaultSize: { defaultSize: {
width: 200, width: 210,
height: 30, height: 30,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
value: { type: 'code', displayName: 'Default value' }, value: { type: 'code', displayName: 'Default value' },
placeholder: { type: 'code', displayName: 'Placeholder' } placeholder: { type: 'code', displayName: 'Placeholder' },
}, },
events: {}, events: {},
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
value: 0, value: 0,
@ -328,12 +337,12 @@ export const componentTypes = [
}, },
properties: { properties: {
value: { value: '' }, value: { value: '' },
placeholder: { value: '0' } placeholder: { value: '0' },
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -347,11 +356,11 @@ export const componentTypes = [
height: 30, height: 30,
}, },
validation: { validation: {
customRule: { type: 'code', displayName: 'Custom validation' } customRule: { type: 'code', displayName: 'Custom validation' },
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
defaultValue: { type: 'code', displayName: 'Default value' }, defaultValue: { type: 'code', displayName: 'Default value' },
@ -361,8 +370,8 @@ export const componentTypes = [
}, },
events: {}, events: {},
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
value: '', value: '',
@ -373,7 +382,7 @@ export const componentTypes = [
showOnMobile: { value: false }, showOnMobile: { value: false },
}, },
validation: { validation: {
customRule: { value: null } customRule: { value: null },
}, },
properties: { properties: {
defaultValue: { value: '' }, defaultValue: { value: '' },
@ -383,8 +392,8 @@ export const componentTypes = [
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -394,12 +403,12 @@ export const componentTypes = [
description: 'A single checkbox', description: 'A single checkbox',
component: 'Checkbox', component: 'Checkbox',
defaultSize: { defaultSize: {
width: 200, width: 150,
height: 30, height: 30,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
label: { type: 'code', displayName: 'Label' }, label: { type: 'code', displayName: 'Label' },
@ -410,8 +419,8 @@ export const componentTypes = [
}, },
styles: { styles: {
textColor: { type: 'color', displayName: 'Text Color' }, textColor: { type: 'color', displayName: 'Text Color' },
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: {}, exposedVariables: {},
definition: { definition: {
@ -425,8 +434,8 @@ export const componentTypes = [
events: [], events: [],
styles: { styles: {
textColor: { value: '#000' }, textColor: { value: '#000' },
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -436,12 +445,12 @@ export const componentTypes = [
description: 'Radio buttons', description: 'Radio buttons',
component: 'RadioButton', component: 'RadioButton',
defaultSize: { defaultSize: {
width: 200, width: 210,
height: 30, height: 30,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
label: { type: 'code', displayName: 'Label' }, label: { type: 'code', displayName: 'Label' },
@ -454,8 +463,8 @@ export const componentTypes = [
}, },
styles: { styles: {
textColor: { type: 'color', displayName: 'Text Color' }, textColor: { type: 'color', displayName: 'Text Color' },
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: {}, exposedVariables: {},
definition: { definition: {
@ -473,8 +482,8 @@ export const componentTypes = [
events: [], events: [],
styles: { styles: {
textColor: { value: '#000' }, textColor: { value: '#000' },
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -484,12 +493,12 @@ export const componentTypes = [
description: 'Toggle Switch', description: 'Toggle Switch',
component: 'ToggleSwitch', component: 'ToggleSwitch',
defaultSize: { defaultSize: {
width: 130, width: 150,
height: 30, height: 30,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
label: { type: 'code', displayName: 'Label' }, label: { type: 'code', displayName: 'Label' },
@ -499,8 +508,9 @@ export const componentTypes = [
}, },
styles: { styles: {
textColor: { type: 'color', displayName: 'Text Color' }, textColor: { type: 'color', displayName: 'Text Color' },
visibility: {type: 'code', displayName: 'Visibility'}, toggleSwitchColor: { type: 'color', displayName: 'Toggle Switch Color' },
disabledState: {type: 'code', displayName: 'Disable'} visibility: { type: 'code', displayName: 'Visibility' },
disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: {}, exposedVariables: {},
definition: { definition: {
@ -514,8 +524,9 @@ export const componentTypes = [
events: [], events: [],
styles: { styles: {
textColor: { value: '#000' }, textColor: { value: '#000' },
visibility: {value: '{{true}}'}, toggleSwitchColor: { value: '#3c92dc' },
disabledState: {value: '{{false}}'} visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -525,12 +536,12 @@ export const componentTypes = [
description: 'Text area form field', description: 'Text area form field',
component: 'TextArea', component: 'TextArea',
defaultSize: { defaultSize: {
width: 250, width: 240,
height: 100, height: 100,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
value: { type: 'code', displayName: 'Default value' }, value: { type: 'code', displayName: 'Default value' },
@ -538,8 +549,8 @@ export const componentTypes = [
}, },
events: {}, events: {},
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
value: {}, value: {},
@ -555,8 +566,8 @@ export const componentTypes = [
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -567,19 +578,19 @@ export const componentTypes = [
component: 'DaterangePicker', component: 'DaterangePicker',
defaultSize: { defaultSize: {
width: 300, width: 300,
height: 32, height: 30,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
format: { type: 'code', displayName: 'Format' }, format: { type: 'code', displayName: 'Format' },
}, },
events: {}, events: {},
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
endDate: {}, endDate: {},
@ -595,8 +606,8 @@ export const componentTypes = [
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -606,22 +617,22 @@ export const componentTypes = [
description: 'Display markdown or HTML', description: 'Display markdown or HTML',
component: 'Text', component: 'Text',
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
text: { type: 'code', displayName: 'Text' }, text: { type: 'code', displayName: 'Text' },
loadingState: { type: 'code', displayName: 'Show loading state' }, loadingState: { type: 'code', displayName: 'Show loading state' },
}, },
defaultSize: { defaultSize: {
width: 200, width: 120,
height: 30, height: 30,
}, },
events: [], events: [],
styles: { styles: {
textColor: { type: 'color', displayName: 'Text Color' }, textColor: { type: 'color', displayName: 'Text Color' },
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: {}, exposedVariables: {},
definition: { definition: {
@ -637,8 +648,8 @@ export const componentTypes = [
events: [], events: [],
styles: { styles: {
textColor: { value: '#000' }, textColor: { value: '#000' },
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -647,13 +658,13 @@ export const componentTypes = [
displayName: 'Image', displayName: 'Image',
description: 'Display an Image', description: 'Display an Image',
defaultSize: { defaultSize: {
width: 200, width: 210,
height: 200, height: 210,
}, },
component: 'Image', component: 'Image',
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
source: { type: 'code', displayName: 'URL' }, source: { type: 'code', displayName: 'URL' },
@ -662,8 +673,8 @@ export const componentTypes = [
onClick: { displayName: 'On click' }, onClick: { displayName: 'On click' },
}, },
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: {}, exposedVariables: {},
definition: { definition: {
@ -677,8 +688,8 @@ export const componentTypes = [
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -687,21 +698,20 @@ export const componentTypes = [
displayName: 'Container', displayName: 'Container',
description: 'Wrapper for multiple components', description: 'Wrapper for multiple components',
defaultSize: { defaultSize: {
width: 200, width: 210,
height: 200, height: 200,
}, },
component: 'Container', component: 'Container',
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
properties: {
}, },
properties: {},
events: {}, events: {},
styles: { styles: {
backgroundColor: { type: 'color' }, backgroundColor: { type: 'color' },
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: {}, exposedVariables: {},
definition: { definition: {
@ -715,8 +725,8 @@ export const componentTypes = [
events: [], events: [],
styles: { styles: {
backgroundColor: { value: '#fff' }, backgroundColor: { value: '#fff' },
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value:'{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -725,16 +735,16 @@ export const componentTypes = [
displayName: 'Dropdown', displayName: 'Dropdown',
description: 'Select one value from options', description: 'Select one value from options',
defaultSize: { defaultSize: {
width: 210, width: 240,
height: 30, height: 30,
}, },
component: 'DropDown', component: 'DropDown',
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
validation: { validation: {
customRule: { type: 'code', displayName: 'Custom validation' } customRule: { type: 'code', displayName: 'Custom validation' },
}, },
properties: { properties: {
label: { type: 'code', displayName: 'Label' }, label: { type: 'code', displayName: 'Label' },
@ -746,8 +756,8 @@ export const componentTypes = [
onSelect: { displayName: 'On select' }, onSelect: { displayName: 'On select' },
}, },
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
value: null, value: null,
@ -758,7 +768,7 @@ export const componentTypes = [
showOnMobile: { value: false }, showOnMobile: { value: false },
}, },
validation: { validation: {
customRule: { value: null } customRule: { value: null },
}, },
properties: { properties: {
label: { value: 'Select' }, label: { value: 'Select' },
@ -769,8 +779,8 @@ export const componentTypes = [
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -779,13 +789,13 @@ export const componentTypes = [
displayName: 'Multiselect', displayName: 'Multiselect',
description: 'Select multiple values from options', description: 'Select multiple values from options',
defaultSize: { defaultSize: {
width: 210, width: 240,
height: 30, height: 30,
}, },
component: 'Multiselect', component: 'Multiselect',
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
label: { type: 'code', displayName: 'Label' }, label: { type: 'code', displayName: 'Label' },
@ -797,8 +807,8 @@ export const componentTypes = [
onSelect: { displayName: 'On select' }, onSelect: { displayName: 'On select' },
}, },
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
values: {}, values: {},
@ -817,8 +827,8 @@ export const componentTypes = [
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -832,16 +842,16 @@ export const componentTypes = [
height: 210, height: 210,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
placeholder: { type: 'code', displayName: 'Placeholder' }, placeholder: { type: 'code', displayName: 'Placeholder' },
}, },
events: {}, events: {},
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
value: {}, value: {},
@ -856,8 +866,8 @@ export const componentTypes = [
}, },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -867,12 +877,12 @@ export const componentTypes = [
description: 'Display Google Maps', description: 'Display Google Maps',
component: 'Map', component: 'Map',
defaultSize: { defaultSize: {
width: 400, width: 420,
height: 400, height: 420,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
initialLocation: { initialLocation: {
@ -903,8 +913,8 @@ export const componentTypes = [
onMarkerClick: { displayName: 'On marker click' }, onMarkerClick: { displayName: 'On marker click' },
}, },
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
center: {}, center: {},
@ -925,8 +935,8 @@ export const componentTypes = [
addNewMarkers: { value: '{{false}}' }, addNewMarkers: { value: '{{false}}' },
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -940,16 +950,16 @@ export const componentTypes = [
height: 300, height: 300,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: {}, properties: {},
events: { events: {
onDetect: { displayName: 'On detect' }, onDetect: { displayName: 'On detect' },
}, },
styles: { styles: {
visibility: {type: 'code', displayName: 'Visibility'}, visibility: { type: 'code', displayName: 'Visibility' },
disabledState: {type: 'code', displayName: 'Disable'} disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
lastDetectedValue: '', lastDetectedValue: '',
@ -962,8 +972,8 @@ export const componentTypes = [
properties: {}, properties: {},
events: [], events: [],
styles: { styles: {
visibility: {value: '{{true}}'}, visibility: { value: '{{true}}' },
disabledState: {value: '{{false}}'} disabledState: { value: '{{false}}' },
}, },
}, },
}, },
@ -973,12 +983,12 @@ export const componentTypes = [
description: 'Star rating', description: 'Star rating',
component: 'StarRating', component: 'StarRating',
defaultSize: { defaultSize: {
width: 220, width: 240,
height: 30, height: 30,
}, },
others: { others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? ' }, showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
}, },
properties: { properties: {
label: { type: 'code', displayName: 'Label' }, label: { type: 'code', displayName: 'Label' },
@ -992,8 +1002,9 @@ export const componentTypes = [
}, },
styles: { styles: {
textColor: { type: 'color', displayName: 'Star Color' }, textColor: { type: 'color', displayName: 'Star Color' },
visibility: {type: 'code', displayName: 'Visibility'}, labelColor: { type: 'color', displayName: 'Label Color' },
disabledState: {type: 'code', displayName: 'Disable'} visibility: { type: 'code', displayName: 'Visibility' },
disabledState: { type: 'code', displayName: 'Disable' },
}, },
exposedVariables: { exposedVariables: {
value: 0, value: 0,
@ -1014,8 +1025,9 @@ export const componentTypes = [
events: [], events: [],
styles: { styles: {
textColor: { value: '#ffb400' }, textColor: { value: '#ffb400' },
visibility: {value: '{{true}}'}, labelColor: { value: '#333' },
disabledState: {value: '{{false}}'} visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
}, },
}, },
}, },

View file

@ -1,39 +1,35 @@
import React from 'react'; import React from 'react';
export const ConfigHandle = function ConfigHandle({ export const ConfigHandle = function ConfigHandle({ id, component, configHandleClicked, dragRef, removeComponent }) {
id, return (
component, <div className="config-handle" ref={dragRef}>
configHandleClicked, <span
dragRef, style={{ cursor: 'move' }}
removeComponent onClick={(e) => {
}) { e.preventDefault();
e.stopPropagation();
return <div className="config-handle" ref={dragRef}> configHandleClicked(id, component);
<span }}
style={{cursor: 'move'}} className="badge badge bg-azure-lt"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); configHandleClicked(id, component) }} role="button"
className="badge badge bg-azure-lt" >
role="button" <img
> style={{ cursor: 'pointer', marginRight: '5px' }}
<img src="/assets/images/icons/menu.svg"
style={{cursor: 'pointer'}} width="8"
src="/assets/images/icons/menu.svg" height="8"
width="8"
height="8"
style={{marginRight: '5px'}}
/>
{component.name}
</span>
<img
style={{cursor: 'pointer'}}
src="/assets/images/icons/trash.svg"
width="12"
role="button"
className="mx-2"
height="12"
onClick={() => removeComponent({id})}
style={{marginRight: '5px'}}
/> />
{component.name}
</span>
<img
style={{ cursor: 'pointer', marginRight: '5px' }}
src="/assets/images/icons/trash.svg"
width="12"
role="button"
className="mx-2"
height="12"
onClick={() => removeComponent({ id })}
/>
</div> </div>
} );
};

View file

@ -7,10 +7,10 @@ import update from 'immutability-helper';
import { componentTypes } from './Components/components'; import { componentTypes } from './Components/components';
import { computeComponentName } from '@/_helpers/utils'; import { computeComponentName } from '@/_helpers/utils';
function uuidv4() { function uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)); return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
} }
export const Container = ({ export const Container = ({
@ -31,13 +31,12 @@ export const Container = ({
deviceWindowWidth, deviceWindowWidth,
scaleValue, scaleValue,
selectedComponent, selectedComponent,
darkMode darkMode,
}) => { }) => {
const styles = { const styles = {
width: currentLayout === 'mobile' ? deviceWindowWidth : 1292, width: currentLayout === 'mobile' ? deviceWindowWidth : 1292,
height: 2400, height: 2400,
position: 'absolute' position: 'absolute',
}; };
const components = appDefinition.components; const components = appDefinition.components;
@ -55,8 +54,8 @@ export const Container = ({
setBoxes( setBoxes(
update(boxes, { update(boxes, {
[id]: { [id]: {
$merge: { layouts } $merge: { layouts },
} },
}) })
); );
console.log('new boxes - 1', boxes); console.log('new boxes - 1', boxes);
@ -67,17 +66,18 @@ export const Container = ({
useEffect(() => { useEffect(() => {
console.log('new boxes - 2', boxes); console.log('new boxes - 2', boxes);
appDefinitionChanged({ ...appDefinition, components: boxes }); appDefinitionChanged({ ...appDefinition, components: boxes });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [boxes]); }, [boxes]);
const { draggingState } = useDragLayer((monitor) => { const { draggingState } = useDragLayer((monitor) => {
if(monitor.isDragging()) { if (monitor.isDragging()) {
if(!monitor.getItem().parent) { if (!monitor.getItem().parent) {
return { draggingState: true } return { draggingState: true };
} else { } else {
return { draggingState: false } return { draggingState: false };
} }
} else { } else {
return { draggingState: false } return { draggingState: false };
} }
}); });
@ -89,8 +89,7 @@ export const Container = ({
() => ({ () => ({
accept: ItemTypes.BOX, accept: ItemTypes.BOX,
drop(item, monitor) { drop(item, monitor) {
if (item.parent) {
if(item.parent) {
return; return;
} }
@ -112,7 +111,7 @@ export const Container = ({
let deltaX = 0; let deltaX = 0;
let deltaY = 0; let deltaY = 0;
if(delta) { if (delta) {
deltaX = delta.x; deltaX = delta.x;
deltaY = delta.y; deltaY = delta.y;
} }
@ -135,13 +134,12 @@ export const Container = ({
...boxes[id]['layouts'][item.currentLayout], ...boxes[id]['layouts'][item.currentLayout],
top: top, top: top,
left: left, left: left,
} },
} },
} },
}; };
setBoxes(newBoxes); setBoxes(newBoxes);
} else { } else {
// This is a new component // This is a new component
componentMeta = componentTypes.find((component) => component.component === item.component.component); componentMeta = componentTypes.find((component) => component.component === item.component.component);
@ -153,8 +151,8 @@ export const Container = ({
const offsetFromLeftOfWindow = canvasBoundingRect.left; const offsetFromLeftOfWindow = canvasBoundingRect.left;
const currentOffset = monitor.getSourceClientOffset(); const currentOffset = monitor.getSourceClientOffset();
left = Math.round(currentOffset.x + (currentOffset.x * (1 - zoomLevel)) - offsetFromLeftOfWindow); left = Math.round(currentOffset.x + currentOffset.x * (1 - zoomLevel) - offsetFromLeftOfWindow);
top = Math.round(currentOffset.y + (currentOffset.y * (1 - zoomLevel)) - offsetFromTopOfWindow); top = Math.round(currentOffset.y + currentOffset.y * (1 - zoomLevel) - offsetFromTopOfWindow);
id = uuidv4(); id = uuidv4();
@ -162,7 +160,7 @@ export const Container = ({
[left, top] = doSnapToGrid(left, top); [left, top] = doSnapToGrid(left, top);
} }
if(item.currentLayout === 'mobile') { if (item.currentLayout === 'mobile') {
componentData.definition.others.showOnDesktop.value = false; componentData.definition.others.showOnDesktop.value = false;
componentData.definition.others.showOnMobile.value = true; componentData.definition.others.showOnMobile.value = true;
} }
@ -177,14 +175,14 @@ export const Container = ({
left: left, left: left,
width: componentMeta.defaultSize.width, width: componentMeta.defaultSize.width,
height: componentMeta.defaultSize.height, height: componentMeta.defaultSize.height,
} },
} },
} },
}); });
} }
return undefined; return undefined;
} },
}), }),
[moveBox] [moveBox]
); );
@ -199,10 +197,10 @@ export const Container = ({
top: 100, top: 100,
left: 0, left: 0,
width: 445, width: 445,
height: 500 height: 500,
}; };
let { left, top, width, height } = boxes[id]['layouts'][currentLayout] || defaultData; let { left, top, width, height } = boxes[id]['layouts'][currentLayout] || defaultData;
top = y; top = y;
left = x; left = x;
@ -210,6 +208,8 @@ export const Container = ({
width = width + deltaWidth; width = width + deltaWidth;
height = height + deltaHeight; height = height + deltaHeight;
// [width, height] = doSnapToGrid(width, height)
let newBoxes = { let newBoxes = {
...boxes, ...boxes,
[id]: { [id]: {
@ -218,10 +218,13 @@ export const Container = ({
...boxes[id]['layouts'], ...boxes[id]['layouts'],
[currentLayout]: { [currentLayout]: {
...boxes[id]['layouts'][currentLayout], ...boxes[id]['layouts'][currentLayout],
width, height, top, left width,
} height,
} top,
} left,
},
},
},
}; };
setBoxes(newBoxes); setBoxes(newBoxes);
@ -239,74 +242,77 @@ export const Container = ({
...boxes[id].component.definition, ...boxes[id].component.definition,
properties: { properties: {
...boxes[id].component.definition.properties, ...boxes[id].component.definition.properties,
[param]: value [param]: value,
} },
} },
} },
} },
} },
}) })
); );
} }
} }
return ( return (
<div ref={drop} style={styles} className={`real-canvas ${isDragging || isResizing ? ' show-grid' : ''}`}> <div ref={drop} style={styles} className={`real-canvas ${isDragging || isResizing ? 'show-grid' : ''}`}>
{Object.keys(boxes).map((key) => { {Object.keys(boxes).map((key) => {
const box = boxes[key]; const box = boxes[key];
const canShowInCurrentLayout = box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value; const canShowInCurrentLayout =
box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value;
if(!box.parent && canShowInCurrentLayout) { if (!box.parent && canShowInCurrentLayout) {
return <DraggableBox return (
onComponentClick={onComponentClick} <DraggableBox
onEvent={onEvent} onComponentClick={onComponentClick}
onComponentOptionChanged={onComponentOptionChanged} onEvent={onEvent}
onComponentOptionsChanged={onComponentOptionsChanged} onComponentOptionChanged={onComponentOptionChanged}
key={key} onComponentOptionsChanged={onComponentOptionsChanged}
currentState={currentState} key={key}
onResizeStop={onResizeStop} currentState={currentState}
paramUpdated={paramUpdated} onResizeStop={onResizeStop}
id={key} paramUpdated={paramUpdated}
{...boxes[key]} id={key}
mode={mode} {...boxes[key]}
resizingStatusChanged={(status) => setIsResizing(status)} mode={mode}
inCanvas={true} resizingStatusChanged={(status) => setIsResizing(status)}
zoomLevel={zoomLevel} inCanvas={true}
configHandleClicked={configHandleClicked} zoomLevel={zoomLevel}
removeComponent={removeComponent} configHandleClicked={configHandleClicked}
currentLayout={currentLayout} removeComponent={removeComponent}
scaleValue={scaleValue} currentLayout={currentLayout}
deviceWindowWidth={deviceWindowWidth} scaleValue={scaleValue}
isSelectedComponent={selectedComponent? selectedComponent.id === key : false} deviceWindowWidth={deviceWindowWidth}
darkMode={darkMode} isSelectedComponent={selectedComponent ? selectedComponent.id === key : false}
containerProps={{ darkMode={darkMode}
mode, containerProps={{
snapToGrid, mode,
onComponentClick, snapToGrid,
onEvent, onComponentClick,
appDefinition, onEvent,
appDefinitionChanged, appDefinition,
currentState, appDefinitionChanged,
onComponentOptionChanged, currentState,
onComponentOptionsChanged, onComponentOptionChanged,
appLoading, onComponentOptionsChanged,
zoomLevel, appLoading,
configHandleClicked, zoomLevel,
removeComponent, configHandleClicked,
currentLayout, removeComponent,
scaleValue, currentLayout,
deviceWindowWidth, scaleValue,
selectedComponent, deviceWindowWidth,
darkMode selectedComponent,
}} darkMode,
/> }}
/>
);
} }
} })}
)}
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && ( {Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
<div className="mx-auto w-50 p-5 bg-light no-components-box" style={{ marginTop: '10%'}}> <div className="mx-auto w-50 p-5 bg-light no-components-box" style={{ marginTop: '10%' }}>
<center className="text-muted">You haven&apos;t added any components yet. Drag components from the right sidebar and drop here.</center> <center className="text-muted">
You haven&apos;t added any components yet. Drag components from the right sidebar and drop here.
</center>
</div> </div>
)} )}
{appLoading && ( {appLoading && (

View file

@ -10,13 +10,13 @@ const layerStyles = {
left: 0, left: 0,
top: 0, top: 0,
width: '100%', width: '100%',
height: '100%' height: '100%',
}; };
function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout) { function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout) {
if (!initialOffset || !currentOffset) { if (!initialOffset || !currentOffset) {
return { return {
display: 'none' display: 'none',
}; };
} }
let { x, y } = currentOffset; let { x, y } = currentOffset;
@ -28,18 +28,19 @@ function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout)
const realCanvasDelta = realCanvasBoundingRect.x - canvasContainerBoundingRect.x; const realCanvasDelta = realCanvasBoundingRect.x - canvasContainerBoundingRect.x;
if (id) { // Dragging within the canvas if (id) {
// Dragging within the canvas
x = Math.round(item.layouts[currentLayout].left + delta.x); x = Math.round(item.layouts[currentLayout].left + delta.x);
y = Math.round(item.layouts[currentLayout].top + delta.y); y = Math.round(item.layouts[currentLayout].top + delta.y);
} else {
} else { // New component being dragged from components sidebar // New component being dragged from components sidebar
const offsetFromTopOfWindow = realCanvasBoundingRect.top; const offsetFromTopOfWindow = realCanvasBoundingRect.top;
const offsetFromLeftOfWindow = realCanvasBoundingRect.left; const offsetFromLeftOfWindow = realCanvasBoundingRect.left;
const zoomLevel = item.zoomLevel; const zoomLevel = item.zoomLevel;
x = Math.round(currentOffset.x + (currentOffset.x * (1 - zoomLevel)) - offsetFromLeftOfWindow); x = Math.round(currentOffset.x + currentOffset.x * (1 - zoomLevel) - offsetFromLeftOfWindow);
y = Math.round(currentOffset.y + (currentOffset.y * (1 - zoomLevel)) - offsetFromTopOfWindow); y = Math.round(currentOffset.y + currentOffset.y * (1 - zoomLevel) - offsetFromTopOfWindow);
} }
[x, y] = snapToGrid(x, y); [x, y] = snapToGrid(x, y);
@ -49,24 +50,22 @@ function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout)
const transform = `translate(${x}px, ${y}px)`; const transform = `translate(${x}px, ${y}px)`;
return { return {
transform, transform,
WebkitTransform: transform WebkitTransform: transform,
}; };
} }
export const CustomDragLayer = ({ currentLayout }) => { export const CustomDragLayer = ({ currentLayout }) => {
const { const { itemType, isDragging, item, initialOffset, currentOffset, delta } = useDragLayer((monitor) => ({
itemType, isDragging, item, initialOffset, currentOffset, delta
} = useDragLayer((monitor) => ({
item: monitor.getItem(), item: monitor.getItem(),
itemType: monitor.getItemType(), itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(), initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(), currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(), isDragging: monitor.isDragging(),
delta: monitor.getDifferenceFromInitialOffset() delta: monitor.getDifferenceFromInitialOffset(),
})); }));
function renderItem() { function renderItem() {
switch (itemType) { switch (itemType) {
case ItemTypes.BOX: case ItemTypes.BOX:
return <BoxDragPreview item={item} currentLayout={currentLayout}/>; return <BoxDragPreview item={item} currentLayout={currentLayout} />;
default: default:
return null; return null;
} }
@ -78,9 +77,7 @@ export const CustomDragLayer = ({ currentLayout }) => {
return ( return (
<div style={layerStyles}> <div style={layerStyles}>
<div style={getItemStyles(delta, item, initialOffset, currentOffset, currentLayout)}> <div style={getItemStyles(delta, item, initialOffset, currentOffset, currentLayout)}>{renderItem()}</div>
{renderItem()}
</div>
</div> </div>
); );
}; };

View file

@ -3,10 +3,9 @@ import { datasourceService, authenticationService } from '@/_services';
import Modal from 'react-bootstrap/Modal'; import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button'; import Button from 'react-bootstrap/Button';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { dataBaseSources, apiSources, DataSourceTypes } from './DataSourceTypes';
import { defaultOptions } from './DefaultOptions'; import { defaultOptions } from './DefaultOptions';
import { TestConnection } from './TestConnection'; import { TestConnection } from './TestConnection';
import { SourceComponents } from './SourceComponents'; import { DataBaseSources, ApiSources, DataSourceTypes, SourceComponents } from './SourceComponents';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import config from 'config'; import config from 'config';
@ -204,7 +203,7 @@ class DataSourceManager extends React.Component {
<div> <div>
<div className="row row-deck"> <div className="row row-deck">
<h4 className="text-muted mb-2">DATABASES</h4> <h4 className="text-muted mb-2">DATABASES</h4>
{dataBaseSources.map((dataSource) => ( {DataBaseSources.map((dataSource) => (
<div className="col-md-2" key={dataSource.name}> <div className="col-md-2" key={dataSource.name}>
<div className="card mb-3" role="button" onClick={() => this.selectDataSource(dataSource)}> <div className="card mb-3" role="button" onClick={() => this.selectDataSource(dataSource)}>
<div className="card-body"> <div className="card-body">
@ -227,7 +226,7 @@ class DataSourceManager extends React.Component {
</div> </div>
<div className="row row-deck mt-2"> <div className="row row-deck mt-2">
<h4 className="text-muted mb-2">APIS</h4> <h4 className="text-muted mb-2">APIS</h4>
{apiSources.map((dataSource) => ( {ApiSources.map((dataSource) => (
<div className="col-md-2" key={dataSource.name}> <div className="col-md-2" key={dataSource.name}>
<div className="card" role="button" onClick={() => this.selectDataSource(dataSource)}> <div className="card" role="button" onClick={() => this.selectDataSource(dataSource)}>
<div className="card-body"> <div className="card-body">
@ -292,7 +291,11 @@ class DataSourceManager extends React.Component {
<div className="col"> <div className="col">
<small> <small>
<a href={`https://docs.tooljet.io/docs/data-sources/${selectedDataSource.kind}`} target="_blank"> <a
href={`https://docs.tooljet.io/docs/data-sources/${selectedDataSource.kind}`}
target="_blank"
rel="noreferrer"
>
Read documentation Read documentation
</a> </a>
</small> </small>

View file

@ -1,226 +0,0 @@
export const dataBaseSources = [
{
name: 'PostgreSQL',
kind: 'postgresql',
options: {
host: { type: 'string' },
port: { type: 'string' },
database: { type: 'string' },
username: { type: 'string' },
password: { type: 'string', encrypted: true }
},
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
}
},
{
name: 'MySQL',
kind: 'mysql',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
host: { type: 'string' },
port: { type: 'string' },
database: { type: 'string' },
username: { type: 'string' },
password: { type: 'string', encrypted: true }
}
},
{
name: 'SQL Server',
kind: 'mssql',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
host: { type: 'string' },
port: { type: 'string' },
database: { type: 'string' },
username: { type: 'string' },
password: { type: 'string', encrypted: true }
}
},
{
name: 'MongoDB',
kind: 'mongodb',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
host: { type: 'string' },
port: { type: 'string' },
username: { type: 'string' },
password: { type: 'string', encrypted: true },
connection_type: { type: 'options'},
connection_string: { type: 'string', encrypted: true }
}
},
{
name: 'Firestore',
kind: 'firestore',
exposedVariables: {
isLoading: {},
data: [],
rawData: []
},
options: {
gcp_key: { type: 'string', encrypted: true }
}
},
{
name: 'DynamoDB',
kind: 'dynamodb',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
region: { type: 'string' },
access_key: { type: 'string' },
secret_key: { type: 'string', encrypted: true }
}
},
{
name: 'Elasticsearch',
kind: 'elasticsearch',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
host: { type: 'string' },
port: { type: 'string' },
username: { type: 'string' },
password: { type: 'string', encrypted: true }
}
},
{
name: 'Redis',
kind: 'redis',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
host: { type: 'string' },
port: { type: 'string' },
username: { type: 'string' },
password: { type: 'string', encrypted: true }
}
}
];
export const apiSources = [
{
name: 'Rest API',
kind: 'restapi',
options: {
url: { type: 'string' },
auth_type: { type: 'string' },
grant_type: { type: 'string' },
add_token_to: { type: 'string' },
header_prefix: { type: 'string' },
access_token_url: { type: 'string' },
client_id: { type: 'string' },
client_secret: { type: 'string', encrypted: true },
scopes: { type: 'string' },
auth_url: { type: 'string' },
client_auth: { type: 'string' },
headers: { type: 'array' },
custom_auth_params: { type: 'array' }
},
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
customTesting: true
},
{
name: 'GraphQL',
kind: 'graphql',
options: {
url: { type: 'string' },
headers: { type: 'array' },
url_params: { type: 'array' },
body: { type: 'array' },
},
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
customTesting: true
},
{
name: 'Stripe',
kind: 'stripe',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
api_key: { type: 'string', encrypted: true }
},
customTesting: true
},
{
name: 'Airtable',
kind: 'airtable',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
api_key: { type: 'string', encrypted: true }
},
customTesting: true
},
{
name: 'Google Sheets',
kind: 'googlesheets',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
api_key: { type: 'string', encrypted: true }
},
customTesting: true,
hideSave: true
},
{
name: 'Slack',
kind: 'slack',
exposedVariables: {
isLoading: {},
data: {},
rawData: {}
},
options: {
api_key: { type: 'string', encrypted: true }
},
customTesting: true,
hideSave: true
}
];
export const DataSourceTypes = [
...dataBaseSources,
...apiSources
];

View file

@ -5,7 +5,7 @@ export const defaultOptions = {
database: { value: '' }, database: { value: '' },
username: { value: '' }, username: { value: '' },
password: { value: '' }, password: { value: '' },
ssl_enabled: { value: true } ssl_enabled: { value: true },
}, },
mysql: { mysql: {
host: { value: 'localhost' }, host: { value: 'localhost' },
@ -20,13 +20,13 @@ export const defaultOptions = {
port: { value: 1433 }, port: { value: 1433 },
database: { value: '' }, database: { value: '' },
username: { value: '' }, username: { value: '' },
password: { value: '' } password: { value: '' },
}, },
redis: { redis: {
host: { value: 'localhost' }, host: { value: 'localhost' },
port: { value: 6379 }, port: { value: 6379 },
username: { value: '' }, username: { value: '' },
password: { value: '' } password: { value: '' },
}, },
mongodb: { mongodb: {
database: { value: '' }, database: { value: '' },
@ -35,7 +35,7 @@ export const defaultOptions = {
username: { value: '' }, username: { value: '' },
password: { value: '' }, password: { value: '' },
connection_type: { value: 'manual' }, connection_type: { value: 'manual' },
connection_string: { value: ''} connection_string: { value: '' },
}, },
elasticsearch: { elasticsearch: {
@ -43,16 +43,16 @@ export const defaultOptions = {
host: { value: 'localhost' }, host: { value: 'localhost' },
port: { value: 9200 }, port: { value: 9200 },
username: { value: '' }, username: { value: '' },
password: { value: '' } password: { value: '' },
}, },
stripe: { stripe: {
api_key: { value: '' } api_key: { value: '' },
}, },
airtable: { airtable: {
api_key: { value: '' } api_key: { value: '' },
}, },
firestore: { firestore: {
gcp_key: { value: '' } gcp_key: { value: '' },
}, },
restapi: { restapi: {
url: { value: '' }, url: { value: '' },
@ -67,22 +67,22 @@ export const defaultOptions = {
auth_url: { value: '' }, auth_url: { value: '' },
client_auth: { value: 'header' }, client_auth: { value: 'header' },
headers: { value: [['', '']] }, headers: { value: [['', '']] },
custom_auth_params: { value: [['', '']] } custom_auth_params: { value: [['', '']] },
}, },
graphql: { graphql: {
url: { value: '' }, url: { value: '' },
headers: { value: [['', '']] }, headers: { value: [['', '']] },
url_params: { value: [['', '']] } url_params: { value: [['', '']] },
}, },
googlesheets: { googlesheets: {
access_type: { value: 'read' } access_type: { value: 'read' },
}, },
slack: { slack: {
access_type: { value: 'read' } access_type: { value: 'read' },
}, },
dynamodb: { dynamodb: {
region: { value: ''}, region: { value: '' },
access_key: { value: ''}, access_key: { value: '' },
secret_key: { value: ''} secret_key: { value: '' },
} },
}; };

View file

@ -4,6 +4,19 @@
"title": "Airtable datasource", "title": "Airtable datasource",
"description": "A schema defining airtable datasource", "description": "A schema defining airtable datasource",
"type": "object", "type": "object",
"source": {
"name": "Airtable",
"kind": "airtable",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"api_key": { "type": "string", "encrypted": true }
},
"customTesting": true
},
"properties": { "properties": {
"api_key": { "api_key": {
"$label": "API key", "$label": "API key",

View file

@ -4,6 +4,20 @@
"title": "Google Sheets datasource", "title": "Google Sheets datasource",
"description": "A schema defining google sheets datasource", "description": "A schema defining google sheets datasource",
"type": "object", "type": "object",
"source": {
"name": "Google Sheets",
"kind": "googlesheets",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"api_key": { "type": "string", "encrypted": true }
},
"customTesting": true,
"hideSave": true
},
"properties": { "properties": {
"sheets": { "sheets": {
"$label": "", "$label": "",

View file

@ -4,6 +4,22 @@
"title": "Graphql datasource", "title": "Graphql datasource",
"description": "A schema defining graphql datasource", "description": "A schema defining graphql datasource",
"type": "object", "type": "object",
"source": {
"name": "GraphQL",
"kind": "graphql",
"options": {
"url": { "type": "string" },
"headers": { "type": "array" },
"url_params": { "type": "array" },
"body": { "type": "array" }
},
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"customTesting": true
},
"properties": { "properties": {
"url": { "url": {
"$label": "URL", "$label": "URL",

View file

@ -4,6 +4,31 @@
"title": "Restapi datasource", "title": "Restapi datasource",
"description": "A schema defining restapi datasource", "description": "A schema defining restapi datasource",
"type": "object", "type": "object",
"source": {
"name": "Rest API",
"kind": "restapi",
"options": {
"url": { "type": "string" },
"auth_type": { "type": "string" },
"grant_type": { "type": "string" },
"add_token_to": { "type": "string" },
"header_prefix": { "type": "string" },
"access_token_url": { "type": "string" },
"client_id": { "type": "string" },
"client_secret": { "type": "string", "encrypted": true },
"scopes": { "type": "string" },
"auth_url": { "type": "string" },
"client_auth": { "type": "string" },
"headers": { "type": "array" },
"custom_auth_params": { "type": "array" }
},
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"customTesting": true
},
"properties": { "properties": {
"url": { "url": {
"$label": "URL", "$label": "URL",

View file

@ -4,6 +4,20 @@
"title": "Slack datasource", "title": "Slack datasource",
"description": "A schema defining slack datasource", "description": "A schema defining slack datasource",
"type": "object", "type": "object",
"source": {
"name": "Slack",
"kind": "slack",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"api_key": { "type": "string", "encrypted": true }
},
"customTesting": true,
"hideSave": true
},
"properties": { "properties": {
"sheets": { "sheets": {
"$label": "", "$label": "",

View file

@ -4,6 +4,19 @@
"title": "Stripe datasource", "title": "Stripe datasource",
"description": "A schema defining stripe datasource", "description": "A schema defining stripe datasource",
"type": "object", "type": "object",
"source": {
"name": "Stripe",
"kind": "stripe",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"api_key": { "type": "string", "encrypted": true }
},
"customTesting": true
},
"properties": { "properties": {
"api_key": { "api_key": {
"$label": "API key", "$label": "API key",

View file

@ -4,6 +4,20 @@
"title": "Dynamodb datasource", "title": "Dynamodb datasource",
"description": "A schema defining dynamodb datasource", "description": "A schema defining dynamodb datasource",
"type": "object", "type": "object",
"source": {
"name": "DynamoDB",
"kind": "dynamodb",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"region": { "type": "string" },
"access_key": { "type": "string" },
"secret_key": { "type": "string", "encrypted": true }
}
},
"properties": { "properties": {
"region": { "region": {
"$label": "Region", "$label": "Region",

View file

@ -4,6 +4,21 @@
"title": "Elastic search datasource", "title": "Elastic search datasource",
"description": "A schema defining elastic search datasource", "description": "A schema defining elastic search datasource",
"type": "object", "type": "object",
"source": {
"name": "Elasticsearch",
"kind": "elasticsearch",
"exposedVariables": {
"isLoading": {},
"data": {},
"rawData": {}
},
"options": {
"host": { "type": "string" },
"port": { "type": "string" },
"username": { "type": "string" },
"password": { "type": "string", "encrypted": true }
}
},
"properties": { "properties": {
"host": { "host": {
"$label": "Host", "$label": "Host",
@ -21,13 +36,13 @@
"$label": "Username", "$label": "Username",
"$key": "username", "$key": "username",
"type": "text", "type": "text",
"description": "Enter username field" "description": "Enter username"
}, },
"password": { "password": {
"$label": "Password", "$label": "Password",
"$key": "password", "$key": "password",
"type": "password", "type": "password",
"description": "Enter password field" "description": "Enter password"
} }
}, },
"required": ["scheme", "host", "port", "password"] "required": ["scheme", "host", "port", "password"]

View file

@ -4,6 +4,18 @@
"title": "Firestore datasource", "title": "Firestore datasource",
"description": "A schema defining firestore datasource", "description": "A schema defining firestore datasource",
"type": "object", "type": "object",
"source": {
"name": "Firestore",
"kind": "firestore",
"exposedVariables": {
"isLoading": {},
"data": [],
"rawData": []
},
"options": {
"gcp_key": { "type": "string", "encrypted": true }
}
},
"properties": { "properties": {
"gcp_key": { "gcp_key": {
"$label": "Private key", "$label": "Private key",

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