diff --git a/.env.example b/.env.example index f16ffef92a..04f5a45891 100644 --- a/.env.example +++ b/.env.example @@ -32,3 +32,6 @@ DISABLE_SIGNUPS= APM_VENDOR= SENTRY_DNS= SENTRY_DEBUG= + +# FEATURE TOGGLE +COMMENT_FEATURE_ENABLE= \ No newline at end of file diff --git a/.version b/.version index c18d72be30..899f24fc75 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.8.1 \ No newline at end of file +0.9.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5d6867a81..bdaba83a7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,17 +12,17 @@ We love your input! We want to make contributing to this project as easy and tra [Docker](https://docs.tooljet.io/docs/contributing-guide/setup/docker) [Ubuntu](https://docs.tooljet.io/docs/contributing-guide/setup/ubuntu) -## We Develop with Github -We use github to host code, to track issues and feature requests, as well as accept pull requests. +## We Develop with GitHub +We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. ## First-time contributors -We've tagged some issues to make it easy to get started :) +We've tagged some issues to make it easy to get started :smile: [Good first issues](https://github.com/ToolJet/ToolJet/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) Looking for ReactJS issues? Check out our [Frontend issues](https://github.com/ToolJet/ToolJet/issues?q=is%3Aissue+is%3Aopen+label%3Afrontend) Add a comment on the issue and wait for the issue to be assigned before you start working on it. This helps to avoid multiple people working on similar issues. -## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests +## We Use [GitHub Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests Pull requests are the best way to propose changes to the codebase (we use [Git-Flow](https://nvie.com/posts/a-successful-git-branching-model/)). We actively welcome your pull requests: 1. Fork the repo and create your branch from `develop`. Please create the branch in the format feature/- (eg: feature/176-chart-widget) @@ -35,7 +35,7 @@ Pull requests are the best way to propose changes to the codebase (we use [Git-F ## Any contributions you make will be under the AGPL v3 License In short, when you submit code changes, your submissions are understood to be under the same [AGPL v3 License](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. -## Report bugs using Github's [issues](https://github.com/ToolJet/ToolJet/issues) +## Report bugs using GitHub's [issues](https://github.com/ToolJet/ToolJet/issues) We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/ToolJet/ToolJet/issues/new/choose). It's that easy! **Great Bug Reports** tend to have: diff --git a/README.md b/README.md index 9029124884..e60fb6d275 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@


- Build and deploy internal tools. + Build and deploy internal tools

-ToolJet is an **open-source no-code framework** to build and deploy internal tools quickly without much effort from the engineering teams. You can connect to your data sources such as databases ( like PostgreSQL, MongoDB, Elasticsearch, etc ), API endpoints ( ToolJet supports importing OpenAPI spec & OAuth2 authorization), and external services ( like Stripe, Slack, Google Sheets, Airtable ) and use our pre-built UI widgets to build internal tools. +ToolJet is an **open-source no-code framework** to build and deploy internal tools quickly without much effort from the engineering teams. You can connect to your data sources, such as databases (like PostgreSQL, MongoDB, Elasticsearch, etc), API endpoints (ToolJet supports importing OpenAPI spec & OAuth2 authorization), and external services (like Stripe, Slack, Google Sheets, Airtable) and use our pre-built UI widgets to build internal tools. ![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/tooljet/tooljet-ce) ![GitHub contributors](https://img.shields.io/github/contributors/tooljet/tooljet) @@ -27,7 +27,7 @@ ToolJet is an **open-source no-code framework** to build and deploy internal too ## Features -- Visual app builder with widgets such as tables, charts, modals, buttons, dropdowns, and more. +- Visual app builder with widgets, such as tables, charts, modals, buttons, dropdowns, and more. - Mobile 📱 & desktop layouts 🖥 - Dark mode 🌛 - Connect to databases, APIs, and external services. @@ -52,7 +52,7 @@ You can deploy ToolJet on Heroku for free using the one-click-deployment button ## Examples -[Building a Github contributor leaderboard using ToolJet](https://blog.tooljet.io/building-a-github-contributor-leaderboard-using-tooljet/)
+[Building a GitHub contributor leaderboard using ToolJet](https://blog.tooljet.io/building-a-github-contributor-leaderboard-using-tooljet/)
## Documentation Documentation is available at https://docs.tooljet.io. diff --git a/docs/docs/actions/_category_.json b/docs/docs/actions/_category_.json new file mode 100644 index 0000000000..0fce8d65a0 --- /dev/null +++ b/docs/docs/actions/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Actions Reference", + "position": 6, + "collapsed": true +} \ No newline at end of file diff --git a/docs/docs/actions/set-localstorage.md b/docs/docs/actions/set-localstorage.md new file mode 100644 index 0000000000..999fb0a197 --- /dev/null +++ b/docs/docs/actions/set-localstorage.md @@ -0,0 +1,28 @@ +--- +sidebar_position: 1 +sidebar_label: Set localStorage +--- + +# Set localStorage + +This action allows you to specify a `key` and its corresponding `value` to be stored in localStorage. + +## Example: App that stores a name in localStorage and displays it on reload + +1. Add an input field, button and a text as shown +Set local storage sample app + +2. Select the button and add a `Set localStorage` action with `key` set to `name` and value pointing at the value of the text field +Set local storage sample app + +3. Select the text label we've added and set its value to the name item from localStorage +Set local storage sample app + +4. Now save the application, this is important as we're about to reload the page. + +5. Type in anything you wish on the input box and click on the button +Set local storage sample app + +6. Reload the page, you'll see that the value stored in local storage is persisted and it is displayed on screen! +Set local storage sample app + diff --git a/docs/docs/contributing-guide/setup/Mac OS.md b/docs/docs/contributing-guide/setup/Mac OS.md index 8272366fbd..eb7798ef79 100644 --- a/docs/docs/contributing-guide/setup/Mac OS.md +++ b/docs/docs/contributing-guide/setup/Mac OS.md @@ -3,7 +3,7 @@ sidebar_position: 1 --- # Mac OS -Follow these steps to setup and run ToolJet on Mac OS for development purposes. Open terminal and run the commands below. We recommend reading our guide on [architecture](/docs/deployment/architecture) of ToolJet before proceeding. +Follow these steps to setup and run ToolJet on macOS for development purposes. Open terminal and run the commands below. We recommend reading our guide on [architecture](/docs/deployment/architecture) of ToolJet before proceeding. ## Setting up diff --git a/docs/docs/contributing-guide/setup/docker.md b/docs/docs/contributing-guide/setup/docker.md index ba2a7587f3..ceddd96b6a 100644 --- a/docs/docs/contributing-guide/setup/docker.md +++ b/docs/docs/contributing-guide/setup/docker.md @@ -176,4 +176,4 @@ docker-compose run --rm server npm run test ## Troubleshooting -Please open a new issue at https://github.com/ToolJet/ToolJet/issues or join our slack channel (https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg) if you encounter any issues when trying to run ToolJet locally. +Please open a new issue at https://github.com/ToolJet/ToolJet/issues or join our Slack channel (https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg) if you encounter any issues when trying to run ToolJet locally. diff --git a/docs/docs/data-sources/airtable.md b/docs/docs/data-sources/airtable.md index 9f62c783dc..fe749bcf7c 100644 --- a/docs/docs/data-sources/airtable.md +++ b/docs/docs/data-sources/airtable.md @@ -14,7 +14,7 @@ Airtable API has a rate limit, and at the time of writing this documentation, th ::: :::tip -This guide assumes that you have already gone throgh [Adding a datasource +This guide assumes that you have already gone through [Adding a datasource ](/docs/tutorial/adding-a-datasource) tutorial. ::: diff --git a/docs/docs/data-sources/dynamodb.md b/docs/docs/data-sources/dynamodb.md new file mode 100644 index 0000000000..a7e9483e83 --- /dev/null +++ b/docs/docs/data-sources/dynamodb.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 11 +--- + +# DynamoDB + +ToolJet can connect to DynamoDB to read and write data. + +## Connection + +To add a new DynamoDB, click on the `+` button on data sources panel at the left-bottom corner of the app editor. Select DynamoDB from the modal that pops up. + +ToolJet requires the following to connect to your DynamoDB. + +- **Region** +- **Access key** +- **Secret key** + +It is recommended to create a new IAM user for the database so that you can control the access levels of ToolJet. + +ToolJet - Dynamo connection + +Click on 'Test connection' button to verify if the credentials are correct and that the database is accessible to ToolJet server. Click on 'Save' button to save the datasource. + +## Querying DynamoDB + +Click on `+` button of the query manager at the bottom panel of the editor and select the database added in the previous step as the datasource. Select the operation that you want to perform and click 'Save' to save the query. + +ToolJet - Dynamo query + +Click on the 'run' button to run the query. NOTE: Query should be saved before running. + +:::tip +Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations) +::: diff --git a/docs/docs/data-sources/firestore.md b/docs/docs/data-sources/firestore.md index 832600fa9d..056ddf0d0c 100644 --- a/docs/docs/data-sources/firestore.md +++ b/docs/docs/data-sources/firestore.md @@ -17,9 +17,14 @@ To generate a new key, check out Firestore's official documentation: [https://cl Once the key is downloaded, click on `+` button of data sources panel at the left-bottom corner of the app editor. Select Firestore from the modal that pops up. Paste the key in the field for GCP key. Click on 'Test connection' button to verify if the service account can access Firestore from ToolJet server. Click on 'Save' button to save the datasource. +ToolJet - Datasource Firestore + ## Querying Firestore -Click on `+` button of the query manager at the bottom panel of the editor and select the database added in the previous step as the datasource. +Click on `+` button of the query manager at the bottom panel of the editor and select the database added in the previous step as the datasource. + +ToolJet - Firestore connection + Select the operation that you want to perform on Firestore and click 'Save' to save the query. :::tip diff --git a/docs/docs/deployment/env-vars.md b/docs/docs/deployment/env-vars.md index bc31f81e4a..6630234d07 100644 --- a/docs/docs/deployment/env-vars.md +++ b/docs/docs/deployment/env-vars.md @@ -34,7 +34,7 @@ ToolJet server uses lockbox to encrypt datasource credentials. You should set th ToolJet server uses a secure 64 byte hexadecimal string to encrypt session cookies. You should set the environment variable `SECRET_KEY_BASE`. :::tip -If you have `openssl` installed, you can run the following commands to generate the the value for `LOCKBOX_MASTER_KEY` and `SECRET_KEY_BASE`. +If you have `openssl` installed, you can run the following commands to generate the value for `LOCKBOX_MASTER_KEY` and `SECRET_KEY_BASE`. For `LOCKBOX_MASTER_KEY` use `openssl rand -hex 32` For `SECRET_KEY_BASE` use `openssl rand -hex 64` @@ -42,7 +42,7 @@ For `SECRET_KEY_BASE` use `openssl rand -hex 64` #### Disabling signups ( optional ) -If want to restrict the signups and allow new users only by invitations, set the environment variable `DISABLE_SIGNUPS` to `true`. +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. diff --git a/docs/docs/sso/google.md b/docs/docs/sso/google.md index 14d8f01338..6ea9051716 100644 --- a/docs/docs/sso/google.md +++ b/docs/docs/sso/google.md @@ -43,7 +43,7 @@ Lastly, supply the environment variable `SSO_GOOGLE_OAUTH2_CLIENT_ID` to your de ### Restrict to your domain Set the environment variable `RESTRICTED_DOMAIN` to ensure that ToolJet verifies the domain of the user who signs in via SSO, on the server side. -If you're setting this environment variable, please make sure that the value does not contain any protocols, sub domains or slashes. It should +If you're setting this environment variable, please make sure that the value does not contain any protocols, subdomains or slashes. It should simply be `yourdomain.com`. ::: diff --git a/docs/docs/tutorial/actions.md b/docs/docs/tutorial/actions.md new file mode 100644 index 0000000000..3dc294143f --- /dev/null +++ b/docs/docs/tutorial/actions.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 6 +--- + +# Adding actions + +ToolJet supports several actions that can be invoked as the handler for any `event` that is triggered in an application. + +## To add actions + +To attach an action for component events, click on the component's handle, and then click on the `Add handler` button on the +inspector panel available on the right side. + +To attach an action for query events, select the query, go to the `advanced` tab and then click on the `Add handler` button. + +## Available actions + +Some of the actions that ToolJet Support are + + Action| Description| + ----| ----------- | + Show alert | Show an alert message as a bootstrap toast | + Run query | Run any of the data queries that you have created | + Open webpage | Go to another webpage in a new tab | + Goto app | Go to another ToolJet application | + Show modal | Open any modal that you've added | + Close modal | Close any modal that you've added if its already open | + Copy to clipboard | Copy any available text that you see on the application to clipboard | + Set localStorage | Set a key and corresponding value to localStorage | \ No newline at end of file diff --git a/docs/docs/tutorial/debugger.md b/docs/docs/tutorial/debugger.md index 8de5ffc907..3a7ad6587d 100644 --- a/docs/docs/tutorial/debugger.md +++ b/docs/docs/tutorial/debugger.md @@ -4,7 +4,7 @@ sidebar_position: 11 # Debugger -The debugger captures errors errors that happens while running the queries. For example, when a database query fails due to the unavailability of a database or when a REST API query fails due to an incorrect URL, the errors will be displayed on the debugger. The debugger also displays relevant data related to the error along with the error message. Debugger is located on the left-sidebar. +The debugger captures errors that happens while running the queries. For example, when a database query fails due to the unavailability of a database or when a REST API query fails due to an incorrect URL, the errors will be displayed on the debugger. The debugger also displays relevant data related to the error along with the error message. Debugger is located on the left-sidebar. ToolJet - Debugger diff --git a/docs/docs/tutorial/tracking.md b/docs/docs/tutorial/tracking.md index a89e029867..8f8c53e279 100644 --- a/docs/docs/tutorial/tracking.md +++ b/docs/docs/tutorial/tracking.md @@ -11,7 +11,7 @@ ToolJet does not store any data fetched from the datasources. ToolJet acts as a ## Server :::tip -Self-hosted version of ToolJet pings our server to fetch latest product updates every 24 hours. You can disable this by setting the value of `CHECK_FOR_UPDATES` environment variable to `0`. This feature is enabled by default. +Self-hosted version of ToolJet pings our server to fetch the latest product updates every 24 hours. You can disable this by setting the value of `CHECK_FOR_UPDATES` environment variable to `0`. This feature is enabled by default. ::: ## Client diff --git a/docs/docs/widgets/Image.md b/docs/docs/widgets/Image.md index 23ea8d1707..ec312aa0fd 100644 --- a/docs/docs/widgets/Image.md +++ b/docs/docs/widgets/Image.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 --- # Image diff --git a/docs/docs/widgets/calendar.md b/docs/docs/widgets/calendar.md new file mode 100644 index 0000000000..717d308ba3 --- /dev/null +++ b/docs/docs/widgets/calendar.md @@ -0,0 +1,109 @@ +--- +sidebar_position: 22 +--- + +# Calendar +Calendar widget comes with the following features: +- Day, month and week level views +- Events +- Resource scheduling + +ToolJet - Widget Reference - Calendar + +### Properties + +#### Date format +Determines the format in which any date passed to the calendar via any of the properties will be parsed. +It also determines the format in which any date made available by the calendar via exposed variables will be displayed. +It uses the date format conventions of [moment.js](https://momentjs.com/). +#### Default date +Determines the date on which the calendar's view will be centered on. +If the calendar is on `month` view, it will show the month on which this date exists. +If the calendar is on `week` view, it will show the week on which this date exists. +This property needs to be formatted using the `Date format` property which is configurable on the inspector. +#### Events +`Events` property should contain an array of objects, each of which describes the events that the calendar needs to display. + +Assuming that you set the date format to `MM-DD-YYYY HH:mm:ss A Z`, setting the `Events` property to the following code snippet will display an event titled `Sample Event` at the first hour of this day, as displayed in the image of calendar at the beginning of this page. + +```javascript +{{[ + { + title: 'Sample event', + start: `${moment().startOf('day').format('MM-DD-YYYY HH:mm:ss A Z')}`, + end: `${moment().endOf('day').format('MM-DD-YYYY HH:mm:ss A Z')}`, + allDay: false, + tooltip: 'Sample event', + color: 'lightgreen', + } +]}} +``` + +##### Event object properties + +| Name | Description | +|------|-------------| +| title | Title of the event | +| start | The date(and time) on which this event begins. Needs to be formatted in the `Date format` you've supplied | +| end | The date(and time) on which this event ends. Needs to be formatted in the `Date format` you've supplied | +| allDay | Optional. Qualifies the event as an 'All day event', which will pin it to date headers on `day` and `week` level views | +| tooltip | Tooltip which will be display when the user hovers over the event | +| color | Background color of the event, any css supported color name or hex code can be used | +| textOrientation | Optional. If it is set to `vertical`, the title of the event will be oriented vertically. | +| resourceId | Applicable only if you're using resource scheduling. This is the id of the resource to which this event correspond to. | + +You may supply any other additional property to the event(s). These additional properties will available to you when the calendar widget +exposes any of the events via its exposed variables. + +#### Resources + +Specifying resources will make the calendar categorize `week` view and `day` view for each of the resources specified. + + For example, to categorize week/day view into for three rooms, we specify `resources` this way: + +```javascript +{{ + [ + {resourceId: 1, title: 'Room A'}, + {resourceId: 2, title: 'Room B'}, + {resourceId: 3, title: 'Room C'}, + ] +}} +``` + +If we specify the `resourceId` of any of the events as `1`, then that event will be assigned to `Room A`, generating the following calendar, assuming that we've set the view to `day` and are viewing the day on which this event exists. + +ToolJet - Widget Reference - Calendar Resources + +#### Default view + +Determines whether the calendar would display a `day`, a `week` or a `month`. +Setting this property to anything other than these values will make the calendar default to `month` view. + +#### Show toolbar + +Determines whether the calendar toolbar should be displayed or not. + +#### Show view switcher + +Determinues whether the calendar's buttons that allow user to switch between `month`, `week` and `day` level views will be displayed. +### Styles +#### Cell size in views classified by resource + +When `resources` are specified, the calendar could take up quite a lot of horizontal space, making the horizontal scroll bar of calendar having to be relied upon all the time. + +If we set this property to `compact`, the cell sizes will be smaller in `week` and `day` views. + +### Events + +#### On Event selected + +This event is fired when the user clicks on a calendar event. + +Last selected event is exposed as `selectedEvent`. + +#### on Slot selected + +This event is fired when the user either clicks on an calendar slot(empty cell or empty space of a cell with event) or when they click and drag to select multiple slots. + +Last selected slot(s) are exposed as `selectedSlots`. \ No newline at end of file diff --git a/docs/docs/widgets/divider.md b/docs/docs/widgets/divider.md index 8edd95159b..515cf41589 100644 --- a/docs/docs/widgets/divider.md +++ b/docs/docs/widgets/divider.md @@ -1,5 +1,5 @@ --- -sidebar_position: 20 +sidebar_position: 6 --- # Divider diff --git a/docs/docs/widgets/dropdown.md b/docs/docs/widgets/dropdown.md index 2481c4b5c5..e56440229d 100644 --- a/docs/docs/widgets/dropdown.md +++ b/docs/docs/widgets/dropdown.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 --- # Dropdown diff --git a/docs/docs/widgets/filepicker.md b/docs/docs/widgets/filepicker.md new file mode 100644 index 0000000000..edbffe2f87 --- /dev/null +++ b/docs/docs/widgets/filepicker.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 6 +--- + +# Filepicker + +Filepicker widget allows the user to drag and drop files or upload files by browsing the filesystem and selecting one or more files in a directory. + +ToolJet - Widget Reference - Filepicker + +:::info + File types must be a 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. +::: + +ToolJet - Widget Reference - Filepicker file types + +:::tip +[MIME](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) type determination is not reliable across platforms. CSV files, for example, are reported as text/plain under macOS but as application/vnd.ms-excel under Windows. +::: + +## Event: On file selected + +On file selected event can be triggered when one or more files are selected. + + +#### Properties + +| properties | description | +| ----------- | ----------- | +| Use Drop zone | creates a drag & drop zone. Files can be dragged and dropped to the "drag & drop" zone. | +| Use File Picker | On clicking it invokes the default OS file prompt.| +| Pick mulitple files | Allows drag and drop (or selection from the file dialog) of multiple files. `Pick multiple files` is disabled by default. | +| Max file count | The maximum accepted number of files The default value is `2`.| +| Accept file types| By providing types, you can make the dropzone accept specific file types and reject the others. | +| Max size limit| Maximum file size (in bytes).| +| Min size limit| Minimum file size (in bytes).| + +:::tip:: +Files can be accepted or rejected based on the file types, maximum file count, maximum file size (in bytes) and minimum file size (in bytes). +If `Pick mulitple files` is set to false and additional files are dropped, all files besides the first will be rejected. +Any file that does not have a size in the range of `Max size limit` and `Min size limit` will be rejected. +::: \ No newline at end of file diff --git a/docs/docs/widgets/map.md b/docs/docs/widgets/map.md index 057893ea93..a2fe5d0f57 100644 --- a/docs/docs/widgets/map.md +++ b/docs/docs/widgets/map.md @@ -1,10 +1,10 @@ --- -sidebar_position: 8 +sidebar_position: 9 --- # Map -The map widget can be used to pick or select locations on the google map with the location's coordinates. +The map widget can be used to pick or select locations on the Google map with the location's coordinates. ToolJet - Widget Reference - Map diff --git a/docs/docs/widgets/modal.md b/docs/docs/widgets/modal.md index 577cf4f2bf..335ec9f8bc 100644 --- a/docs/docs/widgets/modal.md +++ b/docs/docs/widgets/modal.md @@ -1,5 +1,5 @@ --- -sidebar_position: 9 +sidebar_position: 10 --- # Modal diff --git a/docs/docs/widgets/multiselect.md b/docs/docs/widgets/multiselect.md index c5fee05f21..600d52e318 100644 --- a/docs/docs/widgets/multiselect.md +++ b/docs/docs/widgets/multiselect.md @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 11 --- # Multiselect diff --git a/docs/docs/widgets/number-input.md b/docs/docs/widgets/number-input.md index 308bbeb42c..d6a00f9f1d 100644 --- a/docs/docs/widgets/number-input.md +++ b/docs/docs/widgets/number-input.md @@ -1,5 +1,5 @@ --- -sidebar_position: 16 +sidebar_position: 12 --- # Number Input diff --git a/docs/docs/widgets/qr-scanner.md b/docs/docs/widgets/qr-scanner.md index 0d6019e555..ebe43f6160 100644 --- a/docs/docs/widgets/qr-scanner.md +++ b/docs/docs/widgets/qr-scanner.md @@ -1,5 +1,5 @@ --- -sidebar_position: 11 +sidebar_position: 13 --- # QR Scanner diff --git a/docs/docs/widgets/radio-button.md b/docs/docs/widgets/radio-button.md index 83e9c6e8d2..3bb02b2571 100644 --- a/docs/docs/widgets/radio-button.md +++ b/docs/docs/widgets/radio-button.md @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 14 --- # Radio Button diff --git a/docs/docs/widgets/rich-text-editor.md b/docs/docs/widgets/rich-text-editor.md index 8c77f8a4df..f4136fb6ae 100644 --- a/docs/docs/widgets/rich-text-editor.md +++ b/docs/docs/widgets/rich-text-editor.md @@ -1,5 +1,5 @@ --- -sidebar_position: 13 +sidebar_position: 15 --- # Rich Text Editor diff --git a/docs/docs/widgets/star.md b/docs/docs/widgets/star.md index 6117bb648a..b903962ade 100644 --- a/docs/docs/widgets/star.md +++ b/docs/docs/widgets/star.md @@ -1,5 +1,5 @@ --- -sidebar_position: 14 +sidebar_position: 16 --- # Star rating @@ -10,7 +10,7 @@ Star rating widget can be used to display as well as input ratings. The widget s ### Event: On Change -This event is triggered when an star is clicked. +This event is triggered when a star is clicked. #### Properties diff --git a/docs/docs/widgets/table.md b/docs/docs/widgets/table.md index 446fa0cb8e..76f8b544a1 100644 --- a/docs/docs/widgets/table.md +++ b/docs/docs/widgets/table.md @@ -1,5 +1,5 @@ --- -sidebar_position: 15 +sidebar_position: 17 --- # Table @@ -112,7 +112,7 @@ If the data of a cell is changed, "save changes" button will be shown at the bot | changeSet | Object with row number as the key and object of edited fields and their values as the value | | dataUpdates | Just like changeSet but includes the data of the entire row | | selectedRow | The data of the row that was last clicked. `selectedRow` also changes when an action button is clicked | -| searchText | The value of the search field if server-side paginaton is enabled | +| searchText | The value of the search field if server-side pagination is enabled | #### Events diff --git a/docs/docs/widgets/text-input.md b/docs/docs/widgets/text-input.md index cb6a779119..4f32e38801 100644 --- a/docs/docs/widgets/text-input.md +++ b/docs/docs/widgets/text-input.md @@ -1,5 +1,5 @@ --- -sidebar_position: 16 +sidebar_position: 18 --- # Text Input diff --git a/docs/docs/widgets/text.md b/docs/docs/widgets/text.md index 01357b819c..6dabf39fcd 100644 --- a/docs/docs/widgets/text.md +++ b/docs/docs/widgets/text.md @@ -1,5 +1,5 @@ --- -sidebar_position: 17 +sidebar_position: 19 --- # Text diff --git a/docs/docs/widgets/textarea.md b/docs/docs/widgets/textarea.md index 90cf16bfc5..0260a9140d 100644 --- a/docs/docs/widgets/textarea.md +++ b/docs/docs/widgets/textarea.md @@ -1,5 +1,5 @@ --- -sidebar_position: 18 +sidebar_position: 20 --- # Textarea diff --git a/docs/docs/widgets/toggle-switch.md b/docs/docs/widgets/toggle-switch.md index 88b8aad919..e891228382 100644 --- a/docs/docs/widgets/toggle-switch.md +++ b/docs/docs/widgets/toggle-switch.md @@ -1,5 +1,5 @@ --- -sidebar_position: 19 +sidebar_position: 21 --- # Toggle Switch diff --git a/docs/static/img/actions/localstorage/sample-app-1.png b/docs/static/img/actions/localstorage/sample-app-1.png new file mode 100644 index 0000000000..fa4e86f32f Binary files /dev/null and b/docs/static/img/actions/localstorage/sample-app-1.png differ diff --git a/docs/static/img/actions/localstorage/sample-app-2.png b/docs/static/img/actions/localstorage/sample-app-2.png new file mode 100644 index 0000000000..093ed05bcb Binary files /dev/null and b/docs/static/img/actions/localstorage/sample-app-2.png differ diff --git a/docs/static/img/actions/localstorage/sample-app-3.png b/docs/static/img/actions/localstorage/sample-app-3.png new file mode 100644 index 0000000000..12356cf0ff Binary files /dev/null and b/docs/static/img/actions/localstorage/sample-app-3.png differ diff --git a/docs/static/img/actions/localstorage/sample-app-4.png b/docs/static/img/actions/localstorage/sample-app-4.png new file mode 100644 index 0000000000..4063223e72 Binary files /dev/null and b/docs/static/img/actions/localstorage/sample-app-4.png differ diff --git a/docs/static/img/actions/localstorage/sample-app-5.png b/docs/static/img/actions/localstorage/sample-app-5.png new file mode 100644 index 0000000000..f438b22ac5 Binary files /dev/null and b/docs/static/img/actions/localstorage/sample-app-5.png differ diff --git a/docs/static/img/datasource-reference/build-query-confirmation.png b/docs/static/img/datasource-reference/build-query-confirmation.png new file mode 100644 index 0000000000..df27334329 Binary files /dev/null and b/docs/static/img/datasource-reference/build-query-confirmation.png differ diff --git a/docs/static/img/datasource-reference/dynamo-connect.png b/docs/static/img/datasource-reference/dynamo-connect.png new file mode 100644 index 0000000000..bf005e35e5 Binary files /dev/null and b/docs/static/img/datasource-reference/dynamo-connect.png differ diff --git a/docs/static/img/datasource-reference/dynamo-query.png b/docs/static/img/datasource-reference/dynamo-query.png new file mode 100644 index 0000000000..467b908be3 Binary files /dev/null and b/docs/static/img/datasource-reference/dynamo-query.png differ diff --git a/docs/static/img/datasource-reference/firestore/firestore-intro.gif b/docs/static/img/datasource-reference/firestore/firestore-intro.gif new file mode 100644 index 0000000000..5bea8fc08e Binary files /dev/null and b/docs/static/img/datasource-reference/firestore/firestore-intro.gif differ diff --git a/docs/static/img/datasource-reference/firestore/firestore-query.png b/docs/static/img/datasource-reference/firestore/firestore-query.png new file mode 100644 index 0000000000..b9e27f23b0 Binary files /dev/null and b/docs/static/img/datasource-reference/firestore/firestore-query.png differ diff --git a/docs/static/img/redis/connect.png b/docs/static/img/redis/connect.png index cc9b63bfd6..3afca941c3 100644 Binary files a/docs/static/img/redis/connect.png and b/docs/static/img/redis/connect.png differ diff --git a/docs/static/img/widgets/calendar/calendar-day.png b/docs/static/img/widgets/calendar/calendar-day.png new file mode 100644 index 0000000000..740f9f09e3 Binary files /dev/null and b/docs/static/img/widgets/calendar/calendar-day.png differ diff --git a/docs/static/img/widgets/calendar/calendar-resource.png b/docs/static/img/widgets/calendar/calendar-resource.png new file mode 100644 index 0000000000..3bd551187c Binary files /dev/null and b/docs/static/img/widgets/calendar/calendar-resource.png differ diff --git a/docs/static/img/widgets/calendar/calendar-week.png b/docs/static/img/widgets/calendar/calendar-week.png new file mode 100644 index 0000000000..81a9743620 Binary files /dev/null and b/docs/static/img/widgets/calendar/calendar-week.png differ diff --git a/docs/static/img/widgets/calendar/calendar1.png b/docs/static/img/widgets/calendar/calendar1.png new file mode 100644 index 0000000000..a6d6a090e2 Binary files /dev/null and b/docs/static/img/widgets/calendar/calendar1.png differ diff --git a/docs/static/img/widgets/filepicker/file-types.gif b/docs/static/img/widgets/filepicker/file-types.gif new file mode 100644 index 0000000000..c6d34393cc Binary files /dev/null and b/docs/static/img/widgets/filepicker/file-types.gif differ diff --git a/docs/static/img/widgets/filepicker/filepicker.gif b/docs/static/img/widgets/filepicker/filepicker.gif new file mode 100644 index 0000000000..9cd80fbe9c Binary files /dev/null and b/docs/static/img/widgets/filepicker/filepicker.gif differ diff --git a/frontend/.babelrc b/frontend/.babelrc index 95dcc30324..c6bd560899 100644 --- a/frontend/.babelrc +++ b/frontend/.babelrc @@ -1,7 +1,12 @@ { - "presets": ["@babel/preset-env", "@babel/preset-react"], + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], "plugins": [ - ["@babel/plugin-proposal-class-properties"], + [ + "@babel/plugin-proposal-class-properties" + ], [ "console-source", { @@ -9,11 +14,13 @@ // 0 = full file path (Default) // 1 = file name ONLY // 2 = file name and last segment - "splitSegment": "/" // How to split the path - NOT REQUIRED // Default is / for Linux and OSX // Windows users can use "\\" here if needed } + ], + [ + "@babel/transform-runtime" ] ] -} +} \ No newline at end of file diff --git a/frontend/assets/images/icons/editor/comments/send.png b/frontend/assets/images/icons/editor/comments/send.png new file mode 100644 index 0000000000..366df11944 Binary files /dev/null and b/frontend/assets/images/icons/editor/comments/send.png differ diff --git a/frontend/assets/images/icons/editor/datasources/gcs.svg b/frontend/assets/images/icons/editor/datasources/gcs.svg new file mode 100644 index 0000000000..d30e003085 --- /dev/null +++ b/frontend/assets/images/icons/editor/datasources/gcs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/assets/images/icons/editor/datasources/s3.svg b/frontend/assets/images/icons/editor/datasources/s3.svg new file mode 100644 index 0000000000..725199615b --- /dev/null +++ b/frontend/assets/images/icons/editor/datasources/s3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/assets/images/icons/editor/left-sidebar/comments.svg b/frontend/assets/images/icons/editor/left-sidebar/comments.svg new file mode 100644 index 0000000000..eadef129fc --- /dev/null +++ b/frontend/assets/images/icons/editor/left-sidebar/comments.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/images/icons/editor/left-sidebar/play.svg b/frontend/assets/images/icons/editor/left-sidebar/play.svg new file mode 100644 index 0000000000..fba1b14bc6 --- /dev/null +++ b/frontend/assets/images/icons/editor/left-sidebar/play.svg @@ -0,0 +1,9 @@ + + + play + + + + + + \ No newline at end of file diff --git a/frontend/assets/images/icons/widgets/calendar.svg b/frontend/assets/images/icons/widgets/calendar.svg new file mode 100644 index 0000000000..ec39ebc307 --- /dev/null +++ b/frontend/assets/images/icons/widgets/calendar.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/assets/images/icons/widgets/filepicker.svg b/frontend/assets/images/icons/widgets/filepicker.svg new file mode 100644 index 0000000000..7deb6e08be --- /dev/null +++ b/frontend/assets/images/icons/widgets/filepicker.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e944c761e2..72b571e3c0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "frontend", "version": "0.1.0", "dependencies": { "@babel/core": "^7.4.3", @@ -14,6 +15,7 @@ "@react-google-maps/api": "^2.1.1", "@sentry/react": "^6.12.0", "@sentry/tracing": "^6.12.0", + "@svgr/webpack": "^5.5.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", @@ -27,6 +29,7 @@ "dompurify": "^2.2.7", "draft-js": "^0.11.7", "draft-js-export-html": "^1.4.1", + "emoji-mart": "^3.0.1", "fuse.js": "^6.4.6", "history": "^4.9.0", "html-webpack-plugin": "^5.3.2", @@ -38,6 +41,7 @@ "plotly.js-basic-dist-min": "^1.58.4", "query-string": "^6.13.6", "react": "^16.14.0", + "react-big-calendar": "^0.38.0", "react-bootstrap": "^1.5.2", "react-color": "^2.19.3", "react-copy-to-clipboard": "^5.0.3", @@ -46,10 +50,13 @@ "react-dnd": "^14.0.2", "react-dnd-html5-backend": "^14.0.0", "react-dom": "^16.14.0", + "react-dropzone": "^11.4.2", "react-easy-sort": "^0.2.1", + "react-hot-toast": "^2.1.1", "react-json-view": "^1.21.3", "react-lazyload": "^3.2.0", "react-loading-skeleton": "^2.2.0", + "react-mentions": "^4.3.0", "react-plotly.js": "^2.5.1", "react-qr-reader": "^2.2.1", "react-rnd": "^10.3.0", @@ -73,7 +80,6 @@ "@cypress/react": "^5.9.0", "@cypress/webpack-dev-server": "^1.3.1", "@cypress/webpack-preprocessor": "^5.9.0", - "@svgr/webpack": "^5.5.0", "cypress": "^7.4.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", @@ -2692,7 +2698,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "dev": true, "engines": { "node": ">=10" }, @@ -2705,7 +2710,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "dev": true, "engines": { "node": ">=10" }, @@ -2718,7 +2722,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2731,7 +2734,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -2744,7 +2746,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "dev": true, "engines": { "node": ">=10" }, @@ -2757,7 +2758,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "dev": true, "engines": { "node": ">=10" }, @@ -2770,7 +2770,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "dev": true, "engines": { "node": ">=10" }, @@ -2783,7 +2782,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -2796,7 +2794,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", - "dev": true, "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", @@ -2819,7 +2816,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "dev": true, "dependencies": { "@svgr/plugin-jsx": "^5.5.0", "camelcase": "^6.2.0", @@ -2837,7 +2833,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true, "engines": { "node": ">=10" }, @@ -2849,7 +2844,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -2865,7 +2859,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2881,7 +2874,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -2899,7 +2891,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -2908,7 +2899,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -2917,7 +2907,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "dev": true, "dependencies": { "@babel/types": "^7.12.6" }, @@ -2933,7 +2922,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "dev": true, "dependencies": { "@babel/core": "^7.12.3", "@svgr/babel-preset": "^5.5.0", @@ -2952,7 +2940,6 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.14.5", "@babel/generator": "^7.15.0", @@ -2982,7 +2969,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -2991,7 +2977,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3000,7 +2985,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "dev": true, "dependencies": { "cosmiconfig": "^7.0.0", "deepmerge": "^4.2.2", @@ -3018,7 +3002,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -3034,7 +3017,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3043,7 +3025,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3059,7 +3040,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -3077,7 +3057,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -3086,7 +3065,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -3095,7 +3073,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "dev": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/plugin-transform-react-constant-elements": "^7.12.1", @@ -3118,7 +3095,6 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.14.5", "@babel/generator": "^7.15.0", @@ -3148,7 +3124,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -3162,7 +3137,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -3171,7 +3145,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4734,6 +4707,14 @@ "node": ">= 4.5.0" } }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" + } + }, "node_modules/autoprefixer": { "version": "9.8.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", @@ -7614,6 +7595,11 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/date-arithmetic": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", + "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==" + }, "node_modules/date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -8263,6 +8249,18 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, + "node_modules/emoji-mart": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", + "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -10116,6 +10114,22 @@ "webpack": "^4.0.0" } }, + "node_modules/file-selector": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz", + "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/file-selector/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -14225,6 +14239,11 @@ "node": ">= 0.6" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -17396,6 +17415,47 @@ "pure-color": "^1.2.0" } }, + "node_modules/react-big-calendar": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-0.38.0.tgz", + "integrity": "sha512-eoVkt9gTo+f1HBL09+o7dYLxp6QxHv52fcn50P5PfaWp3S98uGLQqoqsvghT85koMKvGfDVa5V0+J7yHcaF07Q==", + "dependencies": { + "@babel/runtime": "^7.1.5", + "clsx": "^1.0.4", + "date-arithmetic": "^4.1.0", + "dom-helpers": "^5.1.0", + "invariant": "^2.2.4", + "lodash": "^4.17.11", + "lodash-es": "^4.17.11", + "memoize-one": "^5.1.1", + "prop-types": "^15.7.2", + "react-overlays": "^4.1.1", + "uncontrollable": "^7.0.0" + }, + "peerDependencies": { + "react": "^16.6.1 || ^17", + "react-dom": "^16.6.1 || ^17" + } + }, + "node_modules/react-big-calendar/node_modules/react-overlays": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz", + "integrity": "sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@popperjs/core": "^2.5.3", + "@restart/hooks": "^0.3.25", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/react-bootstrap": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.2.tgz", @@ -17843,6 +17903,22 @@ "prop-types": "^15.6.0" } }, + "node_modules/react-dropzone": { + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.4.2.tgz", + "integrity": "sha512-ocYzYn7Qgp0tFc1gQtUTOaHHSzVTwhWHxxY+r7cj2jJTPfMTZB5GWSJHdIVoxsl+EQENpjJ/6Zvcw0BqKZQ+Eg==", + "dependencies": { + "attr-accept": "^2.2.1", + "file-selector": "^0.2.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 16.8" + } + }, "node_modules/react-easy-sort": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-0.2.1.tgz", @@ -17866,6 +17942,35 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" }, + "node_modules/react-hot-toast": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.1.1.tgz", + "integrity": "sha512-Odrp4wue0fHh0pOfZt5H+9nWCMtqs3wdlFSzZPp7qsxfzmbE26QmGWIh6hG43CukiPeOjA8WQhBJU8JwtWvWbQ==", + "dependencies": { + "goober": "^2.0.35" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-hot-toast/node_modules/csstype": { + "version": "2.6.18", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", + "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==", + "peer": true + }, + "node_modules/react-hot-toast/node_modules/goober": { + "version": "2.0.41", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.0.41.tgz", + "integrity": "sha512-kwjegMT5018zWydhOQlQneCgCtrKJaPsru7TaBWmTYV0nsMeUrM6L6O8JmNYb9UbPMgWcmtf+9p4Y3oJabIH1A==", + "peerDependencies": { + "csstype": "^2.6.2" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -17911,6 +18016,41 @@ "react": "^15.6.1 || ^16.0.0 || ^17.0.0" } }, + "node_modules/react-mentions": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.3.0.tgz", + "integrity": "sha512-qr60FhczwzDX/0ZHWKaV8nriDx0a/+totqa7z1jGiz8YipNMp3ReAJMfA+UUlIVj1lnbCzpFUAo/bTQBW0IVlg==", + "dependencies": { + "@babel/runtime": "7.4.5", + "invariant": "^2.2.4", + "prop-types": "^15.5.8", + "substyle": "^9.1.0" + }, + "peerDependencies": { + "react": ">=16.8.3", + "react-dom": ">=16.8.3" + } + }, + "node_modules/react-mentions/node_modules/@babel/runtime": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", + "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", + "dependencies": { + "regenerator-runtime": "^0.13.2" + } + }, + "node_modules/react-mentions/node_modules/substyle": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz", + "integrity": "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==", + "dependencies": { + "@babel/runtime": "^7.3.4", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": ">=16.8.3" + } + }, "node_modules/react-moment-proptypes": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.8.1.tgz", @@ -26359,56 +26499,47 @@ "@svgr/babel-plugin-add-jsx-attribute": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "dev": true + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==" }, "@svgr/babel-plugin-remove-jsx-attribute": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "dev": true + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==" }, "@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "dev": true + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==" }, "@svgr/babel-plugin-replace-jsx-attribute-value": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "dev": true + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==" }, "@svgr/babel-plugin-svg-dynamic-title": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "dev": true + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==" }, "@svgr/babel-plugin-svg-em-dimensions": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "dev": true + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==" }, "@svgr/babel-plugin-transform-react-native-svg": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "dev": true + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==" }, "@svgr/babel-plugin-transform-svg-component": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "dev": true + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==" }, "@svgr/babel-preset": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", - "dev": true, "requires": { "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", @@ -26424,7 +26555,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "dev": true, "requires": { "@svgr/plugin-jsx": "^5.5.0", "camelcase": "^6.2.0", @@ -26434,14 +26564,12 @@ "camelcase": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" }, "cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, "requires": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -26454,7 +26582,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -26464,7 +26591,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -26475,14 +26601,12 @@ "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" } } }, @@ -26490,7 +26614,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "dev": true, "requires": { "@babel/types": "^7.12.6" } @@ -26499,7 +26622,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "dev": true, "requires": { "@babel/core": "^7.12.3", "@svgr/babel-preset": "^5.5.0", @@ -26511,7 +26633,6 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", - "dev": true, "requires": { "@babel/code-frame": "^7.14.5", "@babel/generator": "^7.15.0", @@ -26533,14 +26654,12 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -26548,7 +26667,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "dev": true, "requires": { "cosmiconfig": "^7.0.0", "deepmerge": "^4.2.2", @@ -26559,7 +26677,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, "requires": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -26571,14 +26688,12 @@ "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -26588,7 +26703,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -26599,14 +26713,12 @@ "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" } } }, @@ -26614,7 +26726,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "dev": true, "requires": { "@babel/core": "^7.12.3", "@babel/plugin-transform-react-constant-elements": "^7.12.1", @@ -26630,7 +26741,6 @@ "version": "7.15.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", - "dev": true, "requires": { "@babel/code-frame": "^7.14.5", "@babel/generator": "^7.15.0", @@ -26653,7 +26763,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -26663,14 +26772,12 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -27925,6 +28032,11 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + }, "autoprefixer": { "version": "9.8.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", @@ -30244,6 +30356,11 @@ } } }, + "date-arithmetic": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", + "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==" + }, "date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -30786,6 +30903,15 @@ } } }, + "emoji-mart": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", + "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", + "requires": { + "@babel/runtime": "^7.0.0", + "prop-types": "^15.6.0" + } + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -32208,6 +32334,21 @@ "schema-utils": "^2.5.0" } }, + "file-selector": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz", + "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==", + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -35386,6 +35527,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -37974,6 +38120,41 @@ "pure-color": "^1.2.0" } }, + "react-big-calendar": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-0.38.0.tgz", + "integrity": "sha512-eoVkt9gTo+f1HBL09+o7dYLxp6QxHv52fcn50P5PfaWp3S98uGLQqoqsvghT85koMKvGfDVa5V0+J7yHcaF07Q==", + "requires": { + "@babel/runtime": "^7.1.5", + "clsx": "^1.0.4", + "date-arithmetic": "^4.1.0", + "dom-helpers": "^5.1.0", + "invariant": "^2.2.4", + "lodash": "^4.17.11", + "lodash-es": "^4.17.11", + "memoize-one": "^5.1.1", + "prop-types": "^15.7.2", + "react-overlays": "^4.1.1", + "uncontrollable": "^7.0.0" + }, + "dependencies": { + "react-overlays": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz", + "integrity": "sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "@popperjs/core": "^2.5.3", + "@restart/hooks": "^0.3.25", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + } + } + } + }, "react-bootstrap": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.2.tgz", @@ -38311,6 +38492,16 @@ "prop-types": "^15.6.0" } }, + "react-dropzone": { + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.4.2.tgz", + "integrity": "sha512-ocYzYn7Qgp0tFc1gQtUTOaHHSzVTwhWHxxY+r7cj2jJTPfMTZB5GWSJHdIVoxsl+EQENpjJ/6Zvcw0BqKZQ+Eg==", + "requires": { + "attr-accept": "^2.2.1", + "file-selector": "^0.2.2", + "prop-types": "^15.7.2" + } + }, "react-easy-sort": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-0.2.1.tgz", @@ -38332,6 +38523,28 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" }, + "react-hot-toast": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.1.1.tgz", + "integrity": "sha512-Odrp4wue0fHh0pOfZt5H+9nWCMtqs3wdlFSzZPp7qsxfzmbE26QmGWIh6hG43CukiPeOjA8WQhBJU8JwtWvWbQ==", + "requires": { + "goober": "^2.0.35" + }, + "dependencies": { + "csstype": { + "version": "2.6.18", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", + "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==", + "peer": true + }, + "goober": { + "version": "2.0.41", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.0.41.tgz", + "integrity": "sha512-kwjegMT5018zWydhOQlQneCgCtrKJaPsru7TaBWmTYV0nsMeUrM6L6O8JmNYb9UbPMgWcmtf+9p4Y3oJabIH1A==", + "requires": {} + } + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -38367,6 +38580,36 @@ "@emotion/core": "^10.0.22" } }, + "react-mentions": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.3.0.tgz", + "integrity": "sha512-qr60FhczwzDX/0ZHWKaV8nriDx0a/+totqa7z1jGiz8YipNMp3ReAJMfA+UUlIVj1lnbCzpFUAo/bTQBW0IVlg==", + "requires": { + "@babel/runtime": "7.4.5", + "invariant": "^2.2.4", + "prop-types": "^15.5.8", + "substyle": "^9.1.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", + "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "substyle": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz", + "integrity": "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==", + "requires": { + "@babel/runtime": "^7.3.4", + "invariant": "^2.2.4" + } + } + } + }, "react-moment-proptypes": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.8.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 83f8b1c30a..d44ad27a84 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "@react-google-maps/api": "^2.1.1", "@sentry/react": "^6.12.0", "@sentry/tracing": "^6.12.0", + "@svgr/webpack": "^5.5.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", @@ -22,10 +23,11 @@ "classnames": "^2.3.1", "dompurify": "^2.2.7", "draft-js": "^0.11.7", - "html-webpack-plugin": "^5.3.2", "draft-js-export-html": "^1.4.1", + "emoji-mart": "^3.0.1", "fuse.js": "^6.4.6", "history": "^4.9.0", + "html-webpack-plugin": "^5.3.2", "immutability-helper": "^3.1.1", "lodash": "^4.17.21", "moment": "^2.29.1", @@ -34,6 +36,7 @@ "plotly.js-basic-dist-min": "^1.58.4", "query-string": "^6.13.6", "react": "^16.14.0", + "react-big-calendar": "^0.38.0", "react-bootstrap": "^1.5.2", "react-color": "^2.19.3", "react-copy-to-clipboard": "^5.0.3", @@ -42,10 +45,13 @@ "react-dnd": "^14.0.2", "react-dnd-html5-backend": "^14.0.0", "react-dom": "^16.14.0", + "react-dropzone": "^11.4.2", "react-easy-sort": "^0.2.1", + "react-hot-toast": "^2.1.1", "react-json-view": "^1.21.3", "react-lazyload": "^3.2.0", "react-loading-skeleton": "^2.2.0", + "react-mentions": "^4.3.0", "react-plotly.js": "^2.5.1", "react-qr-reader": "^2.2.1", "react-rnd": "^10.3.0", @@ -61,15 +67,14 @@ "semver": "^5.7.1", "tinycolor2": "^1.4.2", "uuid": "8.3.2", - "yup": "^0.27.0", "webpack": "^5.55.1", - "webpack-cli": "^4.8.0" + "webpack-cli": "^4.8.0", + "yup": "^0.27.0" }, "devDependencies": { "@cypress/react": "^5.9.0", "@cypress/webpack-dev-server": "^1.3.1", "@cypress/webpack-preprocessor": "^5.9.0", - "@svgr/webpack": "^5.5.0", "cypress": "^7.4.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", @@ -98,4 +103,4 @@ "production": [], "development": [] } -} \ No newline at end of file +} diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index 808201e6bc..e8f6a597fe 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -10,6 +10,7 @@ import { InvitationPage } from '@/InvitationPage'; import { Authorize } from '@/Oauth2'; import { Editor, Viewer } from '@/Editor'; import '@/_styles/theme.scss'; +import 'emoji-mart/css/emoji-mart.css'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { ManageGroupPermissions } from '@/ManageGroupPermissions'; @@ -20,6 +21,7 @@ import { OnboardingModal } from '@/Onboarding/OnboardingModal'; import { ForgotPassword } from '@/ForgotPassword'; import { ResetPassword } from '@/ResetPassword'; import { lt } from 'semver'; +import { Toaster } from 'react-hot-toast'; class App extends React.Component { constructor(props) { @@ -63,102 +65,119 @@ class App extends React.Component { } return ( - -
- {updateAvailable && ( -
-

Update available

-

A new version of ToolJet has been released.

- - + + + + + + + + + + + + + + +
+ + + ); } } diff --git a/frontend/src/Editor/Box.jsx b/frontend/src/Editor/Box.jsx index 1dca4b5692..8bd2b82b7d 100644 --- a/frontend/src/Editor/Box.jsx +++ b/frontend/src/Editor/Box.jsx @@ -21,7 +21,9 @@ import { ToggleSwitch } from './Components/Toggle'; import { RadioButton } from './Components/RadioButton'; import { StarRating } from './Components/StarRating'; import { Divider } from './Components/Divider'; +import { FilePicker } from './Components/FilePicker'; import { PasswordInput } from './Components/PasswordInput'; +import { Calendar } from './Components/Calendar'; import { renderTooltip } from '../_helpers/appUtils'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import '@/_styles/custom.scss'; @@ -51,7 +53,9 @@ const AllComponents = { RadioButton, StarRating, Divider, + FilePicker, PasswordInput, + Calendar, }; export const Box = function Box({ @@ -72,6 +76,7 @@ export const Box = function Box({ containerProps, darkMode, removeComponent, + mode, }) { const backgroundColor = yellow ? 'yellow' : ''; @@ -91,7 +96,12 @@ export const Box = function Box({ const resolvedStyles = resolveStyles(component, currentState); const exposedVariables = currentState?.components[component.name] ?? {}; - const fireEvent = (eventName, options) => onEvent(eventName, { ...options, component }); + const fireEvent = (eventName, options) => { + if (mode === 'edit' && eventName === 'onClick') { + onComponentClick(id, component); + } + onEvent(eventName, { ...options, component }); + }; const validate = (value) => validateWidget({ ...{ widgetValue: value }, diff --git a/frontend/src/Editor/Comment/CommentActions.jsx b/frontend/src/Editor/Comment/CommentActions.jsx new file mode 100644 index 0000000000..e36f1c6f20 --- /dev/null +++ b/frontend/src/Editor/Comment/CommentActions.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import cx from 'classnames'; + +import { useSpring, animated } from 'react-spring'; +import usePopover from '@/_hooks/use-popover'; + +import OptionsIcon from './icons/options.svg'; +// import OptionsSelectedIcon from './icons/options-selected.svg'; +import useRouter from '@/_hooks/use-router'; + +import { commentsService } from '@/_services'; + +const CommentActions = ({ + socket, + commentId, + comment, + setEditCommentId, + setEditComment, + fetchComments, + isCommentOwner, +}) => { + const [open, trigger, content, setOpen] = usePopover(false); + const popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 }); + const router = useRouter(); + + const handleDelete = async () => { + await commentsService.deleteComment(commentId); + fetchComments(); + setOpen(false); + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'notifications', appId: router.query.id }, + }) + ); + }; + + const handleEdit = async () => { + setEditComment(comment); + setEditCommentId(commentId); + setOpen(false); + }; + + return ( +
+ {isCommentOwner && ( + <> + + + + +
+
+ Edit +
+ {/* TODO: Add a popup confirmation on delete */} +
+ Delete +
+
+
+ + )} +
+ ); +}; + +export default CommentActions; diff --git a/frontend/src/Editor/Comment/CommentBody.jsx b/frontend/src/Editor/Comment/CommentBody.jsx new file mode 100644 index 0000000000..2c7b1a598c --- /dev/null +++ b/frontend/src/Editor/Comment/CommentBody.jsx @@ -0,0 +1,88 @@ +import React from 'react'; +import cx from 'classnames'; +import Spinner from '@/_ui/Spinner'; + +import { isEmpty } from 'lodash'; +import moment from 'moment'; +import CommentActions from './CommentActions'; + +moment.updateLocale('en', { + relativeTime: { + past: '%s', + s: 'just now', + }, +}); + +const CommentBody = ({ socket, thread, isLoading, setEditComment, setEditCommentId, fetchComments }) => { + const bottomRef = React.useRef(); + + const scrollToBottom = () => { + bottomRef?.current?.scrollIntoView({ + behavior: 'instant', + block: 'center', + }); + }; + + React.useEffect(() => { + scrollToBottom(); + }, [thread]); + + React.useLayoutEffect(() => { + scrollToBottom(); + }, []); + + const getComment = (comment) => { + var regex = /(\()([^)]+)(\))/g; + return comment.replace(regex, '$2'); + }; + + const getContent = () => { + if (isEmpty(thread)) return
There are no comments to display
; + + const currentUser = JSON.parse(localStorage.getItem('currentUser')); + return ( +
+ {thread.map(({ id, comment, createdAt, user = {} }) => { + return ( +
+
+ {`${user?.firstName} ${user?.lastName}`}{' '} + +
+ +
{moment(createdAt).fromNow()}
+

+

+ ); + })} +
+ ); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( + <> +
+ {getContent()} +
+
+ + ); +}; + +export default CommentBody; diff --git a/frontend/src/Editor/Comment/CommentFooter.jsx b/frontend/src/Editor/Comment/CommentFooter.jsx new file mode 100644 index 0000000000..7b32759f0d --- /dev/null +++ b/frontend/src/Editor/Comment/CommentFooter.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import cx from 'classnames'; +import { Picker } from 'emoji-mart'; + +import TextareaMentions from '@/_ui/Mentions'; +import Button from '@/_ui/Button'; +import useShortcuts from '@/_hooks/use-shortcuts'; +import usePopover from '@/_hooks/use-popover'; + +function CommentFooter({ editComment = '', editCommentId, handleSubmit }) { + const [comment, setComment] = React.useState(editComment); + const [loading, setLoading] = React.useState(false); + const [open, trigger, content, setOpen] = usePopover(false); + + React.useEffect(() => { + setComment(editComment); + }, [editComment]); + + const handleClick = async () => { + setLoading(true); + await handleSubmit(comment, editCommentId); + setComment(''); + setLoading(false); + }; + + const addEmoji = (emoji) => { + setComment(comment + ' ' + emoji.native); + setOpen(false); + }; + + useShortcuts(['Meta', 'Enter'], () => handleClick(), [comment]); + + return ( + <> +
+ +
+
+
+
+ +
+
+ + + +
+
+ +
+
+
+ + ); +} + +export default CommentFooter; diff --git a/frontend/src/Editor/Comment/CommentHeader.jsx b/frontend/src/Editor/Comment/CommentHeader.jsx new file mode 100644 index 0000000000..b637668fb6 --- /dev/null +++ b/frontend/src/Editor/Comment/CommentHeader.jsx @@ -0,0 +1,114 @@ +import React from 'react'; +import cx from 'classnames'; +import toast from 'react-hot-toast'; + +import { commentsService } from '@/_services'; + +import { pluralize } from '@/_helpers/utils'; + +import Spinner from '@/_ui/Spinner'; +import useRouter from '@/_hooks/use-router'; + +import UnResolvedIcon from './icons/unresolved.svg'; +import ResolvedIcon from './icons/resolved.svg'; + +const CommentHeader = ({ socket, count = 0, threadId, isResolved, isThreadOwner, fetchThreads, close }) => { + const [spinning, setSpinning] = React.useState(false); + const router = useRouter(); + + const handleResolved = async () => { + setSpinning(true); + await commentsService.updateThread(threadId, { isResolved: !isResolved }); + setSpinning(false); + fetchThreads(); + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'notifications', appId: router.query.id }, + }) + ); + if (!isResolved) { + toast.success('Thread resolved'); + } else { + toast('Thread unresolved'); + } + }; + + const handleDelete = async () => { + await commentsService.deleteThread(threadId); + toast.success('Thread deleted'); + fetchThreads(); + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'notifications', appId: router.query.id }, + }) + ); + }; + + const getResolveIcon = () => { + if (spinning) return ; + + if (isResolved) return ; + + return ; + }; + + const getIcon = () => { + if (isResolved) + return ( + + + + ); + + return ( + + + + ); + }; + + return ( +
+
+ {getIcon()} {pluralize(count, 'comment')} +
+
+ + {getResolveIcon()} + + + + +
+ + + +
+
+
+ ); +}; + +export default CommentHeader; diff --git a/frontend/src/Editor/Comment/icons/options-selected.svg b/frontend/src/Editor/Comment/icons/options-selected.svg new file mode 100644 index 0000000000..c395cd1ed8 --- /dev/null +++ b/frontend/src/Editor/Comment/icons/options-selected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/Editor/Comment/icons/options.svg b/frontend/src/Editor/Comment/icons/options.svg new file mode 100644 index 0000000000..e28a73feed --- /dev/null +++ b/frontend/src/Editor/Comment/icons/options.svg @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/Editor/Comment/icons/resolved.svg b/frontend/src/Editor/Comment/icons/resolved.svg new file mode 100644 index 0000000000..1b6c7a6942 --- /dev/null +++ b/frontend/src/Editor/Comment/icons/resolved.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/Editor/Comment/icons/unresolved.svg b/frontend/src/Editor/Comment/icons/unresolved.svg new file mode 100644 index 0000000000..0600cbf145 --- /dev/null +++ b/frontend/src/Editor/Comment/icons/unresolved.svg @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/frontend/src/Editor/Comment/index.jsx b/frontend/src/Editor/Comment/index.jsx new file mode 100644 index 0000000000..fc216653ed --- /dev/null +++ b/frontend/src/Editor/Comment/index.jsx @@ -0,0 +1,170 @@ +import React from 'react'; +import cx from 'classnames'; +import { useSpring, animated } from 'react-spring'; + +import { useDrag } from 'react-dnd'; +import { ItemTypes } from '@/Editor/ItemTypes'; +import CommentHeader from '@/Editor/Comment/CommentHeader'; +import CommentBody from '@/Editor/Comment/CommentBody'; +import CommentFooter from '@/Editor/Comment/CommentFooter'; +import usePopover from '@/_hooks/use-popover'; +import { commentsService } from '@/_services'; +import useRouter from '@/_hooks/use-router'; + +const Comment = ({ socket, x, y, threadId, user = {}, isResolved, fetchThreads, appVersionsId }) => { + const [loading, setLoading] = React.useState(true); + const [editComment, setEditComment] = React.useState(''); + const [editCommentId, setEditCommentId] = React.useState(''); + const [thread, setThread] = React.useState([]); + const [placement, setPlacement] = React.useState('left'); + const [open, trigger, content, setOpen] = usePopover(false); + const [, drag] = useDrag(() => ({ + type: ItemTypes.COMMENT, + item: { threadId, name: 'comment' }, + })); + const router = useRouter(); + + React.useEffect(() => { + // Listen for messages + // TODO: add check if user is the initiator of this event, don't fetch data + socket?.addEventListener('message', function (event) { + if (event.data === threadId) fetchData(); + }); + }, []); + + React.useLayoutEffect(() => { + const { left } = trigger?.ref?.current?.getBoundingClientRect(); + + if (left < 460) setPlacement('right'); + else setPlacement('left'); + }, [trigger]); + + async function fetchData() { + const { data } = await commentsService.getComments(threadId, appVersionsId); + setThread(data); + setLoading(false); + } + + React.useEffect(() => { + if (open) { + fetchData(); + } else { + // resetting the query param + router.push(window.location.pathname); + } + }, [open]); + + React.useEffect(() => { + if (router.query.threadId === threadId) { + setOpen(true); + } else { + setOpen(false); + } + }, [router]); + + const handleSubmit = async (comment) => { + await commentsService.createComment({ + threadId, + comment, + appVersionsId, + }); + socket.send( + JSON.stringify({ + event: 'events', + data: { message: threadId, appId: router.query.id }, + }) + ); + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'notifications', appId: router.query.id }, + }) + ); + fetchData(); + }; + + const handleEdit = async (comment, cid) => { + await commentsService.updateComment(cid, { comment }); + fetchData(); + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'notifications', appId: router.query.id }, + }) + ); + }; + + const commentFadeStyle = useSpring({ from: { opacity: 0 }, to: { opacity: 1 } }); + const popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 }); + + const currentUser = JSON.parse(localStorage.getItem('currentUser')); + + return ( + <> + setOpen(false)} + onDragEnd={() => setOpen(true)} + > + + e.stopPropagation()} + > + + setTimeout(() => { + setOpen(false); + }, 0) + } + socket={socket} + threadId={threadId} + fetchThreads={fetchThreads} + isThreadOwner={currentUser.id === user.id} + isResolved={isResolved} + /> + + + + + {open &&
e.stopPropagation()} />} + + ); +}; + +export default Comment; diff --git a/frontend/src/Editor/CommentNotifications/Content.jsx b/frontend/src/Editor/CommentNotifications/Content.jsx new file mode 100644 index 0000000000..9d6c9e8fc6 --- /dev/null +++ b/frontend/src/Editor/CommentNotifications/Content.jsx @@ -0,0 +1,133 @@ +import React from 'react'; +import cx from 'classnames'; +import { isEmpty } from 'lodash'; +import { pluralize } from '@/_helpers/utils'; +import moment from 'moment'; +import usePopover from '@/_hooks/use-popover'; +import { useSpring, animated } from 'react-spring'; +import useRouter from '@/_hooks/use-router'; + +import Spinner from '@/_ui/Spinner'; + +const Content = ({ notifications, loading }) => { + const router = useRouter(); + const [selectedCommentId, setSelectedCommentId] = React.useState(router.query.commentId); + const [open, trigger, content] = usePopover(false); + const popoverFadeStyle = useSpring({ opacity: open ? 1 : 0 }); + + React.useEffect(() => { + if (router.query?.commentId) setSelectedCommentId(router.query?.commentId); + else setSelectedCommentId(''); + }, [router]); + + const getComment = (comment) => { + var regex = /(\()([^)]+)(\))/g; + return comment.replace(regex, '$2'); + }; + + const getContent = () => { + if (isEmpty(notifications)) + return ( +
+

{loading ? : 'No messages to show'}

+
+ ); + + return ( +
+ {notifications.map(({ comment, count }) => { + return ( +
{ + router.push({ + pathname: window.location.pathname, + search: `?threadId=${comment.thread.id}&commentId=${comment.id}`, + }); + }} + key={comment.id} + > +
+ + {`${comment.user?.firstName} ${comment.user?.lastName}`}{' '} + +
{moment(comment.createdAt).fromNow()}
+
+
+
{`${count - 1} replies`}
+
+ ); + })} +
+ ); + }; + + // TODO: move filter to separate file + return ( +
+ {!loading && ( +
+ + Total {pluralize(notifications.length, 'comment')} + + {/*
+ + + + e.stopPropagation()} + > +
+
+ Show all +
+ + + +
+
+
+ Only mention of you +
+ + + +
+
+
+
+
*/} +
+ )} +
{getContent()}
+
+ ); +}; + +export default Content; diff --git a/frontend/src/Editor/CommentNotifications/index.jsx b/frontend/src/Editor/CommentNotifications/index.jsx new file mode 100644 index 0000000000..9f632509d8 --- /dev/null +++ b/frontend/src/Editor/CommentNotifications/index.jsx @@ -0,0 +1,83 @@ +import '@/_styles/editor/comment-notifications.scss'; + +import React from 'react'; +import Tabs from 'react-bootstrap/Tabs'; +import Tab from 'react-bootstrap/Tab'; + +import { commentsService } from '@/_services'; + +import TabContent from './Content'; + +import useRouter from '@/_hooks/use-router'; + +const CommentNotifications = ({ socket, toggleComments, appVersionsId }) => { + const [notifications, setNotifications] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [key, setKey] = React.useState('active'); + + const router = useRouter(); + + async function fetchData(k) { + const isResolved = k === 'resolved'; + setLoading(true); + const { data } = await commentsService.getNotifications(router.query.id, isResolved, appVersionsId); + setLoading(false); + setNotifications(data); + } + React.useEffect(() => { + fetchData(); + }, []); + + React.useEffect(() => { + // Listen for messages + socket?.addEventListener('message', function (event) { + if (event.data === 'notifications') fetchData(); + }); + }, []); + + return ( +
+
+ Comments +
+ + + +
+
+ + { + setKey(k); + setLoading(true); + await fetchData(k); + setLoading(false); + }} + className="dflex justify-content-center" + > + + + + + + + +
+ ); +}; + +export default CommentNotifications; diff --git a/frontend/src/Editor/Comments.jsx b/frontend/src/Editor/Comments.jsx new file mode 100644 index 0000000000..45dc8ef607 --- /dev/null +++ b/frontend/src/Editor/Comments.jsx @@ -0,0 +1,52 @@ +import '@/_styles/editor/comments.scss'; + +import React from 'react'; +import { isEmpty } from 'lodash'; + +import Comment from './Comment'; +import { commentsService } from '@/_services'; + +import useRouter from '@/_hooks/use-router'; + +const Comments = ({ newThread = {}, appVersionsId, socket }) => { + const [threads, setThreads] = React.useState([]); + const router = useRouter(); + + async function fetchData() { + const { data } = await commentsService.getThreads(router.query.id, appVersionsId); + setThreads(data); + } + + React.useEffect(() => { + fetchData(); + }, []); + + React.useEffect(() => { + // Listen for messages + socket?.addEventListener('message', function (event) { + if (event.data === 'threads') fetchData(); + }); + }, []); + + React.useEffect(() => { + !isEmpty(newThread) && setThreads([...threads, newThread]); + }, [newThread]); + + if (isEmpty(threads)) return null; + + return threads.map((thread) => { + const { id } = thread; + return ( + + ); + }); +}; + +export default Comments; diff --git a/frontend/src/Editor/Components/Calendar.jsx b/frontend/src/Editor/Components/Calendar.jsx new file mode 100644 index 0000000000..3d1121f351 --- /dev/null +++ b/frontend/src/Editor/Components/Calendar.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Calendar as ReactCalendar, momentLocalizer } from 'react-big-calendar'; +import moment from 'moment'; +import 'react-big-calendar/lib/css/react-big-calendar.css'; + +const localizer = momentLocalizer(moment); + +const prepareEvent = (event, dateFormat) => ({ + ...event, + start: moment(event.start, dateFormat).toDate(), + end: moment(event.end, dateFormat).toDate(), +}); + +const parseDate = (date, dateFormat) => moment(date, dateFormat).toDate(); + +const allowedCalendarViews = ['month', 'week', 'day']; + +export const Calendar = function ({ height, width, properties, styles, fireEvent, darkMode }) { + const style = { height, width }; + const resourcesParam = properties.resources?.length === 0 ? {} : { resources: properties.resources }; + + const events = properties.events ? properties.events.map((event) => prepareEvent(event, properties.dateFormat)) : []; + const defaultDate = parseDate(properties.defaultDate, properties.dateFormat); + + const eventPropGetter = (event) => { + const backgroundColor = event.color; + const textStyle = + event.textOrientation === 'vertical' ? { writingMode: 'vertical-rl', textOrientation: 'mixed' } : {}; + const style = { backgroundColor, ...textStyle, padding: 3, paddingLeft: 5, paddingRight: 5 }; + + return { style }; + }; + + const slotSelectHandler = (calendarSlots) => { + const { slots, start, end, resourceId, action } = calendarSlots; + const formattedSlots = slots.map((slot) => moment(slot).format(properties.dateFormat)); + const formattedStart = moment(start).format(properties.dateFormat); + const formattedEnd = moment(end).format(properties.dateFormat); + + const selectedSlots = { + slots: formattedSlots, + start: formattedStart, + end: formattedEnd, + resourceId, + action, + }; + + fireEvent('onCalendarSlotSelect', { selectedSlots }); + }; + + const defaultView = allowedCalendarViews.includes(properties.defaultView) + ? properties.defaultView + : allowedCalendarViews[0]; + + return ( +
+ fireEvent('onCalendarEventSelect', { calendarEvent })} + selectable={true} + onSelectSlot={slotSelectHandler} + toolbar={properties.displayToolbar} + eventPropGetter={eventPropGetter} + tooltipAccessor="tooltip" + popup={true} + /> +
+ ); +}; diff --git a/frontend/src/Editor/Components/Chart.jsx b/frontend/src/Editor/Components/Chart.jsx index b0920f3347..5d9f81259b 100644 --- a/frontend/src/Editor/Components/Chart.jsx +++ b/frontend/src/Editor/Components/Chart.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; // Use plotly basic bundle @@ -8,7 +8,6 @@ const Plot = createPlotlyComponent(Plotly); export const Chart = function Chart({ id, width, height, component, onComponentClick, currentState, darkMode }) { const [loadingState, setLoadingState] = useState(false); - const [chartData, setChartData] = useState([]); const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const disabledState = component.definition.styles?.disabledState?.value ?? false; @@ -87,8 +86,8 @@ export const Chart = function Chart({ id, width, height, component, onComponentC const data = resolveReferences(dataString, currentState, []); - useEffect(() => { - let rawData = data || []; + const computeChartData = (data, dataString) => { + let rawData = data; if (typeof rawData === 'string') { try { rawData = JSON.parse(dataString); @@ -122,9 +121,11 @@ export const Chart = function Chart({ id, width, height, component, onComponentC ]; } - setChartData(newData); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, chartType]); + return newData; + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const memoizedChartData = useMemo(() => computeChartData(data, dataString), [data, dataString]); return (
{ event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > {loadingState === true ? ( @@ -143,7 +144,7 @@ export const Chart = function Chart({ id, width, height, component, onComponentC
) : ( { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} >
diff --git a/frontend/src/Editor/Components/Datepicker.jsx b/frontend/src/Editor/Components/Datepicker.jsx index 3d0ddc1d7f..c879b45ce1 100644 --- a/frontend/src/Editor/Components/Datepicker.jsx +++ b/frontend/src/Editor/Components/Datepicker.jsx @@ -87,7 +87,7 @@ export const Datepicker = function Datepicker({ style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} >
diff --git a/frontend/src/Editor/Components/FilePicker.jsx b/frontend/src/Editor/Components/FilePicker.jsx new file mode 100644 index 0000000000..9321d6a67c --- /dev/null +++ b/frontend/src/Editor/Components/FilePicker.jsx @@ -0,0 +1,257 @@ +import React, { useEffect, useMemo } from 'react'; +import { useDropzone } from 'react-dropzone'; +import { resolveWidgetFieldValue } from '@/_helpers/utils'; +import { toast } from 'react-toastify'; + +export const FilePicker = ({ width, height, component, currentState, onComponentOptionChanged, onEvent, darkMode }) => { + //* properties definitions + const enableDropzone = component.definition.properties.enableDropzone?.value ?? true; + const enablePicker = component.definition.properties?.enablePicker?.value ?? true; + const maxFileCount = component.definition.properties.maxFileCount?.value ?? 2; + const enableMultiple = component.definition.properties.enableMultiple?.value ?? false; + const fileType = component.definition.properties.fileType?.value ?? 'image/*'; + const maxSize = component.definition.properties.maxSize?.value ?? 1048576; + const minSize = component.definition.properties.minSize?.value ?? 0; + + const parsedEnableDropzone = + typeof enableDropzone !== 'boolean' ? resolveWidgetFieldValue(enableDropzone, currentState) : true; + const parsedEnablePicker = + typeof enablePicker !== 'boolean' ? resolveWidgetFieldValue(enablePicker, currentState) : true; + const parsedMaxFileCount = + typeof maxFileCount !== 'number' ? resolveWidgetFieldValue(maxFileCount, currentState) : maxFileCount; + const parsedEnableMultiple = + typeof enableMultiple !== 'boolean' ? resolveWidgetFieldValue(enableMultiple, currentState) : enableMultiple; + const parsedFileType = resolveWidgetFieldValue(fileType, currentState); + const parsedMinSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(minSize, currentState) : minSize; + const parsedMaxSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(maxSize, currentState) : maxSize; + + //* styles definitions + const widgetVisibility = component.definition.styles?.visibility?.value ?? true; + const disabledState = component.definition.styles?.disabledState?.value ?? false; + + const parsedDisabledState = + typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; + const parsedWidgetVisibility = + typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility, currentState) : widgetVisibility; + + const bgThemeColor = darkMode ? '#232E3C' : '#fff'; + + const baseStyle = { + flex: 1, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '20px', + borderWidth: 1.5, + borderRadius: 2, + borderColor: '#42536A', + borderStyle: 'dashed', + color: '#bdbdbd', + outline: 'none', + transition: 'border .24s ease-in-out', + display: parsedWidgetVisibility ? 'flex' : 'none', + width, + height, + backgroundColor: !parsedDisabledState && bgThemeColor, + }; + + const activeStyle = { + borderColor: '#2196f3', + }; + + const acceptStyle = { + borderColor: '#00e676', + }; + + const rejectStyle = { + borderColor: '#ff1744', + }; + + const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject, acceptedFiles, fileRejections } = + useDropzone({ + accept: parsedFileType, + noClick: !parsedEnablePicker, + noDrag: !parsedEnableDropzone, + noKeyboard: true, + maxFiles: parsedMaxFileCount, + minSize: parsedMinSize, + maxSize: parsedMaxSize, + multiple: parsedEnableMultiple, + disabled: parsedDisabledState, + }); + + const style = useMemo( + () => ({ + ...baseStyle, + ...(isDragActive && parsedEnableDropzone ? activeStyle : {}), + ...(isDragAccept && parsedEnableDropzone ? acceptStyle : {}), + ...(isDragReject && parsedEnableDropzone ? rejectStyle : {}), + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [baseStyle, isDragActive, isDragAccept, acceptStyle, isDragReject] + ); + + const [accepted, setAccepted] = React.useState(false); + const [showSelectdFiles, setShowSelectedFiles] = React.useState(false); + const [selectedFiles, setSelectedFiles] = React.useState([]); + + useEffect(() => { + if (acceptedFiles.length === 0) { + onComponentOptionChanged(component, 'file', []); + } + + if (acceptedFiles.length !== 0) { + const fileData = parsedEnableMultiple ? [...selectedFiles] : []; + acceptedFiles.map((acceptedFile) => { + return new Promise((resolve, reject) => { + let reader = new FileReader(); + reader.onload = (result) => { + //* Resolve both the FileReader result and its original file. + resolve([result, acceptedFile]); + }; + //* Reads contents of the file as a text string. + reader.readAsText(acceptedFile); + }).then((zippedResults) => { + //? Run the callback after all files have been read. + const fileSelected = { + name: zippedResults[1].name, + content: zippedResults[0].srcElement.result, + type: zippedResults[1].type, + }; + + fileData.push(fileSelected); + }); + }); + + setSelectedFiles(fileData); + onComponentOptionChanged(component, 'file', fileData).then(() => + onEvent('onFileSelected', { component }).then(() => { + setAccepted(true); + return new Promise(function (resolve, reject) { + setTimeout(() => { + setShowSelectedFiles(true); + setAccepted(false); + resolve(); + }, 600); + }); + }) + ); + } + + if (fileRejections.length > 0) { + fileRejections.map((rejectedFile) => + toast.error(rejectedFile.errors[0].message, { hideProgressBar: true, autoClose: 3000 }) + ); + } + + return () => { + setAccepted(false); + setShowSelectedFiles(false); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [acceptedFiles.length, fileRejections.length]); + + const clearSelectedFiles = (index) => { + setSelectedFiles((prevState) => { + const copy = JSON.parse(JSON.stringify(prevState)); + copy.splice(index, 1); + return copy; + }); + }; + + useEffect(() => { + if (selectedFiles.length === 0) { + setShowSelectedFiles(false); + } + onComponentOptionChanged(component, 'file', selectedFiles); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedFiles]); + + return ( +
+ {showSelectdFiles ? ( + + {selectedFiles.map((acceptedFile, index) => ( + <> +
+ 0} + feedback={acceptedFile.name} + cls="text-secondary d-flex justify-content-start file-list" + /> +
+
+ +
+ + ))} +
+ ) : ( + //* Dropzone +
+ + + + + + + +
+ )} +
+ ); +}; + +FilePicker.Signifiers = ({ signifier, feedback, cls }) => { + if (signifier) { + return
{feedback === null ?
:

{feedback}

}
; + } + + return null; +}; + +FilePicker.AcceptedFiles = ({ children, width, height, showFilezone, bgThemeColor }) => { + const styles = { + borderWidth: 1.5, + borderRadius: 2, + borderColor: '#42536A', + borderStyle: 'dashed', + color: '#bdbdbd', + outline: 'none', + padding: '5px', + overflowX: 'hidden', + overflowY: 'auto', + scrollbarWidth: 'none', + width, + height, + backgroundColor: bgThemeColor, + }; + return ( + + ); +}; diff --git a/frontend/src/Editor/Components/Image.jsx b/frontend/src/Editor/Components/Image.jsx index f30fa677bc..b1f05c00a3 100644 --- a/frontend/src/Editor/Components/Image.jsx +++ b/frontend/src/Editor/Components/Image.jsx @@ -31,7 +31,7 @@ export const Image = function Image({ id, width, height, component, onComponentC style={{ display: parsedWidgetVisibility ? '' : 'none' }} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > } debounce={500}> diff --git a/frontend/src/Editor/Components/Map/Map.jsx b/frontend/src/Editor/Components/Map/Map.jsx index 75be5ce4c0..a80507620c 100644 --- a/frontend/src/Editor/Components/Map/Map.jsx +++ b/frontend/src/Editor/Components/Map/Map.jsx @@ -114,7 +114,7 @@ export const Map = function Map({ style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} className="map-widget" > diff --git a/frontend/src/Editor/Components/Multiselect.jsx b/frontend/src/Editor/Components/Multiselect.jsx index 41d8824927..c91ad8c44b 100644 --- a/frontend/src/Editor/Components/Multiselect.jsx +++ b/frontend/src/Editor/Components/Multiselect.jsx @@ -58,7 +58,7 @@ export const Multiselect = function Multiselect({ style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} >
diff --git a/frontend/src/Editor/Components/NumberInput.jsx b/frontend/src/Editor/Components/NumberInput.jsx index 9f10c5708d..e0bc2f8f35 100644 --- a/frontend/src/Editor/Components/NumberInput.jsx +++ b/frontend/src/Editor/Components/NumberInput.jsx @@ -45,7 +45,7 @@ export const NumberInput = function NumberInput({ disabled={parsedDisabledState} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} onChange={(e) => { setNumber(parseInt(e.target.value)); diff --git a/frontend/src/Editor/Components/RadioButton.jsx b/frontend/src/Editor/Components/RadioButton.jsx index 9ea96d9608..d141d5ae3e 100644 --- a/frontend/src/Editor/Components/RadioButton.jsx +++ b/frontend/src/Editor/Components/RadioButton.jsx @@ -89,7 +89,7 @@ export const RadioButton = function RadioButton({ style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > diff --git a/frontend/src/Editor/Components/RichTextEditor.jsx b/frontend/src/Editor/Components/RichTextEditor.jsx index b205ac2f6f..f6e6714a19 100644 --- a/frontend/src/Editor/Components/RichTextEditor.jsx +++ b/frontend/src/Editor/Components/RichTextEditor.jsx @@ -38,7 +38,7 @@ export const RichTextEditor = function RichTextEditor({ style={{ width: `${width}px`, height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} style={{ display: parsedWidgetVisibility ? '' : 'none' }} > diff --git a/frontend/src/Editor/Components/Table/Table.jsx b/frontend/src/Editor/Components/Table/Table.jsx index 1f22f8e8a6..f147837534 100644 --- a/frontend/src/Editor/Components/Table/Table.jsx +++ b/frontend/src/Editor/Components/Table/Table.jsx @@ -38,6 +38,7 @@ export function Table({ onComponentOptionChanged, onComponentOptionsChanged, darkMode, + fireEvent, }) { const color = component.definition.styles.textColor.value; const actions = component.definition.properties.actions || { value: [] }; @@ -189,7 +190,7 @@ export function Table({ [index]: { ...obj }, }; - onComponentOptionsChanged(component, [ + return onComponentOptionsChanged(component, [ ['dataUpdates', newDataUpdates], ['changeSet', newChangeset], ]); @@ -510,7 +511,15 @@ export function Table({ readOnly={!column.isEditable} activeColor={column.activeColor} onChange={(value) => { - handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); + handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original).then( + () => { + fireEvent('OnTableToggleCellChanged', { + column: column, + rowId: cell.row.id, + row: cell.row.original, + }); + } + ); }} />
@@ -767,7 +776,7 @@ export function Table({ style={{ width: `${width}px`, height: `${height}px`, display: parsedWidgetVisibility ? '' : 'none' }} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > {/* Show top bar unless search box is disabled and server pagination is enabled */} @@ -950,7 +959,7 @@ export function Table({
-

Filters

+

Filters

-
@@ -1017,16 +1029,16 @@ export function Table({ {filters.length === 0 && (
- no filters yet. + no filters yet.
)}
- -
diff --git a/frontend/src/Editor/Components/Text.jsx b/frontend/src/Editor/Components/Text.jsx index 33031a0ffd..17b21b0c79 100644 --- a/frontend/src/Editor/Components/Text.jsx +++ b/frontend/src/Editor/Components/Text.jsx @@ -58,7 +58,7 @@ export const Text = function Text({ id, width, height, component, onComponentCli style={computedStyles} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > {!loadingState &&
} diff --git a/frontend/src/Editor/Components/TextInput.jsx b/frontend/src/Editor/Components/TextInput.jsx index d35d5789cc..448e872122 100644 --- a/frontend/src/Editor/Components/TextInput.jsx +++ b/frontend/src/Editor/Components/TextInput.jsx @@ -56,7 +56,7 @@ export const TextInput = function TextInput({ disabled={parsedDisabledState} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} onChange={(e) => { setText(e.target.value); diff --git a/frontend/src/Editor/Components/Toggle.jsx b/frontend/src/Editor/Components/Toggle.jsx index 15958f4901..2259e943f4 100644 --- a/frontend/src/Editor/Components/Toggle.jsx +++ b/frontend/src/Editor/Components/Toggle.jsx @@ -65,7 +65,7 @@ export const ToggleSwitch = ({ style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + onComponentClick(id, component, event); }} > diff --git a/frontend/src/Editor/Components/components.js b/frontend/src/Editor/Components/components.js index a7a601dd02..ae9f98afeb 100644 --- a/frontend/src/Editor/Components/components.js +++ b/frontend/src/Editor/Components/components.js @@ -1001,10 +1001,10 @@ export const componentTypes = [ value: `{{ [{"lat": 40.7128, "lng": -73.935242}] }}`, }, canSearch: { - value: `{{true}}` , + value: `{{true}}`, }, + addNewMarkers: { value: `{{true}}` }, }, - addNewMarkers: { value: '{{false}}' }, events: [], styles: { visibility: { value: '{{true}}' }, @@ -1133,8 +1133,140 @@ export const componentTypes = [ properties: {}, events: [], styles: { - dividerColor: { value: '#E7E8EA' }, visibility: { value: '{{true}}' }, + dividerColor: { value: '#E7E8EA' }, + }, + }, + }, + { + name: 'FilePicker', + displayName: 'File Picker', + description: 'File Picker', + component: 'FilePicker', + defaultSize: { + width: 270, + height: 100, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + enableDropzone: { type: 'code', displayName: 'Use Drop zone' }, + enablePicker: { type: 'code', displayName: 'Use File Picker' }, + enableMultiple: { type: 'code', displayName: 'Pick mulitple files' }, + maxFileCount: { type: 'code', displayName: 'Max file count' }, + fileType: { type: 'code', displayName: 'Accept file types' }, + maxSize: { type: 'code', displayName: 'Max size limit (Bytes)' }, + minSize: { type: 'code', displayName: 'Min size limit (Bytes)' }, + }, + events: { onFileSelected: { displayName: 'On File Selected' } }, + styles: { + visibility: { type: 'code', displayName: 'Visibility' }, + disabledState: { type: 'code', displayName: 'Disable' }, + }, + exposedVariables: { + file: [{ name: [], content: [], type: [] }], + }, + definition: { + others: { + showOnDesktop: { value: true }, + showOnMobile: { value: false }, + }, + properties: { + enableDropzone: { value: '{{true}}' }, + enablePicker: { value: '{{true}}' }, + maxFileCount: { value: '{{2}}' }, + enableMultiple: { value: '{{false}}' }, + fileType: { value: '{{"image/*"}}' }, + maxSize: { value: '{{1048576}}' }, + minSize: { value: '{{50}}' }, + }, + events: [], + styles: { + visibility: { value: '{{true}}' }, + disabledState: { value: '{{false}}' }, + }, + }, + }, + { + name: 'Calendar', + displayName: 'Calendar', + description: 'Calendar', + component: 'Calendar', + defaultSize: { + width: 700, + height: 600, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + dateFormat: { type: 'code', displayName: 'Date format' }, + defaultDate: { type: 'code', displayName: 'Default date' }, + events: { type: 'code', displayName: 'Events' }, + resources: { type: 'code', displayName: 'Resources' }, + defaultView: { type: 'code', displayName: 'Default view' }, + displayToolbar: { type: 'toggle', displayName: 'Show toolbar' }, + displayViewSwitcher: { type: 'toggle', displayName: 'Show view switcher' }, + highlightToday: { type: 'toggle', displayName: 'Highlight today' }, + }, + events: { + onCalendarEventSelect: { displayName: 'On Event Select' }, + onCalendarSlotSelect: { displayName: 'On Slot Select' }, + }, + styles: { + visibility: { type: 'code', displayName: 'Visibility' }, + cellSizeInViewsClassifiedByResource: { + type: 'select', + displayName: 'Cell size in views classified by resource', + options: [ + { name: 'Compact', value: 'compact' }, + { name: 'Spacious', value: 'spacious' }, + ], + }, + }, + exposedVariables: { + selectedEvent: {}, + selectedSlots: {}, + }, + definition: { + others: { + showOnDesktop: { value: true }, + showOnMobile: { value: false }, + }, + properties: { + dateFormat: { + value: 'MM-DD-YYYY HH:mm:ss A Z', + }, + defaultDate: { + value: '{{moment().format("MM-DD-YYYY HH:mm:ss A Z")}}', + }, + events: { + value: + "{{[\n\t\t{\n\t\t\t title: 'Sample event',\n\t\t\t start: `${moment().startOf('day').format('MM-DD-YYYY HH:mm:ss A Z')}`,\n\t\t\t end: `${moment().endOf('day').format('MM-DD-YYYY HH:mm:ss A Z')}`,\n\t\t\t allDay: false,\n\t\t\t color: '#4D72DA'\n\t\t}\n]}}", + }, + resources: { + value: '{{[]}}', + }, + defaultView: { + value: "{{'month'}}", + }, + displayToolbar: { + value: true, + }, + displayViewSwitcher: { + value: true, + }, + highlightToday: { + value: true, + }, + }, + events: [], + styles: { + visibility: { value: '{{true}}' }, + cellSizeInViewsClassifiedByResource: { value: 'spacious' }, }, }, }, diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index fb2aff4563..6862e8fb1b 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -1,4 +1,5 @@ import React, { useCallback, useState, useEffect } from 'react'; +import cx from 'classnames'; import { useDrop, useDragLayer } from 'react-dnd'; import { ItemTypes } from './ItemTypes'; import { DraggableBox } from './DraggableBox'; @@ -6,6 +7,11 @@ import { snapToGrid as doSnapToGrid } from './snapToGrid'; import update from 'immutability-helper'; import { componentTypes } from './Components/components'; import { computeComponentName } from '@/_helpers/utils'; +import useRouter from '@/_hooks/use-router'; +import Comments from './Comments'; +import { commentsService } from '@/_services'; +import config from 'config'; +import Spinner from '@/_ui/Spinner'; function uuidv4() { return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => @@ -32,6 +38,9 @@ export const Container = ({ scaleValue, selectedComponent, darkMode, + showComments, + appVersionsId, + socket, }) => { const styles = { width: currentLayout === 'mobile' ? deviceWindowWidth : 1292, @@ -44,6 +53,9 @@ export const Container = ({ const [boxes, setBoxes] = useState(components); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); + const [commentsPreviewList, setCommentsPreviewList] = useState([]); + const [newThread, addNewThread] = useState({}); + const router = useRouter(); useEffect(() => { setBoxes(components); @@ -87,12 +99,27 @@ export const Container = ({ const [, drop] = useDrop( () => ({ - accept: ItemTypes.BOX, - drop(item, monitor) { + accept: [ItemTypes.BOX, ItemTypes.COMMENT], + async drop(item, monitor) { if (item.parent) { return; } + if (item.name === 'comment') { + const canvasBoundingRect = document.getElementsByClassName('real-canvas')[0].getBoundingClientRect(); + const offsetFromTopOfWindow = canvasBoundingRect.top; + const offsetFromLeftOfWindow = canvasBoundingRect.left; + const currentOffset = monitor.getSourceClientOffset(); + + const x = Math.round(currentOffset.x + currentOffset.x * (1 - zoomLevel) - offsetFromLeftOfWindow); + const y = Math.round(currentOffset.y + currentOffset.y * (1 - zoomLevel) - offsetFromTopOfWindow); + + const element = document.getElementById(`thread-${item.threadId}`); + element.style.transform = `translate(${x}px, ${y}px)`; + commentsService.updateThread(item.threadId, { x, y }); + return undefined; + } + let layouts = item['layouts']; const currentLayoutOptions = layouts ? layouts[item.currentLayout] : {}; @@ -252,8 +279,124 @@ export const Container = ({ } } + React.useEffect(() => { + console.log('current component => ', selectedComponent); + }, [selectedComponent]); + + const handleAddThread = async (e) => { + e.stopPropogation && e.stopPropogation(); + const elementIndex = commentsPreviewList.length; + setCommentsPreviewList([ + ...commentsPreviewList, + { + x: e.nativeEvent.offsetX, + y: e.nativeEvent.offsetY, + }, + ]); + const { data } = await commentsService.createThread({ + appId: router.query.id, + x: e.nativeEvent.offsetX, + y: e.nativeEvent.offsetY, + appVersionsId, + }); + + // Remove the temporary loader preview + const _commentsPreviewList = [...commentsPreviewList]; + _commentsPreviewList.splice(elementIndex, 1); + setCommentsPreviewList(_commentsPreviewList); + + // Update the threads on all connected clients using websocket + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'threads', appId: router.query.id }, + }) + ); + + // Update the list of threads on the current users page + addNewThread(data); + }; + + const handleAddThreadOnComponent = async (_, __, e) => { + e.stopPropogation && e.stopPropogation(); + + const canvasBoundingRect = document.getElementsByClassName('real-canvas')[0].getBoundingClientRect(); + const offsetFromTopOfWindow = canvasBoundingRect.top; + const offsetFromLeftOfWindow = canvasBoundingRect.left; + + const x = Math.round(e.screenX + e.screenX * (1 - zoomLevel) - offsetFromLeftOfWindow); + const y = Math.round(e.screenY + e.screenY * (1 - zoomLevel) - offsetFromTopOfWindow); + + const elementIndex = commentsPreviewList.length; + setCommentsPreviewList([ + ...commentsPreviewList, + { + x: e.nativeEvent.offsetX, + y: e.nativeEvent.offsetY - 130, + }, + ]); + const { data } = await commentsService.createThread({ + appId: router.query.id, + x, + y: y - 130, + appVersionsId, + }); + + // Remove the temporary loader preview + const _commentsPreviewList = [...commentsPreviewList]; + _commentsPreviewList.splice(elementIndex, 1); + setCommentsPreviewList(_commentsPreviewList); + + // Update the threads on all connected clients using websocket + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'threads', appId: router.query.id }, + }) + ); + + // Update the list of threads on the current users page + addNewThread(data); + }; + + if (showComments) { + const currentUser = JSON.parse(localStorage.getItem('currentUser')); + const currentUserInitials = `${currentUser.first_name?.charAt(0)}${currentUser.last_name?.charAt(0)}`; + styles.cursor = `url("data:image/svg+xml,%3Csvg width='34' height='34' viewBox='0 0 34 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='17' cy='17' r='15.25' fill='white' stroke='%23FCAA0D' stroke-width='2.5' opacity='0.5' /%3E%3Ctext x='10' y='20' fill='%23000' opacity='0.5' font-family='inherit' font-size='11.2' font-weight='500' color='%23656d77'%3E%3C/text%3E%3C/svg%3E%0A"), text`; + } + return ( -
+
+ {config.COMMENT_FEATURE_ENABLE && showComments && ( + <> + + {commentsPreviewList.map((previewComment, index) => ( +
+ +
+ ))} + + )} {Object.keys(boxes).map((key) => { const box = boxes[key]; const canShowInCurrentLayout = @@ -262,7 +405,9 @@ export const Container = ({ if (!box.parent && canShowInCurrentLayout) { return ( { isDragging: monitor.isDragging(), delta: monitor.getDifferenceFromInitialOffset(), })); + + if (itemType === ItemTypes.COMMENT) return null; function renderItem() { switch (itemType) { case ItemTypes.BOX: diff --git a/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx b/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx index 65fca59273..db81c9ec63 100644 --- a/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx +++ b/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx @@ -5,7 +5,13 @@ import Button from 'react-bootstrap/Button'; import { toast } from 'react-toastify'; import { defaultOptions } from './DefaultOptions'; import { TestConnection } from './TestConnection'; -import { DataBaseSources, ApiSources, DataSourceTypes, SourceComponents } from './SourceComponents'; +import { + DataBaseSources, + ApiSources, + DataSourceTypes, + SourceComponents, + CloudStorageSources, +} from './SourceComponents'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import config from 'config'; @@ -73,6 +79,7 @@ class DataSourceManager extends React.Component { dataSourceMeta: {}, selectedDataSource: null, options: {}, + connectionTestError: null, }); }; @@ -110,23 +117,29 @@ class DataSourceManager extends React.Component { encrypted: keyMeta ? keyMeta.encrypted : false, }; }); - - if (selectedDataSource.id) { - this.setState({ isSaving: true }); - datasourceService.save(selectedDataSource.id, appId, name, parsedOptions).then(() => { - this.setState({ isSaving: false }); - this.hideModal(); - toast.success('Datasource Saved', { hideProgressBar: true, position: 'top-center' }); - this.props.dataSourcesChanged(); - }); + if (name.trim() !== ''){ + if (selectedDataSource.id) { + this.setState({ isSaving: true }); + datasourceService.save(selectedDataSource.id, appId, name, parsedOptions).then(() => { + this.setState({ isSaving: false }); + this.hideModal(); + toast.success('Datasource Saved', { hideProgressBar: true, position: 'top-center' }); + this.props.dataSourcesChanged(); + }); + } else { + this.setState({ isSaving: true }); + datasourceService.create(appId, name, kind, parsedOptions).then(() => { + this.setState({ isSaving: false }); + this.hideModal(); + toast.success('Datasource Added', { hideProgressBar: true, position: 'top-center' }); + this.props.dataSourcesChanged(); + }); + } } else { - this.setState({ isSaving: true }); - datasourceService.create(appId, name, kind, parsedOptions).then(() => { - this.setState({ isSaving: false }); - this.hideModal(); - toast.success('Datasource Added', { hideProgressBar: true, position: 'top-center' }); - this.props.dataSourcesChanged(); - }); + toast.error( + "The name of datasource should not be empty", + { hideProgressBar: true, position: 'top-center' } + ); } }; @@ -228,7 +241,30 @@ class DataSourceManager extends React.Component {

APIS

{ApiSources.map((dataSource) => (
-
this.selectDataSource(dataSource)}> +
this.selectDataSource(dataSource)}> +
+
+ + +

+

+ {dataSource.name} +
+
+
+
+ ))} +
+
+

CLOUD STORAGES

+ {CloudStorageSources.map((dataSource) => ( +
+
this.selectDataSource(dataSource)}>
-
-
-
- -
+ +
+
)} diff --git a/frontend/src/Editor/DataSourceManager/DefaultOptions.js b/frontend/src/Editor/DataSourceManager/DefaultOptions.js index cd6c2f1bac..9543d0ab88 100644 --- a/frontend/src/Editor/DataSourceManager/DefaultOptions.js +++ b/frontend/src/Editor/DataSourceManager/DefaultOptions.js @@ -85,4 +85,12 @@ export const defaultOptions = { access_key: { value: '' }, secret_key: { value: '' }, }, + s3: { + access_key: { value: '' }, + secret_key: { value: '' }, + region: { value: '' }, + }, + gcs: { + private_key: { value: '' }, + }, }; diff --git a/frontend/src/Editor/DataSourceManager/SourceComponents/Database/Gcs.schema.json b/frontend/src/Editor/DataSourceManager/SourceComponents/Database/Gcs.schema.json new file mode 100644 index 0000000000..1f6a283ec9 --- /dev/null +++ b/frontend/src/Editor/DataSourceManager/SourceComponents/Database/Gcs.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/", + "$id": "https://tooljet.io/Gcs.schema.json", + "title": "Google Cloud Storage datasource", + "description": "A schema defining GCS datasource", + "type": "object", + "source": { + "name": "GCS", + "kind": "gcs", + "exposedVariables": { + "isLoading": {}, + "data": {}, + "rawData": {} + }, + "options": { + "private_key": { "type": "string", "encrypted": true } + } + }, + "properties": { + "private_key": { + "$label": "Private key", + "$key": "private_key", + "type": "textarea", + "description": "Enter JSON private key for service account" + } + }, + "required": ["private_key"] +} diff --git a/frontend/src/Editor/DataSourceManager/SourceComponents/Database/S3.schema.json b/frontend/src/Editor/DataSourceManager/SourceComponents/Database/S3.schema.json new file mode 100644 index 0000000000..4b76d5afad --- /dev/null +++ b/frontend/src/Editor/DataSourceManager/SourceComponents/Database/S3.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/", + "$id": "https://tooljet.io/S3.schema.json", + "title": "AWS S3 datasource", + "description": "A schema defining AWS S3 datasource", + "type": "object", + "source": { + "name": "AWS S3", + "kind": "s3", + "exposedVariables": { + "isLoading": {}, + "data": {}, + "rawData": {} + }, + "options": { + "access_key": { "type": "string" }, + "secret_key": { "type": "string", "encrypted": true }, + "region": { "type": "string" } + } + }, + "properties": { + "access_key": { + "$label": "Access key", + "$key": "access_key", + "type": "text", + "description": "Enter access key" + }, + "secret_key": { + "$label": "Secret key", + "$key": "secret_key", + "type": "password", + "description": "Enter secret key" + }, + "region": { + "$label": "Region", + "$key": "region", + "type": "dropdown", + "description": "Single select dropdown for region", + "$options": [ + { "name": "US East (Ohio)", "value": "us-east-2" }, + { "name": "US East (N. Virginia)", "value": "us-east-1" }, + { "name": "US West (N. California)", "value": "us-west-1" }, + { "name": "US West (Oregon)", "value": "us-west-2" }, + { "name": "Africa (Cape Town)", "value": "af-south-1" }, + { "name": "Asia Pacific (Hong Kong)", "value": "ap-east-1" }, + { "name": "Asia Pacific (Mumbai)", "value": "ap-south-1" }, + { "name": "Asia Pacific (Osaka)", "value": "ap-northeast-3" }, + { "name": "Asia Pacific (Seoul)", "value": "ap-northeast-2" }, + { "name": "Asia Pacific (Singapore)", "value": "ap-southeast-1" }, + { "name": "Asia Pacific (Sydney)", "value": "ap-southeast-2" }, + { "name": "Asia Pacific (Tokyo)", "value": "ap-northeast-1" }, + { "name": "Canada (Central)", "value": "ca-central-1" }, + { "name": "China (Beijing)", "value": "cn-north-1" }, + { "name": "China (Ningxia)", "value": "cn-northwest-1" }, + { "name": "Europe (Frankfurt)", "value": "eu-central-1" }, + { "name": "Europe (Ireland)", "value": "eu-west-1" }, + { "name": "Europe (London)", "value": "eu-west-2" }, + { "name": "Europe (Milan)", "value": "eu-south-1" }, + { "name": "Europe (Paris)", "value": "eu-west-3" }, + { "name": "Europe (Stockholm)", "value": "eu-north-1" }, + { "name": "Middle East (Bahrain)", "value": "me-south-1" }, + { "name": "South America (São Paulo)", "value": "sa-east-1" }, + { "name": "AWS GovCloud (US-East)", "value": "us-gov-east-1" }, + { "name": "AWS GovCloud (US-West)", "value": "us-gov-west-1" } + ] + } + }, + "required": ["access_key", "secret_key", "region"] +} diff --git a/frontend/src/Editor/DataSourceManager/SourceComponents/index.js b/frontend/src/Editor/DataSourceManager/SourceComponents/index.js index 8f632351e2..6ff97c81cb 100644 --- a/frontend/src/Editor/DataSourceManager/SourceComponents/index.js +++ b/frontend/src/Editor/DataSourceManager/SourceComponents/index.js @@ -1,55 +1,45 @@ -import React from "react"; +import React from 'react'; -import DynamicForm from "@/_components/DynamicForm"; +import DynamicForm from '@/_components/DynamicForm'; -import AirtableSchema from "./Api/Airtable.schema.json"; -import RestapiSchema from "./Api/Restapi.schema.json"; -import GraphqlSchema from "./Api/Graphql.schema.json"; -import StripeSchema from "./Api/Stripe.schema.json"; -import GooglesheetSchema from "./Api/Googlesheets.schema.json"; -import SlackSchema from "./Api/Slack.schema.json"; +// API sources +import AirtableSchema from './Api/Airtable.schema.json'; +import RestapiSchema from './Api/Restapi.schema.json'; +import GraphqlSchema from './Api/Graphql.schema.json'; +import StripeSchema from './Api/Stripe.schema.json'; +import GooglesheetSchema from './Api/Googlesheets.schema.json'; +import SlackSchema from './Api/Slack.schema.json'; -import DynamodbSchema from "./Database/Dynamodb.schema.json"; -import ElasticsearchSchema from "./Database/Elasticsearch.schema.json"; -import RedisSchema from "./Database/Redis.schema.json"; -import FirestoreSchema from "./Database/Firestore.schema.json"; -import MongodbSchema from "./Database/Mongodb.schema.json"; -import PostgresqlSchema from "./Database/Postgresql.schema.json"; -import MysqlSchema from "./Database/Mysql.schema.json"; -import MssqlSchema from "./Database/Mssql.schema.json"; +// Database sources +import DynamodbSchema from './Database/Dynamodb.schema.json'; +import ElasticsearchSchema from './Database/Elasticsearch.schema.json'; +import RedisSchema from './Database/Redis.schema.json'; +import FirestoreSchema from './Database/Firestore.schema.json'; +import MongodbSchema from './Database/Mongodb.schema.json'; +import PostgresqlSchema from './Database/Postgresql.schema.json'; +import MysqlSchema from './Database/Mysql.schema.json'; +import MssqlSchema from './Database/Mssql.schema.json'; -const Airtable = ({ ...rest }) => ( - -); -const Restapi = ({ ...rest }) => ( - -); -const Graphql = ({ ...rest }) => ( - -); +// Cloud storage sources +import S3Schema from './Database/S3.schema.json'; +import GcsSchema from './Database/Gcs.schema.json'; + +const Airtable = ({ ...rest }) => ; +const Restapi = ({ ...rest }) => ; +const Graphql = ({ ...rest }) => ; const Stripe = ({ ...rest }) => ; -const Googlesheets = ({ ...rest }) => ( - -); +const Googlesheets = ({ ...rest }) => ; const Slack = ({ ...rest }) => ; -const Dynamodb = ({ ...rest }) => ( - -); -const Elasticsearch = ({ ...rest }) => ( - -); +const Dynamodb = ({ ...rest }) => ; +const Elasticsearch = ({ ...rest }) => ; const Redis = ({ ...rest }) => ; -const Firestore = ({ ...rest }) => ( - -); -const Mongodb = ({ ...rest }) => ( - -); -const Postgresql = ({ ...rest }) => ( - -); +const Firestore = ({ ...rest }) => ; +const Mongodb = ({ ...rest }) => ; +const Postgresql = ({ ...rest }) => ; const Mysql = ({ ...rest }) => ; const Mssql = ({ ...rest }) => ; +const S3 = ({ ...rest }) => ; +const Gcs = ({ ...rest }) => ; export const DataBaseSources = [ DynamodbSchema.source, @@ -69,7 +59,8 @@ export const ApiSources = [ GooglesheetSchema.source, SlackSchema.source, ]; -export const DataSourceTypes = [...DataBaseSources, ...ApiSources]; +export const CloudStorageSources = [S3Schema.source, GcsSchema.source]; +export const DataSourceTypes = [...DataBaseSources, ...ApiSources, ...CloudStorageSources]; export const SourceComponents = { Elasticsearch, @@ -86,4 +77,6 @@ export const SourceComponents = { Graphql, Mysql, Mssql, + S3, + Gcs, }; diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 2ea680afc0..2bd37d2c94 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -16,7 +16,6 @@ import { SaveAndPreview } from './SaveAndPreview'; import { onComponentOptionChanged, onComponentOptionsChanged, - onComponentClick, onEvent, onQueryConfirm, onQueryCancel, @@ -26,8 +25,10 @@ import { } from '@/_helpers/appUtils'; import { Confirm } from './Viewer/Confirm'; import ReactTooltip from 'react-tooltip'; +import CommentNotifications from './CommentNotifications'; import { WidgetManager } from './WidgetManager'; import Fuse from 'fuse.js'; +import config from 'config'; import queryString from 'query-string'; class Editor extends React.Component { @@ -60,6 +61,7 @@ class Editor extends React.Component { loadingDataQueries: true, showQueryEditor: true, showLeftSidebar: true, + showComments: false, zoomLevel: 1.0, currentLayout: 'desktop', scaleValue: 1, @@ -82,40 +84,67 @@ class Editor extends React.Component { isDeletingDataQuery: false, showHiddenOptionsForDataQueryId: null, showQueryConfirmation: false, + socket: null, }; } componentDidMount() { - const appId = this.props.match.params.id; this.fetchApps(0); - - appService.getApp(appId).then((data) => { - const dataDefinition = data.definition || { components: {} }; - this.setState( - { - app: data, - isLoading: false, - editingVersion: data.editing_version, - appDefinition: { ...this.state.appDefinition, ...dataDefinition }, - slug: data.slug, - }, - () => { - computeComponentState(this, this.state.appDefinition.components).then(() => { - console.log('Default component state computed and set'); - this.runQueries(data.data_queries); - }); - } - ); - }); - + this.fetchApp(); this.fetchDataSources(); this.fetchDataQueries(); + config.COMMENT_FEATURE_ENABLE && this.initWebSocket(); this.setState({ currentSidebarTab: 2, selectedComponent: null, }); } + componentWillUnmount() { + if (this.state.socket) { + this.state.socket?.close(); + } + } + + getWebsocketUrl = () => { + const re = /https?:\/\//g; + if (re.test(config.apiUrl)) return config.apiUrl.replace(/(^\w+:|^)\/\//, '').replace('/api', ''); + + return window.location.host; + }; + + initWebSocket = () => { + // TODO: add retry policy + const socket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${this.getWebsocketUrl()}`); + + const appId = this.props.match.params.id; + + // Connection opened + socket.addEventListener('open', function (event) { + console.log('connection established', event); + socket.send( + JSON.stringify({ + event: 'subscribe', + data: appId, + }) + ); + }); + + // Connection closed + socket.addEventListener('close', function (event) { + console.log('connection closed', event); + }); + + // Listen for possible errors + socket.addEventListener('error', function (event) { + console.log('WebSocket error: ', event); + }); + + this.setState({ + socket, + }); + }; + fetchDataSources = () => { this.setState( { @@ -196,6 +225,29 @@ class Editor extends React.Component { ); }; + fetchApp = () => { + const appId = this.props.match.params.id; + + appService.getApp(appId).then((data) => { + const dataDefinition = data.definition || { components: {} }; + this.setState( + { + app: data, + isLoading: false, + editingVersion: data.editing_version, + appDefinition: { ...this.state.appDefinition, ...dataDefinition }, + slug: data.slug, + }, + () => { + computeComponentState(this, this.state.appDefinition.components).then(() => { + console.log('Default component state computed and set'); + this.runQueries(data.data_queries); + }); + } + ); + }); + }; + setAppDefinitionFromVersion = (version) => { this.appDefinitionChanged(version.definition || { components: {} }); this.setState({ @@ -470,6 +522,10 @@ class Editor extends React.Component { this.setState({ showLeftSidebar: !this.state.showLeftSidebar }); }; + toggleComments = () => { + this.setState({ showComments: !this.state.showComments }); + }; + configHandleClicked = (id, component) => { this.switchSidebarTab(1); this.setState({ selectedComponent: { id, component } }); @@ -541,6 +597,7 @@ class Editor extends React.Component { isDeletingDataQuery, apps, defaultComponentStateComputed, + showComments, } = this.state; const appLink = slug ? `/applications/${slug}` : ''; @@ -575,11 +632,10 @@ class Editor extends React.Component { > -

- - +

+ + -

{this.state.app && ( )}
@@ -687,6 +744,7 @@ class Editor extends React.Component {
@@ -707,6 +766,9 @@ class Editor extends React.Component {
{defaultComponentStateComputed && ( onEvent(this, eventName, options)} + onEvent={(eventName, options) => onEvent(this, eventName, options, 'edit')} onComponentOptionChanged={(component, optionName, value) => onComponentOptionChanged(this, component, optionName, value) } @@ -731,7 +793,6 @@ class Editor extends React.Component { onComponentClick={(id, component) => { this.setState({ selectedComponent: { id, component } }); this.switchSidebarTab(1); - onComponentClick(this, id, component); }} /> )} @@ -887,6 +948,13 @@ class Editor extends React.Component { > )}
+ {config.COMMENT_FEATURE_ENABLE && showComments && ( + + )}
diff --git a/frontend/src/Editor/Inspector/Components/Table.jsx b/frontend/src/Editor/Inspector/Components/Table.jsx index dface2da73..adb1435c06 100644 --- a/frontend/src/Editor/Inspector/Components/Table.jsx +++ b/frontend/src/Editor/Inspector/Components/Table.jsx @@ -38,6 +38,7 @@ class Table extends React.Component { currentState, actionPopOverRootClose: true, showPopOver: false, + columnPopOverRootClose: true, }; } @@ -104,6 +105,21 @@ class Table extends React.Component { this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties'); }; + columnEventChanged = (columnForWhichEventsAreChanged, events) => { + const columns = this.props.component.component.definition.properties.columns.value; + + const newColumns = columns.map((column) => { + if (column.id === columnForWhichEventsAreChanged.id) { + const newColumn = { ...column, events }; + return newColumn; + } else { + return column; + } + }); + + this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties'); + }; + columnPopover = (column, index) => { return ( @@ -241,6 +257,24 @@ class Table extends React.Component { onChange={(name, value, color) => this.onColumnItemChange(index, 'activeColor', color)} />
+ this.columnEventChanged(column, events)} + apps={this.props.apps} + popOverCallback={(showing) => { + this.setState({ columnPopOverRootClose: !showing }); + }} + />
)} @@ -553,7 +587,12 @@ class Table extends React.Component { {columns.value.map((item, index) => (
- +
diff --git a/frontend/src/Editor/Inspector/Elements/Color.jsx b/frontend/src/Editor/Inspector/Elements/Color.jsx index 139ef508a9..396fe7d952 100644 --- a/frontend/src/Editor/Inspector/Elements/Color.jsx +++ b/frontend/src/Editor/Inspector/Elements/Color.jsx @@ -34,7 +34,15 @@ export const Color = ({ param, definition, onChange, paramType, componentMeta })
setShowPicker(true)}>
{definition.value}
diff --git a/frontend/src/Editor/ItemTypes.js b/frontend/src/Editor/ItemTypes.js index fc6a86a55c..70e8744c23 100644 --- a/frontend/src/Editor/ItemTypes.js +++ b/frontend/src/Editor/ItemTypes.js @@ -1,3 +1,5 @@ export const ItemTypes = { BOX: 'box', + COMMENT: 'comment', + NEW_COMMENT: 'new_comment' }; diff --git a/frontend/src/Editor/LeftSidebar/SidebarComment.jsx b/frontend/src/Editor/LeftSidebar/SidebarComment.jsx new file mode 100644 index 0000000000..bf5c89e249 --- /dev/null +++ b/frontend/src/Editor/LeftSidebar/SidebarComment.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import cx from 'classnames'; +import { LeftSidebarItem } from './sidebar-item'; + +export const LeftSidebarComment = ({ toggleComments, appVersionsId }) => { + const [isActive, toggleActive] = React.useState(false); + return ( + { + toggleActive(!isActive); + toggleComments(); + }} + /> + ); +}; diff --git a/frontend/src/Editor/LeftSidebar/index.js b/frontend/src/Editor/LeftSidebar/index.js index 29cd8c5942..522d5096f4 100644 --- a/frontend/src/Editor/LeftSidebar/index.js +++ b/frontend/src/Editor/LeftSidebar/index.js @@ -9,7 +9,9 @@ import { LeftSidebarZoom } from './sidebar-zoom'; import { DarkModeToggle } from '../../_components/DarkModeToggle'; import useRouter from '../../_hooks/use-router'; import { LeftSidebarDebugger } from './SidebarDebugger'; +import { LeftSidebarComment } from './SidebarComment'; import { ConfirmDialog } from '@/_components'; +import config from 'config'; export const LeftSidebar = ({ appId, @@ -18,10 +20,12 @@ export const LeftSidebar = ({ globals, components, queries, + toggleComments, onZoomChanged, dataSources = [], dataSourcesChanged, errorLogs, + appVersionsId, }) => { const router = useRouter(); const [showLeaveDialog, setShowLeaveDialog] = useState(false); @@ -35,6 +39,9 @@ export const LeftSidebar = ({ dataSourcesChanged={dataSourcesChanged} /> + {config.COMMENT_FEATURE_ENABLE && ( + + )} setShowLeaveDialog(true)} tip="Back to home" diff --git a/frontend/src/Editor/LeftSidebar/sidebar-item.js b/frontend/src/Editor/LeftSidebar/sidebar-item.js index a776e3e33d..af6ba590b7 100644 --- a/frontend/src/Editor/LeftSidebar/sidebar-item.js +++ b/frontend/src/Editor/LeftSidebar/sidebar-item.js @@ -2,7 +2,17 @@ import React from 'react'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import Tooltip from 'react-bootstrap/Tooltip'; -export const LeftSidebarItem = ({ tip = '', className, icon, text, onClick, badge = false, count, ...rest }) => { +export const LeftSidebarItem = ({ + tip = '', + className, + icon, + commentBadge, + text, + onClick, + badge = false, + count, + ...rest +}) => { return ( )} {badge && } + {commentBadge && } {text && text}
); }; +function CommentBadge() { + return ( + + + + ); +} + function NotificationBadge({ count }) { const fontSize = count > 999 ? '7.5px' : '8.5px'; return ( @@ -39,4 +65,5 @@ function NotificationBadge({ count }) { ); } +LeftSidebarItem.CommentBadge = CommentBadge; LeftSidebarItem.Badge = NotificationBadge; diff --git a/frontend/src/Editor/LeftSidebar/sidebar-zoom.js b/frontend/src/Editor/LeftSidebar/sidebar-zoom.js index 419428c04b..d4ba7c326a 100644 --- a/frontend/src/Editor/LeftSidebar/sidebar-zoom.js +++ b/frontend/src/Editor/LeftSidebar/sidebar-zoom.js @@ -1,5 +1,5 @@ import React from 'react'; -import usePopover from '../../_hooks/use-popover'; +import usePopover from '@/_hooks/use-popover'; import { LeftSidebarItem } from './sidebar-item'; export const LeftSidebarZoom = ({ onZoomChanged }) => { diff --git a/frontend/src/Editor/QueryManager/QueryEditors/Gcs.jsx b/frontend/src/Editor/QueryManager/QueryEditors/Gcs.jsx new file mode 100644 index 0000000000..7d064bbebd --- /dev/null +++ b/frontend/src/Editor/QueryManager/QueryEditors/Gcs.jsx @@ -0,0 +1,245 @@ +import React from 'react'; +import 'codemirror/theme/duotone-light.css'; +import SelectSearch, { fuzzySearch } from 'react-select-search'; +import { CodeHinter } from '../../CodeBuilder/CodeHinter'; +import { changeOption } from './utils'; + +class Gcs extends React.Component { + constructor(props) { + super(props); + + this.state = { + options: this.props.options, + }; + } + + componentDidMount() { + this.setState({ + options: this.props.options, + }); + } + + changeOperation = (operation) => { + this.setState( + { + options: { + ...this.state.options, + operation, + }, + }, + () => { + this.props.optionsChanged(this.state.options); + } + ); + }; + + render() { + const { options } = this.state; + + return ( +
+ {options && ( +
+
+ + { + this.changeOperation(value); + }} + filterOptions={fuzzySearch} + placeholder="Select.." + /> +
+ + {['list_files'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'prefix', value)} + /> +
+
+ )} + + {['get_file'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'file', value)} + /> +
+
+ )} + + {['upload_file'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'file', value)} + /> +
+
+ + changeOption(this, 'contentType', value)} + /> +
+
+ + changeOption(this, 'data', value)} + /> +
+
+ )} + + {['signed_url_for_get'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'file', value)} + /> +
+
+ + changeOption(this, 'expiresIn', value)} + /> +
+
+ )} + + {['signed_url_for_put'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'file', value)} + /> +
+
+ + changeOption(this, 'expiresIn', value)} + /> +
+
+ + changeOption(this, 'contentType', value)} + /> +
+
+ )} +
+ )} +
+ ); + } +} + +export { Gcs }; diff --git a/frontend/src/Editor/QueryManager/QueryEditors/S3.jsx b/frontend/src/Editor/QueryManager/QueryEditors/S3.jsx new file mode 100644 index 0000000000..00fe531127 --- /dev/null +++ b/frontend/src/Editor/QueryManager/QueryEditors/S3.jsx @@ -0,0 +1,235 @@ +import React from 'react'; +import 'codemirror/theme/duotone-light.css'; +import SelectSearch, { fuzzySearch } from 'react-select-search'; +import { CodeHinter } from '../../CodeBuilder/CodeHinter'; +import { changeOption } from './utils'; + +class S3 extends React.Component { + constructor(props) { + super(props); + + this.state = { + options: this.props.options, + }; + } + + componentDidMount() { + this.setState({ + options: this.props.options, + }); + } + + changeOperation = (operation) => { + this.setState( + { + options: { + ...this.state.options, + operation, + }, + }, + () => { + this.props.optionsChanged(this.state.options); + } + ); + }; + + render() { + const { options } = this.state; + + return ( +
+ {options && ( +
+
+ + { + this.changeOperation(value); + }} + filterOptions={fuzzySearch} + placeholder="Select.." + /> +
+ + {['list_objects'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ )} + + {['get_object'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'key', value)} + /> +
+
+ )} + + {['upload_object'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'key', value)} + /> +
+
+ + changeOption(this, 'contentType', value)} + /> +
+
+ + changeOption(this, 'data', value)} + /> +
+
+ )} + + {['signed_url_for_get'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'key', value)} + /> +
+
+ + changeOption(this, 'expiresIn', value)} + /> +
+
+ )} + + {['signed_url_for_put'].includes(this.state.options.operation) && ( +
+
+ + changeOption(this, 'bucket', value)} + /> +
+
+ + changeOption(this, 'key', value)} + /> +
+
+ + changeOption(this, 'expiresIn', value)} + /> +
+
+ + changeOption(this, 'contentType', value)} + /> +
+
+ )} +
+ )} +
+ ); + } +} + +export { S3 }; diff --git a/frontend/src/Editor/QueryManager/QueryEditors/index.js b/frontend/src/Editor/QueryManager/QueryEditors/index.js index 83e227edf4..b13e2967ed 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/index.js +++ b/frontend/src/Editor/QueryManager/QueryEditors/index.js @@ -12,6 +12,8 @@ import { Dynamodb } from './Dynamodb'; import { Airtable } from './Airtable'; import { Graphql } from './Graphql'; import { Mssql } from './Mssql'; +import { S3 } from './S3'; +import { Gcs } from './Gcs'; export const allSources = { Restapi, @@ -28,4 +30,6 @@ export const allSources = { Airtable, Graphql, Mssql, + S3, + Gcs, }; diff --git a/frontend/src/Editor/QueryManager/constants.js b/frontend/src/Editor/QueryManager/constants.js index 5009e65adf..74bf24bc30 100644 --- a/frontend/src/Editor/QueryManager/constants.js +++ b/frontend/src/Editor/QueryManager/constants.js @@ -32,6 +32,8 @@ export const defaultOptions = { }, slack: {}, dynamodb: {}, + s3: {}, + gcs: {}, airtable: {}, mssql: {}, }; diff --git a/frontend/src/Editor/SaveAndPreview.jsx b/frontend/src/Editor/SaveAndPreview.jsx index f06157e2bb..722e742ab0 100644 --- a/frontend/src/Editor/SaveAndPreview.jsx +++ b/frontend/src/Editor/SaveAndPreview.jsx @@ -15,6 +15,7 @@ class SaveAndPreview extends React.Component { appId: props.appId, isLoading: true, showVersionForm: false, + newVersionName: '', }; } @@ -38,25 +39,42 @@ class SaveAndPreview extends React.Component { hideModal = () => { this.setState({ showModal: false, + showVersionForm: false, }); }; createVersion = () => { const newVersionName = this.state.newVersionName; const appId = this.props.appId; - this.setState({ creatingVersion: true }); - appVersionService.create(appId, newVersionName).then(() => { - this.setState({ showVersionForm: false, creatingVersion: false }); - toast.success('Version Created', { + if (newVersionName.trim() !== '') { + this.setState({ creatingVersion: true }); + appVersionService.create(appId, newVersionName).then(() => { + this.setState({ showVersionForm: false, creatingVersion: false }); + toast.success('Version Created', { + hideProgressBar: true, + position: 'top-center', + containerId: this.state.appId, + }); + this.fetchVersions(); + // eslint-disable-next-line no-undef + this.props.setAppDefinitionFromVersion(version); + }); + } else { + toast.error('The name of version should not be empty', { hideProgressBar: true, position: 'top-center', - containerId: this.state.appId, }); this.fetchVersions(); + this.props.fetchApp(); // eslint-disable-next-line no-undef this.props.setAppDefinitionFromVersion(version); - }); + } + }; + + editVersion = (version) => () => { + this.props.setAppDefinitionFromVersion(version); + this.props.fetchApp(); }; saveVersion = (versionId) => { @@ -82,6 +100,7 @@ class SaveAndPreview extends React.Component { containerId: this.state.appId, }); + this.props.fetchApp(); this.props.onVersionDeploy(versionId); }); }; @@ -177,7 +196,7 @@ class SaveAndPreview extends React.Component {
@@ -382,10 +383,7 @@ class HomePage extends React.Component { )}
-
+
{ launch{' '} @@ -485,8 +482,8 @@ class HomePage extends React.Component { { diff --git a/frontend/src/LoginPage/LoginPage.jsx b/frontend/src/LoginPage/LoginPage.jsx index 8ebf2ec29c..01fe1cccb0 100644 --- a/frontend/src/LoginPage/LoginPage.jsx +++ b/frontend/src/LoginPage/LoginPage.jsx @@ -3,6 +3,7 @@ import { authenticationService } from '@/_services'; import { toast } from 'react-toastify'; import { Link } from 'react-router-dom'; import queryString from 'query-string'; +import { validateEmail } from "../_helpers/utils"; class LoginPage extends React.Component { constructor(props) { @@ -34,6 +35,16 @@ class LoginPage extends React.Component { const { email, password } = this.state; + if(!validateEmail(email) || !password || !password.trim()) { + toast.error('Invalid email or password', { + toastId: 'toast-login-auth-error', + hideProgressBar: true, + position: 'top-center', + }); + this.setState({ isLoading: false }); + return; + } + authenticationService.login(email, password).then( () => { const params = queryString.parse(this.props.location.search); diff --git a/frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx b/frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx index 9d2bfbc999..cc74ace50d 100644 --- a/frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx +++ b/frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { authenticationService } from '@/_services'; import { groupPermissionService } from '../_services/groupPermission.service'; import 'react-toastify/dist/ReactToastify.css'; -import { Header } from '@/_components'; +import { Header, ConfirmDialog } from '@/_components'; import { toast } from 'react-toastify'; import { Link } from 'react-router-dom'; @@ -17,6 +17,8 @@ class ManageGroupPermissions extends React.Component { creatingGroup: false, showNewGroupForm: false, newGroupName: null, + isDeletingGroup: false, + showGroupDeletionConfirmation: false, }; } @@ -81,10 +83,23 @@ class ManageGroupPermissions extends React.Component { }; deleteGroup = (groupPermissionId) => { + this.setState({ showGroupDeletionConfirmation: true, groupToBeDeleted: groupPermissionId }); + }; + + cancelDeleteGroupDialog = () => { + this.setState({ + isDeletingGroup: false, + groupToBeDeleted: null, + showGroupDeletionConfirmation: false, + }); + }; + + executeGroupDeletion = () => { + this.setState({ isDeletingGroup: true }); groupPermissionService - .del(groupPermissionId) + .del(this.state.groupToBeDeleted) .then(() => { - toast.success('Group has been deleted', { + toast.success('Group deleted successfully', { hideProgressBar: true, position: 'top-center', }); @@ -92,13 +107,30 @@ class ManageGroupPermissions extends React.Component { }) .catch(({ error }) => { toast.error(error, { hideProgressBar: true, position: 'top-center' }); + }) + .finally(() => { + this.cancelDeleteGroupDialog(); }); }; render() { - const { isLoading, showNewGroupForm, creatingGroup, groups } = this.state; + const { + isLoading, + showNewGroupForm, + creatingGroup, + groups, + isDeletingGroup, + showGroupDeletionConfirmation } = this.state; return (
+ this.executeGroupDeletion()} + onCancel={() => this.cancelDeleteGroupDialog()} + /> +
diff --git a/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx b/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx index 878f94df73..8586b2a2f8 100644 --- a/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx +++ b/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx @@ -305,7 +305,13 @@ class ManageOrgUsers extends React.Component {
{user.status} {user.status === 'invited' && 'invitation_token' in user ? ( @@ -320,7 +326,7 @@ class ManageOrgUsers extends React.Component { width="15" height="15" style={{ - cursor: 'pointer' + cursor: 'pointer', }} > diff --git a/frontend/src/ResetPassword/ResetPasswordPage.jsx b/frontend/src/ResetPassword/ResetPasswordPage.jsx index 661afa60bd..bb56693a6d 100644 --- a/frontend/src/ResetPassword/ResetPasswordPage.jsx +++ b/frontend/src/ResetPassword/ResetPasswordPage.jsx @@ -21,8 +21,10 @@ class ResetPassword extends React.Component { handleClick = (event) => { event.preventDefault(); - const { password, password_confirmation } = this.state; - if (password !== password_confirmation) { + const { token, password, password_confirmation } = this.state; + if(!token || !password || !password_confirmation) { + toast.error("Please fill all field(s)"); + } else if (password !== password_confirmation) { toast.error("Password don't match"); this.setState({ password: '', diff --git a/frontend/src/SettingsPage/SettingsPage.jsx b/frontend/src/SettingsPage/SettingsPage.jsx index 397db5b5a4..3f8c7b01ae 100644 --- a/frontend/src/SettingsPage/SettingsPage.jsx +++ b/frontend/src/SettingsPage/SettingsPage.jsx @@ -6,6 +6,7 @@ import { toast } from 'react-toastify'; function SettingsPage(props) { const [firstName, setFirstName] = React.useState(authenticationService.currentUserValue.first_name); + const email=authenticationService.currentUserValue.email; const [lastName, setLastName] = React.useState(authenticationService.currentUserValue.last_name); const [currentpassword, setCurrentPassword] = React.useState(''); const [newPassword, setNewPassword] = React.useState(''); @@ -13,6 +14,13 @@ function SettingsPage(props) { const [passwordChangeInProgress, setPasswordChangeInProgress] = React.useState(false); const updateDetails = async () => { + if (!firstName || !lastName) { + toast.warn("Name can't be empty!", { + hideProgressBar: true, + position: 'top-left', + }); + return; + } setUpdateInProgress(true); const updatedDetails = await userService.updateCurrentUser(firstName, lastName); authenticationService.updateCurrentUserDetails(updatedDetails); @@ -70,7 +78,7 @@ function SettingsPage(props) {
- +
+
+
+
+ + +
+
+
{ // eslint-disable-next-line no-unused-vars diff --git a/frontend/src/_components/ConfirmDialog.jsx b/frontend/src/_components/ConfirmDialog.jsx index 841db68acb..f705290e4d 100644 --- a/frontend/src/_components/ConfirmDialog.jsx +++ b/frontend/src/_components/ConfirmDialog.jsx @@ -10,6 +10,7 @@ export function ConfirmDialog({ show, message, onConfirm, onCancel, confirmButto }, [show]); const handleClose = () => { + onCancel(); setShow(false); }; @@ -17,18 +18,13 @@ export function ConfirmDialog({ show, message, onConfirm, onCancel, confirmButto onConfirm(); }; - const handleCancel = () => { - onCancel(); - handleClose(); - }; - return ( <>
{message} - -

+

diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index e02433d4be..539fb54b87 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -134,7 +134,7 @@ function executeAction(_ref, event, mode) { case 'run-query': { const { queryId, queryName } = event; - return runQuery(_ref, queryId, queryName); + return runQuery(_ref, queryId, queryName, true, mode); } case 'open-webpage': { @@ -224,6 +224,48 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { ); } + if (eventName === 'onCalendarEventSelect') { + const { component, calendarEvent } = options; + _self.setState( + { + currentState: { + ..._self.state.currentState, + components: { + ..._self.state.currentState.components, + [component.name]: { + ..._self.state.currentState.components[component.name], + selectedEvent: { ...calendarEvent }, + }, + }, + }, + }, + () => { + executeActionsForEventId(_ref, 'onCalendarEventSelect', component, mode); + } + ); + } + + if (eventName === 'onCalendarSlotSelect') { + const { component, selectedSlots } = options; + _self.setState( + { + currentState: { + ..._self.state.currentState, + components: { + ..._self.state.currentState.components, + [component.name]: { + ..._self.state.currentState.components[component.name], + selectedSlots, + }, + }, + }, + }, + () => { + executeActionsForEventId(_ref, 'onCalendarSlotSelect', component, mode); + } + ); + } + if (eventName === 'onTableActionButtonClicked') { const { component, data, action, rowId } = options; _self.setState( @@ -240,14 +282,45 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { }, }, }, - () => { - if (action) { - action.events?.forEach((event) => { + async () => { + if (action && action.events) { + for (const event of action.events) { if (event.actionId) { // the event param uses a hacky workaround for using same format used by event manager ( multiple handlers ) - executeAction(_self, { ...event, ...event.options }, mode); + await executeAction(_self, { ...event, ...event.options }, mode); } - }); + } + } else { + console.log('No action is associated with this event'); + } + } + ); + } + + if (eventName === 'OnTableToggleCellChanged') { + const { component, column, rowId, row } = options; + _self.setState( + { + currentState: { + ..._self.state.currentState, + components: { + ..._self.state.currentState.components, + [component.name]: { + ..._self.state.currentState.components[component.name], + selectedRow: row, + selectedRowId: rowId, + }, + }, + }, + }, + async () => { + if (column && column.events) { + for (const event of column.events) { + if (event.actionId) { + // the event param uses a hacky workaround for using same format used by event manager ( multiple handlers ) + await executeAction(_self, { ...event, ...event.options }, mode); + } + } } else { console.log('No action is associated with this event'); } @@ -269,6 +342,7 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { 'onSelectionChange', 'onSelect', 'onClick', + 'onFileSelected', ].includes(eventName) ) { const { component } = options; @@ -366,7 +440,7 @@ export function previewQuery(_ref, query) { }); } -export function runQuery(_ref, queryId, queryName, confirmed = undefined) { +export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode) { const query = _ref.state.app.data_queries.find((query) => query.id === queryId); let dataQuery = {}; @@ -515,7 +589,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined) { }, () => { resolve(); - onEvent(_self, 'onDataQuerySuccess', { definition: { events: dataQuery.options.events } }); + onEvent(_self, 'onDataQuerySuccess', { definition: { events: dataQuery.options.events } }, mode); } ); }) diff --git a/frontend/src/_helpers/http-client.js b/frontend/src/_helpers/http-client.js new file mode 100644 index 0000000000..abe8eb8a88 --- /dev/null +++ b/frontend/src/_helpers/http-client.js @@ -0,0 +1,90 @@ +import config from 'config'; +import { authenticationService } from '@/_services'; + +const HttpVerb = { + Get: 'GET', + Post: 'POST', + Put: 'PUT', + Patch: 'PATCH', + Delete: 'DELETE', +}; + +class HttpClient { + constructor(args = {}) { + this.host = args.host || config.apiUrl; + this.namespace = args.namespace || ''; // TODO: add versioning (/v1) to all endpoints (https://docs.nestjs.com/techniques/versioning#uri-versioning-type) + this.headers = { + 'content-type': 'application/json', + ...args.headers, + }; + } + + extractResponseHeaders(response) { + const object = {}; + response.headers.forEach((value, key) => { + object[key] = value; + }); + return object; + } + + async request(method, url, data) { + const endpoint = this.host + this.namespace + url; + const options = { + method, + headers: this.headers, + }; + const user = JSON.parse(localStorage.getItem('currentUser')) || {}; + if (user?.auth_token) { + options.headers['Authorization'] = `Bearer ${user?.auth_token}`; + } + if (data) { + options.body = JSON.stringify(data); + } + const response = await fetch(endpoint, options); + const payload = { + status: response.status, + statusText: response.statusText, + headers: this.extractResponseHeaders(response), + }; + const text = await response.text(); + try { + payload.data = JSON.parse(text); + if (!response.ok) { + // TODO: add 403 to the below [401] array? + if ([401].indexOf(response.status) !== -1) { + // auto logout if 401 Unauthorized or 403 Forbidden response returned from api + authenticationService.logout(); + } + + throw payload; + } + } catch (err) { + payload.data = []; + } finally { + // eslint-disable-next-line no-unsafe-finally + return payload; + } + } + + get(url) { + return this.request(HttpVerb.Get, url); + } + + post(url, data) { + return this.request(HttpVerb.Post, url, data); + } + + put(url, data) { + return this.request(HttpVerb.Put, url, data); + } + + patch(url, data) { + return this.request(HttpVerb.Patch, url, data); + } + + delete(url) { + return this.request(HttpVerb.Delete, url); + } +} + +export default HttpClient; diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js index fcf96b7f33..4ef0cc66a2 100644 --- a/frontend/src/_helpers/utils.js +++ b/frontend/src/_helpers/utils.js @@ -23,6 +23,8 @@ export function findProp(obj, prop, defval) { return obj; } +export const pluralize = (count, noun, suffix = 's') => `${count} ${noun}${count !== 1 ? suffix : ''}`; + export function resolve(data, state) { if (data.startsWith('{{queries.') || data.startsWith('{{globals.') || data.startsWith('{{components.')) { let prop = data.replace('{{', '').replace('}}', ''); @@ -230,3 +232,8 @@ export function validateWidget({ validationObject, widgetValue, currentState, cu validationError, }; } + +export function validateEmail(email) { + const emailRegex = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; + return emailRegex.test(email) +} \ No newline at end of file diff --git a/frontend/src/_hooks/use-mouse-position.jsx b/frontend/src/_hooks/use-mouse-position.jsx new file mode 100644 index 0000000000..9c5875bb60 --- /dev/null +++ b/frontend/src/_hooks/use-mouse-position.jsx @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react'; + +export default function useMousePosition() { + const [mousePosition, setMousePosition] = useState({ x: null, y: null }); + + useEffect(() => { + const mouseMoveHandler = (event) => { + const { clientX, clientY } = event; + setMousePosition({ x: clientX, y: clientY }); + }; + document.addEventListener('mousemove', mouseMoveHandler); + + return () => { + document.removeEventListener('mousemove', mouseMoveHandler); + }; + }, []); + + return mousePosition; +} diff --git a/frontend/src/_hooks/use-popover.jsx b/frontend/src/_hooks/use-popover.jsx index 93aa930a17..6a1831c4b5 100644 --- a/frontend/src/_hooks/use-popover.jsx +++ b/frontend/src/_hooks/use-popover.jsx @@ -1,5 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { useRef, useState, useEffect, useCallback } from 'react'; +import { isEmpty } from 'lodash'; + const noop = () => {}; const useEscapeHandler = (handler = noop, dependencies = []) => { const escapeHandler = (e) => { @@ -8,9 +10,8 @@ const useEscapeHandler = (handler = noop, dependencies = []) => { } }; useEffect(() => { - document === null || document === void 0 ? void 0 : document.addEventListener('keyup', escapeHandler); - return () => - document === null || document === void 0 ? void 0 : document.removeEventListener('keyup', escapeHandler); + isEmpty(document) ? undefined : document.addEventListener('keyup', escapeHandler); + return () => (isEmpty(document) ? undefined : document.removeEventListener('keyup', escapeHandler)); }, dependencies); }; const useClickOutside = (dependencies, handler = noop) => { @@ -26,13 +27,9 @@ const useClickOutside = (dependencies, handler = noop) => { callbackRef.current = handler; }); useEffect(() => { - document === null || document === void 0 - ? void 0 - : document.addEventListener('click', outsideClickHandler, { capture: true }); + isEmpty(document) ? undefined : document.addEventListener('click', outsideClickHandler, { capture: true }); return () => - document === null || document === void 0 - ? void 0 - : document.removeEventListener('click', outsideClickHandler, { capture: true }); + isEmpty(document) ? undefined : document.removeEventListener('click', outsideClickHandler, { capture: true }); }, dependencies); return ref; }; @@ -40,7 +37,10 @@ const role = 'dialog'; const usePopover = (defaultOpen = false) => { const triggerRef = useRef(null); const [open, setOpen] = useState(defaultOpen); - const toggle = useCallback(() => setOpen(!open), []); + const toggle = useCallback((e) => { + e.stopPropagation(); + setOpen(!open); + }, []); const close = useCallback(() => setOpen(false), []); useEscapeHandler(close, []); const contentRef = useClickOutside([], open ? close : undefined); diff --git a/frontend/src/_hooks/use-shortcuts.jsx b/frontend/src/_hooks/use-shortcuts.jsx new file mode 100644 index 0000000000..14f59c6d29 --- /dev/null +++ b/frontend/src/_hooks/use-shortcuts.jsx @@ -0,0 +1,35 @@ +/* eslint-disable react-hooks/exhaustive-deps */ + +// https://keycode.info/ this cheatsheet gives info about key presses + +import { useEffect, useCallback } from 'react'; +const setsEqual = (setA, setB) => setA.size === setB.size && !Array.from(setA).some((v) => !setB.has(v)); +const useShortcuts = (keys, callback, deps) => { + const memoizedCallback = useCallback(callback, deps || []); + const targetKeys = new Set(keys.map((key) => key.toLowerCase())); + const pressedKeys = new Set(); + function onKeyPressed(event) { + pressedKeys.add(event.key.toLowerCase()); + if (setsEqual(pressedKeys, targetKeys)) { + memoizedCallback(); + } + } + function onKeyUp(event) { + pressedKeys.delete(event.key.toLowerCase()); + } + function onWindowBlur() { + pressedKeys.clear(); + } + useEffect(() => { + window.addEventListener('keydown', onKeyPressed); + window.addEventListener('keyup', onKeyUp); + window.addEventListener('blur', onWindowBlur); + return () => { + window.removeEventListener('keydown', onKeyPressed); + window.removeEventListener('keyup', onKeyUp); + window.removeEventListener('blur', onWindowBlur); + }; + }, [memoizedCallback]); +}; + +export default useShortcuts; diff --git a/frontend/src/_services/comments.service.js b/frontend/src/_services/comments.service.js new file mode 100644 index 0000000000..b5efc14e82 --- /dev/null +++ b/frontend/src/_services/comments.service.js @@ -0,0 +1,53 @@ +import HttpClient from '@/_helpers/http-client'; + +// TODO: antipattern to initialize a new instance @ every service +// TODO: use singleton pattern and move it to a static variable on page load +const adapter = new HttpClient(); + +function getThreads(appId, appVersionsId) { + return adapter.get(`/threads/${appId}/all?appVersionsId=${appVersionsId}`); +} + +function createThread(data) { + return adapter.post(`/threads/create`, data); +} + +function updateThread(threadId, data) { + return adapter.patch(`/threads/edit/${threadId}`, data); +} + +function deleteThread(threadId) { + return adapter.delete(`/threads/delete/${threadId}`); +} + +function getComments(threadId, appVersionsId) { + return adapter.get(`/comments/${threadId}/all?appVersionsId=${appVersionsId}`); +} + +function createComment(data) { + return adapter.post(`/comments/create`, data); +} + +function updateComment(commentId, data) { + return adapter.patch(`/comments/edit/${commentId}`, data); +} + +function deleteComment(commentId) { + return adapter.delete(`/comments/delete/${commentId}`); +} + +function getNotifications(appId, isResolved, appVersionsId) { + return adapter.get(`/comments/${appId}/notifications?isResolved=${isResolved}&appVersionsId=${appVersionsId}`); +} + +export const commentsService = { + getThreads, + createThread, + updateThread, + deleteThread, + getComments, + createComment, + updateComment, + deleteComment, + getNotifications, +}; diff --git a/frontend/src/_services/index.js b/frontend/src/_services/index.js index ea84450dd5..b199769045 100644 --- a/frontend/src/_services/index.js +++ b/frontend/src/_services/index.js @@ -9,3 +9,4 @@ export * from './organization_user.service'; export * from './openapi.service'; export * from './folder.service'; export * from './tooljet.service'; +export * from './comments.service'; diff --git a/frontend/src/_styles/custom.scss b/frontend/src/_styles/custom.scss index 93780ed242..903f97b537 100644 --- a/frontend/src/_styles/custom.scss +++ b/frontend/src/_styles/custom.scss @@ -34,3 +34,7 @@ div[data-disabled='true'] { padding-left: .90rem; padding-right: .90rem; } + +.accepted-files .file-list { + margin-bottom: -0.25rem; +} diff --git a/frontend/src/_styles/editor/comment-notifications.scss b/frontend/src/_styles/editor/comment-notifications.scss new file mode 100644 index 0000000000..336085caa8 --- /dev/null +++ b/frontend/src/_styles/editor/comment-notifications.scss @@ -0,0 +1,76 @@ +@import '../colors.scss'; + +.comment-notification-sidebar { + .comment-notification-user { + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 17px; + letter-spacing: 0em; + text-align: left; + text-transform: capitalize; + } + + .comment-notification-count { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 15px; + letter-spacing: 0em; + text-align: left; + color: #656D77; + } + .comment-notification-message { + margin-top: 8px; + } + + .comment-notification { + padding: 1rem 1rem; + } + + .comment-notification-selected { + background: #DEEAFF; + } + + .comment-notification-header { + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + letter-spacing: 0em; + text-align: left; + } + + .comment-notification-nav-item { + background-color: #fff !important; + } + + .comment-notification-filter-popover { + width: 160px; + height: 78px; + border: 0.5px solid #E1E1E1 !important; + border-radius: 4px !important; + top: 25px !important; + right: -30px; + left: -150px !important; + padding: 12px; + } + + .card-header, .card { + border: 0 !important; + box-shadow: none !important; + } + + .count { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0em; + text-align: left; + } + + .nav-tabs .nav-link { + margin-bottom: 0; + } +} \ No newline at end of file diff --git a/frontend/src/_styles/editor/comments.scss b/frontend/src/_styles/editor/comments.scss new file mode 100644 index 0000000000..f82d6b30f7 --- /dev/null +++ b/frontend/src/_styles/editor/comments.scss @@ -0,0 +1,97 @@ +@import '../colors.scss'; + +.mentioned-user { + color: #218DE3; +} + +.comments { + z-index: 2; + display: inline-block; + position: absolute; + margin: 0.2rem; + + &.open { + z-index: 3; + } + + .card-header { + border: 0 !important; + } + + .mentioned-user { + color: #218DE3; + } + + .comment { + border: 2.5px solid #FCAA0D; + } + + .comment-open { + border: 2.5px solid #218DE3 !important; + } + + .comment-body { + max-height: 400px; + } + + .resolved { + border: 2px solid #8991A0; + } + + .comment-popover { + top: 40px; + width: 20rem; + max-width: 30rem; + } + + .open-left { + right: 0; + left: auto; + } + + .open-right { + right: 0; + } + + .comment-actions-popover { + width: 110px; + border-radius: 4px; + left: -85px; + top: 20px; + } + + .comment-action { + font-size: 12px; + padding: 12px !important; + font-weight: 500; + } + + .comment-author { + font-size: 14px; + } + .comment-time { + font-size: 12px; + } + .comment-body { + font-size: 14px; + } +} + +.comment-overlay { + position: fixed; + top: 0; + right: 0; + left: 0; + bottom: 0; + background: transparent; + height: 1000vh; +} + +.comment-preview-bubble { + opacity: 0.5; + display: inline !important; +} + +.comment-preview-bubble-border { + border: 2.5px solid #FCAA0D; +} \ No newline at end of file diff --git a/frontend/src/_styles/left-sidebar.scss b/frontend/src/_styles/left-sidebar.scss index dfd1786839..967aea8c05 100644 --- a/frontend/src/_styles/left-sidebar.scss +++ b/frontend/src/_styles/left-sidebar.scss @@ -101,4 +101,9 @@ .no-border { border: 0; } + .comment-badge { + position: absolute; + bottom: 13px; + right: 6px; + } } \ No newline at end of file diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 88a0ed2ffc..0a615bb736 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -14,6 +14,15 @@ body { } } +.emoji-mart-scroll { + border-bottom: 0; + margin-bottom: 6px; +} + +.emoji-mart-scroll + .emoji-mart-bar { + display: none; +} + .editor { .header-container { max-width: 100%; @@ -1155,6 +1164,10 @@ tr:focus { } } +.mentions-popover { + +} + .input-icon { .input-icon-addon { display: none; @@ -1186,10 +1199,6 @@ body { color: #3e525b; } -.navbar-brand-image { - height: 1.6rem; -} - .RichEditor-root { background: #fff; border: 1px solid #ddd; @@ -1817,9 +1826,14 @@ input:focus-visible { .theme-dark .navbar .navbar-nav .nav-link.active, .theme-dark .navbar .navbar-nav .nav-link.show, .theme-dark .navbar .navbar-nav .show > .nav-link { - color: #fff; + color:#fff; + } + .form-check > .form-check-input:not(:checked){ + background-color:#fff; + } + .form-check-label{ + color:white; } - .left-sidebar .active { background: #333c48; } @@ -2052,6 +2066,11 @@ input:focus-visible { filter: brightness(0) invert(1); } + .launch-btn { + filter: brightness(0.4) !important; + background: #8d9095; + } + .badge { filter: brightness(1) invert(1); @@ -2150,6 +2169,14 @@ input:focus-visible { border-right: 1px solid #fff; border-bottom: 1px solid #fff; } + + .widget-documentation-link { + background-color: #1f2936; + } + + .widget-documentation-link a { + color: rgb(66, 153, 225); + } } .main-wrapper { @@ -2258,10 +2285,26 @@ input[type='text'] { } } +.show { + display: block; +} +.hide { + display: none; +} + .draggable-box:focus-within { z-index: 2 !important; } +.cursor-wait { + cursor: wait; +} +.cursor-text{ + cursor: text; +} +.cursor-none { + cursor: none; +} .theme-dark .event-action { filter: brightness(0) invert(1); } @@ -2270,6 +2313,10 @@ input[type='text'] { filter: brightness(0) invert(0); } +.disabled { + pointer-events: none; + opacity: 0.4; +} .DateRangePicker { padding: 1.25px 5px; } @@ -2298,12 +2345,162 @@ input[type='text'] { } } +.fw-400 { + font-weight: 400; +} + .fw-500 { font-weight: 500; } +.ligh-gray { + color: #656D77; +} + +.nav-item { + background: #fff !important; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 22px; + letter-spacing: -0.1px; + text-align: left; +} + +.nav-link { + min-width: 100px; + justify-content: center; +} + +.nav-tabs .nav-link.active { + font-weight: 400 !important; + color: #0565FE !important; +} + +.empty-title { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 15px; + letter-spacing: -0.01em; + text-align: left; +} //select-search-action .popover-action-select-search > .select-search .select-search__select { max-height: 200px; overflow-y: scroll; -} \ No newline at end of file +} + +.calendar-widget.compact { + .rbc-time-view-resources .rbc-time-header-content { + min-width: auto; + } + + .rbc-time-view-resources .rbc-day-slot { + min-width: 50px; + } + + .rbc-time-view-resources .rbc-header, + .rbc-time-view-resources .rbc-day-bg { + width: 50px; + } +} + +.calendar-widget.dont-highlight-today { + .rbc-today { + background-color: inherit; + } + + .rbc-current-time-indicator { + display: none; + } +} + +.calendar-widget { + padding: 10px; + background-color: white; + + .rbc-day-slot .rbc-event, .rbc-day-slot .rbc-background-event { + border-left: 3px solid #26598533; + } + + .rbc-toolbar { + font-size: 14px; + } + + .rbc-event { + .rbc-event-label { + display: none; + } + } + + .rbc-off-range-bg { + background-color: #f4f6fa; + } + + .rbc-toolbar { + .rbc-btn-group { + button { + box-shadow: none; + border-radius: 0; + border-width: 1px; + } + } + } +} + +.calendar-widget.hide-view-switcher { + .rbc-toolbar { + .rbc-btn-group:nth-of-type(3) { + display: none; + } + } +} + +.calendar-widget.dark-mode { + background-color: #1d2a39; + + .rbc-toolbar { + button { + color: white; + } + + button:hover, button.rbc-active { + color: black; + } + } + + .rbc-off-range-bg { + background-color: #2b394b; + } + + .rbc-selected-cell { + background-color: #22242d; + } + + .rbc-today { + background-color: #5a7ca8; + } +} + +.calendar-widget.dark-mode.dont-highlight-today { + .rbc-today { + background-color: inherit; + } +} + +.navbar .navbar-nav { + min-height: 2rem; +} + +.navbar-brand-image { + height: 1.6rem; +} + +.nav-tabs .nav-link.active { + font-weight: 400 !important; +} + +.nav-tabs .nav-link { + font-weight: 400 !important; +} diff --git a/frontend/src/_ui/Button/index.js b/frontend/src/_ui/Button/index.js index 415c141c0c..53af73a458 100644 --- a/frontend/src/_ui/Button/index.js +++ b/frontend/src/_ui/Button/index.js @@ -1,11 +1,16 @@ import React from 'react'; -import Button from 'react-bootstrap/Button'; +import cx from 'classnames'; -const Input = ({ disabled, className, onClick, children, ...props }) => { +const Input = ({ disabled, loading, className, onClick, children, ...props }) => { return ( - + ); }; diff --git a/frontend/src/_ui/Mentions/index.js b/frontend/src/_ui/Mentions/index.js new file mode 100644 index 0000000000..6c4fad9a02 --- /dev/null +++ b/frontend/src/_ui/Mentions/index.js @@ -0,0 +1,145 @@ +import React from 'react'; +import { capitalize } from 'lodash'; +import { organizationService } from '@/_services'; + +import { MentionsInput, Mention } from 'react-mentions'; + +// const { emojis } = require('./emojis.json'); + +const Mentions = ({ value, setValue, placeholder }) => { + const [users, setUsers] = React.useState([]); + + // TODO: move this to context etc, this loads the users x number to imports + React.useEffect(() => { + organizationService.getUsers(null).then((data) => { + const _users = data.users.map((u) => ({ + id: u.id, + display: `${capitalize(u.first_name)} ${capitalize(u.last_name)}`, + })); + setUsers(_users); + }); + }, []); + + const queryEmojis = (query) => { + if (query.length === 0) return; + + return; + // const matches = emojis + // .filter((emoji) => { + // return emoji.name.indexOf(query.toLowerCase()) > -1; + // }) + // .slice(0, 10); + // return matches.map(({ emoji }) => ({ id: emoji })); + }; + return ( + setValue(newValue)} + placeholder={placeholder} + > + `@${display}`} + markup="(@__display__)" + data={users} + // style={{ + // backgroundColor: '#218DE3', + // }} + appendSpaceOnAdd + /> + + + ); +}; + +// const Mentions = ({ value, setValue, placeholder }) => { +// const [open, trigger, content, setOpen] = usePopover(false); +// const handleChange = (e) => { +// e.stopPropagation(); +// if (e.target.value.includes('@')) { +// setOpen(true); +// } +// setValue(e.target.value); +// }; + +// let conditionalProps = {}; + +// if (open) { +// conditionalProps = { ...trigger }; +// } +// return ( +// <> +//