diff --git a/.version b/.version index 8adc70fdd9..c18d72be30 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.8.0 \ No newline at end of file +0.8.1 \ No newline at end of file diff --git a/README.md b/README.md index 0891195d6e..9029124884 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,10 @@ Documentation is available at https://docs.tooljet.io. - [Widget Reference](https://docs.tooljet.io/docs/widgets/button) ## Branching model -We use git-flow branching model. The base branch is `develop`. If you are looking for a stable version, please use the main branch or tags labeled as v1.x.x. +We use the git-flow branching model. The base branch is `develop`. If you are looking for a stable version, please use the main branch or tags labeled as v1.x.x. ## Contributing -Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand about our development process, how to propose bugfixes and improvements, and how to build and test your changes to ToolJet.
+Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand about our development process, how to propose bug fixes and improvements, and how to build and test your changes to ToolJet.
## Contributors diff --git a/deploy/ec2/setup_machine.sh b/deploy/ec2/setup_machine.sh index 9e48c45db4..363c732ed8 100644 --- a/deploy/ec2/setup_machine.sh +++ b/deploy/ec2/setup_machine.sh @@ -36,7 +36,7 @@ sudo openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \ # Setup nginx config export SERVER_HOST="${SERVER_HOST:=localhost}" export SERVER_USER="${SERVER_USER:=www-data}" -VARS_TO_SUBSTITUTE="$SERVER_HOST:$SERVER_USER" +VARS_TO_SUBSTITUTE='$SERVER_HOST:$SERVER_USER' envsubst "${VARS_TO_SUBSTITUTE}" < /tmp/nginx.conf > /tmp/nginx-substituted.conf sudo cp /tmp/nginx-substituted.conf /usr/local/openresty/nginx/conf/nginx.conf diff --git a/docs/docs/contributing-guide/tutorials/_category_.json b/docs/docs/contributing-guide/tutorials/_category_.json new file mode 100644 index 0000000000..0d7c9bc587 --- /dev/null +++ b/docs/docs/contributing-guide/tutorials/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Tutorials", + "position": 2, + "collapsed": true +} diff --git a/docs/docs/contributing-guide/tutorials/create-widget.md b/docs/docs/contributing-guide/tutorials/create-widget.md new file mode 100644 index 0000000000..b54053daf2 --- /dev/null +++ b/docs/docs/contributing-guide/tutorials/create-widget.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 1 +--- + +# Creating Widgets +These are some of the most useful properties and functions passed to the widget + +### properties + +The `properties` object will contain the configurable properties of a widget, initially obtained from its definition on `components.js`. +The values inside `properties` changes whenever the developer changes it from the inspector panel of ToolJet editor. + +### exposedVariables + +The `exposedVariables` object will contain the values of all exposed variables as configured in `components.js`. + +### setExposedVariable('exposedVariableName', newValue) + +This function allows you to update the value of an exposed variable to `newValue`. + +### validate(value) + +This function validates the `value` passed based on the validation settings configured on the inspector panel for the widget. +It returns an array `[isValid, validationError]`, which represents respectively, whether the `value` passed is valid, +and the error message if there is one. \ No newline at end of file diff --git a/docs/docs/deployment/env-vars.md b/docs/docs/deployment/env-vars.md index cd383d2f8b..bc31f81e4a 100644 --- a/docs/docs/deployment/env-vars.md +++ b/docs/docs/deployment/env-vars.md @@ -129,3 +129,13 @@ This is required when client is built separately. | variable | description | | ------------------ | ----------------------------------------------------------- | | TOOLJET_SERVER_URL | the URL of ToolJet server ( eg: https://server.tooljet.io ) | + + +#### Asset path ( optionally required ) + +This is required when the assets for the client are to be loaded from elsewhere (eg: CDN). +This can be an absolute path, or relative to main HTML file. + +| variable | description | +| ------------------ | ----------------------------------------------------------- | +| ASSET_PATH | the asset path for the website ( eg: https://app.tooljet.io/) | diff --git a/docs/docs/widgets/datepicker.md b/docs/docs/widgets/datepicker.md index d3de73b636..9376cea56a 100644 --- a/docs/docs/widgets/datepicker.md +++ b/docs/docs/widgets/datepicker.md @@ -23,7 +23,7 @@ Date format should be followed as ISO 8601 as mentioned in the [moment documenta | ----------- | ----------- | | Format | The format of the date selected by the date picker | | Enable time selection | Allows to select time if enabled. Time selection is disabled by default | -| Enable date selection | Allos to select date if enabled. Date selection is enabled by default | +| Enable date selection | Allows to select date if enabled. Date selection is enabled by default | ToolJet - Widget Reference - Datepicker \ No newline at end of file diff --git a/docs/docs/widgets/divider.md b/docs/docs/widgets/divider.md new file mode 100644 index 0000000000..8edd95159b --- /dev/null +++ b/docs/docs/widgets/divider.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 20 +--- + +# Divider + +Divider widget is used to add separator between components. + +ToolJet - Widget Reference - Divider + + +#### Properties + +| properties | description | +| ----------- | ----------- | +| Divider Color | It is used to set the color of the divider. Use hex code to set the background color. | +| Visibility | This property is used to set the visibility of the divider. The property accepts Boolean value. | +| Show on Desktop | This property have toggle switch. If enabled, the divider will display in the desktop view else it will not appear. | +| Show on Mobile | This property have toggle switch. If enabled, the divider will display in the mobile view else it will not appear. | \ No newline at end of file diff --git a/docs/docs/widgets/star.md b/docs/docs/widgets/star.md index 3dd72daaa1..6117bb648a 100644 --- a/docs/docs/widgets/star.md +++ b/docs/docs/widgets/star.md @@ -20,7 +20,7 @@ This event is triggered when an star is clicked. | Label | The text to be used as the label for the star rating. | | Number of stars | Initial number of stars in the list on initial load. `default: 5`| | Default no of selected stars | This property specifies the default count of stars that are selected on the initial load. `default: 5` (integer)| -| Enable half star | Allos selection of half stars if enabled. `default: false` (bool)| +| Enable half star | Allows selection of half stars if enabled. `default: false` (bool)| | Tooltips |This is used for displaying informative tooltips on each star, and it is mapped to the index of the star. `default: []` (array of strings ) | | Star Color | Display color of the star. `default: #ffb400` (color hex) | diff --git a/docs/docs/widgets/table.md b/docs/docs/widgets/table.md index 2d38e5a2b1..446fa0cb8e 100644 --- a/docs/docs/widgets/table.md +++ b/docs/docs/widgets/table.md @@ -137,3 +137,9 @@ If the data of a cell is changed, "save changes" button will be shown at the bot | Show search box | It can be used to show or hide Table Search box. | | Show download button | Show or hide download button at the Table footer. | | Show filter button | Show or hide filter button at the Table footer. | + +#### Styles + +| Style | Description | +| ----------- | ----------- | +| Cell size | This decides the size of table cells. You can choose between a `Compact` size for table cells or a `Spacious` size | \ No newline at end of file diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 8aef8d234e..87458a98e8 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -2,7 +2,7 @@ module.exports = { title: 'ToolJet - Documentation', tagline: 'Build and deploy internal tools.', - url: 'https://tooljet.io', + url: 'https://docs.tooljet.io', baseUrl: '/', onBrokenLinks: 'ignore', onBrokenMarkdownLinks: 'warn', diff --git a/docs/static/img/datasource-reference/mo-connect.png b/docs/static/img/datasource-reference/mo-connect.png index 450644a23c..94602fb13a 100644 Binary files a/docs/static/img/datasource-reference/mo-connect.png and b/docs/static/img/datasource-reference/mo-connect.png differ diff --git a/docs/static/img/datasource-reference/pg-connect.png b/docs/static/img/datasource-reference/pg-connect.png index 5f76bf4c6e..8de9b3f1b9 100644 Binary files a/docs/static/img/datasource-reference/pg-connect.png and b/docs/static/img/datasource-reference/pg-connect.png differ diff --git a/docs/static/img/widgets/divider/divider.gif b/docs/static/img/widgets/divider/divider.gif new file mode 100644 index 0000000000..03d06f7287 Binary files /dev/null and b/docs/static/img/widgets/divider/divider.gif differ diff --git a/frontend/assets/images/icons/widgets/passwordInput.svg b/frontend/assets/images/icons/widgets/passwordInput.svg new file mode 100644 index 0000000000..b8d8ce7444 --- /dev/null +++ b/frontend/assets/images/icons/widgets/passwordInput.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/config/entrypoint.sh b/frontend/config/entrypoint.sh index 502897495e..058ae7cc0e 100755 --- a/frontend/config/entrypoint.sh +++ b/frontend/config/entrypoint.sh @@ -3,7 +3,7 @@ set -eu export SERVER_HOST="${SERVER_HOST:=server}" export SERVER_USER="${SERVER_USER:=root}" -VARS_TO_SUBSTITUTE="$SERVER_HOST:$SERVER_USER" +VARS_TO_SUBSTITUTE='$SERVER_HOST:$SERVER_USER' envsubst "${VARS_TO_SUBSTITUTE}" < /etc/openresty/nginx.conf.template > /etc/openresty/nginx.conf diff --git a/frontend/src/Editor/ActionTypes.js b/frontend/src/Editor/ActionTypes.js index 0111eb2619..c264786d54 100644 --- a/frontend/src/Editor/ActionTypes.js +++ b/frontend/src/Editor/ActionTypes.js @@ -37,4 +37,12 @@ export const ActionTypes = [ id: 'copy-to-clipboard', options: [{ name: 'copy-to-clipboard', type: 'text', default: '' }], }, + { + name: 'Set local storage', + id: 'set-localstorage-value', + options: [ + { name: 'key', type: 'code', default: '' }, + { name: 'value', type: 'code', default: '' }, + ], + }, ]; diff --git a/frontend/src/Editor/Box.jsx b/frontend/src/Editor/Box.jsx index 2d19875484..1dca4b5692 100644 --- a/frontend/src/Editor/Box.jsx +++ b/frontend/src/Editor/Box.jsx @@ -21,9 +21,12 @@ import { ToggleSwitch } from './Components/Toggle'; import { RadioButton } from './Components/RadioButton'; import { StarRating } from './Components/StarRating'; import { Divider } from './Components/Divider'; +import { PasswordInput } from './Components/PasswordInput'; import { renderTooltip } from '../_helpers/appUtils'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import '@/_styles/custom.scss'; +import { resolveProperties, resolveStyles } from './component-properties-resolution'; +import { validateWidget } from '@/_helpers/utils'; const AllComponents = { Button, @@ -48,6 +51,7 @@ const AllComponents = { RadioButton, StarRating, Divider, + PasswordInput, }; export const Box = function Box({ @@ -83,6 +87,16 @@ export const Box = function Box({ } const ComponentToRender = AllComponents[component.component]; + const resolvedProperties = resolveProperties(component, currentState); + const resolvedStyles = resolveStyles(component, currentState); + const exposedVariables = currentState?.components[component.name] ?? {}; + + const fireEvent = (eventName, options) => onEvent(eventName, { ...options, component }); + const validate = (value) => + validateWidget({ + ...{ widgetValue: value }, + ...{ validationObject: component.definition.validation, currentState }, + }); return ( onComponentOptionChanged(component, variable, value)} + fireEvent={fireEvent} + validate={validate} > ) : (
diff --git a/frontend/src/Editor/Components/Button.jsx b/frontend/src/Editor/Components/Button.jsx index cfcec96817..74d283748f 100644 --- a/frontend/src/Editor/Components/Button.jsx +++ b/frontend/src/Editor/Components/Button.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; var tinycolor = require('tinycolor2'); -export const Button = function Button({ id, width, height, component, onComponentClick, currentState }) { +export const Button = function Button({ width, height, component, currentState, fireEvent }) { const [loadingState, setLoadingState] = useState(false); useEffect(() => { @@ -17,11 +17,13 @@ export const Button = function Button({ id, width, height, component, onComponen const text = component.definition.properties.text.value; const backgroundColor = component.definition.styles.backgroundColor.value; const color = component.definition.styles.textColor.value; + const borderRadius = component.definition.styles.borderRadius?.value ?? 3; // using 2 for backward compatibility const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const disabledState = component.definition.styles?.disabledState?.value ?? false; const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; + const parsedBorderRadius = typeof borderRadius !== 'number' ? resolveWidgetFieldValue(borderRadius, currentState) : borderRadius; let parsedWidgetVisibility = widgetVisibility; try { @@ -33,6 +35,7 @@ export const Button = function Button({ id, width, height, component, onComponen const computedStyles = { backgroundColor, color, + borderRadius: `${parsedBorderRadius}px`, width, height, display: parsedWidgetVisibility ? '' : 'none', @@ -46,7 +49,7 @@ export const Button = function Button({ id, width, height, component, onComponen style={computedStyles} onClick={(event) => { event.stopPropagation(); - onComponentClick(id, component); + fireEvent('onClick'); }} > {resolveReferences(text, currentState)} diff --git a/frontend/src/Editor/Components/Datepicker.jsx b/frontend/src/Editor/Components/Datepicker.jsx index f8073952d8..3d0ddc1d7f 100644 --- a/frontend/src/Editor/Components/Datepicker.jsx +++ b/frontend/src/Editor/Components/Datepicker.jsx @@ -48,7 +48,9 @@ export const Datepicker = function Datepicker({ } function onDateChange(event) { - const value = event._isAMomentObject ? event.format(dateFormat.value) : event; + const selectedDateFormat = enableTime ? `${dateFormat.value} LT` : dateFormat.value; + const value = event._isAMomentObject ? event.format(selectedDateFormat) : event; + setDateText(value); onComponentOptionChanged(component, 'value', value); } @@ -99,7 +101,7 @@ export const Datepicker = function Datepicker({ ); }} diff --git a/frontend/src/Editor/Components/DraftEditor.jsx b/frontend/src/Editor/Components/DraftEditor.jsx index a54498f5c9..f882db9b8b 100644 --- a/frontend/src/Editor/Components/DraftEditor.jsx +++ b/frontend/src/Editor/Components/DraftEditor.jsx @@ -46,13 +46,16 @@ class StyleButton extends React.Component { } } -const BLOCK_TYPES = [ +const HEADINGS = [ { label: 'H1', style: 'header-one' }, { label: 'H2', style: 'header-two' }, { label: 'H3', style: 'header-three' }, { label: 'H4', style: 'header-four' }, { label: 'H5', style: 'header-five' }, { label: 'H6', style: 'header-six' }, +] + +const BLOCK_TYPES = [ { label: , style: 'blockquote', @@ -78,6 +81,26 @@ const BlockStyleControls = (props) => { return ( <> +
+ +
+ { + HEADINGS.map((type) => ( + + + + )) + } +
+
{BLOCK_TYPES.map((type) => (
-
+
-
+
-
+
-
+
-
+
{ + const value = currentState?.components[component?.name]?.value; + const [text, setText] = useState(() => value ?? ''); + + const placeholder = component.definition.properties.placeholder.value; + const widgetVisibility = component.definition.styles?.visibility?.value ?? true; + const disabledState = component.definition.styles?.disabledState?.value ?? false; + + const parsedDisabledState = + typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; + + const parsedWidgetVisibility = + typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility, currentState) : widgetVisibility; + + const currentValidState = currentState?.components[component?.name]?.isValid; + + const validationData = validate(value); + + const { isValid, validationError } = validationData; + + if (currentValidState !== isValid) { + onComponentOptionChanged(component, 'isValid', isValid); + } + + return ( +
+ { + event.stopPropagation(); + onComponentClick(id, component); + }} + onChange={(e) => { + setText(e.target.value); + onComponentOptionChanged(component, 'value', e.target.value); + }} + type={'password'} + className={`form-control ${!isValid ? 'is-invalid' : ''} validation-without-icon rounded-0`} + placeholder={placeholder} + value={text} + style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }} + /> + +
{validationError}
+
+ ); +}; diff --git a/frontend/src/Editor/Components/Table/Pagination.jsx b/frontend/src/Editor/Components/Table/Pagination.jsx index bc701d116a..dcf343ba25 100644 --- a/frontend/src/Editor/Components/Table/Pagination.jsx +++ b/frontend/src/Editor/Components/Table/Pagination.jsx @@ -21,7 +21,10 @@ export const Pagination = function Pagination({ setPageCount(lastActivePageIndex); } else if (serverSide || lastActivePageIndex === 0) { setPageIndex(1); + } else { + gotoPage(lastActivePageIndex + 1); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [serverSide, lastActivePageIndex]); function gotoPage(page) { diff --git a/frontend/src/Editor/Components/Table/Table.jsx b/frontend/src/Editor/Components/Table/Table.jsx index e72117efe7..1f22f8e8a6 100644 --- a/frontend/src/Editor/Components/Table/Table.jsx +++ b/frontend/src/Editor/Components/Table/Table.jsx @@ -73,6 +73,8 @@ export function Table({ let tableType = tableTypeProperty ? tableTypeProperty.value : 'table-bordered'; tableType = tableType === '' ? 'table-bordered' : tableType; + const cellSizeType = component.definition.styles.cellSize?.value; + const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const disabledState = component.definition.styles?.disabledState?.value ?? false; @@ -148,7 +150,7 @@ export function Table({ let newFilters = filters; newFilters.splice(index, 1); setFilters(newFilters); - setAllFilters(newFilters); + setAllFilters(newFilters.filter((filter) => filter.id !== '')); } function clearFilters() { @@ -754,6 +756,10 @@ export function Table({ } }, [state.columnResizing.isResizingColumn]); + useEffect(() => { + if (pageCount <= pageIndex) gotoPage(pageCount - 1); + }, [pageCount]); + return (
@@ -888,7 +895,7 @@ export function Table({
{(clientSidePagination || serverSidePagination) && ( { - setText(newText); - onComponentOptionChanged(component, 'value', newText); + setExposedVariable('value', properties.value); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [newText]); - - const placeholder = component.definition.properties.placeholder.value; - const widgetVisibility = component.definition.styles?.visibility?.value ?? true; - const disabledState = component.definition.styles?.disabledState?.value ?? false; - - const parsedDisabledState = - typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; - - let parsedWidgetVisibility = widgetVisibility; - - try { - parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); - } catch (err) { - console.log(err); - } + }, [properties.value]); return ( ); }; diff --git a/frontend/src/Editor/Components/TextInput.jsx b/frontend/src/Editor/Components/TextInput.jsx index 3632d78184..d35d5789cc 100644 --- a/frontend/src/Editor/Components/TextInput.jsx +++ b/frontend/src/Editor/Components/TextInput.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { resolveReferences, resolveWidgetFieldValue, validateWidget } from '@/_helpers/utils'; +import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils'; export const TextInput = function TextInput({ id, @@ -9,6 +9,7 @@ export const TextInput = function TextInput({ onComponentClick, currentState, onComponentOptionChanged, + validate, }) { const placeholder = component.definition.properties.placeholder.value; const widgetVisibility = component.definition.styles?.visibility?.value ?? true; @@ -35,11 +36,7 @@ export const TextInput = function TextInput({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [newText]); - const validationData = validateWidget({ - validationObject: component.definition.validation, - widgetValue: value, - currentState, - }); + const validationData = validate(value); const { isValid, validationError } = validationData; diff --git a/frontend/src/Editor/Components/Toggle.jsx b/frontend/src/Editor/Components/Toggle.jsx index fcf6ed795b..15958f4901 100644 --- a/frontend/src/Editor/Components/Toggle.jsx +++ b/frontend/src/Editor/Components/Toggle.jsx @@ -37,9 +37,9 @@ export const ToggleSwitch = ({ const toggleSwitchColorProperty = component.definition.styles.toggleSwitchColor; const toggleSwitchColor = toggleSwitchColorProperty ? toggleSwitchColorProperty.value : '#3c92dc'; const textColor = textColorProperty ? textColorProperty.value : '#000'; - const widgetVisibility = component.definition.styles?.visibility?.value ?? true; - const disabledState = component.definition.styles?.disabledState?.value ?? false; - + const widgetVisibility = component.definition.styles.visibility?.value ?? true; + const disabledState = component.definition.styles.disabledState?.value ?? false; + const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; diff --git a/frontend/src/Editor/Components/components.js b/frontend/src/Editor/Components/components.js index b267743cf6..a7a601dd02 100644 --- a/frontend/src/Editor/Components/components.js +++ b/frontend/src/Editor/Components/components.js @@ -48,6 +48,14 @@ export const componentTypes = [ { name: 'Striped & bordered', value: 'table-striped table-bordered' }, ], }, + cellSize: { + type: 'select', + displayName: 'Cell size', + options: [ + { name: 'Compact', value: 'compact' }, + { name: 'Spacious', value: 'spacious' }, + ], + }, visibility: { type: 'code', displayName: 'Visibility' }, disabledState: { type: 'code', displayName: 'Disable' }, }, @@ -93,6 +101,7 @@ export const componentTypes = [ textColor: { value: undefined }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + cellSize: { value: 'compact' }, }, }, }, @@ -121,6 +130,7 @@ export const componentTypes = [ textColor: { type: 'color', displayName: 'Text color' }, visibility: { type: 'code', displayName: 'Visibility' }, disabledState: { type: 'code', displayName: 'Disable' }, + borderRadius: { type: 'code', displayName: 'Border radius' }, }, exposedVariables: {}, definition: { @@ -138,6 +148,7 @@ export const componentTypes = [ backgroundColor: { value: '#3c92dc' }, textColor: { value: '#fff' }, visibility: { value: '{{true}}' }, + borderRadius: { value: '{{0}}' }, disabledState: { value: '{{false}}' }, }, }, @@ -346,6 +357,57 @@ export const componentTypes = [ }, }, }, + { + name: 'PasswordInput', + displayName: 'Password Input', + description: 'Password input field for forms', + component: 'PasswordInput', + defaultSize: { + width: 210, + height: 30, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + placeholder: { type: 'code', displayName: 'Placeholder' }, + }, + validation: { + regex: { type: 'code', displayName: 'Regex' }, + minLength: { type: 'code', displayName: 'Min length' }, + maxLength: { type: 'code', displayName: 'Max length' }, + customRule: { type: 'code', displayName: 'Custom validation' }, + }, + events: {}, + styles: { + visibility: { type: 'code', displayName: 'Visibility' }, + disabledState: { type: 'code', displayName: 'Disable' }, + }, + exposedVariables: { + value: '', + }, + definition: { + others: { + showOnDesktop: { value: true }, + showOnMobile: { value: false }, + }, + properties: { + placeholder: { value: 'password' }, + }, + validation: { + regex: { value: '' }, + minLength: { value: null }, + maxLength: { value: null }, + customRule: { value: null }, + }, + events: [], + styles: { + visibility: { value: '{{true}}' }, + disabledState: { value: '{{false}}' }, + }, + }, + }, { name: 'Datepicker', displayName: 'Date Picker', @@ -777,7 +839,7 @@ export const componentTypes = [ }, properties: { label: { value: 'Select' }, - value: { value: '' }, + value: { value: '{{2}}' }, values: { value: '{{[1,2,3]}}' }, display_values: { value: '{{["one", "two", "three"]}}' }, visible: { value: true }, @@ -938,6 +1000,9 @@ export const componentTypes = [ defaultMarkers: { value: `{{ [{"lat": 40.7128, "lng": -73.935242}] }}`, }, + canSearch: { + value: `{{true}}` , + }, }, addNewMarkers: { value: '{{false}}' }, events: [], diff --git a/frontend/src/Editor/ConfigHandle.jsx b/frontend/src/Editor/ConfigHandle.jsx index 3bfb2d4a92..5164a5cd08 100644 --- a/frontend/src/Editor/ConfigHandle.jsx +++ b/frontend/src/Editor/ConfigHandle.jsx @@ -18,6 +18,7 @@ export const ConfigHandle = function ConfigHandle({ id, component, configHandleC src="/assets/images/icons/menu.svg" width="8" height="8" + draggable="false" /> {component.name} @@ -28,6 +29,7 @@ export const ConfigHandle = function ConfigHandle({ id, component, configHandleC role="button" className="mx-2" height="12" + draggable="false" onClick={() => removeComponent({ id })} />
diff --git a/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx b/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx index bd7b0a662a..65fca59273 100644 --- a/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx +++ b/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx @@ -202,7 +202,7 @@ class DataSourceManager extends React.Component { {!selectedDataSource && (
-

DATABASES

+

DATABASES

{DataBaseSources.map((dataSource) => (
this.selectDataSource(dataSource)}> @@ -225,7 +225,7 @@ class DataSourceManager extends React.Component { ))}
-

APIS

+

APIS

{ApiSources.map((dataSource) => (
this.selectDataSource(dataSource)}> diff --git a/frontend/src/Editor/DataSourceManager/SourceComponents/Database/Firestore.schema.json b/frontend/src/Editor/DataSourceManager/SourceComponents/Database/Firestore.schema.json index 43ac997369..8f290f17cf 100644 --- a/frontend/src/Editor/DataSourceManager/SourceComponents/Database/Firestore.schema.json +++ b/frontend/src/Editor/DataSourceManager/SourceComponents/Database/Firestore.schema.json @@ -13,7 +13,10 @@ "rawData": [] }, "options": { - "gcp_key": { "type": "string", "encrypted": true } + "gcp_key": { + "type": "string", + "encrypted": true + } } }, "properties": { @@ -21,9 +24,12 @@ "$label": "Private key", "$key": "gcp_key", "$rows": 15, + "$encrypted": true, "type": "textarea", "description": "Enter private key" } }, - "required": ["gcp_key"] -} + "required": [ + "gcp_key" + ] +} \ No newline at end of file diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 7ab2127720..2ea680afc0 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -309,6 +309,22 @@ class Editor extends React.Component { }); }; + saveAppName = (id, name, notify = false) => { + if (!name.trim()) { + toast.warn("App name can't be empty or whitespace", { + hideProgressBar: true, + position: 'top-center', + }); + + this.setState({ + app: { ...this.state.app, name: this.state.oldName }, + }); + + return; + } + this.saveApp(id, { name }, notify); + }; + renderDataSource = (dataSource) => { const sourceMeta = DataSourceTypes.find((source) => source.kind === dataSource.kind); return ( @@ -445,7 +461,7 @@ class Editor extends React.Component { }; toggleQueryEditor = () => { - this.setState({ showQueryEditor: !this.state.showQueryEditor }); + this.setState((prev) => ({ showQueryEditor: !prev.showQueryEditor })); this.toolTipRefHide.current.style.display = this.state.showQueryEditor ? 'none' : 'flex'; this.toolTipRefShow.current.style.display = this.state.showQueryEditor ? 'flex' : 'none'; }; @@ -473,7 +489,7 @@ class Editor extends React.Component { }; toggleQuerySearch = () => { - this.setState({ showQuerySearchField: !this.state.showQuerySearchField }); + this.setState((prev) => ({ showQuerySearchField: !prev.showQuerySearchField })); }; onVersionDeploy = (versionId) => { @@ -491,8 +507,8 @@ class Editor extends React.Component { }); }; - toolTipRefHide = createRef(null); - toolTipRefShow = createRef(null); + toolTipRefHide = createRef(); + toolTipRefShow = createRef(); render() { const { @@ -569,8 +585,9 @@ class Editor extends React.Component { this.setState({ oldName: e.target.value })} onChange={(e) => this.onNameChanged(e.target.value)} - onBlur={(e) => this.saveApp(this.state.app.id, { name: e.target.value })} + onBlur={(e) => this.saveAppName(this.state.app.id, e.target.value)} className="form-control-plaintext form-control-plaintext-sm" value={this.state.app.name} /> @@ -592,12 +609,12 @@ class Editor extends React.Component { /> this.setState({ currentLayout: 'desktop' })} disabled={currentLayout === 'desktop'} > @@ -620,6 +638,7 @@ class Editor extends React.Component {
)} + + {event.actionId === 'set-localstorage-value' && ( + <> +
+
Key
+
+ handlerChanged(index, 'key', value)} + enablePreview={true} + /> +
+
+
+
Value
+
+ handlerChanged(index, 'value', value)} + enablePreview={true} + /> +
+
+ + )}
diff --git a/frontend/src/Editor/LeftSidebar/index.js b/frontend/src/Editor/LeftSidebar/index.js index db3ab21edc..29cd8c5942 100644 --- a/frontend/src/Editor/LeftSidebar/index.js +++ b/frontend/src/Editor/LeftSidebar/index.js @@ -50,7 +50,7 @@ export const LeftSidebar = ({
- +
{/* */}
diff --git a/frontend/src/Editor/component-properties-resolution.js b/frontend/src/Editor/component-properties-resolution.js new file mode 100644 index 0000000000..7be36e6886 --- /dev/null +++ b/frontend/src/Editor/component-properties-resolution.js @@ -0,0 +1,29 @@ +import { resolveReferences } from '@/_helpers/utils'; + +export const resolveProperties = (component, currentState) => { + if (currentState && currentState.components[component.name]) { + return Object.entries(component.definition.properties).reduce( + (properties, entry) => ({ + ...properties, + ...{ [entry[0]]: resolveReferences(entry[1].value, currentState) }, + }), + {} + ); + } else return {}; +}; + +export const resolveStyles = (component, currentState) => { + if (currentState && currentState.components[component.name]) { + const styles = component.definition.styles; + return Object.entries(styles).reduce((resolvedStyles, entry) => { + const key = entry[0]; + const value = resolveReferences(entry[1].value, currentState); + return { + ...resolvedStyles, + ...{ [key]: value }, + }; + }, {}); + } else { + return {}; + } +}; diff --git a/frontend/src/HomePage/AppMenu.jsx b/frontend/src/HomePage/AppMenu.jsx index f0d41f213a..4ccb419209 100644 --- a/frontend/src/HomePage/AppMenu.jsx +++ b/frontend/src/HomePage/AppMenu.jsx @@ -6,7 +6,16 @@ import Fuse from 'fuse.js'; import { folderService } from '@/_services'; import { toast } from 'react-toastify'; -export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteApp, cloneApp, exportApp }) { +export const AppMenu = function AppMenu({ + app, + folders, + foldersChanged, + deleteApp, + cloneApp, + exportApp, + canCreateApp, + canDeleteApp, +}) { const [addToFolder, setAddToFolder] = useState(false); const [isAdding, setIsAdding] = useState(false); @@ -68,26 +77,32 @@ export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteAp {!addToFolder && (
-
- setAddToFolder(true)}> - Add to folder - -
-
- cloneApp()}> - Clone app - -
-
- exportApp()}> - Export app - -
-
- deleteApp()}> - Delete app - -
+ {canCreateApp && ( + <> +
+ setAddToFolder(true)}> + Add to folder + +
+
+ cloneApp()}> + Clone app + +
+
+ exportApp()}> + Export app + +
+ + )} + {canDeleteApp && ( +
+ deleteApp()}> + Delete app + +
+ )}
)} diff --git a/frontend/src/HomePage/BlankPage.jsx b/frontend/src/HomePage/BlankPage.jsx index c478a1f034..96d299245a 100644 --- a/frontend/src/HomePage/BlankPage.jsx +++ b/frontend/src/HomePage/BlankPage.jsx @@ -1,6 +1,6 @@ import React from 'react'; -export const BlankPage = function BlankPage({ createApp }) { +export const BlankPage = function BlankPage({ createApp, handleImportApp, isImportingApp, fileInput }) { return (
@@ -13,7 +13,7 @@ export const BlankPage = function BlankPage({ createApp }) {

You haven't created any apps yet.

- + + } + Import an app + + + + Read documentation diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 52e680d62e..f6f20080c5 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -88,11 +88,11 @@ class HomePage extends React.Component { appService .createApp() .then((data) => { - console.log(data); _self.props.history.push(`/apps/${data.id}`); }) .catch(({ error }) => { toast.error(error, { hideProgressBar: true, position: 'top-center' }); + _self.setState({ creatingApp: false }); }); }; @@ -139,17 +139,16 @@ class HomePage extends React.Component { document.body.appendChild(link); link.click(); document.body.removeChild(link); - this.fileInput.value = ''; this.setState({ isExportingApp: false }); }) - .catch(({ _error }) => { + .catch((error) => { toast.error('Could not export the app.', { hideProgressBar: true, position: 'top-center', }); - this.fileInput.value = ''; + this.setState({ isExportingApp: false }); - console.log(_error); + console.log(error); }); }; @@ -172,6 +171,7 @@ class HomePage extends React.Component { isImportingApp: false, }); this.fetchApps(this.state.currentPage, this.state.currentFolder.id); + this.fetchFolders(); }) .catch(({ error }) => { toast.error(`Could not import the app: ${error}`, { @@ -191,15 +191,69 @@ class HomePage extends React.Component { isImportingApp: false, }); } + // set file input as null to handle same file upload + event.target.value = null; }; }; - isAppEditable = (app) => { - return app.app_group_permissions.some((p) => p.update); + canUserPerform(user, action, app) { + let permissionGrant; + + switch (action) { + case 'create': + permissionGrant = this.canAnyGroupPerformAction('app_create', user.group_permissions); + break; + case 'read': + case 'update': + permissionGrant = + this.canAnyGroupPerformActionOnApp(action, user.app_group_permissions, app) || + this.isUserOwnerOfApp(user, app); + break; + case 'delete': + permissionGrant = + this.canAnyGroupPerformActionOnApp('delete', user.app_group_permissions, app) || + this.canAnyGroupPerformAction('app_delete', user.group_permissions) || + this.isUserOwnerOfApp(user, app); + break; + default: + permissionGrant = false; + break; + } + + return permissionGrant; + } + + canAnyGroupPerformActionOnApp(action, appGroupPermissions, app) { + if (!appGroupPermissions) { + return false; + } + + const permissionsToCheck = appGroupPermissions.filter((permission) => permission.app_id == app.id); + return this.canAnyGroupPerformAction(action, permissionsToCheck); + } + + canAnyGroupPerformAction(action, permissions) { + if (!permissions) { + return false; + } + + return permissions.some((p) => p[action]); + } + + isUserOwnerOfApp(user, app) { + return user.id == app.user_id; + } + + canCreateApp = () => { + return this.canUserPerform(this.state.currentUser, 'create'); }; - isAppDeletable = (app) => { - return app.app_group_permissions.some((p) => p.delete); + canUpdateApp = (app) => { + return this.canUserPerform(this.state.currentUser, 'update', app); + }; + + canDeleteApp = (app) => { + return this.canUserPerform(this.state.currentUser, 'delete', app); }; executeAppDeletion = () => { @@ -260,7 +314,14 @@ class HomePage extends React.Component { />
- {!isLoading && meta.total_count === 0 && !currentFolder.id && } + {!isLoading && meta.total_count === 0 && !currentFolder.id && ( + + )} {(isLoading || meta.total_count > 0) && (
@@ -286,38 +347,43 @@ class HomePage extends React.Component { {currentFolder.id ? `Folder: ${currentFolder.name}` : 'All applications'}
-
-
- -
-
+ {this.canCreateApp() && ( + <> +
+
+ +
+
-
-
- -
-
+
+
+ +
+
+ + )}
- {isLoadingApps ? ( + {isLoadingGroup || isLoadingApps ? ( - - {isLoadingUsers ? ( + {isLoadingGroup || isLoadingUsers ? ( - {Array.from(Array(4)).map((index) => ( + {Array.from(Array(4)).map((_item, index) => ( diff --git a/frontend/src/_components/DarkModeToggle.jsx b/frontend/src/_components/DarkModeToggle.jsx index 03a3ae3033..e593ef88a9 100644 --- a/frontend/src/_components/DarkModeToggle.jsx +++ b/frontend/src/_components/DarkModeToggle.jsx @@ -3,7 +3,11 @@ import { useSpring, animated } from 'react-spring'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import Tooltip from 'react-bootstrap/Tooltip'; -export const DarkModeToggle = function DarkModeToggle({ darkMode = false, switchDarkMode }) { +export const DarkModeToggle = function DarkModeToggle({ + darkMode = false, + switchDarkMode, + tooltipPlacement = 'bottom', +}) { const toggleDarkMode = () => { switchDarkMode(!darkMode); }; @@ -43,7 +47,7 @@ export const DarkModeToggle = function DarkModeToggle({ darkMode = false, switch return ( {darkMode ? 'Activate light mode' : 'Activate dark mode'}} > diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index a190966022..8057b26c0e 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -1,5 +1,6 @@ import React from 'react'; import Input from '@/_ui/Input'; +import Textarea from '@/_ui/Textarea'; import Select from '@/_ui/Select'; import Headers from '@/_ui/HttpHeaders'; import OAuth from '@/_ui/OAuth'; @@ -17,8 +18,9 @@ const DynamicForm = ({ schema, optionchanged, createDataSource, options, isSavin switch (type) { case 'password': case 'text': - case 'textarea': return Input; + case 'textarea': + return Textarea; case 'dropdown': return Select; case 'toggle': @@ -96,7 +98,7 @@ const DynamicForm = ({ schema, optionchanged, createDataSource, options, isSavin return (
{Object.keys(obj).map((key) => { - const { $label, type } = obj[key]; + const { $label, type, $encrypted } = obj[key]; const Element = getElement(type); @@ -105,7 +107,7 @@ const DynamicForm = ({ schema, optionchanged, createDataSource, options, isSavin {$label && (
- {!isLoading && this.isAppEditable(app) && ( + {!isLoading && this.canUpdateApp(app) && ( - {this.isAppDeletable(app) && ( + {(this.canCreateApp(app) || this.canDeleteApp(app)) && ( this.deleteApp(app)} diff --git a/frontend/src/LoginPage/LoginPage.jsx b/frontend/src/LoginPage/LoginPage.jsx index 9ce1522a85..8ebf2ec29c 100644 --- a/frontend/src/LoginPage/LoginPage.jsx +++ b/frontend/src/LoginPage/LoginPage.jsx @@ -15,6 +15,7 @@ class LoginPage extends React.Component { this.state = { isLoading: false, + showPassword: false, }; } @@ -22,6 +23,10 @@ class LoginPage extends React.Component { this.setState({ [event.target.name]: event.target.value }); }; + handleOnCheck = () => { + this.setState((prev) => ({ showPassword: !prev.showPassword })); + }; + authUser = (e) => { e.preventDefault(); @@ -85,7 +90,7 @@ class LoginPage extends React.Component { +
+ + +
@@ -383,7 +412,7 @@ class ManageGroupPermissionResources extends React.Component { appsInGroup.map((app) => (
{app.name} +
+ + {/* Users Tab */}
@@ -469,7 +500,7 @@ class ManageGroupPermissionResources extends React.Component {
@@ -508,6 +539,77 @@ class ManageGroupPermissionResources extends React.Component {
+ + {/* Permissions Tab */} +
+
+
+ + + + + + + + + + {isLoadingGroup ? ( + + + + + + ) : ( + + + + + + )} + +
ResourcePermissions
+
+
+
+
+
+
+
+
Apps +
+ + +
+
+
+
+
diff --git a/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx b/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx index eddad5a7fd..878f94df73 100644 --- a/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx +++ b/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx @@ -106,13 +106,12 @@ class ManageOrgUsers extends React.Component { if (this.handleValidation()) { let fields = {}; - Object.keys(fields).forEach((key) => { + Object.keys(this.state.fields).map((key) => { fields[key] = ''; }); this.setState({ creatingUser: true, - fields: fields, }); organizationUserService @@ -125,10 +124,11 @@ class ManageOrgUsers extends React.Component { .then(() => { toast.success('User has been created', { hideProgressBar: true, position: 'top-center' }); this.fetchUsers(); - this.setState({ creatingUser: false, showNewUserForm: false }); + this.setState({ creatingUser: false, showNewUserForm: false, fields: fields }); }) .catch(({ error }) => { toast.error(error, { hideProgressBar: true, position: 'top-center' }); + this.setState({ creatingUser: false }); }); } else { this.setState({ creatingUser: false, showNewUserForm: true }); @@ -259,7 +259,7 @@ class ManageOrgUsers extends React.Component { {isLoading ? (
@@ -299,7 +299,7 @@ class ManageOrgUsers extends React.Component {
- + {user.email}