Release: Appbuilder S1 (#10081)

* fix : color for all new columns

* revert

* Fix: selection of copy of selected component for ease (#9818)

* fix: selection of copy of selected component for ease

* add pre selection for clonig as well

* add clone check

* fixes selection of components on empty canvas

* Fix: sizing issues in horizontal divider (#9942)

* fix horizontal divider sizing issues

* fix dark mode color in horizontal divider and remove unused class

* add custom fallback for images when not found (#9943)

* cherry pick error message log changes and fix tjdb error logs in debugger (#9951)

* Fix: mouse release on canvas when properties/styles values selected (#9732)

* fix: mouse release on canvas when properties/styles values selected

* fix: event name

* fix: rest api headers empty state while creating new query (#9729)

* fix: selection issue in table row while editing  (#9944)

* allow selection in table cell

* update classname for selection

* display date picker date as text instead of input in read only mode

* Add new revamped multiselect widget (#9837)

* init textinput revamp

* updated styles panel

* bugfix

* updates

* fix :: accordion

* fix :: styling

* add box shadow , additional property,tooltip

* fix conditional render for styles

* feat :: fixed order of each property and styles

* feat :: styling input

* bugfix

* feat :: add option to add icon

* add option to add icon

* adding option to toggle visibility

* updated password input with new design

* chnaging component location

* bugfix

* style fixes

* fix :: added loader

* updated :: few detailing

* few bugfixes

* fix :: for form widget label

* fixes

* added option to add icon color

* including label field for password input

* fix for label

* fix

* test fix backward compatibility for height

* updates

* revert

* adding key for distinguishing older and newer widgets

* testing

* test

* test

* update

* update

* migration testing

* limit vertical resizing in textinput

* testing

* throw test

* test

* adding check for label length

* fixing edge cases

* removing resize

* backward compatibility height

* backward compatibility

* number input review fixes

* added exposed items

* fixing csa

* ui fixes

* fix height compatibility

* feat :: csa for all inputs and exposed variables

* backward compatibility fixes and validation fixes

* fixes :: textinput positioning of loader and icon

* fix :: password input

* cleanup and fixes

* fixes

* cleanup

* fixes

* review fixes

* review fixes

* typo fix

* fix padding

* review fixes styles component panel

* fix naming

* fix padding

* feat :: toggle switch revamp

* init checkbox

* fixes

* fixes

* switch fixes

* validation fix

* fixes

* cleanup

* height fix

* fix height toggle

* updates

* fix :: icons position

* updates

* cleanup

* updates events , csa

* cleanup

* backward compatibility

* clean

* backward compatibility fix

* label fixed to one line

* feat :: change validation from properties

* ui fixes

* icon name

* removed 'px' text from tooltip

* added onchange event for checkbox

* fixes placeholder

* few updates :: removing label in form

* ui in form

* fire onchange

* update :: number input validation behaviour

* testing fixes

* added side handlers

* removing unwanted fx

* disabling fx for padding field

* ordering change

* fix

* label issue + restricted side handler

* fix :: box shadow bug

* fix

* on change event doesnt propagate exposed vars correctly

* adding debounce for slider value change

* fix :: for modal ooen bug during onfocus event

* test slider

* fix :: bugs regarding state update in checbox , slider , slider bug

* update slider with radix slider

* bugfix

* update tooltip

* fix toggle switch

* fixes : inspector

* fix : checkbox label

* Remove date-fns depedency from table datepicker

* Revert Remove date-fns depedency from table datepicker

* feat : checkbox completed

* update checkbox review changes

* feat : toggle component

* feat : added new toggle component

* fix : colors

* updated review changes

* update name for old and new version

* update

* case change

* update

* update icon

* removed padding from checkbox and toggle

* fix naming

* product review and bugfixes : changes

* fix : checkbox setvalue action

* Update setvalue action in toggle

* fixed: section for legacy and new components

* add new version of dropdown

* Add same styles as other input components

* fix height issues

* Add new revamped multiselect widget

* Fix design review

* fix design issues

* Fix

* Fix merge issues

* Add menu portal target

* resolve code comments

* widget config changes

* add hover for clear icon

* fix

* Fix review comments

* Multiselect changes after dropdown merge

* exposed variables

* Delete unused components

* Multiselect fixes

* Dropdown CSS fixes and multiselect fixes

* Fix merge issues

* fix

* Add highlight text

* Change multiselect UI

* fix error message

* fix multiselect opening closing

* placeholder fix

* fix highlighting in multiselect

* fix : testing bugs

* fix : default value

* Fix merge issues

* Fix Qa bugs

* Fix QA bugs

* Fix codehinter default values

* fix fireEvent on focus

* Fixes

* Provide minwidth to dropdown and multiselect

* Fix search input value not getting updated

---------

Co-authored-by: stepinfwd <stepinfwd@gmail.com>
Co-authored-by: Johnson Cherian <johnsonc.dev@gmail.com>

* Fix: remove mandatory key from password input (#9786)

* Remove date-fns depedency from table datepicker

* Revert Remove date-fns depedency from table datepicker

* remove mandatory key from password input

---------

Co-authored-by: Nakul Nagargade <nakul@tooljet.com>
Co-authored-by: Johnson Cherian <johnsonc.dev@gmail.com>

* feat : Query manager separated to different tabs (#9823)

* change toggle for query manager and revamp preview

* feat : query manger body revamp

* updates

* fix : tranformation switch

* preview integration

* loader safari changes and overflow fix

* fix

* fix : settings tab QM

* revert few changes to fix datasources page

* revert header options change

* zindex fix for query-pane

* fix : events ui

* fix :events widget manager

* code optimised for this file

* QM header fixes

* dark mode changes

* fix : info icon

* open preview drawer on run query

* fix : query manager query section icons update

* update color

* design feedbacks and make preview panel resizable

* update toggle for preview result & increate draggable area

* fix :review changes

* merge fixes

* Merge branch 'appbuilder-1.8' into feature/query-manager-body

* fix : codehinter in disabled state

* ui fix

* code refactor

* cleanup

* fix fontsize in datasource selector popup

* fix border issue in preview container and increase draggable area

* fix : review fixes

* fix: fixed text css formatting for safari support

* Revert "code refactor"

This reverts commit 4763dd11a3.

* typo

* fix : not able to select text in preview

* fix : not able to view add params

* fix selection issue in preview

* fix : add scroll in query  manager when preview is blocking view

---------

Co-authored-by: Kartik Gupta <gupta.kartik18kg@gmail.com>

* Fixes: select all click behaviour on label (#10108)

* fixes: select all click behaviour on label

* fix: legacy component names

* fix: selecto issue (#10107)

* Fix : Prevent component autofill (#10040)

* fix : prevent other component from autofilling data when password is filled from browser suggestions

* optimise

* feat: skip onDragStop execution if drag event is empty (#10118)

* feat: skip onDragStop execution if drag event is empty

* fix: added additonal logs for  error

* display query preview data in preview panel and display transformation failure stacktrace data in previewpanel as well (#10129)

* Fix while adding new rows in table components when ever entered the text and pressed enter it doubles the text (#10112)

* Integration bugfixes appbuilder 1.8 (#10109)

* fix : query maanager duplicate and preview issue

* fix : multiselect breaking on making dynamic options null

* fix : preview and query panel integration bugs

* fix : placeholder

* fix : doc links

* fix : scroll in TJDB filter section

* fix : portal for multiselect

* fixes

* fix : image column table alignment

* fix : doc link for multiselect

* fix : codehinter state being persited across components

* fix :export app qery manager items not coming in correct order

* fix: search icon position

* code refactor

---------

Co-authored-by: Johnson Cherian <johnsonc.dev@gmail.com>

* add z-index to app name info header container (#10116)

* Fix dropdown and multiselect crash on integer labels (#10128)

* cast integer labels to string

* add null check for label and provide default value for empty labels

* empty and null handle for schemas and other values

* revert changes

* Fix: dark mode on preview names (#10136)

* fix: dark mode of preview names

* fix background color of preview

* fix tjdb query import (#10134)

* fix :revert radio button name change (#10153)

* Fix: select issue on multiselect (#10137)

* remove portal from multiselect

* fix: dynamic values for options in dropdown/multiselect

* remove fx from default option

* Fix: delete on options delete in dropdown (#10192)

* fix: delete on options delete

* fix: overlapping of multiselect on parent container

* fix: outside click of multiselect

* hotfix : Table breaking on importing older apps with null value in column (#10185)

* fix : table breaking on importing older apps with null value in column

* fix : table crash , codehinter not saving values upon page change

* remove low priority wrapper from autosave

* remove logs

* added delay to autosave as callback

* fix: dropdown crash on invalid data (#10202)

* revert to previous transformation code , fix darkmode color (#10216)

* fix : doclink for dropdown (#10217)

* fix : Transformations value getting cleared / not getting saved (#10218)

* fix : transformation value not getting saved

* remove dependency

* chore: version update for release

---------

Co-authored-by: stepinfwd <stepinfwd@gmail.com>
Co-authored-by: vjaris42 <vjy239@gmail.com>
Co-authored-by: Kartik Gupta <gupta.kartik18kg@gmail.com>
Co-authored-by: Nakul Nagargade <133095394+nakulnagargade@users.noreply.github.com>
Co-authored-by: Nakul Nagargade <nakul@tooljet.com>
Co-authored-by: Akshay <akshaysasidharan93@gmail.com>
This commit is contained in:
Johnson Cherian 2024-07-01 08:46:22 +05:30 committed by GitHub
parent 17dc2cc9a3
commit 3169d38d63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
118 changed files with 4345 additions and 852 deletions

View file

@ -1 +1 @@
2.63.0
2.64.0

View file

@ -1 +1 @@
2.63.0
2.64.0

View file

@ -0,0 +1,31 @@
import React from 'react';
const DropdownV2 = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 49 48' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill={fill}
fillRule="evenodd"
d="M8.271 12.575a5.382 5.382 0 00-5.382 5.382v12.086a5.383 5.383 0 005.382 5.382h33.236a5.382 5.382 0 005.382-5.382V17.957a5.382 5.382 0 00-5.383-5.382H8.272z"
clipRule="evenodd"
></path>
<path
fill="#3E63DD"
d="M41.506 35.425a5.383 5.383 0 005.382-5.382V17.957a5.382 5.382 0 00-5.382-5.382H24.888v22.85h16.618z"
></path>
<path
fill={fill}
fillRule="evenodd"
d="M30.741 21.823a1.574 1.574 0 011.455-.971h6.296a1.574 1.574 0 011.113 2.687l-3.148 3.148a1.574 1.574 0 01-2.226 0l-3.148-3.148a1.574 1.574 0 01-.342-1.716z"
clipRule="evenodd"
></path>
</svg>
);
export default DropdownV2;

View file

@ -16,6 +16,7 @@ import Divider from './divider.jsx';
import DividerHorizondal from './dividerhorizontal.jsx';
import Downstatistics from './downstatistics.jsx';
import Dropdown from './dropdown.jsx';
import DropdownV2 from './dropdownV2.jsx';
import Filepicker from './filepicker.jsx';
import Form from './form.jsx';
import Frame from './frame.jsx';
@ -31,6 +32,7 @@ import Listview from './listview.jsx';
import Map from './map.jsx';
import Modal from './modal.jsx';
import Multiselect from './multiselect.jsx';
import MultiselectV2 from './multiselectV2.jsx';
import Numberinput from './numberinput.jsx';
import Pagination from './pagination.jsx';
import Passwordinput from './passwordinput.jsx';
@ -54,7 +56,6 @@ import Timeline from './timeline.jsx';
import Timer from './timer.jsx';
import Toggleswitch from './toggleswitch.jsx';
import ToggleSwitchV2 from './toggleswitchV2.jsx';
import Treeselect from './treeselect.jsx';
import Upstatistics from './upstatistics.jsx';
import Verticaldivider from './verticaldivider.jsx';
@ -95,6 +96,8 @@ const WidgetIcon = (props) => {
return <Downstatistics {...props} />;
case 'dropdown':
return <Dropdown {...props} />;
case 'dropdownV2':
return <DropdownV2 {...props} />;
case 'filepicker':
return <Filepicker {...props} />;
case 'form':
@ -125,6 +128,8 @@ const WidgetIcon = (props) => {
return <Modal {...props} />;
case 'multiselect':
return <Multiselect {...props} />;
case 'multiselectV2':
return <MultiselectV2 {...props} />;
case 'numberinput':
return <Numberinput {...props} />;
case 'pagination':
@ -135,7 +140,7 @@ const WidgetIcon = (props) => {
return <Pdf {...props} />;
case 'qrscanner':
return <Qrscanner {...props} />;
case 'radio-button':
case 'radiobutton':
return <RadioButton {...props} />;
case 'rangeslider':
return <Rangeslider {...props} />;

View file

@ -0,0 +1,27 @@
import React from 'react';
const Multiselect = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 49 48' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill={fill}
fillRule="evenodd"
d="M13.889 8.8a4.714 4.714 0 014.714-4.715h23.571A4.714 4.714 0 0146.89 8.8v8.115a4.714 4.714 0 01-4.715 4.714H18.603a4.714 4.714 0 01-4.714-4.714V8.799zm0 22.286a4.714 4.714 0 014.714-4.714h23.571a4.714 4.714 0 014.715 4.714V39.2a4.714 4.714 0 01-4.715 4.715H18.603a4.714 4.714 0 01-4.714-4.715v-8.114z"
clipRule="evenodd"
></path>
<path
fill="#3E63DD"
fillRule="evenodd"
d="M6.032 4.085a3.143 3.143 0 100 6.286 3.143 3.143 0 000-6.286zm0 22.287a3.143 3.143 0 100 6.286 3.143 3.143 0 000-6.286z"
clipRule="evenodd"
></path>
</svg>
);
export default Multiselect;

View file

@ -0,0 +1,5 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.3111 2H4.68889C3.47778 2 2.5 2.97778 2.5 4.18889V19.8111C2.5 21.0222 3.47778 22 4.68889 22H13.4667C13.1333 21.4111 13.1 20.6889 13.3556 20.0778H4.68889C4.54444 20.0778 4.42222 19.9556 4.42222 19.8111V13.7889L8.65556 9.55556C8.95556 9.25556 9.43333 9.25556 9.73333 9.55556L13.6778 13.5C13.7222 13.4333 13.7667 13.3778 13.8222 13.3222C14.6889 12.4778 16.0667 12.4889 16.9111 13.3222L18.4 14.8111L19.8778 13.3222C20.6 12.6333 21.6778 12.5111 22.5 12.9889V4.18889C22.5 2.97778 21.5111 2 20.3111 2ZM15.9556 11.2333C14.4667 11.2333 13.2667 10.0222 13.2667 8.53333C13.2667 7.04444 14.4667 5.84444 15.9556 5.84444C17.4444 5.84444 18.6556 7.04444 18.6556 8.53333C18.6556 10.0222 17.4444 11.2333 15.9556 11.2333Z" fill="#ACB2B9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.0778 20.2889C22.2556 20.6556 22.1778 21.1 21.8778 21.4C21.5778 21.7 21.1333 21.7556 20.7556 21.6C20.6556 21.5444 20.5556 21.4889 20.4778 21.4L19 19.9111L18.3889 19.3L16.2889 21.4C16.0889 21.5889 15.8444 21.6889 15.5889 21.6889C15.3333 21.6889 15.0889 21.5889 14.8889 21.4C14.5 21.0111 14.5 20.3889 14.8889 20L16.9778 17.9111L14.8889 15.8222C14.5556 15.4889 14.5111 14.9667 14.7556 14.5778C14.8 14.5111 14.8333 14.4667 14.8889 14.4111C15.2778 14.0333 15.9 14.0333 16.2778 14.4111L18.3778 16.5111L20.4667 14.4111C20.8556 14.0333 21.4778 14.0333 21.8556 14.4111C22.2333 14.7889 22.2444 15.4222 21.8556 15.8111L19.7667 17.9L21.8556 19.9889C21.9444 20.0778 22.0111 20.1889 22.0556 20.2889H22.0778Z" fill="#ACB2B9"/>
<path d="M22.0778 20.2889C22.2556 20.6556 22.1778 21.1 21.8778 21.4C21.5778 21.7 21.1333 21.7556 20.7556 21.6C20.6556 21.5444 20.5556 21.4889 20.4778 21.4L19 19.9111L18.3889 19.3L16.2889 21.4C16.0889 21.5889 15.8444 21.6889 15.5889 21.6889C15.3333 21.6889 15.0889 21.5889 14.8889 21.4C14.5 21.0111 14.5 20.3889 14.8889 20L16.9778 17.9111L14.8889 15.8222C14.5556 15.4889 14.5111 14.9667 14.7556 14.5778C14.8 14.5111 14.8333 14.4667 14.8889 14.4111C15.2778 14.0333 15.9 14.0333 16.2778 14.4111L18.3778 16.5111L20.4667 14.4111C20.8556 14.0333 21.4778 14.0333 21.8556 14.4111C22.2333 14.7889 22.2444 15.4222 21.8556 15.8111L19.7667 17.9L21.8556 19.9889C21.9444 20.0778 22.0111 20.1889 22.0556 20.2889H22.0778Z" fill="#ACB2B9"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -174,9 +174,9 @@
"goToAllDatasources": "Go to all Datasource",
"send": "Send"
},
"runQueryOnApplicationLoad": "Run this query on application load?",
"confirmBeforeQueryRun": "Request confirmation before running query?",
"notificationOnSuccess": "Show notification on success?",
"runQueryOnApplicationLoad": "Run this query on application load",
"confirmBeforeQueryRun": "Request confirmation before running query",
"notificationOnSuccess": "Show notification on success",
"successMessage": "Success Message",
"queryRanSuccessfully": "Query ran successfully",
"notificationDuration": "Notification duration (s)",
@ -207,6 +207,7 @@
"pageIndex": "Page index",
"component": "Component",
"addHandler": "New event handler",
"addNewEvent": "Add new event",
"addEventHandler": "+ Add event handler",
"emptyMessage": "This {{componentName}} doesn't have any event handlers",
"page": "Page"
@ -286,9 +287,9 @@
"createUpdateDelete": "Create/Update/Delete",
"folder": "Folder"
},
"groupOptions":{
"deleteGroup":"Delete Group",
"duplicateGroup":"Duplicate Group"
"groupOptions": {
"deleteGroup": "Delete Group",
"duplicateGroup": "Duplicate Group"
}
},
"manageSSO": {
@ -496,7 +497,7 @@
"properties": "Properties",
"events": "Events",
"layout": "Layout",
"devices":"Devices",
"devices": "Devices",
"styles": "Styles",
"general": "General",
"validation": "Validation",
@ -728,10 +729,10 @@
"addColumn": "Add column",
"addNewColumn": "Add new column",
"noActionMessage": "This table doesn't have any action buttons",
"horizontalAlignment":"Horizontal alignment",
"textAlignment":"Text alignment",
"deciamalPlaces":"Decimal Places",
"imageFit":"Image fit"
"horizontalAlignment": "Horizontal alignment",
"textAlignment": "Text alignment",
"deciamalPlaces": "Decimal Places",
"imageFit": "Image fit"
},
"Button": {
"displayName": "Button",
@ -944,7 +945,7 @@
"typeComment": "Type your comment here"
},
"Settings": {
"text": "Settings",
"text": "Triggers",
"tip": "Global Settings",
"hideHeader": "Hide header for launched apps",
"maintenanceMode": "Maintenance mode",

View file

@ -18,6 +18,8 @@ const shouldAddBoxShadowAndVisibility = [
'Checkbox',
'Button',
'ToggleSwitchV2',
'DropdownV2',
'MultiselectV2',
];
const BoxUI = (props) => {

View file

@ -3,7 +3,7 @@ import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
import React from 'react';
import cx from 'classnames';
const Switch = ({ value, onChange, meta, paramName, component }) => {
const Switch = ({ value, onChange, cyLabel, meta, paramName, isIcon, component }) => {
const options = meta?.options;
const defaultValue =
paramName == 'defaultValue' && (component == 'Checkbox' || component == 'ToggleSwitchV2') ? `{{${value}}}` : value;

View file

@ -9,12 +9,12 @@ export const Toggle = ({ value, onChange, cyLabel, meta }) => {
className="form-check form-switch mb-0 d-flex justify-content-end"
style={{ marginBottom: '0px', paddingLeft: '28px' }}
>
{meta.toggleLabel && (
{meta?.toggleLabel && (
<span
className="font-weight-400 font-size-12 d-flex align-items-center color-slate12"
style={{ marginRight: '78px' }}
>
{meta.toggleLabel}
{meta?.toggleLabel}
</span>
)}
<input

View file

@ -43,6 +43,8 @@ const MultiLineCodeEditor = (props) => {
showPreview,
paramLabel = '',
delayOnChange = true, // Added this prop to immediately update the onBlurUpdate callback
readOnly = false,
editable = true,
} = props;
const context = useContext(CodeHinterContext);
@ -237,6 +239,8 @@ const MultiLineCodeEditor = (props) => {
}}
className={`codehinter-multi-line-input`}
indentWithTab={true}
readOnly={readOnly}
editable={editable} //for transformations in query manager
/>
</div>
{showPreview && (

View file

@ -330,6 +330,10 @@ const DynamicEditorBridge = (props) => {
const { t } = useTranslation();
const [_, error, value] = type === 'fxEditor' ? resolveReferences(initialValue) : [];
useEffect(() => {
setForceCodeBox(fxActive);
}, [component]);
const fxClass = isEventManagerParam ? 'justify-content-start' : 'justify-content-end';
return (
<div className={cx({ 'codeShow-active': codeShow }, 'wrapper-div-code-editor')}>

View file

@ -102,7 +102,6 @@ export const Checkbox = ({
setExposedVariable('isLoading', loading);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading]);
useEffect(() => {
setExposedVariable('isVisible', visibility);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -112,12 +111,10 @@ export const Checkbox = ({
setExposedVariable('isDisabled', disable);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disable]);
useEffect(() => {
setExposedVariable('isValid', isValid);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isValid]);
useEffect(() => {
setExposedVariable('setLoading', async function (loading) {
setLoading(loading);
@ -255,7 +252,6 @@ export const Checkbox = ({
)}
</>
);
const checkmarkStyle = {
position: 'absolute',
top: '1px',

View file

@ -1,14 +1,20 @@
import React from 'react';
export const Divider = function Divider({ styles, dataCy }) {
export const Divider = function Divider({ styles, dataCy, height, width, darkMode }) {
const { visibility, dividerColor, boxShadow } = styles;
const color = dividerColor ?? '#E7E8EA';
const color =
dividerColor === '' || ['#000', '#000000'].includes(dividerColor) ? (darkMode ? '#fff' : '#000') : dividerColor;
return (
<div
className="hr mt-1"
style={{ display: visibility ? '' : 'none', color: color, opacity: '1', boxShadow }}
className="row"
style={{ display: visibility ? 'flex' : 'none', padding: '0 8px', width, height, alignItems: 'center' }}
data-cy={dataCy}
></div>
>
<div
className="col-6"
style={{ height: '1px', width, backgroundColor: color, padding: '0rem', marginLeft: '0.5rem', boxShadow }}
></div>
</div>
);
};

View file

@ -0,0 +1,89 @@
import React from 'react';
import { components } from 'react-select';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import Loader from '@/ToolJetUI/Loader/Loader';
import './dropdownV2.scss';
import { FormCheck } from 'react-bootstrap';
import cx from 'classnames';
const { MenuList } = components;
// This Menulist also used in MultiselectV2
const CustomMenuList = ({ selectProps, ...props }) => {
const {
onInputChange,
onMenuInputFocus,
showAllOption,
isSelectAllSelected,
optionsLoadingState,
darkMode,
setSelected,
setIsSelectAllSelected,
fireEvent,
inputValue,
menuId,
} = selectProps;
const handleSelectAll = (e) => {
e.target.checked && fireEvent();
if (e.target.checked) {
setSelected(props.options);
} else {
setSelected([]);
}
setIsSelectAllSelected(e.target.checked);
};
return (
<div
id={`dropdown-multiselect-widget-custom-menu-list-${menuId}`}
className={cx('dropdown-multiselect-widget-custom-menu-list', { 'theme-dark dark-theme': darkMode })}
onClick={(e) => e.stopPropagation()}
>
<div className="dropdown-multiselect-widget-search-box-wrapper">
<span>
<SolidIcon name="search01" width="14" />
</span>
<input
autoCorrect="off"
autoComplete="off"
spellCheck="false"
type="text"
value={inputValue}
onChange={(e) =>
onInputChange(e.currentTarget.value, {
action: 'input-change',
})
}
onMouseDown={(e) => {
e.stopPropagation();
e.target.focus();
}}
onTouchEnd={(e) => {
e.stopPropagation();
e.target.focus();
}}
onFocus={onMenuInputFocus}
placeholder="Search"
className="dropdown-multiselect-widget-search-box"
/>
</div>
{showAllOption && !optionsLoadingState && (
<label htmlFor="select-all-checkbox" className="multiselect-custom-menulist-select-all">
<FormCheck id="select-all-checkbox" checked={isSelectAllSelected} onChange={handleSelectAll} />
<span style={{ marginLeft: '4px' }}>Select all</span>
</label>
)}
<MenuList {...props} selectProps={selectProps}>
{optionsLoadingState ? (
<div class="text-center py-4" style={{ minHeight: '188px' }}>
<Loader style={{ zIndex: 3, position: 'absolute' }} width="36" />
</div>
) : (
props.children
)}
</MenuList>
</div>
);
};
export default CustomMenuList;

View file

@ -0,0 +1,24 @@
import React from 'react';
import { components } from 'react-select';
import CheckMark from '@/_ui/Icon/bulkIcons/CheckMark';
import './dropdownV2.scss';
import { highlightText } from './utils';
const CustomOption = (props) => {
return (
<components.Option {...props}>
<div className="cursor-pointer">
{props.isSelected && (
<span style={{ maxHeight: '20px', marginRight: '8px', marginLeft: '-28px' }}>
<CheckMark width={'20'} fill={'var(--primary-brand)'} />
</span>
)}
<span style={{ color: props.isDisabled ? '#889096' : 'unset', wordBreak: 'break-all' }}>
{highlightText(props.label?.toString(), props.selectProps.inputValue)}
</span>
</div>
</components.Option>
);
};
export default CustomOption;

View file

@ -0,0 +1,48 @@
import React from 'react';
import { components } from 'react-select';
import * as Icons from '@tabler/icons-react';
import './dropdownV2.scss';
const { ValueContainer, SingleValue, Placeholder } = components;
const CustomValueContainer = ({ children, ...props }) => {
const selectProps = props.selectProps;
// eslint-disable-next-line import/namespace
const IconElement = Icons[selectProps?.icon] == undefined ? Icons['IconHome2'] : Icons[selectProps?.icon];
return (
<ValueContainer {...props}>
<div className="d-inline-flex">
{selectProps?.doShowIcon && (
<div>
<IconElement
style={{
width: '16px',
height: '16px',
color: selectProps?.iconColor,
marginRight: '2px',
marginBottom: '2px',
}}
/>
</div>
)}
<span className="d-flex" {...props}>
{React.Children.map(children, (child) => {
return child ? (
child
) : props.hasValue ? (
<SingleValue {...props} {...selectProps}>
{selectProps?.getOptionLabel(props?.getValue()[0])}
</SingleValue>
) : (
<Placeholder {...props} key="placeholder" {...selectProps} data={props.getValue()}>
{selectProps.placeholder}
</Placeholder>
);
})}
</span>
</div>
</ValueContainer>
);
};
export default CustomValueContainer;

View file

@ -0,0 +1,455 @@
import { resolveReferences } from '@/_helpers/utils';
import { useCurrentState } from '@/_stores/currentStateStore';
import React, { useState, useEffect, useMemo, useRef } from 'react';
import Select, { components } from 'react-select';
import ClearIndicatorIcon from '@/_ui/Icon/bulkIcons/ClearIndicator';
import TriangleDownArrow from '@/_ui/Icon/bulkIcons/TriangleDownArrow';
import TriangleUpArrow from '@/_ui/Icon/bulkIcons/TriangleUpArrow';
import { useEditorStore } from '@/_stores/editorStore';
import Loader from '@/ToolJetUI/Loader/Loader';
import { has, isObject, pick } from 'lodash';
const tinycolor = require('tinycolor2');
import './dropdownV2.scss';
import CustomValueContainer from './CustomValueContainer';
import CustomMenuList from './CustomMenuList';
import CustomOption from './CustomOption';
import Label from '@/_ui/Label';
import cx from 'classnames';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor } from './utils';
const { DropdownIndicator, ClearIndicator } = components;
const INDICATOR_CONTAINER_WIDTH = 60;
const ICON_WIDTH = 18; // includes flex gap 2px
export const CustomDropdownIndicator = (props) => {
const {
selectProps: { menuIsOpen },
} = props;
return (
<DropdownIndicator {...props}>
{menuIsOpen ? (
<TriangleUpArrow width={'18'} className="cursor-pointer" fill={'var(--borders-strong)'} />
) : (
<TriangleDownArrow width={'18'} className="cursor-pointer" fill={'var(--borders-strong)'} />
)}
</DropdownIndicator>
);
};
export const CustomClearIndicator = (props) => {
return (
<ClearIndicator {...props}>
<ClearIndicatorIcon width={'18'} fill={'var(--borders-strong)'} className="cursor-pointer" />
</ClearIndicator>
);
};
export const DropdownV2 = ({
height,
validate,
properties,
styles,
setExposedVariable,
setExposedVariables,
fireEvent,
darkMode,
onComponentClick,
id,
component,
exposedVariables,
dataCy,
}) => {
const {
label,
value,
advanced,
schema,
placeholder,
loadingState: dropdownLoadingState,
disabledState,
options,
} = properties;
const {
selectedTextColor,
fieldBorderRadius,
justifyContent,
boxShadow,
labelColor,
alignment,
direction,
fieldBorderColor,
fieldBackgroundColor,
labelWidth,
icon,
iconVisibility,
errTextColor,
auto: labelAutoWidth,
iconColor,
accentColor,
padding,
} = styles;
const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value));
const { value: exposedValue } = exposedVariables;
const currentState = useCurrentState();
const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState);
const validationData = validate(currentValue);
const { isValid, validationError } = validationData;
const ref = React.useRef(null);
const [visibility, setVisibility] = useState(properties.visibility);
const [isDropdownLoading, setIsDropdownLoading] = useState(dropdownLoadingState);
const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState);
const [isFocused, setIsFocused] = useState(false);
const [searchInputValue, setSearchInputValue] = useState('');
const _height = padding === 'default' ? `${height}px` : `${height + 4}px`;
const labelRef = useRef();
function findDefaultItem(schema) {
let _schema = schema;
if (!Array.isArray(schema)) {
_schema = [];
}
const foundItem = _schema?.find((item) => item?.default === true);
return !hasVisibleFalse(foundItem?.value) ? foundItem?.value : undefined;
}
const selectOptions = useMemo(() => {
let _options = advanced ? schema : options;
if (Array.isArray(_options)) {
let _selectOptions = _options
.filter((data) => data?.visible?.value)
.map((value) => ({
...value,
isDisabled: value?.disable?.value,
}));
return _selectOptions;
} else {
return [];
}
}, [advanced, schema, options]);
function selectOption(value) {
const val = selectOptions.filter((option) => !option.isDisabled)?.find((option) => option.value === value);
if (val) {
setCurrentValue(value);
fireEvent('onSelect');
}
}
function hasVisibleFalse(value) {
for (let i = 0; i < schema?.length; i++) {
if (schema[i].value === value && schema[i].visible === false) {
return true;
}
}
return false;
}
const onSearchTextChange = (searchText, actionProps) => {
if (actionProps.action === 'input-change') {
setSearchInputValue(searchText);
fireEvent('onSearchTextChanged');
}
};
const handleOutsideClick = (e) => {
let menu = ref.current.querySelector('.select__menu');
if (!ref.current.contains(e.target) || !menu || !menu.contains(e.target)) {
setIsFocused(false);
setSearchInputValue('');
}
};
useEffect(() => {
if (advanced) {
setCurrentValue(findDefaultItem(schema));
} else setCurrentValue(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, value, JSON.stringify(schema)]);
useEffect(() => {
document.addEventListener('mousedown', handleOutsideClick);
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
};
}, []);
useEffect(() => {
if (visibility !== properties.visibility) setVisibility(properties.visibility);
if (isDropdownLoading !== dropdownLoadingState) setIsDropdownLoading(dropdownLoadingState);
if (isDropdownDisabled !== disabledState) setIsDropdownDisabled(disabledState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.visibility, dropdownLoadingState, disabledState]);
// Exposed variables
useEffect(() => {
if (exposedValue !== currentValue) {
const _selectedOption = selectOptions.find((option) => option.value === currentValue);
setExposedVariable('selectedOption', pick(_selectedOption, ['label', 'value']));
}
const _options = selectOptions?.map(({ label, value }) => ({ label, value }));
setExposedVariable('options', _options);
setExposedVariable('selectOption', async function (value) {
let _value = value;
if (isObject(value) && has(value, 'value')) _value = value?.value;
selectOption(_value);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentValue, JSON.stringify(selectOptions)]);
useEffect(() => {
setExposedVariable('label', label);
setExposedVariable('searchText', searchInputValue);
setExposedVariable('isValid', isValid);
setExposedVariable('isVisible', properties.visibility);
setExposedVariable('isLoading', dropdownLoadingState);
setExposedVariable('isDisabled', disabledState);
setExposedVariable('isMandatory', isMandatory);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.visibility, dropdownLoadingState, disabledState, isMandatory, label, searchInputValue, isValid]);
useEffect(() => {
const exposedVariables = {
clear: async function () {
setCurrentValue(null);
},
setVisibility: async function (value) {
setVisibility(value);
},
setLoading: async function (value) {
setIsDropdownLoading(value);
},
setDisable: async function (value) {
setIsDropdownDisabled(value);
},
};
setExposedVariables(exposedVariables);
}, []);
const customStyles = {
container: (base) => ({
...base,
width: '100%',
minWidth: '72px',
}),
control: (provided, state) => {
return {
...provided,
minHeight: _height,
height: _height,
boxShadow: state.isFocused ? boxShadow : boxShadow,
borderRadius: Number.parseFloat(fieldBorderRadius),
borderColor: getInputBorderColor({
isFocused: state.isFocused,
isValid,
fieldBorderColor,
accentColor,
isLoading: isDropdownLoading,
isDisabled: isDropdownDisabled,
}),
backgroundColor: getInputBackgroundColor({
fieldBackgroundColor,
darkMode,
isLoading: isDropdownLoading,
isDisabled: isDropdownDisabled,
}),
'&:hover': {
borderColor: state.isFocused
? getInputFocusedColor({ accentColor })
: tinycolor(fieldBorderColor).darken(24).toString(),
},
};
},
valueContainer: (provided, _state) => ({
...provided,
height: _height,
padding: '0 10px',
justifyContent,
display: 'flex',
gap: '0.13rem',
}),
singleValue: (provided, _state) => ({
...provided,
color:
selectedTextColor !== '#1B1F24'
? selectedTextColor
: isDropdownDisabled || isDropdownLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
maxWidth:
ref?.current?.offsetWidth -
(iconVisibility ? INDICATOR_CONTAINER_WIDTH + ICON_WIDTH : INDICATOR_CONTAINER_WIDTH),
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
input: (provided, _state) => ({
...provided,
color: darkMode ? 'white' : 'black',
margin: '0px',
}),
indicatorSeparator: (_state) => ({
display: 'none',
}),
indicatorsContainer: (provided, _state) => ({
...provided,
height: _height,
marginRight: '10px',
}),
clearIndicator: (provided, _state) => ({
...provided,
padding: '2px',
'&:hover': {
padding: '2px',
backgroundColor: 'var(--interactive-overlays-fill-hover)',
borderRadius: '6px',
},
}),
dropdownIndicator: (provided, _state) => ({
...provided,
padding: '0px',
}),
option: (provided) => ({
...provided,
backgroundColor: 'var(--surfaces-surface-01)',
color:
selectedTextColor !== '#1B1F24'
? selectedTextColor
: isDropdownDisabled || isDropdownLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
padding: '8px 6px 8px 38px',
'&:hover': {
backgroundColor: 'var(--interactive-overlays-fill-hover)',
borderRadius: '8px',
},
display: 'flex',
cursor: 'pointer',
}),
menuList: (provided) => ({
...provided,
padding: '8px',
borderRadius: '8px',
// this is needed otherwise :active state doesn't look nice, gap is required
display: 'flex',
flexDirection: 'column',
gap: '4px !important',
overflowY: 'auto',
backgroundColor: 'var(--surfaces-surface-01)',
}),
menu: (provided) => ({
...provided,
borderRadius: '8px',
boxShadow: 'unset',
margin: 0,
}),
};
const _width = (labelWidth / 100) * 70; // Max width which label can go is 70% for better UX calculate width based on this value
return (
<>
<div
data-cy={`label-${String(component.name).toLowerCase()} `}
className={cx('dropdown-widget', 'd-flex', {
[alignment === 'top' &&
((labelWidth != 0 && label?.length != 0) ||
(labelAutoWidth && labelWidth == 0 && label && label?.length != 0))
? 'flex-column'
: 'align-items-center']: true,
'flex-row-reverse': direction === 'right' && alignment === 'side',
'text-right': direction === 'right' && alignment === 'top',
invisible: !visibility,
visibility: visibility,
})}
style={{
position: 'relative',
whiteSpace: 'nowrap',
width: '100%',
}}
onMouseDown={(event) => {
onComponentClick(id, component, event);
// This following line is needed because sometimes after clicking on canvas then also dropdown remains selected
useEditorStore.getState().actions.setHoveredComponent('');
}}
>
<Label
label={label}
width={labelWidth}
labelRef={labelRef}
darkMode={darkMode}
color={labelColor}
defaultAlignment={alignment}
direction={direction}
auto={labelAutoWidth}
isMandatory={isMandatory}
_width={_width}
/>
<div className="w-100 px-0 h-100" ref={ref}>
<Select
isDisabled={isDropdownDisabled}
value={selectOptions.filter((option) => option.value === currentValue)[0] ?? null}
onChange={(selectedOption, actionProps) => {
if (actionProps.action === 'clear') {
setCurrentValue(null);
}
if (actionProps.action === 'select-option') {
setCurrentValue(selectedOption.value);
fireEvent('onSelect');
}
setIsFocused(false);
}}
options={selectOptions}
styles={customStyles}
isLoading={isDropdownLoading}
onInputChange={onSearchTextChange}
inputValue={searchInputValue}
onFocus={() => {
fireEvent('onFocus');
}}
onMenuInputFocus={() => setIsFocused(true)}
onBlur={() => {
fireEvent('onBlur');
}}
placeholder={placeholder}
menuPortalTarget={document.body}
components={{
MenuList: CustomMenuList,
ValueContainer: CustomValueContainer,
Option: CustomOption,
LoadingIndicator: () => <Loader style={{ right: '11px', zIndex: 3, position: 'absolute' }} width="16" />,
DropdownIndicator: isDropdownLoading ? () => null : CustomDropdownIndicator,
ClearIndicator: CustomClearIndicator,
}}
isClearable
{...{
menuIsOpen: isFocused || undefined,
isFocused: isFocused || undefined,
}}
// select props
icon={icon}
doShowIcon={iconVisibility}
iconColor={iconColor}
isSearchable={false}
darkMode={darkMode}
optionsLoadingState={properties.optionsLoadingState}
menuPlacement="auto"
/>
</div>
</div>
<div
className={`${isValid ? '' : visibility ? 'd-flex' : 'none'}`}
style={{
color: errTextColor,
justifyContent: direction === 'right' ? 'flex-start' : 'flex-end',
fontSize: '11px',
fontWeight: '400',
lineHeight: '16px',
}}
>
{!isValid && validationError}
</div>
</>
);
};

View file

@ -0,0 +1,61 @@
import React from 'react';
export const getInputFocusedColor = ({ accentColor }) => {
if (accentColor !== '#4368E3') {
return accentColor;
}
return 'var(--primary-accent-strong)';
};
export const getInputBorderColor = ({ isValid, isFocused, fieldBorderColor, accentColor, isLoading, isDisabled }) => {
if (!isValid) {
return 'var(--status-error-strong)';
}
if (isFocused) {
return getInputFocusedColor({ accentColor });
}
if (fieldBorderColor !== '#CCD1D5') {
return fieldBorderColor;
}
if (isLoading || isDisabled) {
return '1px solid var(--borders-disabled-on-white)';
}
return 'var(--borders-default)';
};
export const getInputBackgroundColor = ({ fieldBackgroundColor, darkMode, isLoading, isDisabled }) => {
if (!['#ffffff', '#ffffffff', '#fff'].includes(fieldBackgroundColor)) {
return fieldBackgroundColor;
}
if (isLoading || isDisabled) {
if (darkMode) {
return 'var(--surfaces-app-bg-default)';
} else {
return 'var(--surfaces-surface-03)';
}
}
return 'var(--surfaces-surface-01)';
};
export const highlightText = (text = '', highlight) => {
const parts = text?.split(new RegExp(`(${highlight})`, 'gi'));
return (
<span>
{parts.map((part, index) =>
part?.toLowerCase() === highlight?.toLowerCase() ? (
<span key={index} style={{ backgroundColor: '#E3B643' }}>
{part}
</span>
) : (
part
)
)}
</span>
);
};

View file

@ -0,0 +1,21 @@
import React from 'react';
import { components } from 'react-select';
const { Option } = components;
import { FormCheck } from 'react-bootstrap';
import './multiselectV2.scss';
import { highlightText } from '../DropdownV2/utils';
const CustomOption = (props) => {
return (
<Option {...props}>
<div className="d-flex">
<FormCheck checked={props.isSelected} disabled={props?.isDisabled} />
<span style={{ marginLeft: '5px' }}>
{highlightText(props.label?.toString(), props.selectProps.inputValue)}
</span>
</div>
</Option>
);
};
export default CustomOption;

View file

@ -0,0 +1,48 @@
import React from 'react';
import { components } from 'react-select';
import * as Icons from '@tabler/icons-react';
const { ValueContainer, Placeholder } = components;
import './multiselectV2.scss';
const CustomValueContainer = ({ ...props }) => {
const selectProps = props.selectProps;
const values = Array.isArray(selectProps?.value) && selectProps?.value?.map((option) => option.label);
const isAllOptionsSelected = selectProps?.value.length === selectProps.options.length;
const valueContainerWidth = selectProps?.containerRef?.current?.offsetWidth;
// eslint-disable-next-line import/namespace
const IconElement = Icons[selectProps?.icon] == undefined ? Icons['IconHome2'] : Icons[selectProps?.icon];
return (
<ValueContainer {...props}>
<div className="w-full">
<span
ref={selectProps.containerRef}
className="d-flex w-full align-items-center"
style={{ marginBottom: '2px' }}
>
{selectProps?.doShowIcon && (
<IconElement
style={{
width: '16px',
height: '16px',
color: selectProps?.iconColor,
marginRight: '4px',
}}
/>
)}
{!props.hasValue ? (
<Placeholder {...props} key="placeholder" {...selectProps} data={selectProps?.visibleValues}>
{selectProps.placeholder}
</Placeholder>
) : (
<span className="text-truncate" {...props} id="options" style={{ maxWidth: valueContainerWidth }}>
{isAllOptionsSelected ? 'All items are selected.' : values.join(', ')}
</span>
)}
</span>
</div>
</ValueContainer>
);
};
export default CustomValueContainer;

View file

@ -0,0 +1,471 @@
import { resolveReferences } from '@/_helpers/utils';
import { useCurrentState } from '@/_stores/currentStateStore';
import _, { has, isEmpty, isObject } from 'lodash';
import React, { useState, useEffect, useMemo } from 'react';
import Select from 'react-select';
import './multiselectV2.scss';
import CustomMenuList from '../DropdownV2/CustomMenuList';
import { useEditorStore } from '@/_stores/editorStore';
import CustomOption from './CustomOption';
import CustomValueContainer from './CustomValueContainer';
import Loader from '@/ToolJetUI/Loader/Loader';
import cx from 'classnames';
import Label from '@/_ui/Label';
const tinycolor = require('tinycolor2');
import { CustomDropdownIndicator, CustomClearIndicator } from '../DropdownV2/DropdownV2';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor } from '../DropdownV2/utils';
export const MultiselectV2 = ({
id,
component,
height,
properties,
styles,
setExposedVariable,
setExposedVariables,
onComponentClick,
darkMode,
fireEvent,
validate,
width,
}) => {
let {
label,
values,
options,
showAllOption,
disabledState,
advanced,
schema,
placeholder,
loadingState: multiSelectLoadingState,
optionsLoadingState,
} = properties;
const {
selectedTextColor,
fieldBorderRadius,
boxShadow,
labelColor,
alignment,
direction,
fieldBorderColor,
fieldBackgroundColor,
labelWidth,
auto,
icon,
iconVisibility,
errTextColor,
iconColor,
padding,
accentColor,
} = styles;
const [selected, setSelected] = useState([]);
const currentState = useCurrentState();
const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState);
const multiselectRef = React.useRef(null);
const labelRef = React.useRef(null);
const validationData = validate(selected?.length ? selected?.map((option) => option.value) : null);
const { isValid, validationError } = validationData;
const valueContainerRef = React.useRef(null);
const [visibility, setVisibility] = useState(properties.visibility);
const [isMultiSelectLoading, setIsMultiSelectLoading] = useState(multiSelectLoadingState);
const [isMultiSelectDisabled, setIsMultiSelectDisabled] = useState(disabledState);
const [isSelectAllSelected, setIsSelectAllSelected] = useState(false);
const [searchInputValue, setSearchInputValue] = useState('');
const _height = padding === 'default' ? `${height}px` : `${height + 4}px`;
const [isMultiselectOpen, setIsMultiselectOpen] = useState(false);
useEffect(() => {
if (visibility !== properties.visibility) setVisibility(properties.visibility);
if (isMultiSelectLoading !== multiSelectLoadingState) setIsMultiSelectLoading(multiSelectLoadingState);
if (isMultiSelectDisabled !== disabledState) setIsMultiSelectDisabled(disabledState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.visibility, multiSelectLoadingState, disabledState]);
const selectOptions = useMemo(() => {
const _options = advanced ? schema : options;
let _selectOptions = Array.isArray(_options)
? _options
.filter((data) => data?.visible?.value)
.map((value) => ({
...value,
isDisabled: value?.disable?.value,
}))
: [];
return _selectOptions;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, JSON.stringify(schema), JSON.stringify(options)]);
function findDefaultItem(value, isAdvanced, isDefault) {
if (isAdvanced) {
const foundItem = Array.isArray(schema) ? schema.filter((item) => item?.visible && item?.default) : [];
return foundItem;
}
if (isDefault) {
return Array.isArray(selectOptions)
? selectOptions.filter((item) => value?.find((val) => val === item.value))
: [];
} else {
return Array.isArray(selectOptions)
? selectOptions.filter((item) => selected?.find((val) => val.value === item.value))
: [];
}
}
function hasVisibleFalse(value) {
for (let i = 0; i < schema?.length; i++) {
if (schema[i].value === value && schema[i].visible === false) {
return true;
}
}
return false;
}
const onChangeHandler = (items, action) => {
setSelected(items);
if (action.action === 'select-option') {
fireEvent('onSelect');
}
};
useEffect(() => {
let foundItem = findDefaultItem(values, advanced);
setSelected(foundItem);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectOptions]);
useEffect(() => {
let foundItem = findDefaultItem(values, advanced, true);
setSelected(foundItem);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, JSON.stringify(schema), JSON.stringify(values)]);
useEffect(() => {
setExposedVariable(
'selectedOptions',
Array.isArray(selected) && selected?.map(({ label, value }) => ({ label, value }))
);
setExposedVariable(
'options',
Array.isArray(selectOptions) && selectOptions?.map(({ label, value }) => ({ label, value }))
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(selected), selectOptions]);
useEffect(() => {
setExposedVariable('label', label);
setExposedVariable('isVisible', properties.visibility);
setExposedVariable('isLoading', multiSelectLoadingState);
setExposedVariable('isDisabled', disabledState);
setExposedVariable('isMandatory', isMandatory);
setExposedVariable('isValid', isValid);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [label, properties.visibility, multiSelectLoadingState, disabledState, isMandatory, isValid]);
useEffect(() => {
const exposedVariables = {
clear: async function () {
setSelected([]);
},
setVisibility: async function (value) {
setVisibility(value);
},
setLoading: async function (value) {
setIsMultiSelectLoading(value);
},
setDisable: async function (value) {
setIsMultiSelectDisabled(value);
},
};
setExposedVariables(exposedVariables);
}, []);
useEffect(() => {
// Expose selectOption
setExposedVariable('selectOptions', async function (value) {
if (Array.isArray(value)) {
const newSelected = [...selected];
value.forEach((val) => {
// Check if array provided is a list of objects with value key
if (isObject(val) && has(val, 'value')) {
val = val.value;
}
if (
selectOptions.some((option) => option.value === val) &&
!selected.some((option) => option.value === val)
) {
const optionsToAdd = selectOptions.filter(
(option) => option.value === val && !selected.some((selectedOption) => selectedOption.value === val)
);
newSelected.push(...optionsToAdd);
}
});
setSelected(newSelected);
}
});
// Expose deselectOption
setExposedVariable('deselectOptions', async function (value) {
if (Array.isArray(value)) {
// Check if array provided is a list of objects with value key
const _value = value.map((val) => (isObject(val) && has(val, 'value') ? val.value : val));
const newSelected = selected.filter((option) => !_value.includes(option.value));
setSelected(newSelected);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectOptions, selected, setSelected]);
const onSearchTextChange = (searchText, actionProps) => {
if (actionProps.action === 'input-change') {
setSearchInputValue(searchText);
setExposedVariable('searchText', searchText);
fireEvent('onSearchTextChanged');
}
};
const handleClickOutside = (event) => {
let menu = document.getElementById(`dropdown-multiselect-widget-custom-menu-list-${id}`);
if (
multiselectRef.current &&
!multiselectRef.current.contains(event.target) &&
menu &&
!menu.contains(event.target)
) {
if (isMultiselectOpen) {
fireEvent('onBlur');
setIsMultiselectOpen(false);
setSearchInputValue('');
}
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside, { capture: true });
return () => {
document.removeEventListener('mousedown', handleClickOutside, { capture: true });
};
}, [isMultiselectOpen]);
// Handle Select all logic
useEffect(() => {
if (selectOptions?.length === selected?.length) {
setIsSelectAllSelected(true);
} else {
setIsSelectAllSelected(false);
}
}, [selectOptions, selected]);
const customStyles = {
container: (base) => ({
...base,
width: '100%',
minWidth: '72px',
}),
control: (provided, state) => {
return {
...provided,
minHeight: _height,
height: _height,
boxShadow: state.isFocused ? boxShadow : boxShadow,
borderRadius: Number.parseFloat(fieldBorderRadius),
borderColor: getInputBorderColor({
isFocused: state.isFocused || isMultiselectOpen,
isValid,
fieldBorderColor,
accentColor,
isLoading: isMultiSelectLoading,
isDisabled: isMultiSelectDisabled,
}),
backgroundColor: getInputBackgroundColor({
fieldBackgroundColor,
darkMode,
isLoading: isMultiSelectLoading,
isDisabled: isMultiSelectDisabled,
}),
'&:hover': {
borderColor:
state.isFocused || isMultiselectOpen
? getInputFocusedColor({ accentColor })
: tinycolor(fieldBorderColor).darken(24).toString(),
},
};
},
valueContainer: (provided, _state) => ({
...provided,
height: _height,
padding: '0 10px',
display: 'flex',
gap: '0.13rem',
color:
selectedTextColor !== '#1B1F24'
? selectedTextColor
: isMultiSelectLoading || isMultiSelectDisabled
? 'var(--text-disabled)'
: 'var(--text-primary)',
}),
input: (provided, _state) => ({
...provided,
color: darkMode ? 'white' : 'black',
margin: '0px',
}),
indicatorSeparator: (_state) => ({
display: 'none',
}),
indicatorsContainer: (provided, _state) => ({
...provided,
height: _height,
marginRight: '10px',
}),
clearIndicator: (provided, _state) => ({
...provided,
padding: '2px',
'&:hover': {
padding: '2px',
backgroundColor: 'var(--interactive-overlays-fill-hover)',
borderRadius: '6px',
},
}),
dropdownIndicator: (provided, _state) => ({
...provided,
padding: '0px',
}),
option: (provided, _state) => ({
...provided,
backgroundColor: 'var(--surfaces-surface-01)',
color: _state.isDisabled
? 'var(_--text-disbled)'
: selectedTextColor !== '#1B1F24'
? selectedTextColor
: isMultiSelectDisabled || isMultiSelectLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
padding: '8px 6px 8px 12px',
'&:hover': {
backgroundColor: 'var(--interactive-overlays-fill-hover)',
borderRadius: '8px',
},
cursor: 'pointer',
}),
menuList: (provided) => ({
...provided,
padding: '4px',
// this is needed otherwise :active state doesn't look nice, gap is required
display: 'flex',
flexDirection: 'column',
gap: '4px !important',
overflowY: 'auto',
backgroundColor: 'var(--surfaces-surface-01)',
}),
menu: (provided) => ({
...provided,
marginTop: '5px',
}),
};
const _width = (labelWidth / 100) * 70; // Max width which label can go is 70% for better UX calculate width based on this value
return (
<>
<div
ref={multiselectRef}
data-cy={`label-${String(component.name).toLowerCase()} `}
className={cx('multiselect-widget', 'd-flex', {
[alignment === 'top' &&
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
? 'flex-column'
: 'align-items-center']: true,
'flex-row-reverse': direction === 'right' && alignment === 'side',
'text-right': direction === 'right' && alignment === 'top',
invisible: !visibility,
visibility: visibility,
})}
style={{
position: 'relative',
whiteSpace: 'nowrap',
width: '100%',
}}
onMouseDown={(event) => {
onComponentClick(id, component, event);
// This following line is needed because sometimes after clicking on canvas then also dropdown remains selected
useEditorStore.getState().actions.setHoveredComponent('');
}}
onClick={() => {
if (!isMultiSelectDisabled) {
fireEvent('onFocus');
}
setIsMultiselectOpen(!isMultiselectOpen);
}}
>
<Label
label={label}
width={labelWidth}
labelRef={labelRef}
darkMode={darkMode}
color={labelColor}
defaultAlignment={alignment}
direction={direction}
auto={auto}
isMandatory={isMandatory}
_width={_width}
/>
<div className="w-100 px-0 h-100">
<Select
menuId={id}
isDisabled={isMultiSelectDisabled}
value={selected}
onChange={onChangeHandler}
options={selectOptions}
styles={customStyles}
// Only show loading when dynamic options are enabled
isLoading={isMultiSelectLoading}
onInputChange={onSearchTextChange}
inputValue={searchInputValue}
menuIsOpen={isMultiselectOpen}
placeholder={placeholder}
components={{
MenuList: CustomMenuList,
ValueContainer: CustomValueContainer,
Option: CustomOption,
LoadingIndicator: () => <Loader style={{ right: '11px', zIndex: 3, position: 'absolute' }} width="16" />,
ClearIndicator: CustomClearIndicator,
DropdownIndicator: isMultiSelectLoading ? () => null : CustomDropdownIndicator,
}}
isClearable
isMulti
hideSelectedOptions={false}
closeMenuOnSelect={false}
onMenuOpen={() => {
fireEvent('onFocus');
setIsMultiselectOpen(true);
}}
// select props
icon={icon}
doShowIcon={iconVisibility}
containerRef={valueContainerRef}
showAllOption={showAllOption}
isSelectAllSelected={isSelectAllSelected}
setIsSelectAllSelected={setIsSelectAllSelected}
setSelected={setSelected}
iconColor={iconColor}
optionsLoadingState={optionsLoadingState}
darkMode={darkMode}
fireEvent={() => fireEvent('onSelect')}
menuPlacement="auto"
menuPortalTarget={document.body}
/>
</div>
</div>
<div
className={`${isValid ? '' : visibility ? 'd-flex' : 'none'}`}
style={{
color: errTextColor,
justifyContent: direction === 'right' ? 'flex-start' : 'flex-end',
fontSize: '11px',
fontWeight: '400',
lineHeight: '16px',
}}
>
{!isValid && validationError}
</div>
</>
);
};

View file

@ -0,0 +1,38 @@
.value-container-selected-option {
display: flex;
align-items: center;
border-radius: 6px;
border: 1px solid #ECEEF0;
background-color: var(--surfaces-surface-03);
padding-right: 2px;
margin-right: 4px;
line-height: 20px;
padding: 1px 3px 1px 6px;
color: var(--text-primary);
font-weight: 500;
&:hover {
background-color: var(--interactive-overlays-fill-pressed);
}
.value-container-selected-option-delete-icon {
margin-left: 6px;
cursor: pointer;
}
}
.value-container-selected-option-popover {
display: flex;
flex-wrap: wrap;
gap : 6px;
padding: 16px;
color: var(--text-primary);
border-radius: 6px;
background-color: var(--surfaces-surface-01);
font-weight: 500;
}
.multiselect-widget-show-more-popover {
background-color: var(--surfaces-surface-01) !important;
.popover-body {
background-color: var(--surfaces-surface-01) !important
}
}

View file

@ -1,10 +1,10 @@
import React, { useEffect, useRef, useState } from 'react';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import * as Icons from '@tabler/icons-react';
import Loader from '@/ToolJetUI/Loader/Loader';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import Label from '@/_ui/Label';
import { useEditorStore } from '@/_stores/editorStore';
export const PasswordInput = function PasswordInput({
height,
@ -17,6 +17,7 @@ export const PasswordInput = function PasswordInput({
darkMode,
dataCy,
isResizing,
id,
}) {
const textInputRef = useRef();
const labelRef = useRef();
@ -228,6 +229,23 @@ export const PasswordInput = function PasswordInput({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disable]);
const currentPageId = useEditorStore.getState().currentPageId;
const components = useEditorStore.getState().appDefinition?.pages?.[currentPageId]?.components || {};
const isChildOfForm = Object.keys(components).some((key) => {
if (key == id) {
const { parent } = components[key].component;
if (parent) {
const parentComponentTypes = {};
Object.keys(components).forEach((key) => {
const { component } = components[key];
parentComponentTypes[key] = component.component;
});
if (parentComponentTypes[parent] == 'Form') return true;
}
}
return false;
});
const renderInput = () => (
<>
<div
@ -381,6 +399,9 @@ export const PasswordInput = function PasswordInput({
)}
</>
);
const renderContainer = (children) => {
return !isChildOfForm ? <form autoComplete="off">{children}</form> : <div>{children}</div>;
};
return <div>{renderInput()}</div>;
return renderContainer(renderInput());
};

View file

@ -190,7 +190,7 @@ export const CustomSelect = ({
);
};
const CustomMenuList = ({ optionsLoadingState, children, selectProps, inputRef, ...props }) => {
export const CustomMenuList = ({ optionsLoadingState, children, selectProps, inputRef, ...props }) => {
const { onInputChange, inputValue, onMenuInputFocus } = selectProps;
return (

View file

@ -13,22 +13,34 @@ const TjDatepicker = forwardRef(
({ value, onClick, styles, dateInputRef, readOnly, setIsDateInputFocussed, setDateInputValue }, ref) => {
return (
<div className="table-column-datepicker-input-container">
<input
className={cx('table-column-datepicker-input text-truncate', {
'pointer-events-none': readOnly,
})}
value={value}
onClick={onClick}
ref={dateInputRef}
style={styles}
onChange={(e) => {
setIsDateInputFocussed(true);
setDateInputValue(e.target.value);
}}
onFocus={() => {
setDateInputValue(value);
}}
/>
{readOnly ? (
<div
style={{
height: '100%',
width: '100%',
overflow: 'hidden',
}}
>
{value}
</div>
) : (
<input
className={cx('table-column-datepicker-input text-truncate', {
'pointer-events-none': readOnly,
})}
value={value}
onClick={onClick}
ref={dateInputRef}
style={styles}
onChange={(e) => {
setIsDateInputFocussed(true);
setDateInputValue(e.target.value);
}}
onFocus={() => {
setDateInputValue(value);
}}
/>
)}
{!readOnly && (
<span className="cell-icon-display">
<SolidIcon

View file

@ -28,7 +28,7 @@ export const GlobalFilter = ({
style={{ padding: '0.4rem 0.6rem', borderRadius: '6px' }}
>
<div className="d-flex">
<SolidIcon name="search" width="16" height="16" fill={'var(--icons-default)'} />
<SolidIcon name="search" style={{ marginTop: '3px' }} width="16" height="16" fill={'var(--icons-default)'} />
<input
type="text"
className={`align-self-center bg-transparent tj-text tj-text-sm mx-lg-1`}

View file

@ -83,6 +83,7 @@ const StringColumn = ({
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
ref.current.blur();
if (cellValue !== e.target.textContent) {
handleCellValueChange(cell.row.index, column.key || column.name, e.target.textContent, cell.row.original);
}
@ -93,7 +94,7 @@ const StringColumn = ({
e.stopPropagation();
}}
>
<span> {String(cellValue)}</span>
{String(cellValue)}
</div>
);

View file

@ -428,7 +428,11 @@ export function Table({
const tableRef = useRef();
const columnProperties = useDynamicColumn ? generatedColumn : component.definition.properties.columns.value;
const removeNullValues = (arr) => arr.filter((element) => element !== null);
const columnProperties = useDynamicColumn
? generatedColumn
: removeNullValues(component.definition.properties.columns.value);
let columnData = generateColumnsData({
columnProperties,

View file

@ -36,6 +36,7 @@ const Text = ({
});
const { isValid, validationError } = validationData;
const ref = useRef();
const editableCellValueRef = useRef(null);
const nonEditableCellValueRef = useRef();
const [showOverlay, setShowOverlay] = useState(false);
const [hovered, setHovered] = useState(false);
@ -54,6 +55,7 @@ const Text = ({
const _renderTextArea = () => (
<div
ref={editableCellValueRef}
contentEditable={'true'}
className={`${!isValid ? 'is-invalid' : ''} h-100 long-text-input text-container ${
darkMode ? ' textarea-dark-theme' : ''
@ -83,6 +85,7 @@ const Text = ({
onKeyDown={(e) => {
e.persist();
if (e.key === 'Enter' && !e.shiftKey && isEditable) {
editableCellValueRef.current.blur();
const div = e.target;
let content = div.innerHTML;
handleCellValueChange(cell.row.index, column.key || column.name, content, cell.row.original);

View file

@ -561,6 +561,13 @@ export default function generateColumnsData({
{cellValue && (
<img
src={cellValue}
onError={(e) => {
if (!_.get(e, 'target.src', '').includes('/assets/images/image-not-found.svg')) {
e.target.onerror = null;
e.target.src = '/assets/images/image-not-found.svg';
e.target.style = e.target.style + ' border-radius: 0;';
}
}}
style={{
pointerEvents: 'auto',
width: `${column?.width}px`,

View file

@ -165,7 +165,6 @@ export const ToggleSwitchV2 = ({
setExposedVariable('isLoading', loading);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading]);
useEffect(() => {
setExposedVariable('isVisible', visibility);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -175,12 +174,10 @@ export const ToggleSwitchV2 = ({
setExposedVariable('isDisabled', disable);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disable]);
useEffect(() => {
setExposedVariable('isValid', isValid);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isValid]);
useEffect(() => {
setExposedVariable('setLoading', async function (loading) {
setLoading(loading);

View file

@ -1,7 +1,12 @@
.reactMarkdown {
p,h1,h2,h3,h4,h5,h6 {
p,
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 0px;
}
}
@ -9,17 +14,21 @@
.text-widget-section {
scrollbar-color: transparent transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
& ::-webkit-scrollbar {
background-color: transparent;
width: 6px;
}
&::-webkit-scrollbar-track {
& ::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
& ::-webkit-scrollbar-thumb {
background-color: transparent;
}
&:hover{
& :hover {
scrollbar-color: #a0a6ae transparent;
}
}

View file

@ -13,7 +13,7 @@ export const VerticalDivider = function Divider({ styles, height, width, dataCy,
>
<div className="col-6"></div>
<div
className="col-6 border-right"
className="col-6"
style={{ height, width: '1px', backgroundColor: color, padding: '0rem', marginLeft: '0.5rem', boxShadow }}
></div>
</div>

View file

@ -12,7 +12,12 @@ import { commentsService } from '@/_services';
import config from 'config';
import Spinner from '@/_ui/Spinner';
import { useHotkeys } from 'react-hotkeys-hook';
import { addComponents, addNewWidgetToTheEditor, isPDFSupported } from '@/_helpers/appUtils';
import {
addComponents,
addNewWidgetToTheEditor,
isPDFSupported,
calculateMoveableBoxHeight,
} from '@/_helpers/appUtils';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useAppInfo } from '@/_stores/appDataStore';
@ -116,7 +121,7 @@ export const Container = ({
// const [isResizing, setIsResizing] = useState(false);
const [commentsPreviewList, setCommentsPreviewList] = useState([]);
const [newThread, addNewThread] = useState({});
const [isContainerFocused, setContainerFocus] = useState(false);
const [isContainerFocused, setContainerFocus] = useState(true);
const [canvasHeight, setCanvasHeight] = useState(null);
useEffect(() => {
@ -157,7 +162,7 @@ export const Container = ({
if (navigator.clipboard && typeof navigator.clipboard.readText === 'function') {
try {
const cliptext = await navigator.clipboard.readText();
addComponents(
const newComponent = addComponents(
currentPageId,
appDefinition,
appDefinitionChanged,
@ -165,6 +170,7 @@ export const Container = ({
JSON.parse(cliptext),
true
);
setSelectedComponent(newComponent.id, newComponent.component);
} catch (err) {
console.log(err);
}
@ -236,6 +242,7 @@ export const Container = ({
const noOfBoxs = Object.values(boxes || []).length;
useEffect(() => {
updateCanvasHeight(boxes);
noOfBoxs != 0 && setContainerFocus(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [noOfBoxs]);
@ -647,8 +654,13 @@ export const Container = ({
return;
}
if (Object.keys(value)?.length > 0) {
setBoxes((boxes) =>
update(boxes, {
setBoxes((boxes) => {
// Ensure boxes[id] exists
if (!boxes[id]) {
console.error(`Box with id ${id} does not exist`);
return boxes;
}
return update(boxes, {
[id]: {
$merge: {
component: {
@ -663,8 +675,9 @@ export const Container = ({
},
},
},
})
);
});
});
if (!_.isEmpty(opts)) {
paramUpdatesOptsRef.current = opts;
}
@ -1027,26 +1040,6 @@ const WidgetWrapper = ({
// const width = (canvasWidth * layoutData.width) / NO_OF_GRIDS;
const width = gridWidth * layoutData.width;
const calculateMoveableBoxHeight = () => {
// Early return for non input components
if (!['TextInput', 'PasswordInput', 'NumberInput'].includes(componentType)) {
return layoutData?.height;
}
const { alignment = { value: null }, width = { value: null }, auto = { value: null } } = stylesDefinition ?? {};
const resolvedLabel = label?.value?.length ?? 0;
const resolvedWidth = resolveWidgetFieldValue(width?.value) ?? 0;
const resolvedAuto = resolveWidgetFieldValue(auto?.value) ?? false;
let newHeight = layoutData?.height;
if (alignment.value && resolveWidgetFieldValue(alignment.value) === 'top') {
if ((resolvedLabel > 0 && resolvedWidth > 0) || (resolvedAuto && resolvedWidth === 0 && resolvedLabel > 0)) {
newHeight += 20;
}
}
return newHeight;
};
const isWidgetActive = (isSelected || isDragging) && mode !== 'view';
const { label = { value: null } } = propertiesDefinition ?? {};
@ -1055,7 +1048,9 @@ const WidgetWrapper = ({
const styles = {
width: width + 'px',
height: resolvedVisibility ? calculateMoveableBoxHeight() + 'px' : '10px',
height: resolvedVisibility
? calculateMoveableBoxHeight(componentType, layoutData, stylesDefinition, label) + 'px'
: '10px',
transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`,
...(isGhostComponent ? { opacity: 0.5 } : {}),
...(isWidgetActive ? { zIndex: 3 } : {}),

View file

@ -1,7 +1,6 @@
import React, { useState, useCallback } from 'react';
import { getComponentToRender } from '@/_helpers/editorHelpers';
import _ from 'lodash';
import { getComponentsToRenders } from '@/_stores/editorStore';
function deepEqualityCheckusingLoDash(obj1, obj2) {

View file

@ -1021,7 +1021,7 @@ class DataSourceManagerComponent extends React.Component {
<ConfirmDialog
title={'Add datasource'}
show={dataSourceConfirmModalProps.isOpen}
message={`Do you want to add ${dataSourceConfirmModalProps?.dataSource?.name}?`}
message={`Do you want to add ${dataSourceConfirmModalProps?.dataSource?.name}`}
onConfirm={() => createSelectedDataSource(dataSourceConfirmModalProps.dataSource)}
onCancel={this.resetDataSourceConfirmModal}
confirmButtonText={'Add datasource'}

View file

@ -529,7 +529,7 @@ export default function DragContainer({
isDraggingRef.current = false;
}
if (draggedSubContainer) {
if (draggedSubContainer || !e.lastEvent) {
return;
}
@ -568,8 +568,8 @@ export default function DragContainer({
const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
const currentParentId = boxes.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent;
let left = e.lastEvent.translate[0];
let top = e.lastEvent.translate[1];
let left = e.lastEvent?.translate[0];
let top = e.lastEvent?.translate[1];
if (['Listview', 'Kanban'].includes(widgets[draggedOverElemId]?.component?.component)) {
const elemContainer = e.target.closest('.real-canvas');

View file

@ -279,7 +279,6 @@ const EditorComponent = (props) => {
if (didAppDefinitionChanged) {
prevAppDefinition.current = appDefinition;
}
if (mounted && didAppDefinitionChanged && currentPageId) {
const components = appDefinition?.pages[currentPageId]?.components || {};
@ -287,7 +286,7 @@ const EditorComponent = (props) => {
if (appDiffOptions?.skipAutoSave === true || appDiffOptions?.entityReferenceUpdated === true) return;
handleLowPriorityWork(() => autoSave());
handleLowPriorityWork(() => autoSave(), 100);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]);
@ -1968,9 +1967,11 @@ const EditorComponent = (props) => {
}
const handleCanvasContainerMouseUp = (e) => {
const selectedText = window.getSelection().toString();
if (
['real-canvas', 'modal'].includes(e.target.className) &&
useEditorStore.getState()?.selectedComponents?.length
useEditorStore.getState()?.selectedComponents?.length &&
!selectedText
) {
setSelectedComponents(EMPTY_ARRAY);
}

View file

@ -107,6 +107,26 @@ const EditorSelecto = ({ selectionRef, canvasContainerRef, setSelectedComponent,
onScroll={(e) => {
canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10);
}}
dragCondition={(e) => {
// clear browser selection on drag
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
const target = e.inputEvent.target;
if (target.getAttribute('id') === 'real-canvas') {
return true;
}
// if clicked on a component, select it and return false to prevent drag
if (target.closest('.moveable-box')) {
const closest = target.closest('.moveable-box');
const id = closest.getAttribute('widgetid');
const component = appDefinition.pages[currentPageId].components[id].component;
const isMultiSelect = e.inputEvent.shiftKey;
setSelectedComponent(id, component, isMultiSelect);
}
return false;
}}
/>
</>
);

View file

@ -13,9 +13,11 @@ const SHOW_ADDITIONAL_ACTIONS = [
'TextInput',
'NumberInput',
'PasswordInput',
'Button',
'ToggleSwitchV2',
'Checkbox',
'DropdownV2',
'MultiselectV2',
'Button',
];
const PROPERTIES_VS_ACCORDION_TITLE = {
Text: 'Data',
@ -108,6 +110,8 @@ export const baseComponentProperties = (
'Button',
'ToggleSwitchV2',
'Checkbox',
'DropdownV2',
'MultiselectV2',
],
Layout: [],
};

View file

@ -0,0 +1,621 @@
import React, { useState, useEffect } from 'react';
import Accordion from '@/_ui/Accordion';
import { EventManager } from '../EventManager';
import { renderElement } from '../Utils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import List from '@/ToolJetUI/List/List';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import CodeHinter from '@/Editor/CodeEditor';
import { resolveReferences } from '@/_helpers/utils';
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
import ListGroup from 'react-bootstrap/ListGroup';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SortableList from '@/_components/SortableList';
import Trash from '@/_ui/Icon/solidIcons/Trash';
export function Select({ componentMeta, darkMode, ...restProps }) {
const {
layoutPropertyChanged,
component,
dataQueries,
paramUpdated,
currentState,
eventsChanged,
apps,
allComponents,
pages,
} = restProps;
const isMultiSelect = component?.component?.component === 'MultiselectV2';
const isDynamicOptionsEnabled = resolveReferences(
component?.component?.definition?.properties?.advanced?.value,
currentState
);
const constructOptions = () => {
const optionsValue = component?.component?.definition?.properties?.options?.value;
const valuesToResolve = ['label', 'value'];
let options = [];
if (isDynamicOptionsEnabled || typeof optionsValue === 'string') {
options = resolveReferences(optionsValue, currentState);
} else {
options = optionsValue?.map((option) => {
const newOption = { ...option };
valuesToResolve.forEach((key) => {
if (option[key]) {
newOption[key] = resolveReferences(option[key], currentState);
}
});
return newOption;
});
}
return options.map((option) => {
const newOption = { ...option };
Object.keys(option).forEach((key) => {
if (typeof option[key]?.value === 'boolean') {
newOption[key]['value'] = `{{${option[key]?.value}}}`;
}
});
return newOption;
});
};
const _markedAsDefault = resolveReferences(
component?.component?.definition?.properties[isMultiSelect ? 'values' : 'value']?.value,
currentState
);
const [options, setOptions] = useState([]);
const [markedAsDefault, setMarkedAsDefault] = useState(_markedAsDefault);
const [hoveredOptionIndex, setHoveredOptionIndex] = useState(null);
const validations = Object.keys(componentMeta.validation || {});
let properties = [];
let additionalActions = [];
let optionsProperties = [];
for (const [key] of Object.entries(componentMeta?.properties)) {
if (componentMeta?.properties[key]?.section === 'additionalActions') {
additionalActions.push(key);
} else if (componentMeta?.properties[key]?.accordian === 'Options') {
optionsProperties.push(key);
} else {
properties.push(key);
}
}
const getItemStyle = (isDragging, draggableStyle) => ({
userSelect: 'none',
...draggableStyle,
});
const updateAllOptionsParams = (options, props) => {
paramUpdated({ name: 'options' }, 'value', options, 'properties', false, props);
};
const generateNewOptions = () => {
let found = false;
let label = '';
let currentNumber = options.length + 1;
let value = currentNumber;
while (!found) {
label = `option${currentNumber}`;
value = currentNumber.toString();
if (options.find((option) => option.label === label) === undefined) {
found = true;
}
currentNumber += 1;
}
return {
value,
label,
visible: { value: '{{true}}' },
disable: { value: '{{false}}' },
default: { value: '{{false}}' },
};
};
const handleAddOption = () => {
let _option = generateNewOptions();
const _items = [...options, _option];
setOptions(_items);
updateAllOptionsParams(_items);
};
const handleDeleteOption = (index) => {
const _items = options.filter((option, i) => i !== index);
setOptions(_items);
updateAllOptionsParams(_items, { isParamFromDropdownOptions: true });
};
const handleLabelChange = (label, index) => {
const _options = options.map((option, i) => {
if (i === index) {
return {
...option,
label,
};
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
};
const handleValueChange = (value, index) => {
const _options = options.map((option, i) => {
if (i === index) {
return {
...option,
value,
};
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
};
const reorderOptions = async (startIndex, endIndex) => {
const result = [...options];
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
setOptions(result);
updateAllOptionsParams(result);
};
const onDragEnd = ({ source, destination }) => {
if (!destination || source?.index === destination?.index) {
return;
}
reorderOptions(source.index, destination.index);
};
const handleMarkedAsDefaultChange = (value, index) => {
const isMarkedAsDefault = resolveReferences(value, currentState);
if (isMultiSelect) {
const _value = options[index]?.value;
let _markedAsDefault = [];
if (isMarkedAsDefault && !markedAsDefault.includes(_value)) {
_markedAsDefault = [...markedAsDefault, _value];
} else {
_markedAsDefault = markedAsDefault.filter((value) => value !== _value);
}
setMarkedAsDefault(_markedAsDefault);
paramUpdated({ name: 'values' }, 'value', _markedAsDefault, 'properties');
} else {
const _value = isMarkedAsDefault ? options[index]?.value : '';
const _options = options.map((option, i) => {
if (i === index) {
return {
...option,
default: {
...option.default,
value,
},
};
} else {
return {
...option,
default: {
...option.default,
value: `{{false}}`,
},
};
}
});
setOptions(_options);
updateAllOptionsParams(_options);
setMarkedAsDefault(_value);
paramUpdated({ name: 'value' }, 'value', _value, 'properties');
}
};
const handleVisibilityChange = (value, index) => {
const _options = options.map((option, i) => {
if (i === index) {
return {
...option,
visible: {
...option.visible,
value,
},
};
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
};
const handleDisableChange = (value, index) => {
const _options = options.map((option, i) => {
if (i === index) {
return {
...option,
disable: {
...option.disable,
value,
},
};
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
};
const handleOnFxPress = (active, index, key) => {
const _options = options.map((option, i) => {
if (i === index) {
return {
...option,
[key]: {
...option[key],
fxActive: active,
},
};
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
};
useEffect(() => {
setOptions(constructOptions());
}, [isMultiSelect]);
const _renderOverlay = (item, index) => {
return (
<Popover className={`${darkMode && 'dark-theme theme-dark'}`} style={{ minWidth: '248px' }}>
<Popover.Body>
<div className="field mb-3" data-cy={`input-and-label-column-name`}>
<label data-cy={`label-column-name`} className="font-weight-500 mb-1 font-size-12">
{'Option label'}
</label>
<CodeHinter
currentState={currentState}
type={'basic'}
initialValue={item?.label}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'Option label'}
onChange={(value) => handleLabelChange(value, index)}
/>
</div>
<div className="field mb-3" data-cy={`input-and-label-column-name`}>
<label data-cy={`label-column-name`} className="font-weight-500 mb-1 font-size-12">
{'Option value'}
</label>
<CodeHinter
currentState={currentState}
type={'basic'}
initialValue={item?.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'Option value'}
onChange={(value) => handleValueChange(value, index)}
/>
</div>
<div className="field mb-2" data-cy={`input-and-label-column-name`}>
<CodeHinter
currentState={currentState}
initialValue={item?.default?.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
component={component}
type={'fxEditor'}
paramLabel={'Make this default option'}
paramName={'isEditable'}
onChange={(value) => handleMarkedAsDefaultChange(value, index)}
onFxPress={(active) => handleOnFxPress(active, index, 'default')}
fxActive={item?.default?.fxActive}
fieldMeta={{
type: 'toggle',
displayName: 'Make editable',
isFxNotRequired: true,
}}
paramType={'toggle'}
/>
</div>
<div className="field mb-2" data-cy={`input-and-label-column-name`}>
<CodeHinter
currentState={currentState}
initialValue={item?.visible?.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
component={component}
type={'fxEditor'}
paramLabel={'Visibility'}
onChange={(value) => handleVisibilityChange(value, index)}
paramName={'visible'}
onFxPress={(active) => handleOnFxPress(active, index, 'visible')}
fxActive={item?.visible?.fxActive}
fieldMeta={{
type: 'toggle',
displayName: 'Make editable',
}}
paramType={'toggle'}
/>
</div>
<div className="field" data-cy={`input-and-label-column-name`}>
<CodeHinter
currentState={currentState}
initialValue={item?.disable?.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
component={component}
type={'fxEditor'}
paramLabel={'Disable'}
paramName={'disable'}
onChange={(value) => handleDisableChange(value, index)}
onFxPress={(active) => handleOnFxPress(active, index, 'disable')}
fxActive={item?.disable?.fxActive}
fieldMeta={{
type: 'toggle',
displayName: 'Make editable',
}}
paramType={'toggle'}
/>
</div>
</Popover.Body>
</Popover>
);
};
const _renderOptions = () => {
return (
<List style={{ marginBottom: '20px' }}>
<DragDropContext
onDragEnd={(result) => {
onDragEnd(result);
}}
>
<Droppable droppableId="droppable">
{({ innerRef, droppableProps, placeholder }) => (
<div className="w-100" {...droppableProps} ref={innerRef}>
{options?.map((item, index) => {
return (
<Draggable key={item.value} draggableId={item.value} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
>
<OverlayTrigger
trigger="click"
placement="left"
rootClose
overlay={_renderOverlay(item, index)}
>
<div key={item.value}>
<ListGroup.Item
style={{ marginBottom: '8px', backgroundColor: 'var(--slate3)' }}
onMouseEnter={() => setHoveredOptionIndex(index)}
onMouseLeave={() => setHoveredOptionIndex(null)}
{...restProps}
>
<div className="row">
<div className="col-auto d-flex align-items-center">
<SortableList.DragHandle show />
</div>
<div className="col text-truncate cursor-pointer" style={{ padding: '0px' }}>
{item.label}
</div>
<div className="col-auto">
{index === hoveredOptionIndex && (
<ButtonSolid
variant="danger"
size="xs"
className={'delete-icon-btn'}
onClick={(e) => {
e.stopPropagation();
handleDeleteOption(index);
}}
>
<span className="d-flex">
<Trash fill={'var(--tomato9)'} width={12} />
</span>
</ButtonSolid>
)}
</div>
</div>
</ListGroup.Item>
</div>
</OverlayTrigger>
</div>
)}
</Draggable>
);
})}
{placeholder}
</div>
)}
</Droppable>
</DragDropContext>
<AddNewButton onClick={handleAddOption} dataCy="add-new-dropdown-option" className="mt-0">
Add new option
</AddNewButton>
</List>
);
};
let items = [];
items.push({
title: 'Data',
isOpen: true,
children: properties
.filter((property) => !optionsProperties.includes(property))
?.map((property) =>
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode
)
),
});
items.push({
title: 'Options',
isOpen: true,
children: (
<>
{renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'advanced',
'properties',
currentState,
allComponents
)}
{isDynamicOptionsEnabled
? renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'schema',
'properties',
currentState,
allComponents
)
: _renderOptions()}
{isDynamicOptionsEnabled &&
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'optionsLoadingState',
'properties',
currentState,
allComponents
)}
{isMultiSelect &&
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'showAllOption',
'properties',
currentState,
allComponents
)}
</>
),
});
items.push({
title: 'Events',
isOpen: true,
children: (
<EventManager
sourceId={component?.id}
eventSourceType="component"
eventMetaDefinition={componentMeta}
currentState={currentState}
dataQueries={dataQueries}
components={allComponents}
eventsChanged={eventsChanged}
apps={apps}
darkMode={darkMode}
pages={pages}
/>
),
});
items.push({
title: 'Validation',
isOpen: true,
children: validations.map((property) =>
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'validation',
currentState,
allComponents,
darkMode,
componentMeta.validation?.[property]?.placeholder
)
),
});
items.push({
title: `Additional Actions`,
isOpen: true,
children: additionalActions.map((property) => {
return renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode,
componentMeta.properties?.[property]?.placeholder
);
}),
});
items.push({
title: 'Devices',
isOpen: true,
children: (
<>
{renderElement(
component,
componentMeta,
layoutPropertyChanged,
dataQueries,
'showOnDesktop',
'others',
currentState,
allComponents
)}
{renderElement(
component,
componentMeta,
layoutPropertyChanged,
dataQueries,
'showOnMobile',
'others',
currentState,
allComponents
)}
</>
),
});
return <Accordion items={items} />;
}

View file

@ -11,7 +11,6 @@ const NoListItem = ({ text, dataCy = '' }) => {
borderRadius: '6px',
border: '1px dashed var(--slate5)',
color: 'var(--slate8)',
marginBottom: '8px',
}}
>
<span className="d-flex align-items-center" style={{ marginRight: '2px' }}>

View file

@ -18,7 +18,6 @@ import NoListItem from './NoListItem';
import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperties';
import { ColumnPopoverContent } from './ColumnManager/ColumnPopover';
import { useAppDataStore } from '@/_stores/appDataStore';
import { checkIfTableColumnDeprecated } from './ColumnManager/DeprecatedColumnTypeMsg';
const NON_EDITABLE_COLUMNS = ['link', 'image'];
@ -79,17 +78,23 @@ class TableComponent extends React.Component {
}
checkIfAllColumnsAreEditable = (component) => {
const isAllColumnsEditable = component.component?.definition?.properties?.columns?.value
?.filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType))
.every((column) => resolveReferences(column.isEditable));
const columns = component?.component?.definition?.properties?.columns?.value || [];
const filteredColumns = columns.filter((column) => column && !NON_EDITABLE_COLUMNS.includes(column.columnType));
const isAllColumnsEditable = filteredColumns.every((column) =>
resolveReferences(column.isEditable, this.props.currentState)
);
return isAllColumnsEditable;
};
componentDidUpdate(prevProps) {
const prevPropsColumns = prevProps?.component?.component.definition.properties.columns?.value;
const currentPropsColumns = this.props.component.component.definition.properties.columns?.value;
const prevPropsColumns = prevProps?.component?.component?.definition?.properties?.columns?.value || [];
const currentPropsColumns = this.props?.component?.component?.definition?.properties?.columns?.value || [];
if (prevPropsColumns !== currentPropsColumns) {
const isAllColumnsEditable = currentPropsColumns
const filteredColumns = currentPropsColumns.filter((column) => column);
const isAllColumnsEditable = filteredColumns
.filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType))
.every((column) => resolveReferences(column.isEditable));
this.setState({ isAllColumnsEditable });
@ -470,15 +475,17 @@ class TableComponent extends React.Component {
handleMakeAllColumnsEditable = (value) => {
const columns = resolveReferences(this.props.component.component.definition.properties.columns);
const columnValues = columns.value || [];
this.setState({ isAllColumnsEditable: resolveReferences(value) });
const newValue = columns.value.map((column) => ({
...column,
isEditable: !NON_EDITABLE_COLUMNS.includes(column.columnType) ? value : '{{false}}',
}));
const newValue = columnValues
.filter((column) => column)
.map((column) => ({
...column,
isEditable: !NON_EDITABLE_COLUMNS.includes(column.columnType) ? value : '{{false}}',
}));
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties', true);
this.setState({ isAllColumnsEditable: resolveReferences(value) });
};
duplicateColumn = (index) => {
@ -493,6 +500,9 @@ class TableComponent extends React.Component {
render() {
const { dataQueries, component, paramUpdated, componentMeta, components, currentState, darkMode } = this.props;
const columns = component.component.definition.properties.columns;
// Filter out null or undefined values before mapping
const filteredColumns = (columns.value || []).filter((column) => column);
const actions = component.component.definition.properties.actions || { value: [] };
if (!component.component.definition.properties.displaySearchBox)
paramUpdated({ name: 'displaySearchBox' }, 'value', true, 'properties');
@ -564,7 +574,7 @@ class TableComponent extends React.Component {
<Droppable droppableId="droppable">
{({ innerRef, droppableProps, placeholder }) => (
<div className="w-100 d-flex custom-gap-4 flex-column" {...droppableProps} ref={innerRef}>
{columns.value.map((item, index) => {
{filteredColumns.map((item, index) => {
const resolvedItemName = resolveReferences(item.name);
const isEditable = resolveReferences(item.isEditable);
const columnVisibility = item?.columnVisibility ?? true;

View file

@ -1044,7 +1044,7 @@ export const EventManager = ({
const renderAddHandlerBtn = () => {
return (
<AddNewButton onClick={addHandler} dataCy="add-event-handler" className="mt-0" isLoading={eventsCreatedLoader}>
<AddNewButton onClick={addHandler} dataCy="add-event-handler" isLoading={eventsCreatedLoader}>
{t('editor.inspector.eventManager.addHandler', 'New event handler')}
</AddNewButton>
);
@ -1054,7 +1054,7 @@ export const EventManager = ({
return (
<>
{!hideEmptyEventsAlert && <NoListItem text={'No event handlers'} />}
{renderAddHandlerBtn()}
<div className="d-flex">{renderAddHandlerBtn()}</div>
</>
);
}
@ -1063,7 +1063,7 @@ export const EventManager = ({
if (events.length === 0) {
return (
<>
<div className="d-flex">
{renderAddHandlerBtn()}
{!hideEmptyEventsAlert ? (
<div className="text-left">
@ -1078,7 +1078,7 @@ export const EventManager = ({
</small>
</div>
) : null}
</>
</div>
);
}

View file

@ -33,6 +33,7 @@ import Copy from '@/_ui/Icon/solidIcons/Copy';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import classNames from 'classnames';
import { useEditorStore, EMPTY_ARRAY } from '@/_stores/editorStore';
import { Select } from './Components/Select';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const INSPECTOR_HEADER_OPTIONS = [
@ -59,9 +60,11 @@ const NEW_REVAMPED_COMPONENTS = [
'PasswordInput',
'NumberInput',
'Table',
'Button',
'ToggleSwitchV2',
'Checkbox',
'DropdownV2',
'MultiselectV2',
'Button',
];
export const Inspector = ({
@ -168,7 +171,7 @@ export const Inspector = ({
return null;
};
function paramUpdated(param, attr, value, paramType, isParamFromTableColumn = false) {
function paramUpdated(param, attr, value, paramType, isParamFromTableColumn = false, props = {}) {
let newComponent = JSON.parse(JSON.stringify(component));
let newDefinition = deepClone(newComponent.component.definition);
let allParams = newDefinition[paramType] || {};
@ -232,6 +235,67 @@ export const Inspector = ({
componentDefinitionChanged(newComponent, {
componentPropertyUpdated: true,
isParamFromTableColumn: isParamFromTableColumn,
...props,
});
}
// use following function when more than one property needs to be updated
function paramsUpdated(array, isParamFromTableColumn = false) {
let newComponent = JSON.parse(JSON.stringify(component));
let newDefinition = _.cloneDeep(newComponent.component.definition);
array.map((item) => {
const { param, attr, value, paramType } = item;
let allParams = newDefinition[paramType] || {};
const paramObject = allParams[param.name];
if (!paramObject) {
allParams[param.name] = {};
}
if (attr) {
allParams[param.name][attr] = value;
const defaultValue = getDefaultValue(value);
// This is needed to have enable pagination in Table as backward compatible
// Whenever enable pagination is false, we turn client and server side pagination as false
if (
component.component.component === 'Table' &&
param.name === 'enablePagination' &&
!resolveReferences(value, currentState)
) {
if (allParams?.['clientSidePagination']?.[attr]) {
allParams['clientSidePagination'][attr] = value;
}
if (allParams['serverSidePagination']?.[attr]) {
allParams['serverSidePagination'][attr] = value;
}
}
// This case is required to handle for older apps when serverSidePagination is connected to Fx
if (param.name === 'serverSidePagination' && !allParams?.['enablePagination']?.[attr]) {
allParams = {
...allParams,
enablePagination: {
value: true,
},
};
}
if (param.type === 'select' && defaultValue) {
allParams[defaultValue.paramName]['value'] = defaultValue.value;
}
if (param.name === 'secondarySignDisplay') {
if (value === 'negative') {
newDefinition['styles']['secondaryTextColour']['value'] = '#EE2C4D';
} else if (value === 'positive') {
newDefinition['styles']['secondaryTextColour']['value'] = '#36AF8B';
}
}
} else {
allParams[param.name] = value;
}
newDefinition[paramType] = allParams;
newComponent.component.definition = newDefinition;
});
componentDefinitionChanged(newComponent, {
componentPropertyUpdated: true,
isParamFromTableColumn,
});
}
@ -325,6 +389,7 @@ export const Inspector = ({
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
paramsUpdated={paramsUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
components={allComponents}
@ -475,11 +540,20 @@ export const Inspector = ({
);
};
const getDocsLink = (componentMeta) => {
return componentMeta.component == 'ToggleSwitchV2'
? `https://docs.tooljet.io/docs/widgets/toggle-switch`
: `https://docs.tooljet.io/docs/widgets/${convertToKebabCase(componentMeta?.component ?? '')}`;
const component = componentMeta?.component ?? '';
switch (component) {
case 'ToggleSwitchV2':
return 'https://docs.tooljet.io/docs/widgets/toggle-switch';
case 'DropdownV2':
return 'https://docs.tooljet.com/docs/widgets/dropdown';
case 'DropDown':
return 'https://docs.tooljet.com/docs/widgets/dropdown';
case 'MultiselectV2':
return 'https://docs.tooljet.com/docs/widgets/multiselect';
default:
return `https://docs.tooljet.io/docs/widgets/${convertToKebabCase(component)}`;
}
};
const widgetsWithStyleConditions = {
Modal: {
conditions: [
@ -633,6 +707,10 @@ const GetAccordion = React.memo(
case 'Form':
return <Form {...restProps} />;
case 'DropdownV2':
case 'MultiselectV2':
return <Select {...restProps} />;
default: {
return <DefaultComponent {...restProps} />;
}

View file

@ -17,7 +17,7 @@ const ManageEventButton = ({
}) => {
const [isHovered, setIsHovered] = useState(false);
return (
<div style={{ marginBottom: '8px' }}>
<div style={{ marginBottom: '4px' }}>
<div
className="manage-event-btn border-0"
onMouseEnter={() => setIsHovered(true)}

View file

@ -45,8 +45,10 @@ export function renderCustomStyles(
componentConfig.component == 'PasswordInput' ||
componentConfig.component == 'ToggleSwitchV2' ||
componentConfig.component == 'Checkbox' ||
componentConfig.component == 'Button' ||
componentConfig.component == 'Table'
componentConfig.component == 'Table' ||
componentConfig.component == 'DropdownV2' ||
componentConfig.component == 'MultiselectV2' ||
componentConfig.component == 'Button'
) {
const paramTypeConfig = componentMeta[paramType] || {};
const paramConfig = paramTypeConfig[param] || {};

View file

@ -1,29 +1,40 @@
.manage-event-btn {
border-radius: 6px;
background-color: var(--slate3);
&:hover {
background-color: var(--slate4);
}
.event-handler-text {
font-size: 12px;
line-height: 20px;
color: var(--slate12);
font-weight: 500;
}
.event-name-text {
font-size: 12px;
line-height: 20px;
color: var(--slate11);
font-weight: 400;
display: flex;
align-items: center;
}
.event-action {
margin-right: 4px;
}
.query-manager {
.manage-event-btn {
width: 330px;
}
}
.manage-event-btn {
border-radius: 6px;
background-color: var(--interactive-default);
&:hover {
background-color: var(--interactive-hover);
}
.event-handler-text {
font-size: 12px;
line-height: 20px;
color: var(--text-default);
font-weight: 500;
}
.event-name-text {
font-size: 12px;
line-height: 20px;
color: var(--text-default);
font-weight: 400;
display: flex;
align-items: center;
}
.event-action {
margin-right: 4px;
color: var(--text-placeholder);
}
.list-menu-option-btn {
margin-left: 20px;
}
.list-menu-option-btn {
margin-left: 20px;
}
}

View file

@ -8,8 +8,12 @@ import { useEditorActions, useEditorStore } from '@/_stores/editorStore';
function Logs({ logProps, idx }) {
const [open, setOpen] = React.useState(false);
const title = ` [${capitalize(logProps?.type)} ${logProps?.key}]`;
let titleLogType = logProps?.type;
// need to change the titleLogType to query for transformations because if transformation fails, it is eventually a query failure
if (titleLogType === 'transformations') {
titleLogType = 'query';
}
const title = ` [${capitalize(titleLogType)} ${logProps?.key}]`;
const message =
logProps?.type === 'navToDisablePage'
? logProps?.message
@ -17,7 +21,12 @@ function Logs({ logProps, idx }) {
? 'Completed'
: logProps?.type === 'component'
? `Invalid property detected: ${logProps?.message}.`
: `${startCase(logProps?.type)} failed: ${logProps?.message ? logProps?.message : logProps?.error?.message}`;
: `${startCase(logProps?.type)} failed: ${
logProps?.description ||
logProps?.message ||
(isString(logProps?.error?.description) && logProps?.error?.description) || //added string check since description can be an object. eg: runpy
logProps?.error?.message
}`;
const defaultStyles = {
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
@ -122,4 +131,6 @@ function Logs({ logProps, idx }) {
);
}
let isString = (value) => typeof value === 'string' || value instanceof String;
export default Logs;

View file

@ -110,9 +110,7 @@ export const LeftSidebarInspector = ({
if (!_.isEmpty(component) && component.name === key) {
return {
iconName: key,
iconPath: `assets/images/icons/widgets/${
component.component.toLowerCase() === 'radiobutton' ? 'radio-button' : component.component.toLowerCase()
}.svg`,
iconPath: `assets/images/icons/widgets/${component.component.toLowerCase()}.svg`,
className: 'component-icon',
};
}

View file

@ -295,7 +295,7 @@ export const LeftSidebar = forwardRef((props, ref) => {
<ConfirmDialog
show={showLeaveDialog}
message={'The unsaved changes will be lost if you leave the editor, do you want to leave?'}
message={'The unsaved changes will be lost if you leave the editor, do you want to leave'}
onConfirm={() => router.push('/')}
onCancel={() => setShowLeaveDialog(false)}
darkMode={darkMode}

View file

@ -34,7 +34,7 @@ export const CustomToggleSwitch = ({
<label htmlFor={action} className="slider round"></label>
</label>
{label && (
<span className={`${darkMode ? 'color-white' : 'color-light-slate-12'}`} data-cy={`${dataCy}-toggle-label`}>
<span className={`text-default`} data-cy={`${dataCy}-toggle-label`}>
{label}
</span>
)}

View file

@ -77,7 +77,9 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
</div>
)}
<DataSourceIcon source={sources?.[0]} height={16} />
<span className="ms-1 small">{dataSourcesKinds.find((dsk) => dsk.kind === kind)?.name || kind}</span>
<span className="ms-1 small" style={{ fontSize: '13px' }}>
{dataSourcesKinds.find((dsk) => dsk.kind === kind)?.name || kind}
</span>
</div>
),
options: sources.map((source) => ({
@ -85,6 +87,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
<div
key={source.id}
className="py-2 px-2 rounded option-nested-datasource-selector small text-truncate"
style={{ fontSize: '13px' }}
data-tooltip-id="tooltip-for-add-query-dd-option"
data-tooltip-content={decodeEntities(source.name)}
data-cy={`ds-${source.name.toLowerCase()}`}
@ -116,7 +119,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
label: (
<div>
<DataSourceIcon source={source} height={16} />{' '}
<span data-cy={`ds-${source.name.toLowerCase()}`} className="ms-1 small">
<span data-cy={`ds-${source.name.toLowerCase()}`} className="ms-1 small" style={{ fontSize: '13px' }}>
{source.name}
</span>
</div>

View file

@ -81,14 +81,15 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe
) : (
<button
onClick={() => setShowModal((show) => !show)}
className="ms-2"
className=""
id="runjs-param-add-btn"
data-cy="runjs-add-param-button"
style={{ background: 'none' }}
data-cy={`runjs-add-param-button`}
style={{ background: 'none', border: 'none' }}
>
<span className="m-0">
<PlusRectangle fill={darkMode ? '#9BA1A6' : '#687076'} width={15} />
</span>
<p className="m-0 text-default">
<PlusRectangle fill={'var(--icons-default)'} width={15} />
<span style={{ marginLeft: '6px' }}>Add</span>
</p>
</button>
)}
</span>
@ -100,7 +101,7 @@ export const PillButton = ({ name, onClick, onRemove, marginBottom, className, s
<ButtonGroup
aria-label="Parameter"
className={cx({ 'mb-2': marginBottom, ...(className && { [className]: true }) })}
style={{ borderRadius: '6px', marginLeft: '6px', height: '24px', background: '#A1A7AE1F' }}
style={{ borderRadius: '6px', marginRight: '6px', height: '24px', background: 'var(--interactive-default)' }}
>
<Button
size="sm"

View file

@ -53,8 +53,13 @@ const ParameterList = ({
}, [showMore]);
return (
<div className="card-header">
<p style={{ marginRight: '4px', margin: '0px' }}>Parameters</p>
<div className="d-flex">
<p
className="text-placeholder font-weight-medium"
style={{ marginRight: '16px', marginBottom: '0px', width: '140px' }}
>
Parameters
</p>
{formattedParameters
.filter((param) => param.isVisible)
.map((parameter) => {

View file

@ -1,19 +1,44 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { JSONTree } from 'react-json-tree';
import { Tab, ListGroup, Row, Col } from 'react-bootstrap';
import { usePreviewLoading, usePreviewData, useQueryPanelActions } from '@/_stores/queryPanelStore';
import {
usePreviewLoading,
usePreviewData,
usePreviewPanelExpanded,
useQueryPanelStore,
usePreviewPanelHeight,
usePanelHeight,
} from '@/_stores/queryPanelStore';
import { getTheme, tabs } from '../constants';
import RemoveRectangle from '@/_ui/Icon/solidIcons/RemoveRectangle';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import ArrowDownTriangle from '@/_ui/Icon/solidIcons/ArrowDownTriangle';
import { useEventListener } from '@/_hooks/use-event-listener';
const Preview = ({ darkMode }) => {
const Preview = ({ darkMode, calculatePreviewHeight }) => {
const [key, setKey] = useState('raw');
const [isJson, setIsJson] = useState(false);
const [isDragging, setDragging] = useState(false);
const [isTopOfPreviewPanel, setIsTopOfPreviewPanel] = useState(false);
const storedHeight = usePreviewPanelHeight();
// initialize height with stored height if present in state
const heightSetOnce = useRef(!!storedHeight);
const previewPanelExpanded = usePreviewPanelExpanded();
const [height, setHeight] = useState(storedHeight);
const [theme, setTheme] = useState(() => getTheme(darkMode));
const queryPreviewData = usePreviewData();
const previewLoading = usePreviewLoading();
const { setPreviewData } = useQueryPanelActions();
const previewPanelRef = useRef();
const queryPanelHeight = usePanelHeight();
useEffect(() => {
calculatePreviewHeight(height, previewPanelExpanded);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
useQueryPanelStore.getState().actions.updatePreviewPanelHeight(height);
}, [height]);
useEffect(() => {
setTheme(() => getTheme(darkMode));
}, [darkMode]);
@ -45,79 +70,166 @@ const Preview = ({ darkMode }) => {
}
};
useEffect(() => {
// query panel collapse scenario
if (queryPanelHeight === 95 || queryPanelHeight === 0) {
return;
}
if (queryPanelHeight - 85 < 40) {
setHeight(40);
return;
}
if (queryPanelHeight - 85 < height) {
setHeight((queryPanelHeight - 85) * 0.7);
} else if (!heightSetOnce.current) {
setHeight((queryPanelHeight - 85) * 0.7);
heightSetOnce.current = true;
}
}, [queryPanelHeight]);
const onMouseMove = (e) => {
if (previewPanelRef.current) {
const componentTop = Math.round(previewPanelRef.current.getBoundingClientRect().top);
const clientY = e.clientY;
if ((clientY >= componentTop - 12) & (clientY <= componentTop + 1)) {
setIsTopOfPreviewPanel(true);
} else if (isTopOfPreviewPanel) {
setIsTopOfPreviewPanel(false);
}
if (isDragging) {
const parentHeight = queryPanelHeight;
const shift = componentTop - clientY;
const currentHeight = previewPanelRef.current.offsetHeight;
const newHeight = currentHeight + shift;
if (newHeight < 50) {
useQueryPanelStore.getState().actions.setPreviewPanelExpanded(false);
setHeight((queryPanelHeight - 85) * 0.7);
return;
}
if (newHeight > parentHeight - 95) {
return;
}
setHeight(newHeight);
}
}
};
const onMouseUp = () => {
setDragging(false);
calculatePreviewHeight(height, previewPanelExpanded);
};
const onMouseDown = () => {
isTopOfPreviewPanel && setDragging(true);
};
useEventListener('mousemove', onMouseMove);
useEventListener('mouseup', onMouseUp);
return (
<div className="preview-header preview-section d-flex align-items-baseline font-weight-500" ref={previewPanelRef}>
<div className="w-100" style={{ borderRadius: '0px 0px 6px 6px' }}>
<Tab.Container activeKey={key} onSelect={(k) => setKey(k)} defaultActiveKey="raw">
<div className="position-relative">
{previewLoading && (
<center className="position-absolute w-100">
<div className="spinner-border text-azure mt-5" role="status"></div>
</center>
)}
<Row className="py-2 border-bottom preview-section-header m-0">
<Col className="d-flex align-items-center color-slate9">Preview</Col>
<Col className="keys text-center d-flex align-items-center">
<ListGroup
className={`query-preview-list-group rounded ${darkMode ? 'dark' : ''}`}
variant="flush"
style={{ backgroundColor: '#ECEEF0', padding: '2px' }}
>
{tabs.map((tab) => (
<ListGroup.Item
key={tab}
eventKey={tab.toLowerCase()}
disabled={!queryPreviewData || (tab == 'JSON' && !isJson)}
style={{ minWidth: '74px', textAlign: 'center' }}
className="rounded"
>
<span
data-cy={`preview-tab-${String(tab).toLowerCase()}`}
style={{ width: '100%' }}
<div
className={`
preview-header preview-section d-flex flex-column align-items-baseline font-weight-500 ${
previewPanelExpanded ? 'expanded' : ''
}`}
ref={previewPanelRef}
onMouseDown={onMouseDown}
style={{
cursor: previewPanelExpanded && (isDragging || isTopOfPreviewPanel) ? 'row-resize' : 'default',
height: `${height}px`,
...(!previewPanelExpanded && { height: '29px' }),
...(isDragging && {
transition: 'none',
}),
}}
>
<div className="preview-toggle">
<div
onClick={() => {
useQueryPanelStore.getState().actions.setPreviewPanelExpanded(!previewPanelExpanded);
calculatePreviewHeight(height, !previewPanelExpanded);
}}
className="left"
>
<ArrowDownTriangle
width={15}
style={{
transform: !previewPanelExpanded ? 'rotate(180deg)' : '',
transition: 'transform 0.2s ease-in-out',
marginRight: '4px',
}}
/>
<span>Preview</span>
</div>
{previewPanelExpanded && (
<div className="right">
<Tab.Container activeKey={key} onSelect={(k) => setKey(k)} defaultActiveKey="raw">
<Row className="m-0">
<Col className="keys text-center d-flex align-items-center">
<ListGroup
className={`query-preview-list-group rounded ${darkMode ? 'dark' : ''}`}
variant="flush"
style={{ backgroundColor: '#ECEEF0', padding: '2px' }}
>
{tabs.map((tab) => (
<ListGroup.Item
key={tab}
eventKey={tab.toLowerCase()}
disabled={!queryPreviewData || (tab == 'JSON' && !isJson)}
style={{ minWidth: '74px', textAlign: 'center' }}
className="rounded"
>
{tab}
</span>
</ListGroup.Item>
))}
</ListGroup>
</Col>
<Col className="text-right d-flex align-items-center justify-content-end">
{queryPreviewData !== '' && (
<ButtonSolid variant="ghostBlack" size="sm" onClick={() => setPreviewData('')}>
<RemoveRectangle width={17} viewBox="0 0 28 28" fill="var(--slate8)" /> Clear
</ButtonSolid>
)}
</Col>
</Row>
<Row className="m-0">
<Tab.Content
style={{
overflowWrap: 'anywhere',
padding: 0,
border: '1px solid var(--slate5)',
borderBottomLeftRadius: '6px',
borderBottomRightRadius: '6px',
}}
>
<Tab.Pane eventKey="json" transition={false}>
<div className="w-100 preview-data-container" data-cy="preview-json-data-container">
<JSONTree
theme={theme}
data={queryPreviewData}
invertTheme={!darkMode}
collectionLimit={100}
hideRoot={true}
/>
</div>
</Tab.Pane>
<Tab.Pane eventKey="raw" transition={false}>
<div className={`p-3 raw-container preview-data-container`} data-cy="preview-raw-data-container">
{renderRawData()}
</div>
</Tab.Pane>
</Tab.Content>
</Row>
<span
data-cy={`preview-tab-${String(tab).toLowerCase()}`}
style={{ width: '100%' }}
className="rounded"
>
{tab}
</span>
</ListGroup.Item>
))}
</ListGroup>
</Col>
</Row>
</Tab.Container>
</div>
)}
</div>
<div className="preview-content">
<Tab.Container activeKey={key} onSelect={(k) => setKey(k)} defaultActiveKey="raw">
<div className="position-relative h-100">
{previewLoading && (
<center style={{ display: 'grid', placeItems: 'center' }} className="position-absolute w-100 h-100">
<div className="spinner-border text-azure" role="status"></div>
</center>
)}
<Tab.Content
style={{
overflowWrap: 'anywhere',
padding: 0,
border: '1px solid var(--slate5)',
height: '100%',
}}
>
<Tab.Pane eventKey="json" transition={false}>
<div className="w-100 preview-data-container" data-cy="preview-json-data-container">
<JSONTree
theme={theme}
data={queryPreviewData}
invertTheme={!darkMode}
collectionLimit={100}
hideRoot={true}
/>
</div>
</Tab.Pane>
<Tab.Pane eventKey="raw" transition={false}>
<div className={`p-3 raw-container preview-data-container`} data-cy="preview-raw-data-container">
{renderRawData()}
</div>
</Tab.Pane>
</Tab.Content>
</div>
</Tab.Container>
</div>

View file

@ -19,6 +19,7 @@ import { useSelectedQuery, useSelectedDataSource } from '@/_stores/queryPanelSto
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import SuccessNotificationInputs from './SuccessNotificationInputs';
import ParameterList from './ParameterList';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const QueryManagerBody = ({
@ -29,11 +30,13 @@ export const QueryManagerBody = ({
apps,
appDefinition,
setOptions,
activeTab,
}) => {
const { t } = useTranslation();
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const sampleDataSource = useSampleDataSource();
const paramListContainerRef = useRef(null);
const selectedQuery = useSelectedQuery();
const selectedDataSource = useSelectedDataSource();
@ -104,7 +107,43 @@ export const QueryManagerBody = ({
const currentValue = selectedQuery?.options?.[option] ?? false;
optionchanged(option, !currentValue);
};
const optionsChangedforParams = (newOptions) => {
setOptions(newOptions);
updateDataQuery(deepClone(newOptions));
};
const handleAddParameter = (newParameter) => {
const prevOptions = { ...options };
//check if paramname already used
if (!prevOptions?.parameters?.some((param) => param.name === newParameter.name)) {
const newOptions = {
...prevOptions,
parameters: [...(prevOptions?.parameters ?? []), newParameter],
};
optionsChangedforParams(newOptions);
}
};
const handleParameterChange = (index, updatedParameter) => {
const prevOptions = { ...options };
//check if paramname already used
if (!prevOptions?.parameters?.some((param, idx) => param.name === updatedParameter.name && index !== idx)) {
const updatedParameters = [...prevOptions.parameters];
updatedParameters[index] = updatedParameter;
optionsChangedforParams({ ...prevOptions, parameters: updatedParameters });
}
};
const handleParameterRemove = (index) => {
const prevOptions = { ...options };
const updatedParameters = prevOptions.parameters.filter((param, i) => index !== i);
optionsChangedforParams({ ...prevOptions, parameters: updatedParameters });
};
const [previewHeight, setPreviewHeight] = useState(40); //preview non expanded height
const calculatePreviewHeight = (height, previewPanelExpanded) => {
setPreviewHeight(previewPanelExpanded ? height : 40);
};
const renderDataSourcesList = () => {
return (
<div
@ -147,29 +186,41 @@ export const QueryManagerBody = ({
const renderQueryElement = () => {
return (
<div style={{ padding: '0 32px' }}>
<div>
<div
className={cx({
'disabled ': isVersionReleased,
})}
>
<ElementToRender
key={selectedQuery?.id}
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
selectedDataSource={selectedDataSource}
options={selectedQuery?.options}
optionsChanged={optionsChanged}
optionchanged={optionchanged}
currentState={currentState}
darkMode={darkMode}
isEditMode={true} // Made TRUE always to avoid setting default options again
queryName={queryName}
onBlur={handleBlur} // Applies only to textarea, text box, etc. where `optionchanged` is triggered for every character change.
/>
{renderTransformation()}
</div>
<div
className={cx({
'disabled ': isVersionReleased,
})}
>
<div ref={paramListContainerRef} style={{ marginBottom: '16px' }}>
{selectedQuery &&
(selectedDataSource?.kind === 'runjs' ||
selectedDataSource?.kind === 'runpy' ||
selectedDataSource?.kind === 'tooljetdb' ||
(selectedDataSource?.kind === 'restapi' && selectedDataSource?.type !== 'default')) && (
<ParameterList
parameters={options.parameters}
handleAddParameter={handleAddParameter}
handleParameterChange={handleParameterChange}
handleParameterRemove={handleParameterRemove}
currentState={currentState}
darkMode={darkMode}
containerRef={paramListContainerRef}
/>
)}
</div>
<ElementToRender
key={selectedQuery?.id}
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
selectedDataSource={selectedDataSource}
options={selectedQuery?.options}
optionsChanged={optionsChanged}
optionchanged={optionchanged}
currentState={currentState}
darkMode={darkMode}
isEditMode={true} // Made TRUE always to avoid setting default options again
queryName={queryName}
onBlur={handleBlur} // Applies only to textarea, text box, etc. where `optionchanged` is triggered for every character change.
/>
</div>
);
};
@ -179,7 +230,7 @@ export const QueryManagerBody = ({
return (
<div className="d-flex">
<div className={`form-label`}>{t('editor.queryManager.eventsHandler', 'Events')}</div>
<div className="query-manager-events pb-4 flex-grow-1">
<div className="query-manager-events pb-4">
<EventManager
sourceId={selectedQuery?.id}
eventSourceType="data_query" //check
@ -188,7 +239,7 @@ export const QueryManagerBody = ({
components={allComponents}
callerQueryId={selectedQueryId}
apps={apps}
popoverPlacement="top"
popoverPlacement="auto"
pages={
appDefinition?.pages
? Object.entries(appDefinition?.pages).map(([id, page]) => ({
@ -205,13 +256,13 @@ export const QueryManagerBody = ({
const renderQueryOptions = () => {
return (
<div style={{ padding: '0 32px' }}>
<div>
<div
className={cx(`d-flex pb-1`, {
'disabled ': isVersionReleased,
})}
>
<div className="form-label">{t('editor.queryManager.settings', 'Settings')}</div>
<div className="form-label">{t('editor.queryManager.settings', 'Triggers')}</div>
<div className="flex-grow-1">
{Object.keys(customToggles).map((toggle, index) => (
<CustomToggleFlag
@ -225,14 +276,16 @@ export const QueryManagerBody = ({
))}
</div>
</div>
<SuccessNotificationInputs
currentState={currentState}
options={options}
darkMode={darkMode}
optionchanged={optionchanged}
/>
<div className="d-flex">
<div className="form-label">{}</div>
<SuccessNotificationInputs
currentState={currentState}
options={options}
darkMode={darkMode}
optionchanged={optionchanged}
/>
</div>
{renderEventManager()}
<Preview darkMode={darkMode} />
</div>
);
};
@ -245,22 +298,40 @@ export const QueryManagerBody = ({
return '';
}
return (
<div className={cx('mt-2 d-flex px-4 mb-3', { 'disabled ': isVersionReleased })}>
<>
<div className="" ref={paramListContainerRef}>
{selectedQuery && (
<ParameterList
parameters={options.parameters}
handleAddParameter={handleAddParameter}
handleParameterChange={handleParameterChange}
handleParameterRemove={handleParameterRemove}
currentState={currentState}
darkMode={darkMode}
containerRef={paramListContainerRef}
/>
)}
</div>
<div
className={`d-flex query-manager-border-color hr-text-left py-2 form-label font-weight-500 change-data-source`}
className={cx('d-flex', { 'disabled ': isVersionReleased })}
style={{ marginBottom: '16px', marginTop: '12px' }}
>
Data Source
<div
className={`d-flex query-manager-border-color hr-text-left py-2 form-label font-weight-500 change-data-source`}
>
Source
</div>
<div className="d-flex align-items-end" style={{ width: '364px' }}>
<ChangeDataSource
dataSources={selectableDataSources}
value={selectedDataSource}
onChange={(newDataSource) => {
changeDataQuery(newDataSource);
}}
/>
</div>
</div>
<div className="d-flex flex-grow-1">
<ChangeDataSource
dataSources={selectableDataSources}
value={selectedDataSource}
onChange={(newDataSource) => {
changeDataQuery(newDataSource);
}}
/>
</div>
</div>
</>
);
};
@ -268,13 +339,20 @@ export const QueryManagerBody = ({
return (
<div
className={`row row-deck px-2 mt-0 query-details ${
selectedDataSource?.kind === 'tooljetdb' ? 'tooljetdb-query-details' : ''
}`}
className={`query-details ${selectedDataSource?.kind === 'tooljetdb' ? 'tooljetdb-query-details' : ''}`}
style={{ height: `calc(100% - ${previewHeight + 40}px)`, overflowY: 'auto' }} // 40px for preview header height
>
{selectedQuery?.data_source_id && selectedDataSource !== null ? renderChangeDataSource() : null}
{selectedDataSource === null || !selectedQuery ? renderDataSourcesList() : renderQueryElement()}
{selectedDataSource !== null ? renderQueryOptions() : null}
{selectedDataSource === null || !selectedQuery ? (
renderDataSourcesList()
) : (
<>
{selectedQuery?.data_source_id && activeTab === 1 && renderChangeDataSource()}
{activeTab === 1 && renderQueryElement()}
{activeTab === 2 && renderTransformation()}
{activeTab === 3 && renderQueryOptions()}
<Preview darkMode={darkMode} calculatePreviewHeight={calculatePreviewHeight} />
</>
)}
</div>
);
};

View file

@ -20,11 +20,9 @@ import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { Tooltip } from 'react-tooltip';
import { Button } from 'react-bootstrap';
import ParameterList from './ParameterList';
import { decodeEntities } from '@/_helpers/utils';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, setOptions }, ref) => {
export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, setActiveTab, activeTab }, ref) => {
const { renameQuery } = useDataQueriesActions();
const selectedQuery = useSelectedQuery();
const selectedDataSource = useSelectedDataSource();
@ -39,8 +37,6 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
shallow
);
const { updateDataQuery } = useDataQueriesActions();
useEffect(() => {
if (selectedQuery?.name) {
setShowCreateQuery(false);
@ -93,6 +89,15 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
console.log(error, data);
});
};
const tabs = [
{ id: 1, label: 'Setup' },
{
id: 2,
label: 'Transformation',
condition: selectedQuery?.kind !== 'runpy' && selectedQuery?.kind !== 'runjs',
},
{ id: 3, label: 'Settings' },
];
const renderRunButton = () => {
return (
@ -104,7 +109,7 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
>
<button
onClick={() => runQuery(editorRef, selectedQuery?.id, selectedQuery?.name, undefined, 'edit', {}, true)}
className={`border-0 default-secondary-button float-right1 ${buttonLoadingState(isLoading)}`}
className={`border-0 default-secondary-button ${buttonLoadingState(isLoading)}`}
data-cy="query-run-button"
disabled={isInDraft}
{...(isInDraft && {
@ -130,54 +135,19 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
if (selectedQuery === null || showCreateQuery) return;
return (
<>
{renderRunButton()}
<PreviewButton
onClick={previewButtonOnClick}
buttonLoadingState={buttonLoadingState}
isRunButtonLoading={isLoading}
/>
{renderRunButton()}
</>
);
};
const optionsChanged = (newOptions) => {
setOptions(newOptions);
updateDataQuery(deepClone(newOptions));
};
const handleAddParameter = (newParameter) => {
const prevOptions = { ...options };
//check if paramname already used
if (!prevOptions?.parameters?.some((param) => param.name === newParameter.name)) {
const newOptions = {
...prevOptions,
parameters: [...(prevOptions?.parameters ?? []), newParameter],
};
optionsChanged(newOptions);
}
};
const handleParameterChange = (index, updatedParameter) => {
const prevOptions = { ...options };
//check if paramname already used
if (!prevOptions?.parameters?.some((param, idx) => param.name === updatedParameter.name && index !== idx)) {
const updatedParameters = [...prevOptions.parameters];
updatedParameters[index] = updatedParameter;
optionsChanged({ ...prevOptions, parameters: updatedParameters });
}
};
const handleParameterRemove = (index) => {
const prevOptions = { ...options };
const updatedParameters = prevOptions.parameters.filter((param, i) => index !== i);
optionsChanged({ ...prevOptions, parameters: updatedParameters });
};
const paramListContainerRef = useRef(null);
return (
<div className="row header">
<div className="col font-weight-500">
<div className="row header" style={{ padding: '8px 16px' }}>
<div className="col font-weight-500 p-0">
{selectedQuery && (
<NameInput
onInput={executeQueryNameUpdation}
@ -186,24 +156,30 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
isDiabled={isVersionReleased}
/>
)}
<div
className="query-parameters-list col w-100 d-flex justify-content-center font-weight-500"
ref={paramListContainerRef}
>
{selectedQuery && (
<ParameterList
parameters={options.parameters}
handleAddParameter={handleAddParameter}
handleParameterChange={handleParameterChange}
handleParameterRemove={handleParameterRemove}
darkMode={darkMode}
containerRef={paramListContainerRef}
/>
)}
</div>
{selectedQuery && (
<div className="d-flex" style={{ marginBottom: '-15px', gap: '3px' }}>
{tabs.map(
(tab) =>
(tab.condition === undefined || tab.condition) && (
<p
key={tab.id}
className="m-0 d-flex align-items-center h-100"
onClick={() => setActiveTab(tab.id)}
style={{
borderBottom: activeTab === tab.id ? '2px solid #3E63DD' : '',
cursor: 'pointer',
padding: '0px 8px 6px 8px',
color: activeTab === tab.id ? 'var(--text-default)' : 'var(--text-placeholder)',
}}
>
{tab.label}
</p>
)
)}
</div>
)}
</div>
<div className="query-header-buttons me-3">{renderButtons()}</div>
<div className="query-header-buttons">{renderButtons()}</div>
</div>
);
});
@ -215,7 +191,7 @@ const PreviewButton = ({ buttonLoadingState, onClick, isRunButtonLoading }) => {
return (
<button
onClick={onClick}
className={`default-tertiary-button float-right1 ${buttonLoadingState(previewLoading && !isRunButtonLoading)}`}
className={`default-tertiary-button ${buttonLoadingState(previewLoading && !isRunButtonLoading)}`}
data-cy={'query-preview-button'}
>
<span className="query-preview-svg d-flex align-items-center query-icon-wrapper">

View file

@ -8,12 +8,12 @@ export default function SuccessNotificationInputs({ currentState, options, darkM
return <div className="mb-3"></div>;
}
return (
<div className="me-4 mb-3 mt-2 pt-1" style={{ paddingLeft: '112px' }}>
<div className="d-flex">
<label className="form-label" data-cy={'label-success-message-input'} style={{ width: 150 }}>
<div className="flex-grow-1" style={{ margin: '16px 0px' }}>
<div className="d-flex" style={{ marginBottom: '16px' }}>
<label className="form-label align-items-center" data-cy={'label-success-message-input'} style={{ width: 150 }}>
{t('editor.queryManager.successMessage', 'Message')}
</label>
<div className="flex-grow-1">
<div className="flex-grow-1" style={{ maxWidth: '460px' }}>
<CodeHinter
type="basic"
initialValue={options.successMessage}
@ -24,10 +24,14 @@ export default function SuccessNotificationInputs({ currentState, options, darkM
</div>
</div>
<div className="d-flex">
<label className="form-label" data-cy={'label-notification-duration-input'} style={{ width: 150 }}>
<label
className="form-label align-items-center"
data-cy={'label-notification-duration-input'}
style={{ width: 150 }}
>
{t('editor.queryManager.notificationDuration', 'duration (s)')}
</label>
<div className="flex-grow-1 query-manager-input-elem ">
<div className="flex-grow-1 query-manager-input-elem" style={{ maxWidth: '460px' }}>
<input
type="number"
disabled={!options.showSuccessNotification}

View file

@ -1,48 +1,111 @@
import React, { useState, useEffect } from 'react';
import { Popover, OverlayTrigger } from 'react-bootstrap';
import { Tab, ListGroup, Row, Col, Popover, OverlayTrigger } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import Select from '@/_ui/Select';
import { useLocalStorageState } from '@/_hooks/use-local-storage';
import _ from 'lodash';
import { CustomToggleSwitch } from './CustomToggleSwitch';
import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
const noop = () => {};
import { Button } from '@/_ui/LeftSidebar';
import Information from '@/_ui/Icon/solidIcons/Information';
import CodeHinter from '@/Editor/CodeEditor';
const noop = () => {};
export const Transformation = ({ changeOption, options, darkMode, queryId }) => {
const { t } = useTranslation();
const [lang, setLang] = React.useState(options?.transformationLanguage ?? 'javascript');
const defaultValue = {
javascript: `// write your code here
const defaultValue = {
javascript: `// write your code here
// return value will be set as data and the original data will be available as rawData
return data.filter(row => row.amount > 1000);
`,
python: `# write your code here
`,
python: `# write your code here
# return value will be set as data and the original data will be available as rawData
[row for row in data if row['amount'] > 1000]
`,
};
`,
};
const [enableTransformation, setEnableTransformation] = useState(() => options.enableTransformation);
const labelPopoverContent = (darkMode, t) => (
<Popover
id="transformation-popover-container"
className={`${darkMode && 'popover-dark-themed theme-dark dark-theme tj-dark-mode'} p-0`}
>
<p className="transformation-popover" data-cy="transformation-popover">
{t(
'editor.queryManager.transformation.transformationToolTip',
'Transformations can be enabled on queries to transform the query results. ToolJet allows you to transform the query results using two programming languages: JavaScript and Python'
)}
<br />
<a href="https://docs.tooljet.io/docs/tutorial/transformations" target="_blank" rel="noreferrer">
{t('globals.readDocumentation', 'Read documentation')}
</a>
.
</p>
</Popover>
);
const [state, setState] = useLocalStorageState('transformation', defaultValue);
function toggleEnableTransformation() {
setEnableTransformation((prev) => !prev);
changeOption('enableTransformation', !enableTransformation);
const getNonActiveTransformations = (activeLang) => {
switch (activeLang) {
case 'javascript':
return { python: defaultValue.python };
case 'python':
return { javascript: defaultValue.javascript };
default:
return {};
}
};
const EducativeLabel = ({ darkMode }) => {
const popoverContent = (
<Popover
id="transformation-popover-container"
className={`${darkMode && 'popover-dark-themed theme-dark dark-theme'} p-0`}
>
<div className={`transformation-popover card text-center ${darkMode && 'tj-dark-mode'}`}>
<img src="/assets/images/icons/copilot.svg" alt="AI copilot" height={64} width={64} />
<div className="d-flex flex-column card-body">
<h4 className="mb-2">ToolJet x OpenAI</h4>
<p className="mb-2">
<strong style={{ fontWeight: 700, color: '#3E63DD' }}>AI copilot</strong> helps you write your queries
faster. It uses OpenAI&apos;s GPT-3.5 to suggest queries based on your data.
</p>
<Button
onClick={() => window.open('https://docs.tooljet.com/docs/tooljet-copilot', '_blank')}
darkMode={darkMode}
size="sm"
classNames="default-secondary-button"
styles={{ width: '100%', fontSize: '12px', fontWeight: 700, borderColor: darkMode && 'transparent' }}
>
<Button.Content title="Read more" />
</Button>
</div>
</div>
</Popover>
);
return (
<div>
<OverlayTrigger
overlay={popoverContent}
rootClose
trigger="click"
placement="right"
container={document.getElementsByClassName('query-details')[0]}
>
<span style={{ cursor: 'pointer' }} data-cy="transformation-info-icon" className="lh-1">
<Information width={18} fill="#CCD1D5" style={{ position: 'absolute', left: '152px' }} />
</span>
</OverlayTrigger>
</div>
);
};
export const Transformation = ({ changeOption, options, darkMode, queryId }) => {
const [lang, setLang] = useState(options?.transformationLanguage ?? 'javascript');
const [enableTransformation, setEnableTransformation] = useState(options.enableTransformation);
const [state, setState] = useLocalStorageState('transformation', defaultValue);
const { t } = useTranslation();
useEffect(() => {
if (lang !== (options.transformationLanguage ?? 'javascript')) {
changeOption('transformationLanguage', lang);
changeOption('transformation', state[lang]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lang]);
@ -72,138 +135,90 @@ return data.filter(row => row.amount > 1000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryId]);
function getNonActiveTransformations(activeLang) {
switch (activeLang) {
case 'javascript':
return {
python: defaultValue.python,
};
case 'python':
return {
javascript: defaultValue.javascript,
};
default:
break;
}
}
const computeSelectStyles = (darkMode, width) => {
return {
...queryManagerSelectComponentStyle(darkMode, width),
control: (provided) => ({
...provided,
display: 'flex',
boxShadow: 'none',
backgroundColor: darkMode ? '#2b3547' : '#ffffff',
borderRadius: '0 6px 6px 0',
height: 32,
minHeight: 32,
borderWidth: '1px 1px 1px 0',
cursor: 'pointer',
borderColor: darkMode ? 'inherit' : ' #D7DBDF',
'&:hover': {
backgroundColor: darkMode ? '' : '#F8F9FA',
},
'&:active': {
backgroundColor: darkMode ? '' : '#F8FAFF',
borderColor: '#3E63DD',
borderWidth: '1px 1px 1px 1px',
boxShadow: '0px 0px 0px 2px #C6D4F9 ',
},
}),
};
const toggleEnableTransformation = () => {
const newEnableTransformation = !enableTransformation;
setEnableTransformation(newEnableTransformation);
changeOption('enableTransformation', newEnableTransformation);
};
const labelPopoverContent = (
<Popover
id="transformation-popover-container"
className={`${darkMode && 'popover-dark-themed theme-dark dark-theme tj-dark-mode'} p-0`}
>
<p className={`transformation-popover`} data-cy={`transformation-popover`}>
{t(
'editor.queryManager.transformation.transformationToolTip',
'Transformations can be enabled on queries to transform the query results. ToolJet allows you to transform the query results using two programming languages: JavaScript and Python'
)}
<br />
<a href="https://docs.tooljet.io/docs/tutorial/transformations" target="_blank" rel="noreferrer">
{t('globals.readDocumentation', 'Read documentation')}
</a>
.
</p>
</Popover>
);
return (
<div className="field transformation-editor">
<div className="align-items-center gap-2" style={{ display: 'flex', position: 'relative', height: '20px' }}>
<div className="d-flex flex-fill">
<OverlayTrigger
trigger="click"
placement="top"
rootClose
overlay={labelPopoverContent}
container={document.getElementsByClassName('query-details')[0]}
>
<span
className="color-slate9 font-weight-500 form-label"
data-cy={'label-query-transformation'}
style={{ textDecoration: 'underline 2px dashed', textDecorationColor: 'var(--slate8)' }}
>
{t('editor.queryManager.transformation.transformations', 'Transformations')}
</span>
</OverlayTrigger>
<div className="flex-grow-l">
<div className="align-items-center d-flex">
<div className="mb-0">
<span className="d-flex">
<CustomToggleSwitch
isChecked={enableTransformation}
toggleSwitchFunction={toggleEnableTransformation}
action="enableTransformation"
darkMode={darkMode}
dataCy={'transformation'}
/>
<span className="ps-1">Enable</span>
<div className="field transformation-editor">
<div className="align-items-center gap-2 d-flex" style={{ position: 'relative', height: '20px' }}>
<div className="d-flex flex-column">
<div className="mb-0">
<span className="d-flex">
<CustomToggleSwitch
isChecked={enableTransformation}
toggleSwitchFunction={toggleEnableTransformation}
action="enableTransformation"
darkMode={darkMode}
dataCy="transformation"
/>
<OverlayTrigger
trigger="click"
placement="bottom"
rootClose
overlay={labelPopoverContent(darkMode, t)}
container={document.getElementsByClassName('query-details')[0]}
>
<span
style={{ textDecoration: 'underline 2px dotted', textDecorationColor: 'var(--slate8)' }}
className="ps-1 text-default"
>
{t('editor.queryManager.transformation.enableTransformation', 'Enable transformation')}
</span>
</div>
<EducativeLabel darkMode={darkMode} />
</div>
<div></div>
</OverlayTrigger>
</span>
</div>
<div className="d-flex text-placeholder justify-content-end">
<p>Powered by AI copilot</p>
<EducativeLabel darkMode={darkMode} />
</div>
</div>
</div>
<br></br>
<div className="d-flex copilot-codehinter-wrap">
<div className="form-label"></div>
{enableTransformation && (
<div
className="transformation-container"
style={{ marginBottom: '20px', background: `${darkMode ? '#272822' : '#F8F9FA'}` }}
>
<br />
<div className={`d-flex copilot-codehinter-wrap ${!enableTransformation && 'read-only-codehinter'}`}>
{/* <div className="form-label"></div> */}
<div className="col flex-grow-1">
<div style={{ borderRadius: '6px', marginBottom: '20px', background: darkMode ? '#272822' : '#F8F9FA' }}>
<div className="py-3 px-3 d-flex justify-content-between copilot-section-header">
<div className="d-flex">
<div className="d-flex align-items-center border transformation-language-select-wrapper">
<span className="px-2">Language</span>
</div>
<Select
options={[
{ name: 'JavaScript', value: 'javascript' },
{ name: 'Python', value: 'python' },
]}
value={lang}
search={true}
onChange={(value) => {
setLang(value);
changeOption('transformationLanguage', value);
changeOption('transformation', state[value]);
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={computeSelectStyles(darkMode, 140)}
useCustomStyles={true}
/>
</div>
<Tab.Container
activeKey={lang}
onSelect={(value) => {
setLang(value);
changeOption('transformationLanguage', value);
changeOption('transformation', state[value]);
}}
defaultActiveKey="javascript"
>
<Row className="m-0">
<Col className="keys text-center d-flex align-items-center">
<ListGroup
className={`query-preview-list-group rounded ${darkMode ? 'dark' : ''}`}
variant="flush"
style={{ backgroundColor: '#ECEEF0', padding: '2px' }}
>
{['JavaScript', 'Python'].map((tab) => (
<ListGroup.Item
key={tab}
eventKey={tab.toLowerCase()}
style={{ minWidth: '74px', textAlign: 'center' }}
className="rounded"
disabled={!enableTransformation}
>
<span
data-cy={`preview-tab-${tab.toLowerCase()}`}
className="rounded"
style={{ width: '100%' }}
>
{tab}
</span>
</ListGroup.Item>
))}
</ListGroup>
</Col>
</Row>
</Tab.Container>
</div>
<div className="codehinter-border-bottom mx-3"></div>
<CodeHinter
@ -219,67 +234,12 @@ return data.filter(row => row.amount > 1000);
callgpt={noop}
isCopilotEnabled={false}
delayOnChange={false}
readOnly={!enableTransformation}
editable={enableTransformation}
/>
</div>
)}
</div>
</div>
);
};
const EducativeLabel = ({ darkMode }) => {
const popoverContent = (
<Popover
id={`transformation-popover-container`}
className={`${darkMode && 'popover-dark-themed theme-dark dark-theme'} p-0`}
>
<div className={`transformation-popover card text-center ${darkMode && 'tj-dark-mode'}`}>
<img src="/assets/images/icons/copilot.svg" alt="AI copilot" height={64} width={64} />
<div className="d-flex flex-column card-body">
<h4 className="mb-2">ToolJet x OpenAI</h4>
<p className="mb-2">
<strong style={{ fontWeight: 700, color: '#3E63DD' }}>AI copilot</strong> helps you write your queries
faster. It uses OpenAI&apos;s GPT-3.5 to suggest queries based on your data.
</p>
<Button
onClick={() => window.open('https://docs.tooljet.com/docs/tooljet-copilot', '_blank')}
darkMode={darkMode}
size="sm"
classNames="default-secondary-button"
styles={{ width: '100%', fontSize: '12px', fontWeight: 700, borderColor: darkMode && 'transparent' }}
>
<Button.Content title={'Read more'} />
</Button>
</div>
</div>
</Popover>
);
const title = () => {
return (
<>
Powered by <strong style={{ fontWeight: 700, color: '#3E63DD' }}> &nbsp;AI copilot</strong>
</>
);
};
return (
<div className="d-flex align-items-center ">
<Button.UnstyledButton styles={{ height: '28px' }} darkMode={darkMode} classNames="mx-1 copilot-toggle">
<Button.Content title={title} iconSrc={'assets/images/icons/flash.svg'} direction="left" />
</Button.UnstyledButton>
<OverlayTrigger
overlay={popoverContent}
rootClose
trigger="click"
placement="right"
container={document.getElementsByClassName('query-details')[0]}
>
<span style={{ cursor: 'pointer' }} data-cy={`transformation-info-icon`} className="lh-1">
<Information width={18} fill={'var(--indigo9)'} />
</span>
</OverlayTrigger>
</div>
);
};

View file

@ -83,10 +83,10 @@ export default ({
/>
</div>
) : (
<div className="d-flex mb-2" style={{ maxHeight: '32px', marginLeft: '5px' }}>
<div className="d-flex mb-2" style={{ maxHeight: '32px', marginTop: '4px' }}>
<ButtonSolid variant="ghostBlue" size="sm" onClick={() => addNewKeyValuePair(paramType)}>
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
&nbsp;&nbsp;{t('editor.inspector.eventManager.addKeyValueParam', 'Add more')}
{t('editor.inspector.eventManager.addKeyValueParam', 'Add more')}
</ButtonSolid>
</div>
)}

View file

@ -140,14 +140,13 @@ class Restapi extends React.Component {
const queryName = this.props.queryName;
const currentValue = { label: options.method?.toUpperCase(), value: options.method };
return (
<div className={`d-flex`}>
<div className="form-label flex-shrink-0">Request</div>
<div className="flex-grow-1">
<div className={`d-flex flex-column`}>
{this.props.selectedDataSource?.scope == 'global' && <div className="form-label flex-shrink-0"></div>}{' '}
<div className="flex-grow-1 overflow-hidden">
<div className="rest-api-methods-select-element-container">
<div className={`me-2`} style={{ width: '90px', height: '32px' }}>
<label className="font-weight-bold color-slate12">Method</label>
<label className="font-weight-medium color-slate12">Method</label>
<Select
options={[
{ label: 'GET', value: 'get' },
@ -170,15 +169,12 @@ class Restapi extends React.Component {
</div>
<div className={`field w-100 rest-methods-url`}>
<div className="font-weight-bold color-slate12">URL</div>
<div className="font-weight-medium color-slate12">URL</div>
<div className="d-flex">
{dataSourceURL && (
<BaseUrl theme={this.props.darkMode ? 'monokai' : 'default'} dataSourceURL={dataSourceURL} />
)}
<div
className={`flex-grow-1 rest-api-url-codehinter ${dataSourceURL ? 'url-input-group' : ''}`}
style={{ width: '530px' }}
>
<div className={`flex-grow-1 ${dataSourceURL ? 'url-input-group' : ''}`}>
<CodeHinter
type="basic"
initialValue={options.url}
@ -193,21 +189,20 @@ class Restapi extends React.Component {
</div>
</div>
</div>
<div className={`query-pane-restapi-tabs`}>
<Tabs
theme={this.props.darkMode ? 'monokai' : 'default'}
options={this.state.options}
onChange={this.handleChange}
onJsonBodyChange={this.handleJsonBodyChanged}
removeKeyValuePair={this.removeKeyValuePair}
addNewKeyValuePair={this.addNewKeyValuePair}
darkMode={this.props.darkMode}
componentName={queryName}
bodyToggle={this.state.options.body_toggle}
setBodyToggle={this.onBodyToggleChanged}
/>
</div>
</div>
<div className={`query-pane-restapi-tabs`}>
<Tabs
theme={this.props.darkMode ? 'monokai' : 'default'}
options={this.state.options}
onChange={this.handleChange}
onJsonBodyChange={this.handleJsonBodyChanged}
removeKeyValuePair={this.removeKeyValuePair}
addNewKeyValuePair={this.addNewKeyValuePair}
darkMode={this.props.darkMode}
componentName={queryName}
bodyToggle={this.state.options.body_toggle}
setBodyToggle={this.onBodyToggleChanged}
/>
</div>
</div>
);

View file

@ -46,7 +46,7 @@ export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
return (
<div className="row tj-db-field-wrapper">
<div className="tab-content-wrapper mt-2 d-flex">
<div className="tab-content-wrapper d-flex" style={{ marginTop: '16px' }}>
<label className="form-label" data-cy="label-column-filter">
Columns
</label>

View file

@ -133,7 +133,7 @@ const RenderFilterFields = ({
return (
<div className="mt-1 row-container w-100">
<div className="d-flex fields-container">
<div className="field col-4">
<div className="field" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select column"
@ -143,7 +143,7 @@ const RenderFilterFields = ({
width="auto"
/>
</div>
<div className="field col mx-1 col-4">
<div className="field col mx-1" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select operation"
@ -153,7 +153,7 @@ const RenderFilterFields = ({
width="auto"
/>
</div>
<div className="field col-4">
<div className="field" style={{ width: '32%' }}>
{operator === 'is' ? (
<Select
useMenuPortal={true}
@ -173,7 +173,10 @@ const RenderFilterFields = ({
/>
)}
</div>
<div className="col-1 cursor-pointer m-1 mr-2">
<div
className="col-1 cursor-pointer m-1 d-flex align-item-center justify-content-center"
style={{ width: '4%' }}
>
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"

View file

@ -298,7 +298,7 @@ const RenderFilterFields = ({
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container ">
<div className="field col-4">
<div className="field" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select column"
@ -310,7 +310,7 @@ const RenderFilterFields = ({
width={'auto'}
/>
</div>
<div className="field col-4 mx-1">
<div className="field mx-1" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select operation"
@ -320,7 +320,7 @@ const RenderFilterFields = ({
width={'auto'}
/>
</div>
<div className="field col-4">
<div className="field" style={{ width: '32%' }}>
{operator === 'is' ? (
<Select
useMenuPortal={true}
@ -340,7 +340,10 @@ const RenderFilterFields = ({
/>
)}
</div>
<div className="col-1 cursor-pointer m-1 mr-2">
<div
className="col-1 cursor-pointer m-1 d-flex align-item-center justify-content-center"
style={{ width: '4%' }}
>
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"

View file

@ -185,7 +185,7 @@ const RenderFilterFields = ({
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container ">
<div className="field col-4">
<div className="field" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select column"
@ -195,7 +195,7 @@ const RenderFilterFields = ({
width="auto"
/>
</div>
<div className="field col-4 mx-1">
<div className="field mx-1" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select operation"
@ -205,7 +205,7 @@ const RenderFilterFields = ({
width="auto"
/>
</div>
<div className="field col-4">
<div className="field" style={{ width: '32%' }}>
{operator === 'is' ? (
<Select
useMenuPortal={true}
@ -225,7 +225,10 @@ const RenderFilterFields = ({
/>
)}
</div>
<div className="col-1 cursor-pointer m-1 mr-2">
<div
className="col-1 cursor-pointer m-1 d-flex align-item-center justify-content-center"
style={{ width: '4%' }}
>
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"

View file

@ -23,6 +23,13 @@ const QueryManager = ({ mode, appId, darkMode, apps, allComponents, appDefinitio
const selectedQuery = useSelectedQuery();
const { setSelectedDataSource, setQueryToBeRun } = useQueryPanelActions();
const [options, setOptions] = useState({});
const [activeTab, setActiveTab] = useState(1);
useEffect(() => {
if (selectedQuery?.kind == 'runjs' || selectedQuery?.kind == 'runpy') {
setActiveTab(1);
}
}, [selectedQuery?.id]);
useEffect(() => {
setOptions(selectedQuery?.options || {});
@ -66,6 +73,8 @@ const QueryManager = ({ mode, appId, darkMode, apps, allComponents, appDefinitio
editorRef={editorRef}
appId={appId}
setOptions={setOptions}
setActiveTab={setActiveTab}
activeTab={activeTab}
/>
<CodeHinterContext.Provider
value={{
@ -86,6 +95,7 @@ const QueryManager = ({ mode, appId, darkMode, apps, allComponents, appDefinitio
appId={appId}
appDefinition={appDefinition}
setOptions={setOptions}
activeTab={activeTab}
/>
</CodeHinterContext.Provider>
</div>

View file

@ -34,19 +34,19 @@ export const customToggles = {
runOnPageLoad: {
dataCy: 'run-on-app-load',
action: 'runOnPageLoad',
label: 'Run this query on application load?',
label: 'Run this query on application load',
translatedLabel: 'editor.queryManager.runQueryOnApplicationLoad',
},
requestConfirmation: {
dataCy: 'confirmation-before-run',
action: 'requestConfirmation',
label: 'Request confirmation before running query?',
label: 'Request confirmation before running query',
translatedLabel: 'editor.queryManager.confirmBeforeQueryRun',
},
showSuccessNotification: {
dataCy: 'notification-on-success',
action: 'showSuccessNotification',
label: 'Show notification on success?',
label: 'Show notification on success',
translatedLabel: 'editor.queryManager.notificationOnSuccess',
},
};

View file

@ -209,7 +209,7 @@ const FilterandSortPopup = ({ darkMode, selectedDataSources, onFilterDatasources
data-tooltip-content="Show sort/filter"
data-cy={`query-filter-button`}
>
<Filter width="13" height="13" fill="var(--slate12)" />
<Filter width="14" height="14" fill="var(--icons-default)" />
{selectedDataSources.length > 0 && <div className="notification-dot"></div>}
</button>
<Tooltip id="tooltip-for-open-filter" className="tooltip" />

View file

@ -93,35 +93,27 @@ export const QueryDataPane = ({ darkMode, fetchDataQueries, editorRef, appId, to
<div className="data-pane">
<div className={`queries-container ${darkMode && 'theme-dark'} d-flex flex-column h-100`}>
<div className="queries-header row d-flex align-items-center justify-content-between">
<div className="col-auto d-flex">
<button
onClick={toggleQueryEditor}
className="btn-query-panel-header"
data-tooltip-id="tooltip-for-query-panel-header-btn"
data-tooltip-content="Hide query panel"
>
<Minimize width="14" height="14" viewBox="0 0 18 20" stroke="var(--slate12)" />
</button>
<button
onClick={() => {
showSearchBox && setSearchTermForFilters('');
setShowSearchBox((showSearchBox) => !showSearchBox);
}}
className={cx('btn-query-panel-header mx-1', {
active: showSearchBox,
})}
data-tooltip-id="tooltip-for-query-panel-header-btn"
data-tooltip-content="Open quick search"
data-cy="query-search-button"
>
<Search width="14" height="14" fill="var(--slate12)" />
</button>
<div className="col-auto d-flex" style={{ gap: '2px' }}>
<FilterandSortPopup
onFilterDatasourcesChange={handleFilterDatasourcesChange}
selectedDataSources={dataSourcesForFilters}
clearSelectedDataSources={() => setDataSourcesForFilters([])}
darkMode={darkMode}
/>
<button
onClick={() => {
showSearchBox && setSearchTermForFilters('');
setShowSearchBox((showSearchBox) => !showSearchBox);
}}
className={cx('btn-query-panel-header', {
active: showSearchBox,
})}
data-tooltip-id="tooltip-for-query-panel-header-btn"
data-tooltip-content="Open quick search"
data-cy="query-search-button"
>
<Search width="14" height="14" fill="var(--icons-default)" />
</button>
<Tooltip id="tooltip-for-query-panel-header-btn" className="tooltip" />
</div>
<AddDataSourceButton darkMode={darkMode} />
@ -260,11 +252,10 @@ const AddDataSourceButton = ({ darkMode, disabled: _disabled }) => {
}
setShowMenu((show) => !show);
}}
className="px-1 pe-3 ps-2 gap-0"
style={{ height: '28px', width: '28px', padding: '0px' }}
data-cy={`show-ds-popover-button`}
>
<Plus style={{ height: '16px' }} />
Add
<Plus style={{ height: '14px' }} fill="var(--icons-strong)" />
</ButtonSolid>
</span>
</OverlayTrigger>

View file

@ -115,7 +115,7 @@ const QueryPanel = ({
let height = (clientY / window.innerHeight) * 100,
maxLimitReached = false;
if (height > 95) {
if (height > 94) {
height = 30;
maxLimitReached = true;
}
@ -155,32 +155,29 @@ const QueryPanel = ({
return (
<div className={cx({ 'dark-theme theme-dark': darkMode })}>
<div
className="query-pane"
className={`query-pane ${isExpanded ? 'expanded' : 'collapsed'}`}
style={{
height: 40,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
zIndex: 1,
}}
>
<div
style={{ width: '288px', padding: '5px 12px' }}
className="d-flex justify-content- border-end align-items-center"
role="button"
onClick={toggleQueryEditor}
>
<ButtonSolid
variant="ghostBlack"
size="sm"
className="gap-0 p-2 me-2"
data-tooltip-id="tooltip-for-query-panel-footer-btn"
data-tooltip-content="Show query panel"
<div style={{ width: '288px', padding: '5px 12px' }} className="d-flex justify-content align-items-center">
<button
className="mb-0 font-weight-500 text-dark select-none query-manager-toggle-button"
onClick={toggleQueryEditor}
>
<Maximize stroke="var(--slate9)" style={{ height: '14px', width: '14px' }} viewBox={null} />
</ButtonSolid>
<h5 className="mb-0 font-weight-500 cursor-pointer" onClick={toggleQueryEditor}>
Query Manager
</h5>
{isExpanded ? 'Collapse' : 'Expand'}
</button>
<div className="vr" />
<button
onClick={toggleQueryEditor}
className="mb-0 font-weight-500 text-dark select-none query-manager-toggle-button"
>
Queries
</button>
</div>
</div>
<div
@ -191,6 +188,9 @@ const QueryPanel = ({
style={{
height: `calc(100% - ${isExpanded ? height : 100}%)`,
cursor: isDragging || isTopOfQueryPanel ? 'row-resize' : 'default',
...(!isExpanded && {
border: 'none',
}),
}}
>
<div className="row main-row">
@ -203,18 +203,16 @@ const QueryPanel = ({
/>
<div className="query-definition-pane-wrapper">
<div className="query-definition-pane">
<div>
<QueryManager
toggleQueryEditor={toggleQueryEditor}
dataQueries={dataQueries}
dataQueriesChanged={updateDataQueries}
appId={appId}
darkMode={darkMode}
allComponents={allComponents}
appDefinition={appDefinition}
editorRef={editorRef}
/>
</div>
<QueryManager
toggleQueryEditor={toggleQueryEditor}
dataQueries={dataQueries}
dataQueriesChanged={updateDataQueries}
appId={appId}
darkMode={darkMode}
allComponents={allComponents}
appDefinition={appDefinition}
editorRef={editorRef}
/>
</div>
</div>
</div>

View file

@ -5,18 +5,19 @@ import { ItemTypes } from './editorConstants';
import { DraggableBox } from './DraggableBox';
import update from 'immutability-helper';
import _, { isEmpty } from 'lodash';
import { componentTypes } from './WidgetManager/components';
import {
addNewWidgetToTheEditor,
onComponentOptionChanged,
onComponentOptionsChanged,
isPDFSupported,
calculateMoveableBoxHeight,
} from '@/_helpers/appUtils';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { toast } from 'react-hot-toast';
import { restrictedWidgetsObj } from '@/Editor/WidgetManager/restrictedWidgetsConfig';
import { getCurrentState } from '@/_stores/currentStateStore';
import { shallow } from 'zustand/shallow';
import { componentTypes } from './WidgetManager/components';
import { useEditorStore } from '@/_stores/editorStore';
@ -596,6 +597,9 @@ export const SubContainer = ({
gridWidth={gridWidth}
isGhostComponent={key === 'resizingComponentId'}
mode={mode}
propertiesDefinition={box?.component?.definition?.properties}
stylesDefinition={box?.component?.definition?.styles}
componentType={box?.component?.component}
>
<DraggableBox
onComponentClick={onComponentClick}
@ -703,6 +707,9 @@ const SubWidgetWrapper = ({
isResizing,
isGhostComponent,
mode,
stylesDefinition,
propertiesDefinition,
componentType,
}) => {
const { layouts } = widget;
@ -728,9 +735,14 @@ const SubWidgetWrapper = ({
let width = (canvasWidth * layoutData.width) / 43;
width = width > canvasWidth ? canvasWidth : width; //this handles scenarios where the width is set more than canvas for older components
const { label = { value: null } } = propertiesDefinition ?? {};
const styles = {
width: width + 'px',
height: isComponentVisible() ? layoutData.height + 'px' : '10px',
height: isComponentVisible()
? calculateMoveableBoxHeight(componentType, layoutData, stylesDefinition, label) + 'px'
: '10px',
transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`,
...(isGhostComponent ? { opacity: 0.5 } : {}),
};

View file

@ -891,7 +891,7 @@ class ViewerComponent extends React.Component {
>
<Confirm
show={queryConfirmationList.length > 0}
message={'Do you want to run this query?'}
message={'Do you want to run this query'}
onConfirm={(queryConfirmationData) =>
onQueryConfirmOrCancel(this.getViewerRef(), queryConfirmationData, true, 'view')
}

View file

@ -2,6 +2,9 @@ import React from 'react';
import WidgetIcon from '@/../assets/images/icons/widgets';
import { useTranslation } from 'react-i18next';
const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect'];
const NEW_WIDGETS = ['ToggleSwitchV2', 'DropdownV2', 'MultiselectV2'];
const WidgetBox = ({ component, darkMode }) => {
const { t } = useTranslation();
return (
@ -12,8 +15,8 @@ const WidgetBox = ({ component, darkMode }) => {
style={{ height: '100%' }}
data-cy={`widget-list-box-${component.displayName.toLowerCase().replace(/\s+/g, '-')}`}
>
{component.component == 'ToggleSwitch' && <p className="widget-version-old-identifier">Lgcy</p>}
{component.component == 'ToggleSwitchV2' && <p className="widget-version-new-identifier">New</p>}
{LEGACY_WIDGETS.includes(component.component) && <p className="widget-version-old-identifier">Lgcy</p>}
{NEW_WIDGETS.includes(component.component) && <p className="widget-version-new-identifier">New</p>}
<center>
<div
className="widget-svg-container"

View file

@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { SearchBox } from '@/_components';
import { LEGACY_ITEMS } from './WidgetManager/constants';
export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel, darkMode, disabled }) {
const [filteredComponents, setFilteredComponents] = useState(componentTypes);
@ -110,7 +111,7 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
'Multiselect',
'RichTextEditor',
'Checkbox',
'Radio-button',
'RadioButton',
'Datepicker',
'DateRangePicker',
'FilePicker',
@ -118,14 +119,12 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
];
const integrationItems = ['Map'];
const layoutItems = ['Container', 'Listview', 'Tabs', 'Modal'];
const legacyItems = ['ToggleSwitchLegacy'];
filteredComponents.forEach((f) => {
if (searchQuery) allWidgets.push(f);
if (commonItems.includes(f.name)) commonSection.items.push(f);
if (formItems.includes(f.name)) formSection.items.push(f);
else if (integrationItems.includes(f.name)) integrationSection.items.push(f);
else if (legacyItems.includes(f.name)) legacySection.items.push(f);
else if (LEGACY_ITEMS.includes(f.name)) legacySection.items.push(f);
else if (layoutItems.includes(f.name)) layoutsSection.items.push(f);
else otherSection.items.push(f);
});

View file

@ -43,7 +43,7 @@ export const dividerConfig = {
events: [],
styles: {
visibility: { value: '{{true}}' },
dividerColor: { value: '#3e525b' },
dividerColor: { value: '#000000' },
},
},
};

View file

@ -1,6 +1,6 @@
export const dropdownConfig = {
name: 'Dropdown',
displayName: 'Dropdown',
name: 'DropdownLegacy',
displayName: 'Dropdown (Legacy)',
description: 'Single item selector',
defaultSize: {
width: 8,

View file

@ -0,0 +1,326 @@
export const dropdownV2Config = {
name: 'Dropdown',
displayName: 'Dropdown',
description: 'Single item selector',
defaultSize: {
width: 10,
height: 40,
},
component: 'DropdownV2',
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
validation: {
mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
customRule: {
type: 'code',
displayName: 'Custom validation',
placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
},
},
properties: {
label: {
type: 'code',
displayName: 'Label',
validation: {
schema: { type: 'string' },
defaultValue: 'Select',
},
accordian: 'Data',
},
placeholder: {
type: 'code',
displayName: 'Placeholder',
validation: {
schema: { type: 'string' },
defaultValue: 'Select an option',
},
accordian: 'Data',
},
advanced: {
type: 'toggle',
displayName: 'Dynamic options',
validation: {
schema: { type: 'boolean' },
},
accordian: 'Options',
},
value: {
type: 'code',
displayName: 'Default value',
conditionallyRender: {
key: 'advanced',
value: false,
},
validation: {
schema: {
type: 'union',
schemas: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
},
},
accordian: 'Options',
},
schema: {
type: 'code',
displayName: 'Schema',
conditionallyRender: {
key: 'advanced',
value: true,
},
accordian: 'Options',
},
optionsLoadingState: {
type: 'toggle',
displayName: 'Options loading state',
validation: {
schema: { type: 'boolean' },
},
accordian: 'Options',
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
tooltip: {
type: 'code',
displayName: 'Tooltip',
validation: {
schema: { type: 'string' },
defaultValue: 'Enter tooltip text',
},
section: 'additionalActions',
placeholder: 'Enter tooltip text',
},
},
events: {
onSelect: { displayName: 'On select' },
onSearchTextChanged: { displayName: 'On search text changed' },
onFocus: { displayName: 'On focus' },
onBlur: { displayName: 'On blur' },
},
styles: {
labelColor: {
type: 'color',
displayName: 'Color',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
},
alignment: {
type: 'switch',
displayName: 'Alignment',
validation: { schema: { type: 'string' }, defaultValue: 'side' },
options: [
{ displayName: 'Side', value: 'side' },
{ displayName: 'Top', value: 'top' },
],
accordian: 'label',
},
direction: {
type: 'switch',
displayName: 'Direction',
validation: { schema: { type: 'string' }, defaultValue: 'left' },
showLabel: false,
isIcon: true,
options: [
{ displayName: 'alignleftinspector', value: 'left', iconName: 'alignleftinspector' },
{ displayName: 'alignrightinspector', value: 'right', iconName: 'alignrightinspector' },
],
accordian: 'label',
isFxNotRequired: true,
},
labelWidth: {
type: 'slider',
displayName: 'Width',
accordian: 'label',
conditionallyRender: {
key: 'alignment',
value: 'side',
},
isFxNotRequired: true,
},
auto: {
type: 'checkbox',
displayName: 'auto',
showLabel: false,
validation: { schema: { type: 'boolean' } },
accordian: 'label',
conditionallyRender: {
key: 'alignment',
value: 'side',
},
isFxNotRequired: true,
},
fieldBackgroundColor: {
type: 'color',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
fieldBorderColor: {
type: 'color',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
type: 'color',
displayName: 'Accent',
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
selectedTextColor: {
type: 'color',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
type: 'color',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
accordian: 'field',
},
icon: {
type: 'icon',
displayName: 'Icon',
validation: { schema: { type: 'string' }, defaultValue: 'IconHome2' },
accordian: 'field',
visibility: false,
},
iconColor: {
type: 'color',
displayName: '',
showLabel: false,
validation: {
schema: { type: 'string' },
defaultValue: '#6A727C',
},
accordian: 'field',
},
fieldBorderRadius: {
type: 'input',
displayName: 'Border radius',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: '6' },
accordian: 'field',
},
boxShadow: {
type: 'boxShadow',
displayName: 'Box shadow',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: '0px 0px 0px 0px #00000040',
},
accordian: 'field',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'container',
},
},
exposedVariables: {
searchText: '',
label: 'Select',
},
actions: [
{
handle: 'selectOption',
displayName: 'Select option',
params: [{ handle: 'select', displayName: 'Select' }],
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }],
},
{
handle: 'clear',
displayName: 'Clear',
},
{
handle: 'setLoading',
displayName: 'Set loading',
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set disable',
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
validation: {
mandatory: { value: '{{false}}' },
customRule: { value: null },
},
properties: {
advanced: { value: `{{false}}` },
schema: {
value:
"{{[\t{label: 'option1',value: '1',disable: {value: false },visible: {value:true },default: {value: false} },{label: 'option2',value: '2',disable: {value: false },visible:{value: true},default: {value: true} },{label: 'option3',value: '3',disable: {value: false },visible: {value:true },default: {value: false} }\t]}}",
},
options: {
value:
"{{[\t{label: 'option1',value: '1',disable: {value: false },visible: {value:true },default: {value: false} },{label: 'option2',value: '2',disable: {value: false },visible:{value: true},default: {value: true} },{label: 'option3',value: '3',disable: {value: false },visible: {value:true },default: {value: false} }\t]}}",
},
label: { value: 'Select' },
value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },
optionVisibility: { value: '{{[true, true, true]}}' },
optionDisable: { value: '{{[false, false, false]}}' },
tooltip: { value: '' },
},
events: [],
styles: {
labelColor: { value: '#1B1F24' },
labelWidth: { value: '33' },
auto: { value: '{{true}}' },
fieldBorderRadius: { value: '6' },
selectedTextColor: { value: '#1B1F24' },
fieldBorderColor: { value: '#CCD1D5' },
errTextColor: { value: '#D72D39' },
fieldBackgroundColor: { value: '#fff' },
direction: { value: 'left' },
alignment: { value: 'side' },
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000090' },
icon: { value: 'IconHome2' },
iconVisibility: { value: false },
iconColor: { value: '#6A727C' },
accentColor: { value: '#4368E3' },
},
},
};

View file

@ -17,7 +17,9 @@ import { textConfig } from './text';
import { imageConfig } from './image';
import { containerConfig } from './container';
import { dropdownConfig } from './dropdown';
import { dropdownV2Config } from './dropdownV2';
import { multiselectConfig } from './multiselect';
import { multiselectV2Config } from './multiselectV2';
import { richtextareaConfig } from './richtextarea';
import { mapConfig } from './map';
import { qrscannerConfig } from './qrscanner';
@ -71,8 +73,10 @@ export {
textConfig,
imageConfig,
containerConfig,
dropdownConfig,
dropdownConfig, //!Depreciated
dropdownV2Config,
multiselectConfig,
multiselectV2Config, //!Depreciated
richtextareaConfig,
mapConfig,
qrscannerConfig,

View file

@ -1,6 +1,6 @@
export const multiselectConfig = {
name: 'Multiselect',
displayName: 'Multiselect',
name: 'MultiselectLegacy',
displayName: 'Multiselect (Legacy)',
description: 'Multiple item selector',
defaultSize: {
width: 12,

View file

@ -0,0 +1,351 @@
export const multiselectV2Config = {
name: 'Multiselect',
displayName: 'Multiselect',
description: 'Multiple item selector',
defaultSize: {
width: 10,
height: 40,
},
component: 'MultiselectV2',
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
validation: {
mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
customRule: {
type: 'code',
displayName: 'Custom validation',
placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
},
},
actions: [
{
handle: 'selectOptions',
displayName: 'Select Options',
params: [
{
handle: 'option',
displayName: 'Option',
},
],
},
{
handle: 'deselectOptions',
displayName: 'Deselect Options',
params: [
{
handle: 'option',
displayName: 'Option',
},
],
},
{
handle: 'clear',
displayName: 'Clear',
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set loading',
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set disable',
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
},
],
properties: {
label: {
type: 'code',
displayName: 'Label',
validation: {
schema: { type: 'string' },
defaultValue: 'Label',
},
accordian: 'Data',
},
placeholder: {
type: 'code',
displayName: 'Placeholder',
validation: {
schema: { type: 'string' },
defaultValue: 'Select the options',
},
accordian: 'Data',
},
advanced: {
type: 'toggle',
displayName: 'Dynamic options',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
accordian: 'Options',
},
value: {
type: 'code',
displayName: 'Default value',
conditionallyRender: {
key: 'advanced',
value: false,
},
validation: {
schema: {
type: 'union',
schemas: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
},
},
accordian: 'Options',
},
schema: {
type: 'code',
displayName: 'Schema',
conditionallyRender: {
key: 'advanced',
value: true,
},
accordian: 'Options',
},
showAllOption: {
type: 'toggle',
displayName: 'Enable select all option',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
accordian: 'Options',
},
optionsLoadingState: {
type: 'toggle',
displayName: 'Options loading state',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
accordian: 'Options',
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
tooltip: {
type: 'code',
displayName: 'Tooltip',
validation: { schema: { type: 'string' }, defaultValue: '' },
section: 'additionalActions',
placeholder: 'Enter tooltip text',
},
},
events: {
onSelect: { displayName: 'On select' },
onSearchTextChanged: { displayName: 'On search text changed' },
onFocus: { displayName: 'On focus' },
onBlur: { displayName: 'On blur' },
},
styles: {
labelColor: {
type: 'color',
displayName: 'Color',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
},
alignment: {
type: 'switch',
displayName: 'Alignment',
validation: { schema: { type: 'string' }, defaultValue: 'side' },
options: [
{ displayName: 'Side', value: 'side' },
{ displayName: 'Top', value: 'top' },
],
accordian: 'label',
},
direction: {
type: 'switch',
displayName: 'Direction',
validation: { schema: { type: 'string' }, defaultValue: 'left' },
showLabel: false,
isIcon: true,
options: [
{ displayName: 'alignleftinspector', value: 'left', iconName: 'alignleftinspector' },
{ displayName: 'alignrightinspector', value: 'right', iconName: 'alignrightinspector' },
],
accordian: 'label',
isFxNotRequired: true,
},
labelWidth: {
type: 'slider',
displayName: 'Width',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } },
accordian: 'label',
conditionallyRender: {
key: 'alignment',
value: 'side',
},
isFxNotRequired: true,
},
auto: {
type: 'checkbox',
displayName: 'auto',
showLabel: false,
validation: { schema: { type: 'boolean' } },
accordian: 'label',
conditionallyRender: {
key: 'alignment',
value: 'side',
},
isFxNotRequired: true,
},
fieldBackgroundColor: {
type: 'color',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
fieldBorderColor: {
type: 'color',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
type: 'color',
displayName: 'Accent',
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
selectedTextColor: {
type: 'color',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
type: 'color',
displayName: 'Error Text',
validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
accordian: 'field',
},
icon: {
type: 'icon',
displayName: 'Icon',
validation: { schema: { type: 'string' }, defaultValue: 'IconHome2' },
accordian: 'field',
visibility: false,
},
iconColor: {
type: 'color',
displayName: 'Icon color',
validation: {
schema: { type: 'string' },
defaultValue: '#6A727C',
},
accordian: 'field',
showLabel: false,
},
fieldBorderRadius: {
type: 'input',
displayName: 'Border radius',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: '6' },
accordian: 'field',
},
boxShadow: {
type: 'boxShadow',
displayName: 'Box Shadow',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: '0px 0px 0px 0px #00000090',
},
accordian: 'field',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'container',
},
},
exposedVariables: {
searchText: '',
},
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
validation: {
mandatory: { value: false },
customRule: { value: null },
},
properties: {
label: { value: 'Select' },
// value: { value: '{{["1","2"]}}' },
values: { value: '{{["1","2"]}}' },
advanced: { value: `{{false}}` },
showAllOption: { value: '{{false}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select the options' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },
schema: {
value:
"{{[\t{label: 'option1',value: '1',disable: {value: false },visible: {value:true },default: {value: false} },{label: 'option2',value: '2',disable: {value: false },visible:{value: true},default: {value: true} },{label: 'option3',value: '3',disable: {value: false },visible: {value:true },default: {value: false} }\t]}}",
},
options: {
value:
"{{[\t{label: 'option1',value: '1',disable: {value: false },visible: {value:true },default: {value: false} },{label: 'option2',value: '2',disable: {value: false },visible:{value: true},default: {value: true} },{label: 'option3',value: '3',disable: {value: false },visible: {value:true },default: {value: false} }\t]}}",
},
tooltip: { value: '' },
},
events: [],
styles: {
labelColor: { value: '#1B1F24' },
labelWidth: { value: '33' },
auto: { value: '{{true}}' },
fieldBorderRadius: { value: '6' },
selectedTextColor: { value: '#1B1F24' },
fieldBorderColor: { value: '#CCD1D5' },
errTextColor: { value: '#D72D39' },
fieldBackgroundColor: { value: '#fff' },
direction: { value: 'left' },
alignment: { value: 'side' },
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000090' },
icon: { value: 'IconHome2' },
iconVisibility: { value: false },
iconColor: { value: '#6A727C' },
accentColor: { value: '#4368E3' },
},
},
};

View file

@ -213,7 +213,7 @@ export const passinputConfig = {
},
exposedVariables: {
value: '',
mandatory: { value: '{{false}}' },
isMandatory: false,
isVisible: true,
isDisabled: false,
isLoading: false,

View file

@ -1,5 +1,5 @@
export const radiobuttonConfig = {
name: 'Radio-button',
name: 'RadioButton',
displayName: 'Radio Button',
description: 'Select one from multiple choices',
component: 'RadioButton',

View file

@ -1,6 +1,6 @@
export const toggleswitchConfig = {
name: 'ToggleSwitchLegacy',
displayName: 'Toggle Switch',
displayName: 'Toggle Switch (Legacy)',
description: 'User-controlled on-off switch',
component: 'ToggleSwitch',
defaultSize: {

View file

@ -0,0 +1 @@
export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy'];

View file

@ -18,7 +18,9 @@ import {
imageConfig,
containerConfig,
dropdownConfig,
dropdownV2Config,
multiselectConfig,
multiselectV2Config,
richtextareaConfig,
mapConfig,
qrscannerConfig,
@ -74,7 +76,9 @@ export const widgets = [
imageConfig,
containerConfig,
dropdownConfig,
dropdownV2Config,
multiselectConfig,
multiselectV2Config,
richtextareaConfig,
mapConfig,
qrscannerConfig,

View file

@ -5,13 +5,13 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton';
const AddNewButton = ({ children, dataCy, onClick, className = '', isLoading }) => {
return (
<ButtonSolid
variant="secondary"
variant="ghostBlack"
size="md"
className={`add-new-btn ${className}`}
onClick={onClick}
data-cy={dataCy}
leftIcon="plusrectangle"
fill={'var(--indigo9)'}
fill={'#ACB2B9'}
iconWidth={16}
isLoading={isLoading}
>

View file

@ -1,7 +1,9 @@
.add-new-btn {
padding: 6px 16px;
width: 100%;
padding: 6px 16px;
font-weight: 500;
}
border-radius: 8px;
border: 1px solid var(--borders-default, #CCD1D5);
color: var(--text-default, #1B1F24) !important;
width: 100%;
margin: 16px auto 0px auto;
}

View file

@ -474,10 +474,13 @@ const DynamicForm = ({
</div>
)}
<div
className={cx({
'flex-grow-1': isHorizontalLayout && !isSpecificComponent,
'w-100': isHorizontalLayout && type !== 'codehinter',
})}
className={cx(
{
'flex-grow-1': isHorizontalLayout && !isSpecificComponent,
'w-100': isHorizontalLayout && type !== 'codehinter',
},
'dynamic-form-element'
)}
style={{ width: '100%' }}
>
<Element

View file

@ -9,9 +9,10 @@ import {
generateAppActions,
loadPyodide,
isQueryRunnable,
resolveWidgetFieldValue,
} from '@/_helpers/utils';
import { dataqueryService } from '@/_services';
import _, { isArray, isEmpty } from 'lodash';
import _, { isArray, isEmpty, set } from 'lodash';
import moment from 'moment';
import Tooltip from 'react-bootstrap/Tooltip';
import { componentTypes } from '@/Editor/WidgetManager/components';
@ -395,9 +396,6 @@ export async function runTransformation(
currentState.page
);
} catch (err) {
const $error = err.name;
const $errorMessage = _.has(ERROR_TYPES, $error) ? `${$error} : ${err.message}` : err || 'Unknown error';
if (mode === 'edit') toast.error($errorMessage);
result = {
message: err.stack.split('\n')[0],
status: 'failed',
@ -617,7 +615,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'set-table-page': {
setTablePageIndex(event.table, event.pageIndex, _ref);
setTablePageIndex(event.table, event.pageIndex);
break;
}
@ -974,9 +972,12 @@ export function previewQuery(_ref, query, calledFromQuery = false, userSuppliedP
let parameters = userSuppliedParameters;
const queryPanelState = useQueryPanelStore.getState();
const { queryPreviewData } = queryPanelState;
const { setPreviewLoading, setPreviewData } = queryPanelState.actions;
const { setPreviewLoading, setPreviewData, setPreviewPanelExpanded } = queryPanelState.actions;
const queryEvents = useAppDataStore
.getState()
.events.filter((event) => event.target === 'data_query' && event.sourceId === query.id);
setPreviewLoading(true);
setPreviewPanelExpanded(true);
if (queryPreviewData) {
setPreviewData('');
}
@ -1012,26 +1013,8 @@ export function previewQuery(_ref, query, calledFromQuery = false, userSuppliedP
.then(async (data) => {
let finalData = data.data;
if (query.options.enableTransformation) {
finalData = await runTransformation(
_ref,
finalData,
query.options.transformation,
query.options.transformationLanguage,
query,
'edit'
);
}
if (calledFromQuery) {
setPreviewLoading(false);
} else {
setPreviewLoading(false);
setPreviewData(finalData);
}
let queryStatusCode = data?.status ?? null;
const queryStatus = query.kind === 'runpy' ? data?.data?.status ?? 'ok' : data.status;
switch (true) {
// Note: Need to move away from statusText -> statusCode
case queryStatus === 'Bad Request' ||
@ -1041,8 +1024,40 @@ export function previewQuery(_ref, query, calledFromQuery = false, userSuppliedP
queryStatusCode === 400 ||
queryStatusCode === 404 ||
queryStatusCode === 422: {
const err = query.kind == 'tooljetdb' ? data?.error || data : _.isEmpty(data.data) ? data : data.data;
toast.error(`${err.message}`);
let errorData = {};
switch (query.kind) {
case 'runpy':
errorData = data.data;
break;
case 'tooljetdb':
if (data?.error) {
errorData = {
message: data?.error?.message || 'Something went wrong',
description: data?.error?.message || 'Something went wrong',
status: data?.statusText || 'Failed',
data: data?.error || {},
};
} else {
errorData = data;
errorData.description = data.errorMessage || 'Something went wrong';
}
break;
default:
errorData = data;
break;
}
onEvent(_ref, 'onDataQueryFailure', queryEvents);
useCurrentStateStore.getState().actions.setErrors({
[query.name]: {
type: 'query',
kind: query.kind,
data: errorData,
options: options,
},
});
if (!calledFromQuery) setPreviewData(errorData);
break;
}
case queryStatus === 'needs_oauth': {
@ -1055,16 +1070,50 @@ export function previewQuery(_ref, query, calledFromQuery = false, userSuppliedP
queryStatus === 'Created' ||
queryStatus === 'Accepted' ||
queryStatus === 'No Content': {
toast(`Query ${'(' + query.name + ') ' || ''}completed.`, {
icon: '🚀',
if (query.options.enableTransformation) {
finalData = await runTransformation(
_ref,
finalData,
query.options.transformation,
query.options.transformationLanguage,
query,
'edit'
);
if (finalData.status === 'failed') {
useCurrentStateStore.getState().actions.setErrors({
[query.name]: {
type: 'transformations',
data: finalData,
options: options,
},
});
onEvent(_ref, 'onDataQueryFailure', queryEvents);
setPreviewLoading(false);
resolve({ status: data.status, data: finalData });
if (!calledFromQuery) setPreviewData(finalData);
return;
}
}
useCurrentStateStore.getState().actions.setCurrentState({
succededQuery: {
[query.name]: {
type: 'query',
kind: query.kind,
},
},
});
if (!calledFromQuery) setPreviewData(finalData);
onEvent(_ref, 'onDataQuerySuccess', queryEvents, 'edit');
break;
}
}
setPreviewLoading(false);
resolve({ status: data.status, data: finalData });
})
.catch(({ error, data }) => {
.catch((err) => {
const { error, data } = err;
setPreviewLoading(false);
setPreviewData(data);
toast.error(error);
@ -1094,8 +1143,13 @@ export function runQuery(
// const { setPreviewLoading, setPreviewData } = useQueryPanelStore.getState().actions;
const queryPanelState = useQueryPanelStore.getState();
const { queryPreviewData } = queryPanelState;
const { setPreviewLoading, setPreviewData } = queryPanelState.actions;
const { setPreviewLoading, setPreviewData, setPreviewPanelExpanded } = queryPanelState.actions;
if (shouldSetPreviewData) {
setPreviewPanelExpanded(true);
setPreviewLoading(true);
queryPreviewData && setPreviewData('');
}
if (query) {
dataQuery = JSON.parse(JSON.stringify(query));
} else {
@ -1211,6 +1265,7 @@ export function runQuery(
};
} else {
errorData = data;
errorData.description = data.errorMessage || 'Something went wrong';
}
break;
default:
@ -1269,7 +1324,6 @@ export function runQuery(
} else {
let rawData = data.data;
let finalData = data.data;
if (dataQuery.options.enableTransformation) {
finalData = await runTransformation(
_ref,
@ -1299,6 +1353,8 @@ export function runQuery(
});
resolve(finalData);
onEvent(_self, 'onDataQueryFailure', queryEvents);
setPreviewLoading(false);
if (shouldSetPreviewData) setPreviewData(finalData);
return;
}
}
@ -1376,7 +1432,7 @@ export function runQuery(
});
}
export function setTablePageIndex(tableId, index, _ref) {
export function setTablePageIndex(tableId, index) {
if (_.isEmpty(tableId)) {
console.log('No table is associated with this event.');
return Promise.resolve();
@ -1632,6 +1688,7 @@ export const cloneComponents = (
isCloning = true,
isCut = false
) => {
let addedComponent = {};
if (selectedComponents.length < 1) return getSelectedText();
const { components: allComponents } = appDefinition.pages[currentPageId];
@ -1684,7 +1741,7 @@ export const cloneComponents = (
if (isCloning) {
const parentId = allComponents[selectedComponents[0]?.id]?.['component']?.parent ?? undefined;
addComponents(currentPageId, appDefinition, updateAppDefinition, parentId, newComponentObj, true);
addedComponent = addComponents(currentPageId, appDefinition, updateAppDefinition, parentId, newComponentObj, true);
toast.success('Component cloned succesfully');
} else if (isCut) {
navigator.clipboard.writeText(JSON.stringify(newComponentObj));
@ -1699,6 +1756,7 @@ export const cloneComponents = (
return new Promise((resolve) => {
useEditorStore.getState().actions.updateEditorState({
currentSidebarTab: 2,
...(isCloning && { selectedComponents: [{ id: addedComponent.id, component: addedComponent }] }),
});
resolve();
});
@ -1784,6 +1842,7 @@ export const addComponents = (
) => {
const finalComponents = {};
const componentMap = {};
let newComponent = {};
let parentComponent = undefined;
const { isCloning, isCut, newComponents: pastedComponents = [], currentPageId } = newComponentObj;
@ -1831,12 +1890,13 @@ export const addComponents = (
componentData.parent = isParentInMap ? componentMap[isChild] : isChild;
}
const newComponent = {
newComponent = {
component: {
...componentData,
name: componentName,
},
layouts: component.layouts,
id: newComponentId,
};
finalComponents[newComponentId] = newComponent;
@ -1849,7 +1909,10 @@ export const addComponents = (
}
updateNewComponents(pageId, appDefinition, finalComponents, appDefinitionChanged, componentMap, isCut);
!isCloning && toast.success('Component pasted succesfully');
if (!isCloning) {
toast.success('Component pasted succesfully');
}
return newComponent;
};
export const addNewWidgetToTheEditor = (
@ -2240,3 +2303,24 @@ function extractVersion(versionStr) {
export const setMultipleComponentsSelected = (components) => {
useEditorStore.getState().actions.selectMultipleComponents(components);
};
export const calculateMoveableBoxHeight = (componentType, layoutData, stylesDefinition, label) => {
// Early return for non input components
if (!['TextInput', 'PasswordInput', 'NumberInput', 'DropdownV2', 'MultiselectV2'].includes(componentType)) {
return layoutData?.height;
}
const { alignment = { value: null }, width = { value: null }, auto = { value: null } } = stylesDefinition ?? {};
const resolvedLabel = label?.value?.length ?? 0;
const resolvedWidth = resolveWidgetFieldValue(width?.value) ?? 0;
const resolvedAuto = resolveWidgetFieldValue(auto?.value) ?? false;
let newHeight = layoutData?.height;
if (alignment.value && resolveWidgetFieldValue(alignment.value) === 'top') {
if ((resolvedLabel > 0 && resolvedWidth > 0) || (resolvedAuto && resolvedWidth === 0 && resolvedLabel > 0)) {
newHeight += 20;
}
}
return newHeight;
};

View file

@ -9,10 +9,12 @@ import { Container } from '@/Editor/Components/Container';
import { Tabs } from '@/Editor/Components/Tabs';
import { RichTextEditor } from '@/Editor/Components/RichTextEditor';
import { DropDown } from '@/Editor/Components/DropDown';
import { DropdownV2 } from '@/Editor/Components/DropdownV2/DropdownV2';
import { Checkbox } from '@/Editor/Components/Checkbox';
import { Datepicker } from '@/Editor/Components/Datepicker';
import { DaterangePicker } from '@/Editor/Components/DaterangePicker';
import { Multiselect } from '@/Editor/Components/Multiselect';
import { MultiselectV2 } from '@/Editor/Components/MultiselectV2/MultiselectV2';
import { Modal } from '@/Editor/Components/Modal';
import { Chart } from '@/Editor/Components/Chart';
import { Map as MapComponent } from '@/Editor/Components/Map/Map';
@ -84,10 +86,12 @@ export const AllComponents = {
Tabs,
RichTextEditor,
DropDown,
DropdownV2,
Checkbox,
Datepicker,
DaterangePicker,
Multiselect,
MultiselectV2,
Modal,
Chart,
Map: MapComponent,

View file

@ -268,7 +268,7 @@ export function computeComponentName(componentType, currentComponents) {
let currentNumber = currentComponentsForKind.length + 1;
let _componentName = '';
while (!found) {
_componentName = `${componentName.toLowerCase()}${currentNumber}`;
_componentName = `${componentName?.toLowerCase()}${currentNumber}`;
if (
Object.values(currentComponents).find((component) => component.component.name === _componentName) === undefined
) {

View file

@ -4,6 +4,7 @@ import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
const queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {};
const initialState = {
queryPanelHeight: queryManagerPreferences?.isExpanded ? queryManagerPreferences?.queryPanelHeight : 95 ?? 70,
previewPanelHeight: 0,
selectedQuery: null,
selectedDataSource: null,
queryToBeRun: null,
@ -11,6 +12,7 @@ const initialState = {
queryPreviewData: '',
showCreateQuery: false,
nameInputFocussed: false,
previewPanelExpanded: false,
};
export const useQueryPanelStore = create(
@ -19,6 +21,7 @@ export const useQueryPanelStore = create(
...initialState,
actions: {
updateQueryPanelHeight: (newHeight) => set(() => ({ queryPanelHeight: newHeight })),
updatePreviewPanelHeight: (newHeight) => set(() => ({ previewPanelHeight: newHeight })),
setSelectedQuery: (queryId) => {
set(() => {
if (queryId === null) {
@ -34,6 +37,7 @@ export const useQueryPanelStore = create(
setPreviewData: (data) => set({ queryPreviewData: data }),
setShowCreateQuery: (showCreateQuery) => set({ showCreateQuery }),
setNameInputFocussed: (nameInputFocussed) => set({ nameInputFocussed }),
setPreviewPanelExpanded: (previewPanelExpanded) => set({ previewPanelExpanded }),
},
}),
{ name: 'Query Panel Store' }
@ -41,6 +45,7 @@ export const useQueryPanelStore = create(
);
export const usePanelHeight = () => useQueryPanelStore((state) => state.queryPanelHeight);
export const usePreviewPanelHeight = () => useQueryPanelStore((state) => state.previewPanelHeight);
export const useSelectedQuery = () => useQueryPanelStore((state) => state.selectedQuery);
export const useSelectedDataSource = () => useQueryPanelStore((state) => state.selectedDataSource);
export const useQueryToBeRun = () => useQueryPanelStore((state) => state.queryToBeRun);
@ -51,3 +56,4 @@ export const useShowCreateQuery = () =>
useQueryPanelStore((state) => [state.showCreateQuery, state.actions.setShowCreateQuery]);
export const useNameInputFocussed = () =>
useQueryPanelStore((state) => [state.nameInputFocussed, state.actions.setNameInputFocussed]);
export const usePreviewPanelExpanded = () => useQueryPanelStore((state) => state.previewPanelExpanded);

View file

@ -1,6 +1,6 @@
import { schemaUnavailableOptions } from '@/Editor/QueryManager/constants';
import { allOperations } from '@tooljet/plugins/client';
import { capitalize } from 'lodash';
import { capitalize, cloneDeep } from 'lodash';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
@ -10,7 +10,7 @@ export const getDefaultOptions = (source) => {
if (isSchemaUnavailable) {
options = {
...{ ...schemaUnavailableOptions[source.kind] },
...{ ...cloneDeep(schemaUnavailableOptions[source.kind]) },
...(source?.kind != 'runjs' && {
transformationLanguage: 'javascript',
enableTransformation: false,

View file

@ -111,12 +111,13 @@ export function isParamFromTableColumn(appDiff, definition) {
}
export const computeComponentPropertyDiff = (appDiff, definition, opts) => {
if (!opts?.isParamFromTableColumn) {
if (!opts?.isParamFromTableColumn && !opts?.isParamFromDropdownOptions) {
return appDiff;
}
const columnsPath = generatePath(appDiff, 'columns');
const actionsPath = generatePath(appDiff, 'actions');
const deletionHistoryPath = generatePath(appDiff, 'columnDeletionHistory');
const optionsPath = generatePath(appDiff, 'options');
let _diff = deepClone(appDiff);
@ -135,6 +136,10 @@ export const computeComponentPropertyDiff = (appDiff, definition, opts) => {
_diff = updateValueInJson(_diff, deletionHistoryPath, deletionHistoryValue);
}
if (optionsPath) {
const optionsValue = getValueFromJson(definition, optionsPath);
_diff = updateValueInJson(_diff, optionsPath, optionsValue);
}
return _diff;
};
@ -175,6 +180,7 @@ const updateFor = (appDiff, currentPageId, opts, currentLayout) => {
try {
return processingFunction(appDiff, currentPageId, optionsTypes, currentLayout);
} catch (error) {
console.error('Error processing diff for update type: ', updateTypes, appDiff, error);
return { error, updateDiff: {}, type: null, operation: null };
}
}

View file

@ -1,6 +1,6 @@
$white: #fff !default;
$grey: #eee !default;
$black: #000 ;
$black: #000;
$gray: #F3F4F6;
$light-gray: #f8f9fa;
$border-grey-light: #D9DCDE;
@ -14,49 +14,50 @@ $bg-light: #EEF3F9;
$bg-dark: #22272E;
$bg-dark-light: #232e3c;
$color-light-slate-01:#151718;
$color-dark-slate-01:#FBFCFD;
$color-light-slate-02:#F8F9FA;
$color-dark-slate-02:#1A1D1E;
$color-light-slate-03:#F1F3F5;
$color-dark-slate-03:#202425;
$color-light-slate-04:#ECEEF0;
$color-dark-slate-04:#26292B;
$color-light-slate-05:#E6E8EB;
$color-dark-slate-05:#2B2F31;
$color-light-slate-01: #151718;
$color-dark-slate-01: #FBFCFD;
$color-light-slate-02: #F8F9FA;
$color-dark-slate-02: #1A1D1E;
$color-light-slate-03: #F1F3F5;
$color-dark-slate-03: #202425;
$color-light-slate-04: #ECEEF0;
$color-dark-slate-04: #26292B;
$color-light-slate-05: #E6E8EB;
$color-dark-slate-05: #2B2F31;
$color-light-slate-07: #D7DBDF;
$color-dark-slate-07:#3A3F42;
$color-light-slate-08:#C1C8CD;
$color-dark-slate-08:#4C5155;
$color-dark-slate-07: #3A3F42;
$color-light-slate-08: #C1C8CD;
$color-dark-slate-08: #4C5155;
$color-light-slate-09: #889096;
$color-dark-slate-09:#697177;
$color-dark-slate-09: #697177;
$color-light-slate-11 :#687076;
$color-dark-slate-11 : #9BA1A6;
$color-dark-slate-11 : #9BA1A6;
$color-light-slate-12 :#11181C;
$color-dark-slate-12:#ECEDEE;
$color-dark-slate-12: #ECEDEE;
$color-light-indigo-02:#F8FAFF;
$color-dark-indigo-02:#15192D;
$color-light-indigo-03:#F0F4FF;
$color-dark-indigo-03:#192140;
$color-light-indigo-04:#E6EDFE;;
$color-dark-indigo-04:#1C274F;
$color-light-indigo-05:#D9E2FC;
$color-dark-indigo-05:#1F2C5C;
$color-light-indigo-02: #F8FAFF;
$color-dark-indigo-02: #15192D;
$color-light-indigo-03: #F0F4FF;
$color-dark-indigo-03: #192140;
$color-light-indigo-04: #E6EDFE;
;
$color-dark-indigo-04: #1C274F;
$color-light-indigo-05: #D9E2FC;
$color-dark-indigo-05: #1F2C5C;
$color-light-indigo-09 : #3E63DD;
$color-dark-indigo-09:#3E63DD;
$color-dark-indigo-09: #3E63DD;
$color-light-indigo-10: #3A5CCC;
$color-dark-indigo-10:#849DFF;
$color-light-indigo-11:#3451B2;
$color-dark-indigo-11:#849DFF;
$color-dark-indigo-10: #849DFF;
$color-light-indigo-11: #3451B2;
$color-dark-indigo-11: #849DFF;
$color-light-tomato-09:#E54D2E;
$color-light-tomato-09: #E54D2E;
$color-dark-tomato-09 : #E54D2E;
$color-light-tomato-10: #DB4324;
$color-dark-tomato-10: #EC5E41;
$color-light-grass-11:#297C3B;
$color-dark-grass-11:#63C174;
$color-light-grass-11: #297C3B;
$color-dark-grass-11: #63C174;
$color-light-base: #ffffff;
$color-dark-base :#121212;
@ -73,6 +74,7 @@ $primary-light: unquote("rgb(#{$primary-rgb-darker})");
.color-light-green {
color: #46A758;
}
.bg-white {
background-color: $white;
}
@ -80,9 +82,11 @@ $primary-light: unquote("rgb(#{$primary-rgb-darker})");
.bg-light-1 {
background-color: #A6B6CC !important;
}
.bg-black {
background-color: $black !important;
}
.bg-gray {
background-color: $gray;
}
@ -102,6 +106,7 @@ $primary-light: unquote("rgb(#{$primary-rgb-darker})");
.bg-light {
background: $bg-light;
}
.bg-dark {
background: $dark-background;
}
@ -109,45 +114,59 @@ $primary-light: unquote("rgb(#{$primary-rgb-darker})");
.mute-text {
color: #8092AB;
}
.color-white{
.color-white {
color: white !important;
}
.color-muted{
.color-muted {
color: #DCDCDC;
}
.color-muted-darkmode{
.color-muted-darkmode {
color: #646D77;
}
.color-whitish-darkmode{
color :#c9cbcf !important;
.color-whitish-darkmode {
color: #c9cbcf !important;
}
.bg-light-green {
background: #F3FCF3;
}
.bg-light-indigo {
background: #E6EDFE !important;
}
.bg-dark-indigo {
background: #1C274F !important;
}
.bg-light-indigo-09 {
background: $color-light-indigo-09;
}
.color-light-slate-11{
.color-light-slate-11 {
color: $color-light-slate-11;
}
.color-dark-slate-11{
.color-dark-slate-11 {
color: $color-dark-slate-11;
}
.color-light-slate-12{
.color-light-slate-12 {
color: $color-light-slate-12;
}
.color-dark-slate-12{
.color-dark-slate-12 {
color: $color-dark-slate-12;
}
.color-light-gray-c3c3c3{
.color-light-gray-c3c3c3 {
color: #c3c3c3;
}
.color-light-indigo-09 {
color: $color-light-indigo-09;
}
@ -188,14 +207,22 @@ $primary-light: unquote("rgb(#{$primary-rgb-darker})");
background-color: var(--slate3) !important;
}
.color-slate12{
.color-slate12 {
color: var(--slate12) !important;
}
.color-slate09{
.color-slate09 {
color: $color-light-slate-09 !important;
}
.bg-slate6 {
background-color: var(--slate6) !important;
}
.text-default {
color: var(--text-default)
}
.text-placeholder {
color: var(--text-placeholder)
}

View file

@ -99,54 +99,94 @@ div[data-disabled='true'] {
body {
overflow-y: auto !important;
}
//to determine the text alignment of input elements in the table cells
.table-text-align-left{
input{
.table-text-align-left {
input {
text-align: left;
}
textarea{
.jet-table-image-column {
.w-100 {
justify-content: flex-start;
display: flex;
}
}
textarea {
text-align: left;
}
.tags.row,.radio-row{
.tags.row,
.radio-row {
justify-content: left;
}
}
.table-text-align-right{
input{
.table-text-align-right {
input {
text-align: right;
}
textarea{
.jet-table-image-column {
.w-100 {
justify-content: flex-end;
display: flex;
}
}
textarea {
text-align: right;
}
.tags.row,.radio.row{
.tags.row,
.radio.row {
justify-content: right;
}
}
.table-text-align-center{
input{
.table-text-align-center {
input {
text-align: center;
}
.tags.row,.radio.row{
.jet-table-image-column {
.w-100 {
justify-content: center;
display: flex;
}
}
.tags.row,
.radio.row {
justify-content: center;
}
textarea{
textarea {
text-align: center;
}
&.has-datepicker{
.td-container{
.w-100.h-100{
div{
&.has-datepicker {
.td-container {
.w-100.h-100 {
div {
justify-content: center !important;
}
}
}
}
}
.table-text-align-center,.table-text-align-left,.table-text-align-right{
.radio.row{
.table-text-align-center,
.table-text-align-left,
.table-text-align-right {
.radio.row {
width: 100%;
}
.radio.row.g-0 > *{
width: fit-content !important;
.radio.row.g-0>* {
width: fit-content !important;
}
}

View file

@ -98,7 +98,7 @@
//icon
--icons-strong: #6A727C;
--icons-default: #6A727C;
--icons-default: #ACB2B9;
--icons-weak-disabled: #6A727C;
--icons-disabled-on-white: #6A727C;
--icons-on-solid: #FFFFFF;

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