Merge branch 'develop' into release/1.13.0
|
|
@ -34,11 +34,11 @@ SMTP_PASSWORD=
|
|||
SMTP_DOMAIN=
|
||||
SMTP_PORT=
|
||||
|
||||
# DISABLE USER SIGNUPS (true or false). only applicable if MULTI_ORGANIZATION=true
|
||||
# DISABLE USER SIGNUPS (true or false). only applicable if Multi-Workspace feature is enabled
|
||||
DISABLE_SIGNUPS=
|
||||
|
||||
# Enables all multi organization features
|
||||
MULTI_ORGANIZATION=
|
||||
# Disable Multi-Workspace features (true or false)
|
||||
DISABLE_MULTI_WORKSPACE=
|
||||
|
||||
# OBSERVABILITY
|
||||
APM_VENDOR=
|
||||
|
|
|
|||
6
app.json
|
|
@ -34,11 +34,11 @@
|
|||
"value": "--max-old-space-size=4096"
|
||||
},
|
||||
"DISABLE_SIGNUPS": {
|
||||
"description": "Disable sign up in login page only applicable if MULTI_ORGANIZATION=true",
|
||||
"description": "Disable sign up in login page only applicable if Multi-Workspace feature is turned on",
|
||||
"value": "false"
|
||||
},
|
||||
"MULTI_ORGANIZATION": {
|
||||
"description": "Enables multi organization feature",
|
||||
"DISABLE_MULTI_WORKSPACE": {
|
||||
"description": "Disables Multi-Workspace feature",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@ title: MinIO
|
|||
|
||||
ToolJet can connect to minio and perform various operation on them.
|
||||
|
||||
## Supported operations
|
||||
|
||||
- **Read object**
|
||||
- **Put object**
|
||||
- **List buckets**
|
||||
- **List objects in a bucket**
|
||||
- **Presigned url for download**
|
||||
- **Presigned url for upload**
|
||||
|
||||
## Connection
|
||||
|
||||
To add a new minio source, click on the **Add or edit datasource** icon on the left sidebar of the app editor and click on `Add datasource` button. Select Minio from the modal that pops up.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Bulk update multiple rows in table
|
||||
id: bulk-update-multiple-rows
|
||||
title: Bulk update multiple rows in table
|
||||
---
|
||||
|
||||
# Bulk update multiple rows in table
|
||||
|
|
|
|||
137
docs/docs/how-to/upload-files-aws.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
---
|
||||
id: upload-files-aws
|
||||
title: Upload files on AWS S3 bucket
|
||||
---
|
||||
|
||||
# Upload and download files on AWS S3 bucket
|
||||
|
||||
This guide will help you in quickly building a basic UI for uploading or downloading files from AWS S3 buckets.
|
||||
|
||||
Before building the UI, check out the **[docs for AWS S3 data source](/docs/data-sources/s3)** to learn about setting up AWS S3 and adding the data source.
|
||||
|
||||
Once you have successfully added the AWS data source, build a basic UI using the following widgets:
|
||||
- **Dropdown**: For selecting a bucket in S3 storage.
|
||||
- **Table**: For listing all the objects inside the selected bucket in dropdown.
|
||||
- **Text Input**: For getting a path for the file that is to be uploaded.
|
||||
- **File picker**: For uploading the file.
|
||||
- **Button**: This will be used to fire the upload query.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Queries
|
||||
|
||||
We'll create the following queries:
|
||||
|
||||
1. **getBuckets**
|
||||
2. **listObjects**
|
||||
3. **uploadToS3**
|
||||
4. **download**
|
||||
|
||||
### getBuckets
|
||||
|
||||
This query will fetch the list of all the buckets in your S3. Just create a new query, select AWS S3 data souce, and choose **List buckets** operation. Name the query **getBuckets** and click **Save**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Now, let's edit the properties of **dropdown** widget.
|
||||
|
||||
- **Label**: Set the label as Bucket.
|
||||
- **Option values**: Set option values as `{{queries.getBuckets.data.Buckets.map(bucket => bucket['Name'])}}`. We're mapping the data returned by the query as the returned data is array of abjects.
|
||||
- **Option label**: Set option values as `{{queries.getBuckets.data.Buckets.map(bucket => bucket['Name'])}}`. This will display the same option label as option values.
|
||||
|
||||
You can later add an event handler for running the **listObject** query whenever an option is selected from the dropdown.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### listObjects
|
||||
|
||||
This query will list all the objects inside the selected Bucket in dropdown. Select **List objects in a bucket** operation, enter `{{components.dropdown1.value}}` in the Bucket field - this will dynamically get the field value from the selected option in dropdown.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Edit the properties of **table** widget:
|
||||
- **Table data**: `{{queries.listObjects.data['Contents']}}`
|
||||
- **Add Columns**:
|
||||
- **Key**: Set the **Column Name** to `Key` and **Key** to `Key`
|
||||
- **Last Modified**: Set the **Column Name** to `Last Modified` and **Key** to `LastModified`
|
||||
- **Size**: Set the **Column Name** to `Size` and **Key** to `Size`
|
||||
- Add a **Action button**: Set button text to **Copy signed URL**, Add a handler to this button for On Click event and Action to Copy to clipboard, in the text field enter `{{queries.download.data.url}}` - this will get the download url from the **download** query that we will create next.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### download
|
||||
|
||||
Create a new query and select **Signed URL for download** operation. In the Bucket field, enter `{{components.dropdown1.value}}` and in Key enter `{{components.table1.selectedRow.Key}}`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Edit the **properites** of the table, add a Event handler for running the `download` query for `Row clicked` event. This will generate a signed url for download every time a row is clicked on the table.
|
||||
|
||||
### uploadToS3
|
||||
|
||||
Create a new query, select the **Upload object** operation. Enter the following values in their respective fields:
|
||||
- **Bucket**: `{{components.dropdown1.value}}`
|
||||
- **Key**: {{ components.textinput1.value + '/' +components.filepicker1.file[0].name}}`
|
||||
- **Content type**: `{{components.filepicker1.file[0].type}}`
|
||||
- **Upload data**: `{{components.filepicker1.file[0].base64Data}}`
|
||||
- **Encoding**: `base64`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
#### Configure the file picker:
|
||||
|
||||
Click on the widget handle to edit the file picker properties:
|
||||
|
||||
- Change the **Accept file types** to `{{"application/pdf"}}` for the picker to accept only pdf files or `{{"image/*"}}` for the picker to accept only image files . In the screenshot below, we have set the accepted file type property to `{{"application/pdf"}}` so it will allow to select only pdf files:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
- Change the **Max file count** to `{{1}}` as we are only going to upload 1 file at a time.
|
||||
|
||||
- Select a pdf file and hold it in the file picker.
|
||||
|
||||
:::info
|
||||
File types must be valid **[MIME](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)** type according to input element specification or a valid file extension.
|
||||
|
||||
To accept any/all file type(s), set `Accept file types` to an empty value.
|
||||
:::
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Final steps, go to the **Advanced** tab of the **uploadToS3** query and add a query to run **listObjects** query so that whenever a file is uploaded the tabled is refreshed.
|
||||
|
|
@ -49,6 +49,7 @@ The references for data sources and widgets:
|
|||
- **[Build a WhatsApp CRM](https://blog.tooljet.com/build-a-whatsapp-crm-using-tooljet-within-10-mins/)**
|
||||
- **[Build a cryptocurrency dashboard](https://blog.tooljet.com/how-to-build-a-cryptocurrency-dashboard-in-10-minutes/)**
|
||||
- **[Build a Redis GUI](https://blog.tooljet.com/building-a-redis-gui-using-tooljet-in-5-minutes/)**
|
||||
- **[Build a coupon code manager app](https://blog.tooljet.com/build-a-coupon-code-manager-app-in-10-minutes/)**
|
||||
|
||||
## Help and Support
|
||||
- We have extensively documented the features of ToolJet, but in case you are stuck, please feel to e-mail us at **hello@tooljet.com**
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ sidebar_label: Password Login
|
|||
|
||||
# Password Login
|
||||
|
||||
Password login is enabled by default for all organizations. User with admin privilege can enable/disable it.
|
||||
Password login is enabled by default for all workspaces. User with admin privilege can enable/disable it.
|
||||
|
||||
Select `Manage SSO` from organization options
|
||||
Select `Manage SSO` from workspace options
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
|
|||
|
|
@ -73,14 +73,14 @@ You can specify a different server for backend if it is hosted on another server
|
|||
| -------- | ---------------------- |
|
||||
| SERVER_HOST | Configure a hostname for the server as a proxy pass. If no value is set, it defaults to `server`. |
|
||||
|
||||
#### Enable multiple organizations ( optional )
|
||||
#### Disable Multi-Workspace ( optional )
|
||||
|
||||
If you want to enable multiple environments, set the environment variable `MULTI_ORGANIZATION` to `true`.
|
||||
If you want to disable Multi-Workspace feature, set the environment variable `DISABLE_MULTI_WORKSPACE` to `true`.
|
||||
|
||||
|
||||
#### Disabling signups ( optional )
|
||||
|
||||
Sign up is enabled only for multiple organization environment. If you want to restrict the signups and allow new users only by invitations, set the environment variable `DISABLE_SIGNUPS` to `true`.
|
||||
Sign up is enabled only if Multi-Workspace is enabled. If you want to restrict the signups and allow new users only by invitations, set the environment variable `DISABLE_SIGNUPS` to `true`.
|
||||
|
||||
:::tip
|
||||
You will still be able to see the signup page but won't be able to successfully submit the form.
|
||||
|
|
@ -93,7 +93,7 @@ You can set `SERVE_CLIENT` to `true` and the server will attempt to serve the cl
|
|||
|
||||
#### SMTP configuration ( optional )
|
||||
|
||||
ToolJet uses SMTP services to send emails ( Eg: invitation email when you add new users to your organization ).
|
||||
ToolJet uses SMTP services to send emails ( Eg: invitation email when you add new users to your workspace ).
|
||||
|
||||
| variable | description |
|
||||
| ------------------ | ----------------------------------------- |
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ sidebar_label: General Settings
|
|||
|
||||
# Single Sign-On General Settings
|
||||
|
||||
Select `Manage SSO` from organization options
|
||||
Select `Manage SSO` from workspace options
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: GitHub
|
|||
|
||||
# GitHub Single Sign-on
|
||||
|
||||
Select `Manage SSO` from organization options
|
||||
Select `Manage SSO` from workspace options
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
@ -55,4 +55,4 @@ Go to [GitHub Developer settings](https://github.com/settings/developers) and na
|
|||
|
||||
Lastly, enter `Client Id` and `Client Secret` in Git manage SSO page and save.
|
||||
|
||||
The GitHub sign-in button will now be available in your ToolJet login screen if you have not enabled multiple organization.
|
||||
The GitHub sign-in button will now be available in your ToolJet login screen if you have not enabled Multi-Workspace.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: Google
|
|||
|
||||
# Google Single Sign-on
|
||||
|
||||
Select `Manage SSO` from organization options
|
||||
Select `Manage SSO` from workspace options
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ Go to [Google cloud console](https://console.cloud.google.com/) and create a pro
|
|||
|
||||
</div>
|
||||
|
||||
- You'll be asked to select user type in consent screen. To allow only users within your organization, select 'Internal', otherwise,
|
||||
- You'll be asked to select user type in consent screen. To allow only users within your workspace, select 'Internal', otherwise,
|
||||
select 'External'.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
|
@ -82,4 +82,4 @@ Set the `Redirect URL` generated at manage SSO `Google` page under Authorised re
|
|||
|
||||
Lastly, set the `client id` in google manage SSO page. This value will be available from your [Google cloud console credentials page](https://console.cloud.google.com/apis/credentials)
|
||||
|
||||
The Google sign-in button will now be available in your ToolJet login screen, if you are not enabled multiple organization.
|
||||
The Google sign-in button will now be available in your ToolJet login screen, if you are not enabled Multi-Workspace.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ title: Adding a data source
|
|||
# Adding a data source
|
||||
|
||||
:::tip
|
||||
The data sources are created on app level and not on organization level.
|
||||
The data sources are created on app level and not on workspace level.
|
||||
:::
|
||||
|
||||
**Datasource manager** is on the left-sidebar of the app builder. To add a new data source, click on the `Add datasource` button.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ title: Managing Users and Groups
|
|||
|
||||
## Managing Users
|
||||
|
||||
Admin of an organization can add users to the organization. To manage the users in your organization, just go to the **Account menu** on top right corner and click on the **Manage Users**.
|
||||
Admin of a workspace can add users to the workspace. To manage the users in your workspace, just go to the **Workspace menu** on top right corner and click on the **Manage Users**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ Admin of an organization can add users to the organization. To manage the users
|
|||
|
||||
### Inviting users
|
||||
|
||||
Admins can invite anyone to a ToolJet organization using the email address. To invite a user:
|
||||
Admins can invite anyone to a workspace using the email address. To invite a user:
|
||||
|
||||
- On the **Manage Users** page click on the `Invite new user` button.
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ Admins can invite anyone to a ToolJet organization using the email address. To i
|
|||
|
||||
</div>
|
||||
|
||||
- An email including the **Invite Link** to join your organization will be send to the created user. The status will turn from **invited** to **active** after the user successfully joins your organization using the invite link.
|
||||
- An email including the **Invite Link** to join your workspace will be send to the created user. The status will turn from **invited** to **active** after the user successfully joins your workspace using the invite link.
|
||||
|
||||
:::tip
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ You can also copy the invitation url by clicking on the copy icon next to `invit
|
|||
|
||||
### Disabling a user's access
|
||||
|
||||
You can disable any active user's access to your organization by clicking on the **Archive** and then the status of the user will change from **active** to **archived**.
|
||||
You can disable any active user's access to your workspace by clicking on the **Archive** and then the status of the user will change from **active** to **archived**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ Similar to archiving a user's access, you can enable it again by clicking on **U
|
|||
|
||||
## Managing Groups
|
||||
|
||||
On ToolJet, Admins can create groups for users added in an organization and grant them access to particular app(s) with specific permissions. To manage groups, just go to the **Account menu** on top right corner and click on the **Manage Groups**.
|
||||
On ToolJet, Admins can create groups for users added in a workspace and grant them access to particular app(s) with specific permissions. To manage groups, just go to the **Account menu** on top right corner and click on the **Manage Groups**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
@ -115,13 +115,13 @@ Admins can set granular permission like creating/deleting apps or creating folde
|
|||
|
||||
:::tip
|
||||
|
||||
All the activities performed by any Admin or any user in a ToolJet organization is logged in `Audit logs` - including any activity related with managing users and groups.
|
||||
All the activities performed by any Admin or any user in a workspace is logged in `Audit logs` - including any activity related with managing users and groups.
|
||||
|
||||
:::
|
||||
|
||||
### Predefined Groups
|
||||
|
||||
By default, every organization will have two User Groups:
|
||||
By default, every workspace will have two User Groups:
|
||||
|
||||
**1. All Users**
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ This group contains all the users and admins.
|
|||
|
||||
| Apps | Users | Permissions |
|
||||
| ----------- | ----------- | ----------- |
|
||||
| You can add or remove apps. | Modification is disabled. This group will have all the users and admins added in an organization. | You can edit permissions for all the users globally. |
|
||||
| You can add or remove apps. | Modification is disabled. This group will have all the users and admins added in a workspace. | You can edit permissions for all the users globally. |
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ This group contains admins by default. Admins can add more admins or remove the
|
|||
|
||||
| Apps | Users | Permissions |
|
||||
| ----------- | ----------- | ----------- |
|
||||
| Modification is disabled. By default, this group has `Edit` permission for all the apps in an organization | Admins can add or remove users in this group. | Modification is disabled. By default, all the admins can create and delete apps or create folders. |
|
||||
| Modification is disabled. By default, this group has `Edit` permission for all the apps in a workspace | Admins can add or remove users in this group. | Modification is disabled. By default, all the admins can create and delete apps or create folders. |
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
|
|||
92
docs/docs/widgets/custom-component.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
id: custom-component
|
||||
title: Custom Component
|
||||
---
|
||||
|
||||
# Custom Component
|
||||
|
||||
Custom Component can be used to do create your own React component when the needed functionality isn't available in other components.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Properties
|
||||
|
||||
### Data
|
||||
|
||||
The data needs to be an objects which needs to be passed as `data` props to the custom component
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{{{
|
||||
title: "Hi! There",
|
||||
buttonText: "Updated Text",
|
||||
queryName: "runjs1"
|
||||
}}}
|
||||
```
|
||||
|
||||
### Code
|
||||
|
||||
This field is used to add a React code for your custom component. The packages for the custom component can be imported from [Skypack](https://www.skypack.dev/). For example, to import `React` package into the custom component it can be imported as `import React from 'https://cdn.skypack.dev/react'`.
|
||||
|
||||
Tooljet provides 3 props to interact with the app: `data`, `updateData` and `runQuery`.
|
||||
|
||||
- `data` is a shared object between custom component and Tooljet app.
|
||||
- `updateData` is a function which accepts a single object used to update the data passed to the custom component.
|
||||
- `runQuery` is a function which accepts a query name as a string used to run the query from the custom component.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
import React from "https://cdn.skypack.dev/react";
|
||||
import ReactDOM from "https://cdn.skypack.dev/react-dom";
|
||||
import { Button, Container, Link } from "https://cdn.skypack.dev/@material-ui/core";
|
||||
|
||||
const MyCustomComponent = ({data, updateData, runQuery}) => (
|
||||
<Container>
|
||||
<h1>{data.title}</h1>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
onClick={() => {updateData({title: 'Hello World!!'})}}>
|
||||
{data.buttonText}
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
onClick={() => {runQuery(data.queryName)}}
|
||||
>
|
||||
Run Query
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const ConnectedComponent = Tooljet.connectComponent(MyCustomComponent);
|
||||
|
||||
ReactDOM.render(<ConnectedComponent />, document.body);
|
||||
```
|
||||
|
||||
:::info
|
||||
`Tooljet.connectComponent` acts as a HOC and it is required to get access to the data passed into the custom component and run the query
|
||||
:::
|
||||
|
||||
## Layout
|
||||
|
||||
| Layout | description | Expected value |
|
||||
| --------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Show on desktop | Toggle on or off to display desktop view. | You can programmatically determining the value by clicking on `Fx` to set the value `{{true}}` or `{{false}}` |
|
||||
| Show on mobile | Toggle on or off to display mobile view. | You can programmatically determining the value by clicking on `Fx` to set the value `{{true}}` or `{{false}}` |
|
||||
|
||||
## Styles
|
||||
|
||||
| Style | Description |
|
||||
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Visibility | Toggle on or off to control the visibility of the widget. You can programmatically change its value by clicking on the `Fx` button next to it. If `{{false}}` the widget will not visible after the app is deployed. By default, it's set to `{{true}}`. |
|
||||
|
||||
:::info
|
||||
Any property having `Fx` button next to its field can be **programmatically configured**.
|
||||
:::
|
||||
39
docs/docs/widgets/pdf.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
id: pdf
|
||||
title: PDF
|
||||
---
|
||||
|
||||
# PDF
|
||||
|
||||
PDF widget can be used to embed the PDF file either by URL or as a Base64 encoded.
|
||||
|
||||
## Properties
|
||||
|
||||
### File URL
|
||||
|
||||
The URL of the PDF file on the web. `data:application/pdf;base64,` format is supported and the input needs to be prefixed with `data:application/pdf;base64,`
|
||||
|
||||
### Scale page to width
|
||||
|
||||
It can be toggled to adjust the PDF content to fit the width or height of the component
|
||||
|
||||
### Show page controls
|
||||
|
||||
By default, page number, previous & next button is displayed while hovering the PDF file. It can be toggled on or off.
|
||||
|
||||
## Layout
|
||||
|
||||
| Layout | description | Expected value |
|
||||
| --------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Show on desktop | Toggle on or off to display desktop view. | You can programmatically determining the value by clicking on `Fx` to set the value `{{true}}` or `{{false}}` |
|
||||
| Show on mobile | Toggle on or off to display mobile view. | You can programmatically determining the value by clicking on `Fx` to set the value `{{true}}` or `{{false}}` |
|
||||
|
||||
## Styles
|
||||
|
||||
| Style | Description |
|
||||
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Visibility | Toggle on or off to control the visibility of the widget. You can programmatically change its value by clicking on the `Fx` button next to it. If `{{false}}` the widget will not visible after the app is deployed. By default, it's set to `{{true}}`. |
|
||||
|
||||
:::info
|
||||
Any property having `Fx` button next to its field can be **programmatically configured**.
|
||||
:::
|
||||
|
|
@ -103,6 +103,7 @@ const sidebars = {
|
|||
'widgets/circular-progress-bar',
|
||||
'widgets/code-editor',
|
||||
'widgets/container',
|
||||
'widgets/custom-component',
|
||||
'widgets/date-range-picker',
|
||||
'widgets/datepicker',
|
||||
'widgets/divider',
|
||||
|
|
@ -116,6 +117,7 @@ const sidebars = {
|
|||
'widgets/multiselect',
|
||||
'widgets/number-input',
|
||||
'widgets/password-input',
|
||||
'widgets/pdf',
|
||||
'widgets/qr-scanner',
|
||||
'widgets/radio-button',
|
||||
'widgets/range-slider',
|
||||
|
|
@ -160,7 +162,9 @@ const sidebars = {
|
|||
keywords: ['how to'],
|
||||
},
|
||||
items: [
|
||||
'how-to/bulk-update-multiple-rows',
|
||||
'how-to/oauth2-authorization',
|
||||
'how-to/upload-files-aws',
|
||||
'how-to/upload-files-gcs',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
BIN
docs/static/img/how-to/upload-files-aws/download.png
vendored
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/static/img/how-to/upload-files-aws/dropdown.png
vendored
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/static/img/how-to/upload-files-aws/getBuckets.png
vendored
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
docs/static/img/how-to/upload-files-aws/listObjects.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/static/img/how-to/upload-files-aws/table.png
vendored
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
docs/static/img/how-to/upload-files-aws/ui.png
vendored
Normal file
|
After Width: | Height: | Size: 410 KiB |
BIN
docs/static/img/how-to/upload-files-aws/uploadToS3.png
vendored
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/static/img/widgets/custom-component/custom-component.png
vendored
Normal file
|
After Width: | Height: | Size: 343 KiB |
9
frontend/assets/images/icons/widgets/customcomponent.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tools" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 21h4l13 -13a1.5 1.5 0 0 0 -4 -4l-13 13v4" />
|
||||
<line x1="14.5" y1="5.5" x2="18.5" y2="9.5" />
|
||||
<polyline points="12 8 7 3 3 7 8 12" />
|
||||
<line x1="7" y1="8" x2="5.5" y2="9.5" />
|
||||
<polyline points="16 12 21 17 17 21 12 16" />
|
||||
<line x1="16" y1="17" x2="14.5" y2="18.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 570 B |
1
frontend/assets/images/icons/widgets/pdf.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 115.28 122.88" style="enable-background:new 0 0 115.28 122.88" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g><path class="st0" d="M25.38,57h64.88V37.34H69.59c-2.17,0-5.19-1.17-6.62-2.6c-1.43-1.43-2.3-4.01-2.3-6.17V7.64l0,0H8.15 c-0.18,0-0.32,0.09-0.41,0.18C7.59,7.92,7.55,8.05,7.55,8.24v106.45c0,0.14,0.09,0.32,0.18,0.41c0.09,0.14,0.28,0.18,0.41,0.18 c22.78,0,58.09,0,81.51,0c0.18,0,0.17-0.09,0.27-0.18c0.14-0.09,0.33-0.28,0.33-0.41v-11.16H25.38c-4.14,0-7.56-3.4-7.56-7.56 V64.55C17.82,60.4,21.22,57,25.38,57L25.38,57z M29.5,67.4h13.19c2.87,0,5.02,0.68,6.46,2.05c1.43,1.37,2.14,3.31,2.14,5.84 c0,2.59-0.78,4.62-2.34,6.08c-1.56,1.46-3.94,2.19-7.14,2.19h-4.35v9.49H29.5V67.4L29.5,67.4z M37.45,78.37h1.95 c1.54,0,2.62-0.27,3.24-0.8c0.62-0.53,0.93-1.21,0.93-2.04c0-0.81-0.27-1.49-0.81-2.05c-0.54-0.56-1.55-0.84-3.05-0.84h-2.27V78.37 L37.45,78.37z M54.99,67.4h11.78c2.32,0,4.2,0.32,5.63,0.94c1.43,0.63,2.61,1.53,3.55,2.71c0.93,1.18,1.61,2.55,2.02,4.11 c0.42,1.56,0.63,3.22,0.63,4.97c0,2.74-0.31,4.87-0.94,6.38c-0.62,1.51-1.49,2.78-2.6,3.8c-1.11,1.02-2.3,1.7-3.57,2.04 c-1.74,0.47-3.31,0.7-4.72,0.7H54.99V67.4L54.99,67.4z M62.9,73.21v14.01h1.95c1.66,0,2.84-0.19,3.55-0.55 c0.7-0.37,1.25-1.01,1.65-1.92c0.4-0.92,0.6-2.4,0.6-4.45c0-2.72-0.44-4.57-1.33-5.58c-0.89-1-2.36-1.5-4.42-1.5H62.9L62.9,73.21z M82.25,67.4h19.6v5.52H90.21v4.48h9.96v5.2h-9.96v10.46h-7.95V67.4L82.25,67.4z M97.79,57h9.93c4.16,0,7.56,3.41,7.56,7.56v31.42 c0,4.15-3.41,7.56-7.56,7.56h-9.93v13.55c0,1.61-0.65,3.04-1.7,4.1c-1.06,1.06-2.49,1.7-4.1,1.7c-29.44,0-56.59,0-86.18,0 c-1.61,0-3.04-0.64-4.1-1.7c-1.06-1.06-1.7-2.49-1.7-4.1V5.85c0-1.61,0.65-3.04,1.7-4.1c1.06-1.06,2.53-1.7,4.1-1.7h58.72 C64.66,0,64.8,0,64.94,0c0.64,0,1.29,0.28,1.75,0.69h0.09c0.09,0.05,0.14,0.09,0.23,0.18l29.99,30.36c0.51,0.51,0.88,1.2,0.88,1.98 c0,0.23-0.05,0.41-0.09,0.65V57L97.79,57z M67.52,27.97V8.94l21.43,21.7H70.19c-0.74,0-1.38-0.32-1.89-0.78 C67.84,29.4,67.52,28.71,67.52,27.97L67.52,27.97z" fill='#61656F'/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
476
frontend/package-lock.json
generated
|
|
@ -38,6 +38,7 @@
|
|||
"emoji-mart": "^3.0.1",
|
||||
"fuse.js": "^6.4.6",
|
||||
"history": "^4.9.0",
|
||||
"html-loader": "^3.1.0",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"immer": "^9.0.6",
|
||||
"immutability-helper": "^3.1.1",
|
||||
|
|
@ -72,6 +73,7 @@
|
|||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-mentions": "^4.3.0",
|
||||
"react-multi-select-component": "^4.2.3",
|
||||
"react-pdf": "^5.7.2",
|
||||
"react-plotly.js": "^2.5.1",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-rnd": "^10.3.0",
|
||||
|
|
@ -133,6 +135,7 @@
|
|||
"@tooljet-plugins/mssql": "file:packages/mssql",
|
||||
"@tooljet-plugins/mysql": "file:packages/mysql",
|
||||
"@tooljet-plugins/n8n": "file:packages/n8n",
|
||||
"@tooljet-plugins/notion": "file:packages/notion",
|
||||
"@tooljet-plugins/openapi": "file:packages/openapi",
|
||||
"@tooljet-plugins/oracledb": "file:packages/oracledb",
|
||||
"@tooljet-plugins/postgresql": "file:packages/postgresql",
|
||||
|
|
@ -21578,6 +21581,55 @@
|
|||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-loader": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
|
||||
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
|
||||
"dependencies": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-loader/node_modules/loader-utils": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
|
||||
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-loader/node_modules/schema-utils": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "0.2.4",
|
||||
"integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
|
||||
|
|
@ -22190,6 +22242,131 @@
|
|||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/html-loader": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/html-loader/-/html-loader-3.1.0.tgz",
|
||||
"integrity": "sha512-ycMYFRiCF7YANcLDNP72kh3Po5pTcH+bROzdDwh00iVOAY/BwvpuZ1BKPziQ35Dk9D+UD84VGX1Lu/H4HpO4fw==",
|
||||
"dependencies": {
|
||||
"html-minifier-terser": "^6.0.2",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/acorn": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/clean-css": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz",
|
||||
"integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==",
|
||||
"dependencies": {
|
||||
"source-map": "~0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/html-minifier-terser": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||
"integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
|
||||
"dependencies": {
|
||||
"camel-case": "^4.1.2",
|
||||
"clean-css": "^5.2.2",
|
||||
"commander": "^8.3.0",
|
||||
"he": "^1.2.0",
|
||||
"param-case": "^3.0.4",
|
||||
"relateurl": "^0.2.7",
|
||||
"terser": "^5.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"html-minifier-terser": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/terser": {
|
||||
"version": "5.13.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz",
|
||||
"integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==",
|
||||
"dependencies": {
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.8.0-beta.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/terser/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/html-loader/node_modules/terser/node_modules/source-map": {
|
||||
"version": "0.8.0-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
|
||||
"integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/tr46": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
|
||||
"integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-loader/node_modules/webidl-conversions": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
|
||||
},
|
||||
"node_modules/html-loader/node_modules/whatwg-url": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
|
||||
"integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
|
||||
"dependencies": {
|
||||
"lodash.sortby": "^4.7.0",
|
||||
"tr46": "^1.0.1",
|
||||
"webidl-conversions": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser": {
|
||||
"version": "5.1.1",
|
||||
"integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==",
|
||||
|
|
@ -26446,6 +26623,14 @@
|
|||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||
|
|
@ -26770,6 +26955,11 @@
|
|||
"version": "4.6.2",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"node_modules/lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
|
||||
},
|
||||
"node_modules/lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
|
|
@ -26823,6 +27013,14 @@
|
|||
"integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/make-cancellable-promise": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.1.0.tgz",
|
||||
"integrity": "sha512-X5Opjm2xcZsOLuJ+Bnhb4t5yfu4ehlA3OKEYLtqUchgVzL/QaqW373ZUVxVHKwvJ38cmYuR4rAHD2yUvAIkTPA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "2.1.0",
|
||||
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||
|
|
@ -26841,6 +27039,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/make-event-props": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.3.0.tgz",
|
||||
"integrity": "sha512-oWiDZMcVB1/A487251hEWza1xzgCzl6MXxe9aF24l5Bt9N9UEbqTqKumEfuuLhmlhRZYnc+suVvW4vUs8bwO7Q==",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.12",
|
||||
"integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
|
||||
|
|
@ -26940,11 +27146,24 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-class-names": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz",
|
||||
"integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/merge-refs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.0.0.tgz",
|
||||
"integrity": "sha512-WZ4S5wqD9FCR9hxkLgvcHJCBxzXzy3VVE6p8W2OzxRzB+hLRlcadGE2bW9xp2KSzk10rvp4y+pwwKO6JQVguMg=="
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
||||
|
|
@ -27634,8 +27853,7 @@
|
|||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "6.0.1",
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
|
|
@ -27717,6 +27935,19 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "2.12.313",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.12.313.tgz",
|
||||
"integrity": "sha512-1x6iXO4Qnv6Eb+YFdN5JdUzt4pAkxSp3aLAYPX93eQCyg/m7QFzXVWJHJVtoW48CI8HCXju4dSkhQZwoheL5mA==",
|
||||
"peerDependencies": {
|
||||
"worker-loader": "^3.0.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"worker-loader": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
|
|
@ -28757,6 +28988,30 @@
|
|||
"react-dom": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-pdf": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.7.2.tgz",
|
||||
"integrity": "sha512-hdDwvf007V0i2rPCqQVS1fa70CXut17SN3laJYlRHzuqcu8sLLjEoeXihty6c0Ev5g1mw31b8OT8EwRw1s8C4g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"make-cancellable-promise": "^1.0.0",
|
||||
"make-event-props": "^1.1.0",
|
||||
"merge-class-names": "^1.1.1",
|
||||
"merge-refs": "^1.0.0",
|
||||
"pdfjs-dist": "2.12.313",
|
||||
"prop-types": "^15.6.2",
|
||||
"tiny-invariant": "^1.0.0",
|
||||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-plotly.js": {
|
||||
"version": "2.5.1",
|
||||
"integrity": "sha512-Oya14whSHvPsYXdI0nHOGs1pZhMzV2edV7HAW1xFHD58Y73m/LbG2Encvyz1tztL0vfjph0JNhiwO8cGBJnlhg==",
|
||||
|
|
@ -29766,8 +30021,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.19",
|
||||
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
|
|
@ -31324,13 +31580,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-merge/node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-merge/node_modules/shallow-clone": {
|
||||
"version": "3.0.1",
|
||||
"integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
|
||||
|
|
@ -31402,14 +31651,6 @@
|
|||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/source-map-support": {
|
||||
"version": "0.5.20",
|
||||
"integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||
|
|
@ -34499,6 +34740,7 @@
|
|||
"@tooljet-plugins/mssql": "file:packages/mssql",
|
||||
"@tooljet-plugins/mysql": "file:packages/mysql",
|
||||
"@tooljet-plugins/n8n": "file:packages/n8n",
|
||||
"@tooljet-plugins/notion": "file:packages/notion",
|
||||
"@tooljet-plugins/openapi": "file:packages/openapi",
|
||||
"@tooljet-plugins/oracledb": "file:packages/oracledb",
|
||||
"@tooljet-plugins/postgresql": "file:packages/postgresql",
|
||||
|
|
@ -48083,6 +48325,37 @@
|
|||
"flat-cache": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"file-loader": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
|
||||
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"loader-utils": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
|
||||
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-selector": {
|
||||
"version": "0.2.4",
|
||||
"integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
|
||||
|
|
@ -48524,6 +48797,98 @@
|
|||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"html-loader": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/html-loader/-/html-loader-3.1.0.tgz",
|
||||
"integrity": "sha512-ycMYFRiCF7YANcLDNP72kh3Po5pTcH+bROzdDwh00iVOAY/BwvpuZ1BKPziQ35Dk9D+UD84VGX1Lu/H4HpO4fw==",
|
||||
"requires": {
|
||||
"html-minifier-terser": "^6.0.2",
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A=="
|
||||
},
|
||||
"clean-css": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz",
|
||||
"integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==",
|
||||
"requires": {
|
||||
"source-map": "~0.6.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
|
||||
},
|
||||
"html-minifier-terser": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||
"integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
|
||||
"requires": {
|
||||
"camel-case": "^4.1.2",
|
||||
"clean-css": "^5.2.2",
|
||||
"commander": "^8.3.0",
|
||||
"he": "^1.2.0",
|
||||
"param-case": "^3.0.4",
|
||||
"relateurl": "^0.2.7",
|
||||
"terser": "^5.10.0"
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.13.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz",
|
||||
"integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==",
|
||||
"requires": {
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.8.0-beta.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.8.0-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
|
||||
"integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==",
|
||||
"requires": {
|
||||
"whatwg-url": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tr46": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
|
||||
"integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
|
||||
},
|
||||
"whatwg-url": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
|
||||
"integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
|
||||
"requires": {
|
||||
"lodash.sortby": "^4.7.0",
|
||||
"tr46": "^1.0.1",
|
||||
"webidl-conversions": "^4.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"html-minifier-terser": {
|
||||
"version": "5.1.1",
|
||||
"integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==",
|
||||
|
|
@ -51649,6 +52014,11 @@
|
|||
"object.assign": "^4.1.2"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||
},
|
||||
"kleur": {
|
||||
"version": "3.0.3",
|
||||
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||
|
|
@ -51883,6 +52253,11 @@
|
|||
"version": "4.6.2",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
|
|
@ -51932,6 +52307,11 @@
|
|||
"integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=",
|
||||
"optional": true
|
||||
},
|
||||
"make-cancellable-promise": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.1.0.tgz",
|
||||
"integrity": "sha512-X5Opjm2xcZsOLuJ+Bnhb4t5yfu4ehlA3OKEYLtqUchgVzL/QaqW373ZUVxVHKwvJ38cmYuR4rAHD2yUvAIkTPA=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||
|
|
@ -51946,6 +52326,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"make-event-props": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.3.0.tgz",
|
||||
"integrity": "sha512-oWiDZMcVB1/A487251hEWza1xzgCzl6MXxe9aF24l5Bt9N9UEbqTqKumEfuuLhmlhRZYnc+suVvW4vUs8bwO7Q=="
|
||||
},
|
||||
"makeerror": {
|
||||
"version": "1.0.12",
|
||||
"integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
|
||||
|
|
@ -52023,11 +52408,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"merge-class-names": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz",
|
||||
"integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw=="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
|
||||
"dev": true
|
||||
},
|
||||
"merge-refs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.0.0.tgz",
|
||||
"integrity": "sha512-WZ4S5wqD9FCR9hxkLgvcHJCBxzXzy3VVE6p8W2OzxRzB+hLRlcadGE2bW9xp2KSzk10rvp4y+pwwKO6JQVguMg=="
|
||||
},
|
||||
"merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
||||
|
|
@ -52524,8 +52919,7 @@
|
|||
},
|
||||
"parse5": {
|
||||
"version": "6.0.1",
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
|
|
@ -52596,6 +52990,12 @@
|
|||
"pinkie-promise": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pdfjs-dist": {
|
||||
"version": "2.12.313",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.12.313.tgz",
|
||||
"integrity": "sha512-1x6iXO4Qnv6Eb+YFdN5JdUzt4pAkxSp3aLAYPX93eQCyg/m7QFzXVWJHJVtoW48CI8HCXju4dSkhQZwoheL5mA==",
|
||||
"requires": {}
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
|
|
@ -53352,6 +53752,23 @@
|
|||
"warning": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"react-pdf": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.7.2.tgz",
|
||||
"integrity": "sha512-hdDwvf007V0i2rPCqQVS1fa70CXut17SN3laJYlRHzuqcu8sLLjEoeXihty6c0Ev5g1mw31b8OT8EwRw1s8C4g==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"make-cancellable-promise": "^1.0.0",
|
||||
"make-event-props": "^1.1.0",
|
||||
"merge-class-names": "^1.1.1",
|
||||
"merge-refs": "^1.0.0",
|
||||
"pdfjs-dist": "2.12.313",
|
||||
"prop-types": "^15.6.2",
|
||||
"tiny-invariant": "^1.0.0",
|
||||
"tiny-warning": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-plotly.js": {
|
||||
"version": "2.5.1",
|
||||
"integrity": "sha512-Oya14whSHvPsYXdI0nHOGs1pZhMzV2edV7HAW1xFHD58Y73m/LbG2Encvyz1tztL0vfjph0JNhiwO8cGBJnlhg==",
|
||||
|
|
@ -54126,8 +54543,9 @@
|
|||
}
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.19",
|
||||
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
|
|
@ -55042,14 +55460,6 @@
|
|||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.20",
|
||||
"integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==",
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"tapable": {
|
||||
"version": "2.2.1",
|
||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="
|
||||
|
|
@ -55342,10 +55752,6 @@
|
|||
"shallow-clone": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||
},
|
||||
"shallow-clone": {
|
||||
"version": "3.0.1",
|
||||
"integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"emoji-mart": "^3.0.1",
|
||||
"fuse.js": "^6.4.6",
|
||||
"history": "^4.9.0",
|
||||
"html-loader": "^3.1.0",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"immer": "^9.0.6",
|
||||
"immutability-helper": "^3.1.1",
|
||||
|
|
@ -68,6 +69,7 @@
|
|||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-mentions": "^4.3.0",
|
||||
"react-multi-select-component": "^4.2.3",
|
||||
"react-pdf": "^5.7.2",
|
||||
"react-plotly.js": "^2.5.1",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-rnd": "^10.3.0",
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ class ConfirmationPage extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Organization</label>
|
||||
<label className="form-label">Workspace</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class OrganizationInvitationPage extends React.Component {
|
|||
isLoading: false,
|
||||
};
|
||||
this.formRef = React.createRef(null);
|
||||
this.single_organization = window.public_config?.MULTI_ORGANIZATION !== 'true';
|
||||
this.single_organization = window.public_config?.DISABLE_MULTI_WORKSPACE === 'true';
|
||||
}
|
||||
|
||||
handleChange = (event) => {
|
||||
|
|
@ -49,7 +49,7 @@ class OrganizationInvitationPage extends React.Component {
|
|||
})
|
||||
.then(() => {
|
||||
this.setState({ isLoading: false });
|
||||
toast.success(`Added to the organization${isSetPassword ? ' and password has been set ' : ' '}successfully.`, {
|
||||
toast.success(`Added to the workspace${isSetPassword ? ' and password has been set ' : ' '}successfully.`, {
|
||||
position: 'top-center',
|
||||
});
|
||||
this.props.history.push('/login');
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export const AppVersionsManager = function AppVersionsManager({
|
|||
|
||||
return (
|
||||
<div ref={wrapperRef} className="input-group app-version-menu">
|
||||
<span className="input-group-text app-version-menu-sm">App Version</span>
|
||||
<span className="input-group-text app-version-menu-sm">Version</span>
|
||||
<span
|
||||
className={`app-version-name form-select app-version-menu-sm ${appVersions ? '' : 'disabled'}`}
|
||||
onClick={() => {
|
||||
|
|
@ -149,49 +149,58 @@ export const AppVersionsManager = function AppVersionsManager({
|
|||
<div className="app-version-content">
|
||||
{appVersions.map((version) =>
|
||||
releasedVersionId == version.id ? (
|
||||
<div className="row dropdown-item released" key={version.id} onClick={() => selectVersion(version)}>
|
||||
<div className="col-md-4">{version.name}</div>
|
||||
<div className="released-subtext">
|
||||
<img src={'/assets/images/icons/editor/deploy-rocket.svg'} />
|
||||
<span className="px-1">Currently Released</span>
|
||||
<>
|
||||
<div
|
||||
className="row dropdown-item released"
|
||||
key={version.id}
|
||||
onClick={() => selectVersion(version)}
|
||||
>
|
||||
<div className="col-md-4">{version.name}</div>
|
||||
<div className="released-subtext">
|
||||
<img src={'/assets/images/icons/editor/deploy-rocket.svg'} />
|
||||
<span className="px-1">Currently Released</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dropdown-divider m-0"></div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="dropdown-item row"
|
||||
key={version.id}
|
||||
onClick={() => selectVersion(version)}
|
||||
onMouseEnter={() => setMouseHoveredOnVersion(version.id)}
|
||||
onMouseLeave={() => setMouseHoveredOnVersion(null)}
|
||||
>
|
||||
<div className="col-md-4">{version.name}</div>
|
||||
<>
|
||||
<div
|
||||
className="dropdown-item row"
|
||||
key={version.id}
|
||||
onClick={() => selectVersion(version)}
|
||||
onMouseEnter={() => setMouseHoveredOnVersion(version.id)}
|
||||
onMouseLeave={() => setMouseHoveredOnVersion(null)}
|
||||
>
|
||||
<div className="col-md-4">{version.name}</div>
|
||||
|
||||
<div className="col-md-2 offset-md-6">
|
||||
<button
|
||||
className="btn badge bg-azure-lt"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeletingVersionId(version.id);
|
||||
setShowVersionDeletionConfirmation(true);
|
||||
}}
|
||||
disabled={isDeletingVersion}
|
||||
style={{
|
||||
display: mouseHoveredOnVersion === version.id ? 'flex' : 'none',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/assets/images/icons/query-trash-icon.svg"
|
||||
width="12"
|
||||
height="12"
|
||||
className="mx-1"
|
||||
style={{ paddingLeft: '0.6px' }}
|
||||
/>
|
||||
</button>
|
||||
<div className="col-md-2 offset-md-6">
|
||||
<button
|
||||
className="btn badge bg-azure-lt"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeletingVersionId(version.id);
|
||||
setShowVersionDeletionConfirmation(true);
|
||||
}}
|
||||
disabled={isDeletingVersion}
|
||||
style={{
|
||||
display: mouseHoveredOnVersion === version.id ? 'flex' : 'none',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/assets/images/icons/query-trash-icon.svg"
|
||||
width="12"
|
||||
height="12"
|
||||
className="mx-1"
|
||||
style={{ paddingLeft: '0.6px' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dropdown-divider m-0"></div>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
<div className="dropdown-divider"></div>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={() => setShowModal(true)}>
|
||||
<span className="color-primary create-link">Create Version</span>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ import { renderTooltip } from '@/_helpers/appUtils';
|
|||
import { RangeSlider } from './Components/RangeSlider';
|
||||
import { Timeline } from './Components/Timeline';
|
||||
import { SvgImage } from './Components/SvgImage';
|
||||
import { CustomComponent } from './Components/CustomComponent/CustomComponent';
|
||||
import { VerticalDivider } from './Components/verticalDivider';
|
||||
import { PDF } from './Components/PDF';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import '@/_styles/custom.scss';
|
||||
import { resolveProperties, resolveStyles } from './component-properties-resolution';
|
||||
|
|
@ -83,7 +85,9 @@ const AllComponents = {
|
|||
RangeSlider,
|
||||
Timeline,
|
||||
SvgImage,
|
||||
CustomComponent,
|
||||
VerticalDivider,
|
||||
PDF,
|
||||
};
|
||||
|
||||
export const Box = function Box({
|
||||
|
|
@ -110,6 +114,7 @@ export const Box = function Box({
|
|||
parentId,
|
||||
allComponents,
|
||||
extraProps,
|
||||
dataQueries,
|
||||
}) {
|
||||
const backgroundColor = yellow ? 'yellow' : '';
|
||||
|
||||
|
|
@ -214,6 +219,7 @@ export const Box = function Box({
|
|||
validate={validate}
|
||||
parentId={parentId}
|
||||
customResolvables={customResolvables}
|
||||
dataQueries={dataQueries}
|
||||
></ComponentToRender>
|
||||
) : (
|
||||
<div className="m-1" style={{ height: '76px', width: '76px', marginLeft: '18px' }}>
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ export function CodeHinter({
|
|||
};
|
||||
|
||||
const getPreview = () => {
|
||||
if (!enablePreview) return;
|
||||
const [preview, error] = resolveReferences(currentValue, realState, null, {}, true);
|
||||
const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1';
|
||||
|
||||
|
|
@ -181,7 +182,8 @@ export function CodeHinter({
|
|||
};
|
||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||
|
||||
const defaultClassName = className === 'query-hinter' || undefined ? '' : 'code-hinter';
|
||||
const defaultClassName =
|
||||
className === 'query-hinter' || className === 'custom-component' || undefined ? '' : 'code-hinter';
|
||||
|
||||
const ElementToRender = AllElements[TypeMapping[type]];
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,43 @@ import _ from 'lodash';
|
|||
import Fuse from 'fuse.js';
|
||||
|
||||
export function getSuggestionKeys(currentState) {
|
||||
let suggestions = [];
|
||||
_.keys(currentState).forEach((key) => {
|
||||
_.keys(currentState[key]).forEach((key2) => {
|
||||
if (key === 'variables') {
|
||||
return suggestions.push(`${key}.${key2}`);
|
||||
const suggestionList = [];
|
||||
|
||||
const map = new Map();
|
||||
|
||||
const buildMap = (data, path = '') => {
|
||||
const keys = Object.keys(data);
|
||||
keys.forEach((key, index) => {
|
||||
const value = data[key];
|
||||
const _type = Object.prototype.toString.call(value).slice(8, -1);
|
||||
const prevType = map.get(path)?.type;
|
||||
|
||||
let newPath = '';
|
||||
if (path === '') {
|
||||
newPath = key;
|
||||
} else if (prevType === 'Array') {
|
||||
newPath = `${path}[${index}]`;
|
||||
} else {
|
||||
newPath = `${path}.${key}`;
|
||||
}
|
||||
|
||||
if (_type === 'Object') {
|
||||
map.set(newPath, { type: _type });
|
||||
buildMap(value, newPath);
|
||||
}
|
||||
if (_type === 'Array') {
|
||||
map.set(newPath, { type: _type });
|
||||
buildMap(value, newPath);
|
||||
} else {
|
||||
map.set(newPath, { type: _type });
|
||||
}
|
||||
_.keys(currentState[key][key2]).forEach((key3) => {
|
||||
suggestions.push(`${key}.${key2}.${key3}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
return suggestions;
|
||||
};
|
||||
|
||||
buildMap(currentState, '');
|
||||
map.forEach((__, key) => suggestionList.push(key));
|
||||
|
||||
return suggestionList;
|
||||
}
|
||||
|
||||
export function generateHints(word, suggestions) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import iframeContent from './iframe.html';
|
||||
|
||||
export const CustomComponent = (props) => {
|
||||
const { height, properties, styles, id, setExposedVariable, exposedVariables, fireEvent, dataQueries } = props;
|
||||
const { visibility } = styles;
|
||||
const { code, data } = properties;
|
||||
const [customProps, setCustomProps] = useState(data);
|
||||
const iFrameRef = useRef(null);
|
||||
const dataQueryRef = useRef(dataQueries);
|
||||
const customPropRef = useRef(data);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomProps(data);
|
||||
customPropRef.current = data;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(data)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(exposedVariables.data, customProps)) {
|
||||
setExposedVariable('data', customProps);
|
||||
sendMessageToIframe({ message: 'DATA_UPDATED' });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setExposedVariable, customProps, exposedVariables.data]);
|
||||
|
||||
useEffect(() => {
|
||||
sendMessageToIframe({ message: 'CODE_UPDATED' });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [code]);
|
||||
|
||||
useEffect(() => {
|
||||
dataQueryRef.current = dataQueries;
|
||||
}, [dataQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', (e) => {
|
||||
try {
|
||||
if (e.data.from === 'customComponent' && e.data.componentId === id) {
|
||||
if (e.data.message === 'UPDATE_DATA') {
|
||||
setCustomProps({ ...customPropRef.current, ...e.data.updatedObj });
|
||||
} else if (e.data.message === 'RUN_QUERY') {
|
||||
const filteredQuery = dataQueryRef.current.filter((query) => query.name === e.data.queryName);
|
||||
filteredQuery.length === 1 &&
|
||||
fireEvent('onTrigger', { queryId: filteredQuery[0].id, queryName: filteredQuery[0].name });
|
||||
} else {
|
||||
sendMessageToIframe(e.data);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const sendMessageToIframe = ({ message }) => {
|
||||
if (!iFrameRef.current) return;
|
||||
switch (message) {
|
||||
case 'INIT':
|
||||
return iFrameRef.current.contentWindow.postMessage(
|
||||
{
|
||||
message: 'INIT_RESPONSE',
|
||||
componentId: id,
|
||||
data: customProps,
|
||||
code: code,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
case 'CODE_UPDATED':
|
||||
return iFrameRef.current.contentWindow.postMessage(
|
||||
{
|
||||
message: 'CODE_UPDATED',
|
||||
componentId: id,
|
||||
data: customProps,
|
||||
code: code,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
case 'DATA_UPDATED':
|
||||
return iFrameRef.current.contentWindow.postMessage(
|
||||
{
|
||||
message: 'DATA_UPDATED',
|
||||
componentId: id,
|
||||
data: customProps,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card" style={{ display: visibility ? '' : 'none', height }}>
|
||||
<iframe
|
||||
srcDoc={iframeContent}
|
||||
style={{ width: '100%', height: '100%', border: 'none' }}
|
||||
ref={iFrameRef}
|
||||
data-id={id}
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
86
frontend/src/Editor/Components/CustomComponent/iframe.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.17.9/babel.min.js" integrity="sha384-q/mWU54AdnQn35rIhX7g2MtszBgXHwH9exPcvCVnncKy5WoKc457RNDNmm23Fag7" crossorigin="anonymous" referrerpolicy="no-referrer" data-required="true"></script>
|
||||
<script src="https://unpkg.com/react@16.7.0/umd/react.production.min.js" integrity="sha384-bDWFfmoLfqL0ZuPgUiUz3ekiv8NyiuJrrk1wGblri8Nut8UVD6mj7vXhjnenE9vy" crossorigin="anonymous" referrerpolicy="no-referrer" data-required="true"></script>
|
||||
<script src="https://unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js" integrity="sha384-mcyjbblFFAXUUcVbGLbJZR86Xd7La0uD1S7/Snd1tW0N+zhy97geTqVYDQ92c8tI" crossorigin="anonymous" referrerpolicy="no-referrer" data-required="true"></script>
|
||||
</head>
|
||||
<body style='margin: 0'>
|
||||
<script data-required="true">
|
||||
let callbackFn = () => {};
|
||||
let props = {};
|
||||
window.Tooljet = {
|
||||
componentId: window.frameElement.getAttribute('data-id'),
|
||||
subscribe: fn => {
|
||||
fn(props);
|
||||
callbackFn = fn;
|
||||
},
|
||||
runQuery: name => {
|
||||
window.parent.postMessage({
|
||||
from: 'customComponent',
|
||||
message: "RUN_QUERY",
|
||||
queryName: name,
|
||||
componentId: window.Tooljet.componentId
|
||||
}, "*")},
|
||||
updateProps: obj => window.parent.postMessage({
|
||||
from: 'customComponent',
|
||||
message: "UPDATE_DATA",
|
||||
updatedObj: obj,
|
||||
componentId: window.Tooljet.componentId
|
||||
}, "*"),
|
||||
init: () => {
|
||||
window.parent.postMessage({
|
||||
from: 'customComponent',
|
||||
message: "INIT",
|
||||
componentId: window.Tooljet.componentId,
|
||||
}, "*");
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
if(e.data.message === 'CODE_UPDATED' || e.data.message === 'INIT_RESPONSE'){
|
||||
const tags = document.getElementsByTagName("script");
|
||||
for(let i = 0; i < tags.length; i++){
|
||||
if(tags[i].getAttribute("data-required") !== 'true'){
|
||||
tags[i].parentNode.removeChild(tags[i]);
|
||||
i = i - 1;
|
||||
}
|
||||
}
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
script = document.createElement('script');
|
||||
script.text = e.data.code
|
||||
script.type = "text/babel";
|
||||
script.setAttribute("data-type", "module");
|
||||
head.appendChild(script)
|
||||
window.dispatchEvent(new Event('DOMContentLoaded'));
|
||||
props = e.data.data;
|
||||
callbackFn(e.data.data)
|
||||
} else if(e.data.message === 'DATA_UPDATED'){
|
||||
props = e.data.data;
|
||||
callbackFn(e.data.data)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
window.addEventListener('load', function() {
|
||||
window.Tooljet.init();
|
||||
})
|
||||
</script>
|
||||
<script type="text/babel" data-required="true"> window.Tooljet.connectComponent = WrappedComponent => {
|
||||
class ConnectedComponent extends React.Component {
|
||||
constructor() {
|
||||
super(), this.state = {}
|
||||
}
|
||||
componentDidMount() {
|
||||
window.Tooljet.subscribe((e => this.setState({
|
||||
data: e
|
||||
})))
|
||||
}
|
||||
render() {
|
||||
return <WrappedComponent data={this.state?.data ?? {}}
|
||||
updateData={(e) => Tooljet.updateProps(e)}
|
||||
runQuery={(e) => Tooljet.runQuery(e)}/>
|
||||
}
|
||||
}
|
||||
return ConnectedComponent;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
93
frontend/src/Editor/Components/PDF.jsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { Document, Page } from 'react-pdf/dist/esm/entry.webpack';
|
||||
|
||||
export const PDF = React.memo(({ styles, properties, width, height }) => {
|
||||
const { visibility } = styles;
|
||||
const { url, scale, pageControls } = properties;
|
||||
const [numPages, setNumPages] = useState(null);
|
||||
const [pageNumber, setPageNumber] = useState(null);
|
||||
const pageRef = useRef([]);
|
||||
|
||||
const onDocumentLoadSuccess = (document) => {
|
||||
const { numPages: nextNumPages } = document;
|
||||
setNumPages(nextNumPages);
|
||||
setPageNumber(1);
|
||||
};
|
||||
|
||||
const options = {
|
||||
root: document.querySelector('#pdf-wrapper'),
|
||||
rootMargin: '0px',
|
||||
threshold: 0.5,
|
||||
};
|
||||
|
||||
const trackIntersection = (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const currentPage = parseInt(entry.target.getAttribute('data-page-number'));
|
||||
if (pageNumber !== currentPage) setPageNumber(currentPage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (numPages === 0 || numPages === null) return;
|
||||
const observer = new IntersectionObserver(trackIntersection, options);
|
||||
document.querySelectorAll('.react-pdf__Page').forEach((elem) => {
|
||||
if (elem) observer.observe(elem);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [numPages, options]);
|
||||
|
||||
const updatePage = useCallback(
|
||||
(offset) => {
|
||||
pageRef.current[pageNumber + offset - 1].scrollIntoView({ block: 'nearest' });
|
||||
setPageNumber((prevPageNumber) => (prevPageNumber || 1) + offset);
|
||||
},
|
||||
[pageNumber]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display: visibility ? 'flex' : 'none', width: width - 3, height }}>
|
||||
<div className="d-flex position-relative h-100" style={{ margin: '0 auto', overflow: 'hidden' }}>
|
||||
<div className="scrollable h-100 col position-relative" id="pdf-wrapper">
|
||||
<Document file={url} onLoadSuccess={onDocumentLoadSuccess} className="pdf-document">
|
||||
{Array.from(new Array(numPages), (el, index) => (
|
||||
<Page
|
||||
pageNumber={index + 1}
|
||||
width={scale ? width - 12 : undefined}
|
||||
height={scale ? undefined : height}
|
||||
key={`page_${index + 1}`}
|
||||
inputRef={(el) => (pageRef.current[index] = el)}
|
||||
/>
|
||||
))}
|
||||
{pageControls && (
|
||||
<>
|
||||
<div className="pdf-page-controls">
|
||||
<button
|
||||
disabled={pageNumber <= 1}
|
||||
onClick={() => updatePage(-1)}
|
||||
type="button"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<span>
|
||||
{pageNumber} of {numPages}
|
||||
</span>
|
||||
<button
|
||||
disabled={pageNumber >= numPages}
|
||||
onClick={() => updatePage(1)}
|
||||
type="button"
|
||||
aria-label="Next page"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Document>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
import React from 'react';
|
||||
export const Statistics = function Statistics({ height, properties, styles, darkMode }) {
|
||||
const { primaryValueLabel, primaryValue, secondaryValueLabel, secondaryValue, secondarySignDisplay, hideSecondary } =
|
||||
properties;
|
||||
export const Statistics = function Statistics({ width, height, properties, styles, darkMode }) {
|
||||
const {
|
||||
primaryValueLabel,
|
||||
primaryValue,
|
||||
secondaryValueLabel,
|
||||
secondaryValue,
|
||||
secondarySignDisplay,
|
||||
hideSecondary,
|
||||
loadingState,
|
||||
} = properties;
|
||||
const { primaryLabelColour, primaryTextColour, secondaryLabelColour, secondaryTextColour, visibility } = styles;
|
||||
|
||||
const baseStyle = {
|
||||
|
|
@ -64,45 +71,55 @@ export const Statistics = function Statistics({ height, properties, styles, dark
|
|||
|
||||
return (
|
||||
<div style={baseStyle}>
|
||||
<p
|
||||
style={{
|
||||
...letterStyle,
|
||||
...marginStyle,
|
||||
color: primaryLabelColour !== '#8092AB' ? primaryLabelColour : darkMode && '#FFFFFC',
|
||||
}}
|
||||
>
|
||||
{primaryValueLabel}
|
||||
</p>
|
||||
<h2 style={primaryStyle}>{primaryValue}</h2>
|
||||
{hideSecondary ? (
|
||||
''
|
||||
{loadingState === true ? (
|
||||
<div style={{ width }} className="p-2">
|
||||
<center>
|
||||
<div className="spinner-border" role="status"></div>
|
||||
</center>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="d-flex flex-row justify-content-center align-items-baseline">
|
||||
{secondarySignDisplay !== 'negative' ? (
|
||||
<img
|
||||
src="/assets/images/icons/widgets/upstatistics.svg"
|
||||
style={{ ...marginStyle, marginRight: '6.5px' }}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/assets/images/icons/widgets/downstatistics.svg"
|
||||
style={{ ...marginStyle, marginRight: '6.5px' }}
|
||||
/>
|
||||
)}
|
||||
<p style={{ ...secondaryContainerStyle }}>{secondaryValue}</p>
|
||||
</div>
|
||||
<>
|
||||
<p
|
||||
style={{
|
||||
...letterStyle,
|
||||
color: secondaryLabelColour !== '#8092AB' ? secondaryLabelColour : darkMode && '#FFFFFC',
|
||||
padding: '6px 20px 12px 20px ',
|
||||
marginBottom: '0px',
|
||||
...marginStyle,
|
||||
color: primaryLabelColour !== '#8092AB' ? primaryLabelColour : darkMode && '#FFFFFC',
|
||||
}}
|
||||
>
|
||||
{secondaryValueLabel}
|
||||
{primaryValueLabel}
|
||||
</p>
|
||||
</div>
|
||||
<h2 style={primaryStyle}>{primaryValue}</h2>
|
||||
{hideSecondary ? (
|
||||
''
|
||||
) : (
|
||||
<div>
|
||||
<div className="d-flex flex-row justify-content-center align-items-baseline">
|
||||
{secondarySignDisplay !== 'negative' ? (
|
||||
<img
|
||||
src="/assets/images/icons/widgets/upstatistics.svg"
|
||||
style={{ ...marginStyle, marginRight: '6.5px' }}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/assets/images/icons/widgets/downstatistics.svg"
|
||||
style={{ ...marginStyle, marginRight: '6.5px' }}
|
||||
/>
|
||||
)}
|
||||
<p style={{ ...secondaryContainerStyle }}>{secondaryValue}</p>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
...letterStyle,
|
||||
color: secondaryLabelColour !== '#8092AB' ? secondaryLabelColour : darkMode && '#FFFFFC',
|
||||
padding: '6px 20px 12px 20px ',
|
||||
marginBottom: '0px',
|
||||
}}
|
||||
>
|
||||
{secondaryValueLabel}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import DOMPurify from 'dompurify';
|
|||
export const Text = function Text({ height, properties, styles, darkMode }) {
|
||||
const [loadingState, setLoadingState] = useState(false);
|
||||
|
||||
const { textColor, textAlign, visibility, disabledState } = styles;
|
||||
const { textSize, textColor, textAlign, visibility, disabledState } = styles;
|
||||
|
||||
const text = properties.text === 0 || properties.text === false ? properties.text?.toString() : properties.text;
|
||||
|
||||
|
|
@ -27,7 +27,10 @@ export const Text = function Text({ height, properties, styles, darkMode }) {
|
|||
return (
|
||||
<div data-disabled={disabledState} className="text-widget" style={computedStyles}>
|
||||
{!loadingState && (
|
||||
<div style={{ width: '100%' }} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text) }} />
|
||||
<div
|
||||
style={{ width: '100%', fontSize: textSize }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text) }}
|
||||
/>
|
||||
)}
|
||||
{loadingState === true && (
|
||||
<div style={{ width: '100%' }}>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ export const Timeline = function Timeline({ height, darkMode, properties, styles
|
|||
const darkModeStyle = darkMode && 'text-white-50';
|
||||
|
||||
return (
|
||||
<div className="card" style={{ display: visibility ? '' : 'none', height }}>
|
||||
<div
|
||||
className="card"
|
||||
style={{ display: visibility ? '' : 'none', height, overflow: 'auto', overflowWrap: 'normal' }}
|
||||
>
|
||||
<div className="card-body">
|
||||
<ul className={`list list-timeline ${hideDate && 'list-timeline-simple'}`}>
|
||||
{(isArray(data) ? data : []).map((item, index) => (
|
||||
|
|
|
|||
|
|
@ -762,6 +762,7 @@ export const componentTypes = [
|
|||
},
|
||||
events: [],
|
||||
styles: {
|
||||
textSize: { type: 'number', displayName: 'Text Size' },
|
||||
textColor: { type: 'color', displayName: 'Text Color' },
|
||||
textAlign: { type: 'alignButtons', displayName: 'Align Text' },
|
||||
visibility: { type: 'toggle', displayName: 'Visibility' },
|
||||
|
|
@ -781,6 +782,7 @@ export const componentTypes = [
|
|||
events: [],
|
||||
styles: {
|
||||
groupActions: { value: 'left' },
|
||||
textSize: { value: 14 },
|
||||
textColor: { value: '#000' },
|
||||
textAlign: { value: 'left' },
|
||||
visibility: { value: '{{true}}' },
|
||||
|
|
@ -1854,6 +1856,7 @@ export const componentTypes = [
|
|||
secondaryValueLabel: { type: 'code', displayName: 'Secondary value label' },
|
||||
secondaryValue: { type: 'code', displayName: 'Secondary value' },
|
||||
secondarySignDisplay: { type: 'code', displayName: 'Secondary sign display' },
|
||||
loadingState: { type: 'toggle', displayName: 'Loading State' },
|
||||
},
|
||||
events: {},
|
||||
styles: {
|
||||
|
|
@ -1874,6 +1877,7 @@ export const componentTypes = [
|
|||
secondaryValueLabel: { value: 'Last month' },
|
||||
secondaryValue: { value: '2.85' },
|
||||
secondarySignDisplay: { value: 'positive' },
|
||||
loadingState: { value: `{{false}}` },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
|
|
@ -2058,4 +2062,110 @@ export const componentTypes = [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CustomComponent',
|
||||
displayName: 'Custom Component',
|
||||
description: 'Visual representation of a sequence of events',
|
||||
component: 'CustomComponent',
|
||||
properties: {
|
||||
data: { type: 'code', displayName: 'Data' },
|
||||
code: { type: 'code', displayName: 'Code' },
|
||||
},
|
||||
defaultSize: {
|
||||
width: 20,
|
||||
height: 140,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
events: {},
|
||||
styles: {
|
||||
visibility: { type: 'toggle', displayName: 'Visibility' },
|
||||
},
|
||||
exposedVariables: {
|
||||
data: { value: `{{{ title: 'Hi! There', buttonText: 'Update Title'}}}` },
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
visible: { value: '{{true}}' },
|
||||
data: {
|
||||
value: `{{{ title: 'Hi! There', buttonText: 'Update Title'}}}`,
|
||||
},
|
||||
code: {
|
||||
value: `import React from 'https://cdn.skypack.dev/react';
|
||||
import ReactDOM from 'https://cdn.skypack.dev/react-dom';
|
||||
import { Button, Container } from 'https://cdn.skypack.dev/@material-ui/core';
|
||||
const MyCustomComponent = ({data, updateData, runQuery}) => (
|
||||
<Container>
|
||||
<h1>{data.title}</h1>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
onClick={() => {updateData({title: 'Hello World!!'})}}
|
||||
>
|
||||
{data.buttonText}
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
const ConnectedComponent = Tooljet.connectComponent(MyCustomComponent);
|
||||
ReactDOM.render(<ConnectedComponent />, document.body);`,
|
||||
},
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
displayName: 'PDF',
|
||||
description: 'Embed PDF file',
|
||||
component: 'PDF',
|
||||
properties: {
|
||||
url: { type: 'code', displayName: 'File URL' },
|
||||
scale: { type: 'toggle', displayName: 'Scale page to width' },
|
||||
pageControls: { type: 'toggle', displayName: 'Show page controls' },
|
||||
},
|
||||
defaultSize: {
|
||||
width: 20,
|
||||
height: 640,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
events: {},
|
||||
styles: {
|
||||
visibility: { type: 'toggle', displayName: 'Visibility' },
|
||||
},
|
||||
exposedVariables: {},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
url: {
|
||||
value:
|
||||
'https://upload.wikimedia.org/wikipedia/commons/e/ee/Guideline_No._GD-Ed-2214_Marman_Clamp_Systems_Design_Guidelines.pdf',
|
||||
},
|
||||
scale: {
|
||||
value: '{{true}}',
|
||||
},
|
||||
pageControls: {
|
||||
value: `{{true}}`,
|
||||
},
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const ConfigHandle = function ConfigHandle({
|
|||
position,
|
||||
widgetTop,
|
||||
widgetHeight,
|
||||
isMultipleComponentsSelected = false,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -24,7 +25,7 @@ export const ConfigHandle = function ConfigHandle({
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setSelectedComponent(id, component);
|
||||
setSelectedComponent(id, component, e.shiftKey);
|
||||
}}
|
||||
role="button"
|
||||
>
|
||||
|
|
@ -37,17 +38,19 @@ export const ConfigHandle = function ConfigHandle({
|
|||
/>
|
||||
<span>{component.name}</span>
|
||||
</div>
|
||||
<div className="delete-part">
|
||||
<img
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
src="/assets/images/icons/trash-light.svg"
|
||||
width="12"
|
||||
role="button"
|
||||
height="12"
|
||||
draggable="false"
|
||||
onClick={() => removeComponent({ id })}
|
||||
/>
|
||||
</div>
|
||||
{!isMultipleComponentsSelected && (
|
||||
<div className="delete-part">
|
||||
<img
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
src="/assets/images/icons/trash-light.svg"
|
||||
width="12"
|
||||
role="button"
|
||||
height="12"
|
||||
draggable="false"
|
||||
onClick={() => removeComponent({ id })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable import/no-named-as-default */
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
|
@ -14,9 +15,11 @@ import { commentsService } from '@/_services';
|
|||
import config from 'config';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import produce from 'immer';
|
||||
|
||||
export const Container = ({
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
mode,
|
||||
snapToGrid,
|
||||
onComponentClick,
|
||||
|
|
@ -32,7 +35,7 @@ export const Container = ({
|
|||
currentLayout,
|
||||
removeComponent,
|
||||
deviceWindowWidth,
|
||||
selectedComponent,
|
||||
selectedComponents,
|
||||
darkMode,
|
||||
showComments,
|
||||
appVersionsId,
|
||||
|
|
@ -41,11 +44,13 @@ export const Container = ({
|
|||
handleRedo,
|
||||
onComponentHover,
|
||||
hoveredComponent,
|
||||
dataQueries,
|
||||
}) => {
|
||||
const styles = {
|
||||
width: currentLayout === 'mobile' ? deviceWindowWidth : '100%',
|
||||
height: 2400,
|
||||
height: '100%',
|
||||
maxWidth: `${canvasWidth}px`,
|
||||
maxHeight: `${canvasHeight}px`,
|
||||
position: 'absolute',
|
||||
backgroundSize: `${canvasWidth / 43}px 10px`,
|
||||
};
|
||||
|
|
@ -208,7 +213,7 @@ export const Container = ({
|
|||
);
|
||||
|
||||
function onDragStop(e, componentId, direction, currentLayout) {
|
||||
const id = componentId ? componentId : uuidv4();
|
||||
// const id = componentId ? componentId : uuidv4();
|
||||
|
||||
// Get the width of the canvas
|
||||
const canvasBounds = document.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
|
||||
|
|
@ -217,25 +222,24 @@ export const Container = ({
|
|||
|
||||
// Computing the left offset
|
||||
const leftOffset = nodeBounds.x - canvasBounds.x;
|
||||
const left = convertXToPercentage(leftOffset, canvasWidth);
|
||||
const currentLeftOffset = boxes[componentId].layouts[currentLayout].left;
|
||||
const leftDiff = currentLeftOffset - convertXToPercentage(leftOffset, canvasWidth);
|
||||
|
||||
// Computing the top offset
|
||||
const top = nodeBounds.y - canvasBounds.y;
|
||||
// const currentTopOffset = boxes[componentId].layouts[currentLayout].top;
|
||||
const topDiff = boxes[componentId].layouts[currentLayout].top - (nodeBounds.y - canvasBounds.y);
|
||||
|
||||
let newBoxes = {
|
||||
...boxes,
|
||||
[id]: {
|
||||
...boxes[id],
|
||||
layouts: {
|
||||
...boxes[id]['layouts'],
|
||||
[currentLayout]: {
|
||||
...boxes[id]['layouts'][currentLayout],
|
||||
top: top,
|
||||
left: left,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let newBoxes = { ...boxes };
|
||||
|
||||
for (const selectedComponent of selectedComponents) {
|
||||
newBoxes = produce(newBoxes, (draft) => {
|
||||
const topOffset = draft[selectedComponent.id].layouts[currentLayout].top;
|
||||
const leftOffset = draft[selectedComponent.id].layouts[currentLayout].left;
|
||||
|
||||
draft[selectedComponent.id].layouts[currentLayout].top = topOffset - topDiff;
|
||||
draft[selectedComponent.id].layouts[currentLayout].left = leftOffset - leftDiff;
|
||||
});
|
||||
}
|
||||
|
||||
setBoxes(newBoxes);
|
||||
}
|
||||
|
|
@ -307,7 +311,7 @@ export const Container = ({
|
|||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {}, [selectedComponent]);
|
||||
React.useEffect(() => {}, [selectedComponents]);
|
||||
|
||||
const handleAddThread = async (e) => {
|
||||
e.stopPropogation && e.stopPropogation();
|
||||
|
|
@ -463,10 +467,14 @@ export const Container = ({
|
|||
removeComponent={removeComponent}
|
||||
currentLayout={currentLayout}
|
||||
deviceWindowWidth={deviceWindowWidth}
|
||||
isSelectedComponent={selectedComponent ? selectedComponent.id === key : false}
|
||||
isSelectedComponent={
|
||||
mode === 'edit' ? selectedComponents.find((component) => component.id === key) : false
|
||||
}
|
||||
darkMode={darkMode}
|
||||
onComponentHover={onComponentHover}
|
||||
hoveredComponent={hoveredComponent}
|
||||
isMultipleComponentsSelected={selectedComponents?.length > 1 ? true : false}
|
||||
dataQueries={dataQueries}
|
||||
containerProps={{
|
||||
mode,
|
||||
snapToGrid,
|
||||
|
|
@ -483,10 +491,11 @@ export const Container = ({
|
|||
removeComponent,
|
||||
currentLayout,
|
||||
deviceWindowWidth,
|
||||
selectedComponent,
|
||||
selectedComponents,
|
||||
darkMode,
|
||||
onComponentHover,
|
||||
hoveredComponent,
|
||||
dataQueries,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ export const DraggableBox = function DraggableBox({
|
|||
parentId,
|
||||
hoveredComponent,
|
||||
onComponentHover,
|
||||
isMultipleComponentsSelected,
|
||||
dataQueries,
|
||||
}) {
|
||||
const [isResizing, setResizing] = useState(false);
|
||||
const [isDragging2, setDragging] = useState(false);
|
||||
|
|
@ -219,7 +221,7 @@ export const DraggableBox = function DraggableBox({
|
|||
mouseOver || isResizing || isDragging2 || isSelectedComponent ? 'resizer-active' : ''
|
||||
} `}
|
||||
onResize={() => setResizing(true)}
|
||||
onDrag={(e) => {
|
||||
onDrag={(e, direction) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
if (!isDragging2) {
|
||||
|
|
@ -251,7 +253,10 @@ export const DraggableBox = function DraggableBox({
|
|||
position={currentLayoutOptions.top < 15 ? 'bottom' : 'top'}
|
||||
widgetTop={currentLayoutOptions.top}
|
||||
widgetHeight={currentLayoutOptions.height}
|
||||
setSelectedComponent={(id, component) => setSelectedComponent(id, component)}
|
||||
setSelectedComponent={(id, component, multiSelect) =>
|
||||
setSelectedComponent(id, component, multiSelect)
|
||||
}
|
||||
isMultipleComponentsSelected={isMultipleComponentsSelected}
|
||||
/>
|
||||
)}
|
||||
<ErrorBoundary showFallback={mode === 'edit'}>
|
||||
|
|
@ -278,6 +283,7 @@ export const DraggableBox = function DraggableBox({
|
|||
parentId={parentId}
|
||||
allComponents={allComponents}
|
||||
extraProps={extraProps}
|
||||
dataQueries={dataQueries}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
|||
import { computeComponentName } from '@/_helpers/utils';
|
||||
import { defaults, cloneDeep, isEqual, isEmpty, debounce } from 'lodash';
|
||||
import { Container } from './Container';
|
||||
import { EditorKeyHooks } from './EditorKeyHooks';
|
||||
import { CustomDragLayer } from './CustomDragLayer';
|
||||
import { LeftSidebar } from './LeftSidebar';
|
||||
import { componentTypes } from './Components/components';
|
||||
|
|
@ -41,6 +42,7 @@ import RunjsIcon from './Icons/runjs.svg';
|
|||
import EditIcon from './Icons/edit.svg';
|
||||
import MobileSelectedIcon from './Icons/mobile-selected.svg';
|
||||
import DesktopSelectedIcon from './Icons/desktop-selected.svg';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { AppVersionsManager } from './AppVersionsManager';
|
||||
import { SearchBoxComponent } from '@/_ui/Search';
|
||||
import { createWebsocketConnection } from '@/_helpers/websocketConnection';
|
||||
|
|
@ -82,6 +84,7 @@ class Editor extends React.Component {
|
|||
hideHeader: false,
|
||||
appInMaintenance: false,
|
||||
canvasMaxWidth: 1292,
|
||||
canvasMaxHeight: 2400,
|
||||
canvasBackgroundColor: props.darkMode ? '#2f3c4c' : '#edeff5',
|
||||
},
|
||||
};
|
||||
|
|
@ -126,6 +129,8 @@ class Editor extends React.Component {
|
|||
showInitVersionCreateModal: false,
|
||||
showCreateVersionModalPrompt: false,
|
||||
isSourceSelected: false,
|
||||
isSaving: false,
|
||||
saveError: false,
|
||||
};
|
||||
|
||||
this.autoSave = debounce(this.saveEditingVersion, 3000);
|
||||
|
|
@ -144,7 +149,7 @@ class Editor extends React.Component {
|
|||
this.initEventListeners();
|
||||
this.setState({
|
||||
currentSidebarTab: 2,
|
||||
selectedComponent: null,
|
||||
selectedComponents: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +174,12 @@ class Editor extends React.Component {
|
|||
if (!isEqual(prevState.appDefinition, this.state.appDefinition)) {
|
||||
computeComponentState(this, this.state.appDefinition.components);
|
||||
}
|
||||
|
||||
if (config.ENABLE_MULTIPLAYER_EDITING) {
|
||||
if (this.props.othersOnSameVersion.length !== prevProps.othersOnSameVersion.length) {
|
||||
ReactTooltip.rebuild();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isVersionReleased = (version = this.state.editingVersion) => {
|
||||
|
|
@ -179,7 +190,7 @@ class Editor extends React.Component {
|
|||
};
|
||||
|
||||
closeCreateVersionModalPrompt = () => {
|
||||
this.setState({ showCreateVersionModalPrompt: false });
|
||||
this.setState({ isSaving: false, showCreateVersionModalPrompt: false });
|
||||
};
|
||||
|
||||
onMouseMove = (e) => {
|
||||
|
|
@ -389,6 +400,7 @@ class Editor extends React.Component {
|
|||
});
|
||||
this.setState({
|
||||
editingVersion: version,
|
||||
isSaving: false,
|
||||
});
|
||||
|
||||
this.fetchDataSources();
|
||||
|
|
@ -425,9 +437,6 @@ class Editor extends React.Component {
|
|||
};
|
||||
|
||||
switchSidebarTab = (tabIndex) => {
|
||||
if (tabIndex === 2) {
|
||||
this.setState({ selectedComponent: null });
|
||||
}
|
||||
this.setState({
|
||||
currentSidebarTab: tabIndex,
|
||||
});
|
||||
|
|
@ -525,26 +534,55 @@ class Editor extends React.Component {
|
|||
},
|
||||
this.handleAddPatch
|
||||
);
|
||||
this.setState({ appDefinition: newDefinition }, () => {
|
||||
this.setState({ isSaving: true, appDefinition: newDefinition }, () => {
|
||||
if (!opts.skipAutoSave) this.autoSave();
|
||||
});
|
||||
computeComponentState(this, newDefinition.components);
|
||||
};
|
||||
|
||||
handleInspectorView = (component) => {
|
||||
if (this.state.selectedComponent?.hasOwnProperty('component')) {
|
||||
const { id: selectedComponentId } = this.state.selectedComponent;
|
||||
if (selectedComponentId === component.id) {
|
||||
this.setState({ selectedComponent: null });
|
||||
this.switchSidebarTab(2);
|
||||
}
|
||||
}
|
||||
handleInspectorView = () => {
|
||||
this.switchSidebarTab(2);
|
||||
};
|
||||
|
||||
handleSlugChange = (newSlug) => {
|
||||
this.setState({ slug: newSlug });
|
||||
};
|
||||
|
||||
removeComponents = () => {
|
||||
if (!this.isVersionReleased() && this.state?.selectedComponents?.length > 1) {
|
||||
let newDefinition = cloneDeep(this.state.appDefinition);
|
||||
const selectedComponents = this.state?.selectedComponents;
|
||||
|
||||
selectedComponents.forEach((component) => {
|
||||
let childComponents = [];
|
||||
|
||||
if (newDefinition.components[component.id].component.component === 'Tabs') {
|
||||
childComponents = Object.keys(newDefinition.components).filter((key) =>
|
||||
newDefinition.components[key].parent?.startsWith(component.id)
|
||||
);
|
||||
} else {
|
||||
childComponents = Object.keys(newDefinition.components).filter(
|
||||
(key) => newDefinition.components[key].parent === component.id
|
||||
);
|
||||
}
|
||||
|
||||
childComponents.forEach((componentId) => {
|
||||
delete newDefinition.components[componentId];
|
||||
});
|
||||
|
||||
delete newDefinition.components[component.id];
|
||||
});
|
||||
|
||||
toast('Selected components deleted! (⌘Z to undo)', {
|
||||
icon: '🗑️',
|
||||
});
|
||||
this.appDefinitionChanged(newDefinition, {
|
||||
skipAutoSave: this.isVersionReleased(),
|
||||
});
|
||||
this.handleInspectorView();
|
||||
}
|
||||
};
|
||||
|
||||
removeComponent = (component) => {
|
||||
if (!this.isVersionReleased()) {
|
||||
let newDefinition = cloneDeep(this.state.appDefinition);
|
||||
|
|
@ -573,7 +611,7 @@ class Editor extends React.Component {
|
|||
this.appDefinitionChanged(newDefinition, {
|
||||
skipAutoSave: this.isVersionReleased(),
|
||||
});
|
||||
this.handleInspectorView(component);
|
||||
this.handleInspectorView();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -595,6 +633,7 @@ class Editor extends React.Component {
|
|||
);
|
||||
setStateAsync(_self, newDefinition).then(() => {
|
||||
computeComponentState(_self, _self.state.appDefinition.components);
|
||||
this.setState({ isSaving: true });
|
||||
this.autoSave();
|
||||
this.props.ymap?.set('appDef', {
|
||||
newDefinition: newDefinition.appDefinition,
|
||||
|
|
@ -603,6 +642,47 @@ class Editor extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
handleEditorEscapeKeyPress = () => {
|
||||
if (this.state?.selectedComponents?.length > 0) {
|
||||
this.setState({ selectedComponents: [] });
|
||||
this.handleInspectorView();
|
||||
}
|
||||
};
|
||||
|
||||
moveComponents = (direction) => {
|
||||
let appDefinition = JSON.parse(JSON.stringify(this.state.appDefinition));
|
||||
let newComponents = appDefinition.components;
|
||||
|
||||
for (const selectedComponent of this.state.selectedComponents) {
|
||||
newComponents = produce(newComponents, (draft) => {
|
||||
let top = draft[selectedComponent.id].layouts[this.state.currentLayout].top;
|
||||
let left = draft[selectedComponent.id].layouts[this.state.currentLayout].left;
|
||||
|
||||
const gridWidth = (1 * 100) / 43; // width of the canvas grid in percentage
|
||||
|
||||
switch (direction) {
|
||||
case 'ArrowLeft':
|
||||
left = left - gridWidth;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
left = left + gridWidth;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
top = top + 10;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
top = top - 10;
|
||||
break;
|
||||
}
|
||||
|
||||
draft[selectedComponent.id].layouts[this.state.currentLayout].top = top;
|
||||
draft[selectedComponent.id].layouts[this.state.currentLayout].left = left;
|
||||
});
|
||||
}
|
||||
appDefinition.components = newComponents;
|
||||
this.appDefinitionChanged(appDefinition);
|
||||
};
|
||||
|
||||
cloneComponent = (newComponent) => {
|
||||
const appDefinition = JSON.parse(JSON.stringify(this.state.appDefinition));
|
||||
|
||||
|
|
@ -617,6 +697,7 @@ class Editor extends React.Component {
|
|||
appDefinition.globalSettings[key] = value;
|
||||
this.setState(
|
||||
{
|
||||
isSaving: true,
|
||||
appDefinition,
|
||||
},
|
||||
() => {
|
||||
|
|
@ -811,9 +892,22 @@ class Editor extends React.Component {
|
|||
this.setState({ showComments: !this.state.showComments });
|
||||
};
|
||||
|
||||
setSelectedComponent = (id, component) => {
|
||||
this.switchSidebarTab(1);
|
||||
this.setState({ selectedComponent: { id, component } });
|
||||
setSelectedComponent = (id, component, multiSelect = false) => {
|
||||
if (this.state.selectedComponents.length === 0 || !multiSelect) {
|
||||
this.switchSidebarTab(1);
|
||||
} else {
|
||||
this.switchSidebarTab(2);
|
||||
}
|
||||
|
||||
const isAlreadySelected = this.state.selectedComponents.find((component) => component.id === id);
|
||||
|
||||
if (!isAlreadySelected) {
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
selectedComponents: [...(multiSelect ? prevState.selectedComponents : []), { id, component }],
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
filterQueries = (value) => {
|
||||
|
|
@ -867,6 +961,11 @@ class Editor extends React.Component {
|
|||
return canvasBoundingRect?.width;
|
||||
};
|
||||
|
||||
getCanvasHeight = () => {
|
||||
const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0].getBoundingClientRect();
|
||||
return canvasBoundingRect?.height;
|
||||
};
|
||||
|
||||
renderLayoutIcon = (isDesktopSelected) => {
|
||||
if (isDesktopSelected)
|
||||
return (
|
||||
|
|
@ -896,21 +995,31 @@ class Editor extends React.Component {
|
|||
|
||||
saveEditingVersion = () => {
|
||||
if (this.isVersionReleased()) {
|
||||
this.setState({ showCreateVersionModalPrompt: true });
|
||||
this.setState({ isSaving: false, showCreateVersionModalPrompt: true });
|
||||
} else if (!isEmpty(this.state.editingVersion)) {
|
||||
toast.promise(appVersionService.save(this.state.appId, this.state.editingVersion.id, this.state.appDefinition), {
|
||||
loading: 'Saving...',
|
||||
success: () => {
|
||||
this.setState({
|
||||
editingVersion: {
|
||||
...this.state.editingVersion,
|
||||
...{ definition: this.state.appDefinition },
|
||||
appVersionService
|
||||
.save(this.state.appId, this.state.editingVersion.id, this.state.appDefinition)
|
||||
.then(() => {
|
||||
this.setState(
|
||||
{
|
||||
saveError: false,
|
||||
editingVersion: {
|
||||
...this.state.editingVersion,
|
||||
...{ definition: this.state.appDefinition },
|
||||
},
|
||||
},
|
||||
() => {
|
||||
this.setState({
|
||||
isSaving: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ saveError: true, isSaving: false }, () => {
|
||||
toast.error('App could not save.');
|
||||
});
|
||||
return 'Saved!';
|
||||
},
|
||||
error: 'App could not save.',
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -954,7 +1063,7 @@ class Editor extends React.Component {
|
|||
render() {
|
||||
const {
|
||||
currentSidebarTab,
|
||||
selectedComponent = {},
|
||||
selectedComponents = [],
|
||||
appDefinition,
|
||||
appId,
|
||||
slug,
|
||||
|
|
@ -1034,6 +1143,15 @@ class Editor extends React.Component {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={cx('autosave-indicator', {
|
||||
'autosave-indicator-saving': this.state.isSaving,
|
||||
'text-danger': this.state.saveError,
|
||||
'd-none': this.isVersionReleased(),
|
||||
})}
|
||||
>
|
||||
{this.state.isSaving ? <Spinner size="small" /> : 'All changes are saved'}
|
||||
</span>
|
||||
{config.ENABLE_MULTIPLAYER_EDITING && (
|
||||
<RealtimeAvatars
|
||||
updatePresence={this.props.updatePresence}
|
||||
|
|
@ -1056,7 +1174,9 @@ class Editor extends React.Component {
|
|||
<a
|
||||
href={appVersionPreviewLink}
|
||||
target="_blank"
|
||||
className={`btn btn-sm font-500 color-primary ${app?.current_version_id ? '' : 'disabled'}`}
|
||||
className={`btn btn-sm font-500 color-primary border-0 ${
|
||||
app?.current_version_id ? '' : 'disabled'
|
||||
}`}
|
||||
rel="noreferrer"
|
||||
>
|
||||
Preview
|
||||
|
|
@ -1109,7 +1229,7 @@ class Editor extends React.Component {
|
|||
appDefinition={{
|
||||
components: appDefinition.components,
|
||||
queries: dataQueries,
|
||||
selectedComponent: this.state?.selectedComponent,
|
||||
selectedComponent: selectedComponents ? selectedComponents[selectedComponents.length - 1] : {},
|
||||
}}
|
||||
setSelectedComponent={this.setSelectedComponent}
|
||||
removeComponent={this.removeComponent}
|
||||
|
|
@ -1123,7 +1243,7 @@ class Editor extends React.Component {
|
|||
style={{ transform: `scale(${zoomLevel})` }}
|
||||
onClick={(e) => {
|
||||
if (['real-canvas', 'modal'].includes(e.target.className)) {
|
||||
this.switchSidebarTab(2);
|
||||
this.setState({ selectedComponents: [], currentSidebarTab: 2 });
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -1131,7 +1251,9 @@ class Editor extends React.Component {
|
|||
className="canvas-area"
|
||||
style={{
|
||||
width: currentLayout === 'desktop' ? '100%' : '450px',
|
||||
minHeight: +this.state.appDefinition.globalSettings.canvasMaxHeight,
|
||||
maxWidth: +this.state.appDefinition.globalSettings.canvasMaxWidth,
|
||||
maxHeight: +this.state.appDefinition.globalSettings.canvasMaxHeight,
|
||||
backgroundColor: this.state.appDefinition.globalSettings.canvasBackgroundColor,
|
||||
}}
|
||||
>
|
||||
|
|
@ -1145,6 +1267,7 @@ class Editor extends React.Component {
|
|||
<>
|
||||
<Container
|
||||
canvasWidth={this.getCanvasWidth()}
|
||||
canvasHeight={this.getCanvasHeight()}
|
||||
socket={this.socket}
|
||||
showComments={showComments}
|
||||
appVersionsId={this.state?.editingVersion?.id}
|
||||
|
|
@ -1156,7 +1279,7 @@ class Editor extends React.Component {
|
|||
zoomLevel={zoomLevel}
|
||||
currentLayout={currentLayout}
|
||||
deviceWindowWidth={deviceWindowWidth}
|
||||
selectedComponent={selectedComponent}
|
||||
selectedComponents={selectedComponents}
|
||||
appLoading={isLoading}
|
||||
onEvent={this.handleEvent}
|
||||
onComponentOptionChanged={this.handleOnComponentOptionChanged}
|
||||
|
|
@ -1169,6 +1292,7 @@ class Editor extends React.Component {
|
|||
onComponentClick={this.handleComponentClick}
|
||||
onComponentHover={this.handleComponentHover}
|
||||
hoveredComponent={hoveredComponent}
|
||||
dataQueries={dataQueries}
|
||||
/>
|
||||
<CustomDragLayer
|
||||
snapToGrid={true}
|
||||
|
|
@ -1354,6 +1478,7 @@ class Editor extends React.Component {
|
|||
disabled: !this.canUndo,
|
||||
})}
|
||||
width="44"
|
||||
data-tip="undo"
|
||||
height="44"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
|
|
@ -1371,6 +1496,7 @@ class Editor extends React.Component {
|
|||
</svg>
|
||||
<svg
|
||||
title="redo"
|
||||
data-tip="redo"
|
||||
onClick={this.handleRedo}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cx('cursor-pointer icon icon-tabler icon-tabler-arrow-forward-up', {
|
||||
|
|
@ -1395,26 +1521,34 @@ class Editor extends React.Component {
|
|||
{this.renderLayoutIcon(currentLayout === 'desktop')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditorKeyHooks
|
||||
moveComponents={this.moveComponents}
|
||||
handleEditorEscapeKeyPress={this.handleEditorEscapeKeyPress}
|
||||
removeMultipleComponents={this.removeComponents}
|
||||
/>
|
||||
|
||||
{currentSidebarTab === 1 && (
|
||||
<div className="pages-container">
|
||||
{selectedComponent &&
|
||||
{selectedComponents.length === 1 &&
|
||||
!isEmpty(appDefinition.components) &&
|
||||
!isEmpty(appDefinition.components[selectedComponent.id]) ? (
|
||||
!isEmpty(appDefinition.components[selectedComponents[0].id]) ? (
|
||||
<Inspector
|
||||
cloneComponent={this.cloneComponent}
|
||||
moveComponents={this.moveComponents}
|
||||
componentDefinitionChanged={this.componentDefinitionChanged}
|
||||
dataQueries={dataQueries}
|
||||
removeComponent={this.removeComponent}
|
||||
selectedComponentId={selectedComponent.id}
|
||||
selectedComponentId={selectedComponents[0].id}
|
||||
currentState={currentState}
|
||||
allComponents={appDefinition.components}
|
||||
key={selectedComponent.id}
|
||||
key={selectedComponents[0].id}
|
||||
switchSidebarTab={this.switchSidebarTab}
|
||||
apps={apps}
|
||||
darkMode={this.props.darkMode}
|
||||
></Inspector>
|
||||
) : (
|
||||
<div className="mt-5 p-2">Please select a component to inspect</div>
|
||||
<center className="mt-5 p-2">Please select a component to inspect</center>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
21
frontend/src/Editor/EditorKeyHooks.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import useKeyHooks from '@/_hooks/useKeyHooks';
|
||||
|
||||
export const EditorKeyHooks = ({ moveComponents, handleEditorEscapeKeyPress, removeMultipleComponents }) => {
|
||||
const handleHotKeysCallback = (key) => {
|
||||
switch (key) {
|
||||
case 'Escape':
|
||||
handleEditorEscapeKeyPress();
|
||||
break;
|
||||
case 'Backspace':
|
||||
removeMultipleComponents();
|
||||
break;
|
||||
default:
|
||||
moveComponents(key);
|
||||
}
|
||||
};
|
||||
|
||||
useKeyHooks(['up, down, left, right', 'esc', 'backspace'], handleHotKeysCallback);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
81
frontend/src/Editor/Inspector/Components/CustomComponent.jsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import React from 'react';
|
||||
import { renderElement } from '../Utils';
|
||||
import { CodeHinter } from '../../CodeBuilder/CodeHinter';
|
||||
import Accordion from '@/_ui/Accordion';
|
||||
|
||||
export const CustomComponent = function CustomComponent({
|
||||
dataQueries,
|
||||
component,
|
||||
paramUpdated,
|
||||
componentMeta,
|
||||
components,
|
||||
darkMode,
|
||||
currentState,
|
||||
layoutPropertyChanged,
|
||||
}) {
|
||||
const code = component.component.definition.properties.code;
|
||||
const args = component.component.definition.properties.data;
|
||||
|
||||
let items = [];
|
||||
|
||||
items.push({
|
||||
title: 'Data',
|
||||
children: (
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={args.value ?? {}}
|
||||
theme={darkMode ? 'monokai' : 'base16-light'}
|
||||
onChange={(value) => paramUpdated({ name: 'data' }, 'value', value, 'properties')}
|
||||
componentName={`widget/${component.component.name}/data`}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
items.push({
|
||||
title: 'Code',
|
||||
children: (
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={code.value ?? {}}
|
||||
theme={darkMode ? 'monokai' : 'base16-light'}
|
||||
mode="jsx"
|
||||
lineNumbers
|
||||
className="custom-component"
|
||||
onChange={(value) => paramUpdated({ name: 'code' }, 'value', value, 'properties')}
|
||||
componentName={`widget/${component.component.name}/code`}
|
||||
enablePreview={false}
|
||||
height={400}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
items.push({
|
||||
title: 'Layout',
|
||||
isOpen: false,
|
||||
children: (
|
||||
<>
|
||||
{renderElement(
|
||||
component,
|
||||
componentMeta,
|
||||
layoutPropertyChanged,
|
||||
dataQueries,
|
||||
'showOnDesktop',
|
||||
'others',
|
||||
currentState,
|
||||
components
|
||||
)}
|
||||
{renderElement(
|
||||
component,
|
||||
componentMeta,
|
||||
layoutPropertyChanged,
|
||||
dataQueries,
|
||||
'showOnMobile',
|
||||
'others',
|
||||
currentState,
|
||||
components
|
||||
)}
|
||||
</>
|
||||
),
|
||||
});
|
||||
return <Accordion items={items} />;
|
||||
};
|
||||
|
|
@ -12,6 +12,7 @@ import { ConfirmDialog } from '@/_components';
|
|||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { DefaultComponent } from './Components/DefaultComponent';
|
||||
import { FilePicker } from './Components/FilePicker';
|
||||
import { CustomComponent } from './Components/CustomComponent';
|
||||
import useFocus from '@/_hooks/use-Focus';
|
||||
|
||||
export const Inspector = ({
|
||||
|
|
@ -288,6 +289,22 @@ export const Inspector = ({
|
|||
/>
|
||||
);
|
||||
|
||||
case 'CustomComponent':
|
||||
return (
|
||||
<CustomComponent
|
||||
layoutPropertyChanged={layoutPropertyChanged}
|
||||
component={component}
|
||||
paramUpdated={paramUpdated}
|
||||
dataQueries={dataQueries}
|
||||
componentMeta={componentMeta}
|
||||
currentState={currentState}
|
||||
darkMode={darkMode}
|
||||
eventsChanged={eventsChanged}
|
||||
apps={apps}
|
||||
allComponents={allComponents}
|
||||
/>
|
||||
);
|
||||
|
||||
default: {
|
||||
return (
|
||||
<DefaultComponent
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const LeftSidebarGlobalSettings = ({
|
|||
is_maintenance_on,
|
||||
}) => {
|
||||
const [open, trigger, content] = usePopover(false);
|
||||
const { hideHeader, canvasMaxWidth, canvasBackgroundColor } = globalSettings;
|
||||
const { hideHeader, canvasMaxWidth, canvasMaxHeight, canvasBackgroundColor } = globalSettings;
|
||||
const [showPicker, setShowPicker] = React.useState(false);
|
||||
const [showConfirmation, setConfirmationShow] = React.useState(false);
|
||||
const coverStyles = {
|
||||
|
|
@ -75,7 +75,7 @@ export const LeftSidebarGlobalSettings = ({
|
|||
<input
|
||||
type="text"
|
||||
className={`form-control form-control-sm`}
|
||||
placeholder={'Enter canvas max-width'}
|
||||
placeholder={'0'}
|
||||
onChange={(e) => {
|
||||
globalSettingsChanged('canvasMaxWidth', e.target.value);
|
||||
}}
|
||||
|
|
@ -85,6 +85,24 @@ export const LeftSidebarGlobalSettings = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex mb-3">
|
||||
<span className="w-full m-auto">Max height of canvas</span>
|
||||
<div className="position-relative">
|
||||
<div className="input-with-icon">
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control form-control-sm`}
|
||||
placeholder={'0'}
|
||||
onChange={(e) => {
|
||||
const height = e.target.value;
|
||||
if (!Number.isNaN(height) && height <= 2400) globalSettingsChanged('canvasMaxHeight', height);
|
||||
}}
|
||||
value={canvasMaxHeight}
|
||||
/>
|
||||
<span className="input-group-text">px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
<span className="w-full m-auto">Background color of canvas</span>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ class ManageAppUsers extends React.Component {
|
|||
|
||||
<Modal.Footer>
|
||||
<a href="/users" target="_blank" className="btn color-primary mt-3">
|
||||
Manage Organization Users
|
||||
Manage Users
|
||||
</a>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/* eslint-disable import/no-unresolved */
|
||||
import React from 'react';
|
||||
import config from 'config';
|
||||
import Avatar from '@/_ui/Avatar';
|
||||
import { useOthers } from 'y-presence';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { snapToGrid as doSnapToGrid } from './snapToGrid';
|
|||
import update from 'immutability-helper';
|
||||
import { componentTypes } from './Components/components';
|
||||
import { computeComponentName } from '@/_helpers/utils';
|
||||
import produce from 'immer';
|
||||
|
||||
export const SubContainer = ({
|
||||
mode,
|
||||
|
|
@ -35,6 +36,7 @@ export const SubContainer = ({
|
|||
listViewItemOptions,
|
||||
onComponentHover,
|
||||
hoveredComponent,
|
||||
selectedComponents,
|
||||
}) => {
|
||||
const [_containerCanvasWidth, setContainerCanvasWidth] = useState(0);
|
||||
|
||||
|
|
@ -251,9 +253,6 @@ export const SubContainer = ({
|
|||
}
|
||||
|
||||
function onDragStop(e, componentId, direction, currentLayout) {
|
||||
const id = componentId ? componentId : uuidv4();
|
||||
|
||||
// Get the width of the canvas
|
||||
const canvasWidth = getContainerCanvasWidth();
|
||||
const nodeBounds = direction.node.getBoundingClientRect();
|
||||
|
||||
|
|
@ -261,25 +260,24 @@ export const SubContainer = ({
|
|||
|
||||
// Computing the left offset
|
||||
const leftOffset = nodeBounds.x - canvasBounds.x;
|
||||
const left = convertXToPercentage(leftOffset, canvasWidth);
|
||||
const currentLeftOffset = boxes[componentId].layouts[currentLayout].left;
|
||||
const leftDiff = currentLeftOffset - convertXToPercentage(leftOffset, canvasWidth);
|
||||
|
||||
// Computing the top offset
|
||||
const top = nodeBounds.y - canvasBounds.y;
|
||||
const topDiff = boxes[componentId].layouts[currentLayout].top - (nodeBounds.y - canvasBounds.y);
|
||||
|
||||
let newBoxes = {
|
||||
...boxes,
|
||||
[id]: {
|
||||
...boxes[id],
|
||||
layouts: {
|
||||
...boxes[id]['layouts'],
|
||||
[currentLayout]: {
|
||||
...boxes[id]['layouts'][currentLayout],
|
||||
top: top,
|
||||
left: left,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let newBoxes = { ...boxes };
|
||||
|
||||
if (selectedComponents) {
|
||||
for (const selectedComponent of selectedComponents) {
|
||||
newBoxes = produce(newBoxes, (draft) => {
|
||||
const topOffset = draft[selectedComponent.id].layouts[currentLayout].top;
|
||||
const leftOffset = draft[selectedComponent.id].layouts[currentLayout].left;
|
||||
|
||||
draft[selectedComponent.id].layouts[currentLayout].top = topOffset - topDiff;
|
||||
draft[selectedComponent.id].layouts[currentLayout].left = leftOffset - leftDiff;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setBoxes(newBoxes);
|
||||
}
|
||||
|
|
@ -418,7 +416,7 @@ export const SubContainer = ({
|
|||
currentLayout={currentLayout}
|
||||
selectedComponent={selectedComponent}
|
||||
deviceWindowWidth={deviceWindowWidth}
|
||||
isSelectedComponent={selectedComponent ? selectedComponent.id === key : false}
|
||||
isSelectedComponent={mode === 'edit' ? selectedComponents.find((component) => component.id === key) : false}
|
||||
removeComponent={customRemoveComponent}
|
||||
canvasWidth={_containerCanvasWidth}
|
||||
readOnly={readOnly}
|
||||
|
|
@ -427,6 +425,7 @@ export const SubContainer = ({
|
|||
onComponentHover={onComponentHover}
|
||||
hoveredComponent={hoveredComponent}
|
||||
parentId={parentComponent?.name}
|
||||
isMultipleComponentsSelected={selectedComponents?.length > 1 ? true : false}
|
||||
containerProps={{
|
||||
mode,
|
||||
snapToGrid,
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ class Viewer extends React.Component {
|
|||
urlparams: JSON.parse(JSON.stringify(queryString.parse(this.props.location.search))),
|
||||
},
|
||||
},
|
||||
dataQueries: data.data_queries,
|
||||
},
|
||||
() => {
|
||||
computeComponentState(this, data?.definition?.components).then(() => {
|
||||
|
|
@ -185,6 +186,7 @@ class Viewer extends React.Component {
|
|||
deviceWindowWidth,
|
||||
defaultComponentStateComputed,
|
||||
canvasWidth,
|
||||
dataQueries,
|
||||
} = this.state;
|
||||
|
||||
if (this.state.app?.is_maintenance_on) {
|
||||
|
|
@ -232,7 +234,9 @@ class Viewer extends React.Component {
|
|||
className="canvas-area"
|
||||
style={{
|
||||
width: canvasWidth,
|
||||
minHeight: +appDefinition.globalSettings?.canvasMaxHeight || 2400,
|
||||
maxWidth: +appDefinition.globalSettings?.canvasMaxWidth || 1292,
|
||||
maxHeight: +appDefinition.globalSettings?.canvasMaxHeight || 2400,
|
||||
backgroundColor: appDefinition.globalSettings?.canvasBackgroundColor || '#edeff5',
|
||||
}}
|
||||
>
|
||||
|
|
@ -270,6 +274,7 @@ class Viewer extends React.Component {
|
|||
onComponentOptionsChanged(this, component, options)
|
||||
}
|
||||
canvasWidth={this.getCanvasWidth()}
|
||||
dataQueries={dataQueries}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class LoginPage extends React.Component {
|
|||
isGettingConfigs: true,
|
||||
configs: undefined,
|
||||
};
|
||||
this.single_organization = window.public_config?.MULTI_ORGANIZATION !== 'true';
|
||||
this.single_organization = window.public_config?.DISABLE_MULTI_WORKSPACE === 'true';
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -132,7 +132,7 @@ class LoginPage extends React.Component {
|
|||
this.showLoading()
|
||||
) : (
|
||||
<div className="card-body">
|
||||
{!configs && <div className="text-center">No login methods enabled for this organization</div>}
|
||||
{!configs && <div className="text-center">No login methods enabled for this workspace</div>}
|
||||
{configs?.form?.enabled && (
|
||||
<div>
|
||||
<h2 className="card-title text-center mb-4" data-cy="login-page-header">
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export function Google({ settings, updateData }) {
|
|||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Enter Client Secret"
|
||||
placeholder="Enter Client Id"
|
||||
value={clientId}
|
||||
onChange={(e) => setClientId(e.target.value)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { toast } from 'react-hot-toast';
|
|||
import { SearchBox } from './SearchBox';
|
||||
|
||||
export const Organization = function Organization() {
|
||||
const isSingleOrganization = window.public_config?.MULTI_ORGANIZATION !== 'true';
|
||||
const isSingleOrganization = window.public_config?.DISABLE_MULTI_WORKSPACE === 'true';
|
||||
const { admin, organization_id } = authenticationService.currentUserValue;
|
||||
const [organization, setOrganization] = useState(authenticationService.currentUserValue?.organization);
|
||||
const [showCreateOrg, setShowCreateOrg] = useState(false);
|
||||
|
|
@ -21,11 +21,13 @@ export const Organization = function Organization() {
|
|||
const getAvatar = (organization) => {
|
||||
if (!organization) return;
|
||||
|
||||
const orgName = organization.split(' ');
|
||||
const orgName = organization.split(' ').filter((e) => e && !!e.trim());
|
||||
if (orgName.length > 1) {
|
||||
return `${orgName[0]?.[0]}${orgName[1]?.[0]}`;
|
||||
} else {
|
||||
} else if (organization.length >= 2) {
|
||||
return `${organization[0]}${organization[1]}`;
|
||||
} else {
|
||||
return `${organization[0]}${organization[0]}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ export const Organization = function Organization() {
|
|||
|
||||
const createOrganization = () => {
|
||||
if (!(newOrgName && newOrgName.trim())) {
|
||||
toast.error("organization name can't be empty.", {
|
||||
toast.error('Workspace name can not be empty.', {
|
||||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
|
|
@ -66,21 +68,22 @@ export const Organization = function Organization() {
|
|||
setIsCreating(true);
|
||||
organizationService.createOrganization(newOrgName).then(
|
||||
(data) => {
|
||||
setIsCreating(false);
|
||||
authenticationService.updateCurrentUserDetails(data);
|
||||
window.location.href = '/';
|
||||
},
|
||||
() => {
|
||||
toast.error('Error while creating organization', {
|
||||
setIsCreating(false);
|
||||
toast.error('Error while creating workspace', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
setIsCreating(false);
|
||||
};
|
||||
|
||||
const editOrganization = () => {
|
||||
if (!(newOrgName && newOrgName.trim())) {
|
||||
toast.error("organization name can't be empty.", {
|
||||
toast.error('Workspace name can not be empty.', {
|
||||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
|
|
@ -89,13 +92,13 @@ export const Organization = function Organization() {
|
|||
organizationService.editOrganization({ name: newOrgName }).then(
|
||||
() => {
|
||||
authenticationService.updateCurrentUserDetails({ organization: newOrgName });
|
||||
toast.success('Organization updated', {
|
||||
toast.success('Workspace updated', {
|
||||
position: 'top-center',
|
||||
});
|
||||
setOrganization(newOrgName);
|
||||
},
|
||||
() => {
|
||||
toast.error('Error while editing organization', {
|
||||
toast.error('Error while editing workspace', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
|
|
@ -258,7 +261,7 @@ export const Organization = function Organization() {
|
|||
</div>
|
||||
{!isSingleOrganization && (
|
||||
<div className="dropdown-item org-actions">
|
||||
<div onClick={showCreateModal}>Add Organizations</div>
|
||||
<div onClick={showCreateModal}>Add workspace</div>
|
||||
</div>
|
||||
)}
|
||||
{admin && (
|
||||
|
|
@ -281,8 +284,12 @@ export const Organization = function Organization() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="dropdown organization-list" onMouseEnter={() => setIsListOrganizations(false)}>
|
||||
<a href="#" className={`btn ${!isSingleOrganization || admin ? 'dropdown-toggle' : ''}`}>
|
||||
<div className="dropdown organization-list">
|
||||
<a
|
||||
href="#"
|
||||
className={`btn ${!isSingleOrganization || admin ? 'dropdown-toggle' : ''}`}
|
||||
onMouseOver={() => setIsListOrganizations(false)}
|
||||
>
|
||||
<div>{organization}</div>
|
||||
</a>
|
||||
{(!isSingleOrganization || admin) && (
|
||||
|
|
@ -291,14 +298,14 @@ export const Organization = function Organization() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Modal show={showCreateOrg} closeModal={() => setShowCreateOrg(false)} title="Create organization">
|
||||
<Modal show={showCreateOrg} closeModal={() => setShowCreateOrg(false)} title="Create workspace">
|
||||
<div className="row">
|
||||
<div className="col modal-main">
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => setNewOrgName(e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="organization name"
|
||||
placeholder="workspace name"
|
||||
disabled={isCreating}
|
||||
maxLength={25}
|
||||
/>
|
||||
|
|
@ -314,19 +321,19 @@ export const Organization = function Organization() {
|
|||
className={`btn btn-primary ${isCreating ? 'btn-loading' : ''}`}
|
||||
onClick={createOrganization}
|
||||
>
|
||||
Create organization
|
||||
Create workspace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal show={showEditOrg} closeModal={() => setShowEditOrg(false)} title="Edit organization">
|
||||
<Modal show={showEditOrg} closeModal={() => setShowEditOrg(false)} title="Edit workspace">
|
||||
<div className="row">
|
||||
<div className="col modal-main">
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => setNewOrgName(e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="organization name"
|
||||
placeholder="workspace name"
|
||||
disabled={isCreating}
|
||||
value={newOrgName}
|
||||
maxLength={25}
|
||||
|
|
|
|||
|
|
@ -323,6 +323,26 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
|
|||
|
||||
const { customVariables } = options;
|
||||
|
||||
if (eventName === 'onTrigger') {
|
||||
const { component, queryId, queryName } = options;
|
||||
_self.setState(
|
||||
{
|
||||
currentState: {
|
||||
..._self.state.currentState,
|
||||
components: {
|
||||
..._self.state.currentState.components,
|
||||
[component.name]: {
|
||||
..._self.state.currentState.components[component.name],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
runQuery(_ref, queryId, queryName, true, mode);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (eventName === 'onRowClicked') {
|
||||
const { component, data, rowId } = options;
|
||||
_self.setState(
|
||||
|
|
|
|||
9
frontend/src/_hooks/useKeyHooks.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const useKeyHooks = (hotkeys = [], callback) =>
|
||||
useHotkeys(hotkeys.toString(), (e) => {
|
||||
e.preventDefault();
|
||||
callback(e.code);
|
||||
});
|
||||
|
||||
export default useKeyHooks;
|
||||
|
|
@ -114,7 +114,7 @@ button {
|
|||
.editor {
|
||||
.header-container {
|
||||
max-width: 100%;
|
||||
padding: 0 15px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.resizer-active {
|
||||
|
|
@ -262,7 +262,7 @@ button {
|
|||
height: 100%;
|
||||
width: 76px;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
flex: 1 1 auto;
|
||||
|
|
@ -3971,7 +3971,7 @@ input[type="text"] {
|
|||
}
|
||||
|
||||
.app-name {
|
||||
width: 325px;
|
||||
width: 250px;
|
||||
left: 150px;
|
||||
position: absolute;
|
||||
}
|
||||
|
|
@ -4010,7 +4010,7 @@ input[type="text"] {
|
|||
}
|
||||
|
||||
.app-version-menu .dropdown-menu {
|
||||
left: -90px;
|
||||
left: -65px;
|
||||
width: 283px;
|
||||
}
|
||||
|
||||
|
|
@ -4021,7 +4021,7 @@ input[type="text"] {
|
|||
.app-version-menu .released-subtext {
|
||||
font-size: 12px;
|
||||
color: #36af8b;
|
||||
padding: 0;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.app-version-menu .create-link {
|
||||
|
|
@ -5080,8 +5080,9 @@ div#driver-page-overlay {
|
|||
|
||||
.realtime-avatars {
|
||||
position: absolute;
|
||||
left: 35%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.widget-style-field-header{
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
|
|
@ -5114,4 +5115,69 @@ div#driver-page-overlay {
|
|||
padding: 5px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
.autosave-indicator {
|
||||
position: absolute;
|
||||
left: 30%;
|
||||
color: #868aa5;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.autosave-indicator-saving {
|
||||
left: 34.5%;
|
||||
}
|
||||
.pdf-page-controls {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
background: white;
|
||||
opacity: 0;
|
||||
transform: translateX(-50%);
|
||||
transition: opacity ease-in-out 0.2s;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 30px 40px 0 rgba(16, 36, 94, 0.2);
|
||||
|
||||
button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: white;
|
||||
border: 0;
|
||||
font-size: 1.2em;
|
||||
border-radius: 4px;
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
padding: 0 0.5em;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
.pdf-document{
|
||||
canvas{
|
||||
margin: 0px auto;
|
||||
}
|
||||
&:hover {
|
||||
.pdf-page-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const Avatar = ({ text, title = '', borderColor = '' }) => {
|
|||
return (
|
||||
<span
|
||||
data-tip={title}
|
||||
style={{ border: `1px solid ${borderColor}` }}
|
||||
style={{ border: `1.5px solid ${borderColor}` }}
|
||||
className="avatar avatar-sm avatar-rounded animation-fade"
|
||||
>
|
||||
{text}
|
||||
|
|
|
|||
|
|
@ -327,10 +327,10 @@ export const JSONNode = ({ data, ...restProps }) => {
|
|||
<div
|
||||
style={{ width: 'inherit' }}
|
||||
className={`${shouldDisplayIntendedBlock && 'group-border'} ${applySelectedNodeStyles && 'selected-node'}`}
|
||||
onMouseEnter={() => updateHoveredNode(currentNode, currentNodePath)}
|
||||
onMouseLeave={() => updateHoveredNode(null)}
|
||||
>
|
||||
<div
|
||||
onMouseEnter={() => updateHoveredNode(currentNode, currentNodePath)}
|
||||
onMouseLeave={() => updateHoveredNode(null)}
|
||||
className={cx('d-flex', {
|
||||
'group-object-container': shouldDisplayIntendedBlock,
|
||||
'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ const JSONTreeValueNode = ({ data, type }) => {
|
|||
);
|
||||
}
|
||||
|
||||
const value = type === 'String' ? `"${data}"` : String(data);
|
||||
let value = type === 'String' ? `"${data}"` : String(data);
|
||||
if (value.length > 65) {
|
||||
value = `${value.substring(0, 65)} ... "`;
|
||||
}
|
||||
|
||||
const clsForUndefinedOrNull = (type === 'Undefined' || type === 'Null') && 'badge badge-secondary';
|
||||
return (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -157,10 +157,9 @@ export class JSONTreeViewer extends React.Component {
|
|||
newPath = `${path}.${key}`;
|
||||
}
|
||||
|
||||
if (_.isObject(value) || _.isArray(value)) {
|
||||
buildMap(value, newPath);
|
||||
} else if (_.isFunction(value)) {
|
||||
if (_.isObject(value)) {
|
||||
map.set(newPath, { type: _type });
|
||||
buildMap(value, newPath);
|
||||
} else {
|
||||
map.set(newPath, { type: _type });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
const Spinner = ({ ...props }) => (
|
||||
<div {...props} className="spinner-border spinner-border-lg text-muted" role="status" />
|
||||
const Spinner = ({ size = 'large', ...props }) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cx('spinner-border text-muted', {
|
||||
'spinner-border-lg': size === 'large',
|
||||
'spinner-border-sm': size === 'small',
|
||||
})}
|
||||
role="status"
|
||||
/>
|
||||
);
|
||||
|
||||
export default Spinner;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
loader: 'html-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
[build]
|
||||
base = "frontend/"
|
||||
publish = "build/"
|
||||
base = "/"
|
||||
publish = "frontend/build/"
|
||||
command = "npm run build:plugins && npm run build:frontend"
|
||||
|
||||
[template.environment]
|
||||
NODE_ENV = "production"
|
||||
NODE_VERSION = "14.17.3"
|
||||
NPM_VERSION = "7.20.0"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
|
|
|
|||
1
plugins/package-lock.json
generated
|
|
@ -16818,6 +16818,7 @@
|
|||
}
|
||||
},
|
||||
"packages/notion": {
|
||||
"name": "@tooljet-plugins/notion",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^1.0.4",
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export default class GooglesheetsQueryService implements QueryService {
|
|||
} catch (error) {
|
||||
console.log(error.response);
|
||||
|
||||
if (error.response.statusCode === 401) {
|
||||
if (error?.response?.statusCode === 401) {
|
||||
throw new OAuthUnauthorizedClientError('Query could not be completed', error.message, { ...error });
|
||||
}
|
||||
throw new QueryError('Query could not be completed', error.message, {});
|
||||
|
|
|
|||
|
|
@ -63,12 +63,12 @@ export class OauthService {
|
|||
async #findAndActivateUser(email: string, organizationId: string): Promise<User> {
|
||||
const user = await this.usersService.findByEmail(email, organizationId);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('User not exist in the organization');
|
||||
throw new UnauthorizedException('User not exist in the workspace');
|
||||
}
|
||||
const organizationUser: OrganizationUser = user.organizationUsers?.[0];
|
||||
|
||||
if (!organizationUser) {
|
||||
throw new UnauthorizedException('User not exist in the organization');
|
||||
throw new UnauthorizedException('User not exist in the workspace');
|
||||
}
|
||||
if (organizationUser.status != 'active') await this.organizationUsersService.activate(organizationUser);
|
||||
return user;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export class PopulateSSOConfigs1650485473528 implements MigrationInterface {
|
|||
const encryptionService = new EncryptionService();
|
||||
const OrganizationRepository = entityManager.getRepository(Organization);
|
||||
|
||||
const isSingleOrganization = process.env.MULTI_ORGANIZATION !== 'true';
|
||||
const isSingleOrganization = process.env.DISABLE_MULTI_WORKSPACE === 'true';
|
||||
const enableSignUp = process.env.SSO_DISABLE_SIGNUP !== 'true';
|
||||
const domain = process.env.SSO_RESTRICTED_DOMAIN;
|
||||
|
||||
|
|
|
|||
41
server/migrations/1651820577708-PopulateTextSize.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { AppVersion } from '../src/entities/app_version.entity';
|
||||
|
||||
export class PopulateTextSize1651820577708 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const entityManager = queryRunner.manager;
|
||||
const appVersions = await entityManager.find(AppVersion);
|
||||
|
||||
for (const version of appVersions) {
|
||||
const definition = version['definition'];
|
||||
|
||||
if (definition) {
|
||||
const components = definition['components'];
|
||||
|
||||
for (const componentId of Object.keys(components)) {
|
||||
const component = components[componentId];
|
||||
|
||||
if (component.component.component === 'Text') {
|
||||
component.component.definition.styles.textSize = { value: 14 };
|
||||
components[componentId] = {
|
||||
...component,
|
||||
component: {
|
||||
...component.component,
|
||||
definition: {
|
||||
...component.component.definition,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
definition['components'] = components;
|
||||
version.definition = definition;
|
||||
|
||||
await entityManager.update(AppVersion, { id: version.id }, { definition });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ export class OrganizationsController {
|
|||
|
||||
@Get(['/:organizationId/public-configs', '/public-configs'])
|
||||
async getOrganizationDetails(@Param('organizationId') organizationId: string) {
|
||||
if (!organizationId && this.configService.get<string>('MULTI_ORGANIZATION') !== 'true') {
|
||||
if (!organizationId && this.configService.get<string>('DISABLE_MULTI_WORKSPACE') === 'true') {
|
||||
// Request from single organization login page - find one from organization and setting
|
||||
organizationId = (await this.organizationsService.getSingleOrganization()).id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ async function bootstrap() {
|
|||
"'unsafe-inline'",
|
||||
"'unsafe-eval'",
|
||||
'blob:',
|
||||
'https://unpkg.com/@babel/standalone@7.17.9/babel.min.js',
|
||||
'https://unpkg.com/react@16.7.0/umd/react.production.min.js',
|
||||
'https://unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js',
|
||||
'cdn.skypack.dev',
|
||||
],
|
||||
'default-src': [
|
||||
'maps.googleapis.com',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ export class MultiOrganizationGuard implements CanActivate {
|
|||
constructor(private configService: ConfigService) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
return this.configService.get<string>('MULTI_ORGANIZATION') === 'true';
|
||||
return this.configService.get<string>('DISABLE_MULTI_WORKSPACE') !== 'true';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class AppConfigService {
|
|||
'SENTRY_DNS',
|
||||
'SENTRY_DEBUG',
|
||||
'DISABLE_SIGNUPS',
|
||||
'MULTI_ORGANIZATION',
|
||||
'DISABLE_MULTI_WORKSPACE',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export class AuthService {
|
|||
if (!organizationId) {
|
||||
// Global login
|
||||
// Determine the organization to be loaded
|
||||
if (this.configService.get<string>('MULTI_ORGANIZATION') !== 'true') {
|
||||
if (this.configService.get<string>('DISABLE_MULTI_WORKSPACE') === 'true') {
|
||||
// Single organization
|
||||
organization = await this.organizationsService.getSingleOrganization();
|
||||
if (!organization?.ssoConfigs?.find((oc) => oc.sso == 'form' && oc.enabled)) {
|
||||
|
|
@ -72,7 +72,7 @@ export class AuthService {
|
|||
organization = organizationList[0];
|
||||
} else {
|
||||
// no form login enabled organization available for user - creating new one
|
||||
organization = await this.organizationsService.create('Untitled organization', user);
|
||||
organization = await this.organizationsService.create('Untitled workspace', user);
|
||||
}
|
||||
}
|
||||
user.organizationId = organization.id;
|
||||
|
|
@ -122,7 +122,7 @@ export class AuthService {
|
|||
if (!(isNewOrganization || user.isPasswordLogin)) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
if (this.configService.get<string>('MULTI_ORGANIZATION') !== 'true') {
|
||||
if (this.configService.get<string>('DISABLE_MULTI_WORKSPACE') === 'true') {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
const newUser = await this.usersService.findByEmail(user.email, newOrganizationId);
|
||||
|
|
@ -174,7 +174,7 @@ export class AuthService {
|
|||
|
||||
let organization: Organization;
|
||||
// Check if the configs allows user signups
|
||||
if (this.configService.get<string>('MULTI_ORGANIZATION') !== 'true') {
|
||||
if (this.configService.get<string>('DISABLE_MULTI_WORKSPACE') === 'true') {
|
||||
// Single organization checking if organization exist
|
||||
organization = await this.organizationsService.getSingleOrganization();
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ export class AuthService {
|
|||
}
|
||||
}
|
||||
// Create default organization
|
||||
organization = await this.organizationsService.create('Untitled organization');
|
||||
organization = await this.organizationsService.create('Untitled workspace');
|
||||
const user = await this.usersService.create({ email }, organization.id, ['all_users', 'admin'], existingUser, true);
|
||||
await this.organizationUsersService.create(user, organization, true);
|
||||
await this.emailService.sendWelcomeEmail(user.email, user.firstName, user.invitationToken);
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export class EmailService {
|
|||
<p>Hi ${name || ''},</p>
|
||||
<br>
|
||||
<span>
|
||||
${sender} has invited you to use ToolJet organisation ${organisationName}. Use the link below to set up your account and get started.
|
||||
${sender} has invited you to use ToolJet workspace ${organisationName}. Use the link below to set up your account and get started.
|
||||
</span>
|
||||
<br>
|
||||
<a href="${inviteUrl}">${inviteUrl}</a>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class SeedsService {
|
|||
sso: 'form',
|
||||
},
|
||||
],
|
||||
name: 'My organization',
|
||||
name: 'My workspace',
|
||||
});
|
||||
|
||||
await manager.save(organization);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,18 @@ describe('Authentication', () => {
|
|||
});
|
||||
|
||||
describe('Single organization', () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'DISABLE_SIGNUPS':
|
||||
return 'false';
|
||||
case 'DISABLE_MULTI_WORKSPACE':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
it('should create new users and organization', async () => {
|
||||
const response = await request(app.getHttpServer()).post('/api/signup').send({ email: 'test@tooljet.io' });
|
||||
expect(response.statusCode).toBe(201);
|
||||
|
|
@ -52,7 +64,7 @@ describe('Authentication', () => {
|
|||
});
|
||||
|
||||
expect(user.defaultOrganizationId).toBe(user?.organizationUsers?.[0]?.organizationId);
|
||||
expect(organization.name).toBe('Untitled organization');
|
||||
expect(organization.name).toBe('Untitled workspace');
|
||||
|
||||
const groupPermissions = await user.groupPermissions;
|
||||
const groupNames = groupPermissions.map((x) => x.group);
|
||||
|
|
@ -142,8 +154,6 @@ describe('Authentication', () => {
|
|||
switch (key) {
|
||||
case 'DISABLE_SIGNUPS':
|
||||
return 'false';
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
|
|
@ -155,8 +165,6 @@ describe('Authentication', () => {
|
|||
switch (key) {
|
||||
case 'DISABLE_SIGNUPS':
|
||||
return 'true';
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
|
|
@ -182,7 +190,7 @@ describe('Authentication', () => {
|
|||
});
|
||||
|
||||
expect(user.defaultOrganizationId).toBe(user?.organizationUsers?.[0]?.organizationId);
|
||||
expect(organization?.name).toBe('Untitled organization');
|
||||
expect(organization?.name).toBe('Untitled workspace');
|
||||
|
||||
const groupPermissions = await user.groupPermissions;
|
||||
const groupNames = groupPermissions.map((x) => x.group);
|
||||
|
|
@ -255,7 +263,7 @@ describe('Authentication', () => {
|
|||
.send({ email: 'admin@tooljet.io', password: 'password' });
|
||||
expect(response.statusCode).toBe(201);
|
||||
expect(response.body.organization_id).not.toBe(current_organization.id);
|
||||
expect(response.body.organization).toBe('Untitled organization');
|
||||
expect(response.body.organization).toBe('Untitled workspace');
|
||||
});
|
||||
it('should be able to switch between organizations with admin privilage', async () => {
|
||||
const { organization: invited_organization } = await createUser(
|
||||
|
|
|
|||
|
|
@ -13,14 +13,6 @@ describe('organizations controller', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
await clearDB();
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'false';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
|
|
@ -65,28 +57,20 @@ describe('organizations controller', () => {
|
|||
|
||||
describe('create organization', () => {
|
||||
it('should allow only authenticated users to create organization', async () => {
|
||||
await request(app.getHttpServer()).post('/api/organizations').send({ name: 'My organization' }).expect(401);
|
||||
await request(app.getHttpServer()).post('/api/organizations').send({ name: 'My workspace' }).expect(401);
|
||||
});
|
||||
it('should create new organization if multi organization supported', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
it('should create new organization if Multi-Workspace supported', async () => {
|
||||
const { user, organization } = await createUser(app, {
|
||||
email: 'admin@tooljet.io',
|
||||
});
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/organizations')
|
||||
.send({ name: 'My organization' })
|
||||
.send({ name: 'My workspace' })
|
||||
.set('Authorization', authHeaderForUser(user));
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
expect(response.body.organization_id).not.toBe(organization.id);
|
||||
expect(response.body.organization).toBe('My organization');
|
||||
expect(response.body.organization).toBe('My workspace');
|
||||
expect(response.body.admin).toBeTruthy();
|
||||
|
||||
const newUser = await userRepository.findOneOrFail({ where: { id: user.id } });
|
||||
|
|
@ -94,14 +78,6 @@ describe('organizations controller', () => {
|
|||
});
|
||||
|
||||
it('should throw error if name is empty', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/organizations')
|
||||
|
|
@ -111,35 +87,35 @@ describe('organizations controller', () => {
|
|||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should not create new organization if multi organization not supported', async () => {
|
||||
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/organizations')
|
||||
.send({ name: 'My organization' })
|
||||
.set('Authorization', authHeaderForUser(user))
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should create new organization if multi organization supported and user logged in via SSO', async () => {
|
||||
it('should not create new organization if Multi-Workspace not supported', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
case 'DISABLE_MULTI_WORKSPACE':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/organizations')
|
||||
.send({ name: 'My workspace' })
|
||||
.set('Authorization', authHeaderForUser(user))
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should create new organization if Multi-Workspace supported and user logged in via SSO', async () => {
|
||||
const { user, organization } = await createUser(app, {
|
||||
email: 'admin@tooljet.io',
|
||||
});
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/organizations')
|
||||
.send({ name: 'My organization' })
|
||||
.send({ name: 'My workspace' })
|
||||
.set('Authorization', authHeaderForUser(user, null, false));
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
expect(response.body.organization_id).not.toBe(organization.id);
|
||||
expect(response.body.organization).toBe('My organization');
|
||||
expect(response.body.organization).toBe('My workspace');
|
||||
expect(response.body.admin).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -252,6 +228,14 @@ describe('organizations controller', () => {
|
|||
|
||||
describe('get public organization configs', () => {
|
||||
it('should get organization details for all users for single organization', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'DISABLE_MULTI_WORKSPACE':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
const { user } = await createUser(app, {
|
||||
email: 'admin@tooljet.io',
|
||||
});
|
||||
|
|
@ -291,14 +275,6 @@ describe('organizations controller', () => {
|
|||
});
|
||||
|
||||
it('should get organization specific details for all users for multiple organization deployment', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
const { user, organization } = await createUser(app, {
|
||||
email: 'admin@tooljet.io',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,22 +12,17 @@ describe('users controller', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
await clearDB();
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'DISABLE_SIGNUPS':
|
||||
return 'false';
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'false';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
({ app, mockConfig } = await createNestAppInstanceWithEnvMock());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('PATCH /api/users/change_password', () => {
|
||||
it('should allow users to update their password', async () => {
|
||||
const userData = await createUser(app, { email: 'admin@tooljet.io' });
|
||||
|
|
@ -87,17 +82,7 @@ describe('users controller', () => {
|
|||
});
|
||||
|
||||
describe('POST /api/users/set_password_from_token', () => {
|
||||
it('should allow users to setup account after sign up using multi organization', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'DISABLE_SIGNUPS':
|
||||
return 'false';
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
it('should allow users to setup account after sign up using Multi-Workspace', async () => {
|
||||
const invitationToken = uuidv4();
|
||||
const userData = await createUser(app, {
|
||||
email: 'signup@tooljet.io',
|
||||
|
|
@ -125,17 +110,7 @@ describe('users controller', () => {
|
|||
expect(organizationUser.status).toEqual('active');
|
||||
});
|
||||
|
||||
it('should return error if required params are not present - multi organization', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'DISABLE_SIGNUPS':
|
||||
return 'false';
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
it('should return error if required params are not present - Multi-Workspace', async () => {
|
||||
const invitationToken = uuidv4();
|
||||
await createUser(app, {
|
||||
email: 'signup@tooljet.io',
|
||||
|
|
@ -155,6 +130,14 @@ describe('users controller', () => {
|
|||
});
|
||||
|
||||
it('should not allow users to setup account for single organization', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'DISABLE_MULTI_WORKSPACE':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
const invitationToken = uuidv4();
|
||||
await createUser(app, {
|
||||
email: 'signup@tooljet.io',
|
||||
|
|
@ -174,13 +157,11 @@ describe('users controller', () => {
|
|||
expect(response.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('should not allow users to setup account for multi organization and sign up disabled', async () => {
|
||||
it('should not allow users to setup account for Multi-Workspace and sign up disabled', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'DISABLE_SIGNUPS':
|
||||
return 'true';
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
|
|
@ -216,15 +197,6 @@ describe('users controller', () => {
|
|||
organization: org,
|
||||
});
|
||||
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
const signUpResponse = await request(app.getHttpServer())
|
||||
.post('/api/signup')
|
||||
.send({ email: 'invited@tooljet.io' });
|
||||
|
|
@ -269,15 +241,6 @@ describe('users controller', () => {
|
|||
organization: org,
|
||||
});
|
||||
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
const signUpResponse = await request(app.getHttpServer())
|
||||
.post('/api/signup')
|
||||
.send({ email: 'invited@tooljet.io' });
|
||||
|
|
@ -321,15 +284,7 @@ describe('users controller', () => {
|
|||
});
|
||||
|
||||
describe('POST /api/users/accept-invite', () => {
|
||||
it('should allow users to accept invitation when multi organization is enabled', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
});
|
||||
it('should allow users to accept invitation when Multi-Workspace is enabled', async () => {
|
||||
const userData = await createUser(app, {
|
||||
email: 'organizationUser@tooljet.io',
|
||||
status: 'invited',
|
||||
|
|
@ -347,11 +302,11 @@ describe('users controller', () => {
|
|||
expect(organizationUser.status).toEqual('active');
|
||||
});
|
||||
|
||||
it('should allow users to accept invitation when multi organization is disabled', async () => {
|
||||
it('should allow users to accept invitation when Multi-Workspace is disabled', async () => {
|
||||
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'MULTI_ORGANIZATION':
|
||||
return 'false';
|
||||
case 'DISABLE_MULTI_WORKSPACE':
|
||||
return 'true';
|
||||
default:
|
||||
return process.env[key];
|
||||
}
|
||||
|
|
|
|||