mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 13:37:28 +00:00
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:
parent
17dc2cc9a3
commit
3169d38d63
118 changed files with 4345 additions and 852 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
2.63.0
|
||||
2.64.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.63.0
|
||||
2.64.0
|
||||
|
|
|
|||
31
frontend/assets/images/icons/widgets/dropdownV2.jsx
Normal file
31
frontend/assets/images/icons/widgets/dropdownV2.jsx
Normal 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;
|
||||
|
|
@ -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} />;
|
||||
|
|
|
|||
27
frontend/assets/images/icons/widgets/multiselectV2.jsx
Normal file
27
frontend/assets/images/icons/widgets/multiselectV2.jsx
Normal 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;
|
||||
5
frontend/assets/images/image-not-found.svg
Normal file
5
frontend/assets/images/image-not-found.svg
Normal 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 |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ const shouldAddBoxShadowAndVisibility = [
|
|||
'Checkbox',
|
||||
'Button',
|
||||
'ToggleSwitchV2',
|
||||
'DropdownV2',
|
||||
'MultiselectV2',
|
||||
];
|
||||
|
||||
const BoxUI = (props) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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')}>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
89
frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx
Normal file
89
frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx
Normal 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;
|
||||
24
frontend/src/Editor/Components/DropdownV2/CustomOption.jsx
Normal file
24
frontend/src/Editor/Components/DropdownV2/CustomOption.jsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
455
frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
Normal file
455
frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
61
frontend/src/Editor/Components/DropdownV2/utils.js
Normal file
61
frontend/src/Editor/Components/DropdownV2/utils.js
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
471
frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx
Normal file
471
frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 } : {}),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
};
|
||||
|
|
|
|||
621
frontend/src/Editor/Inspector/Components/Select.jsx
Normal file
621
frontend/src/Editor/Inspector/Components/Select.jsx
Normal 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} />;
|
||||
}
|
||||
|
|
@ -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' }}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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] || {};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'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'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' }}> 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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
{t('editor.inspector.eventManager.addKeyValueParam', 'Add more')}
|
||||
{t('editor.inspector.eventManager.addKeyValueParam', 'Add more')}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 } : {}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const dividerConfig = {
|
|||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
dividerColor: { value: '#3e525b' },
|
||||
dividerColor: { value: '#000000' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const dropdownConfig = {
|
||||
name: 'Dropdown',
|
||||
displayName: 'Dropdown',
|
||||
name: 'DropdownLegacy',
|
||||
displayName: 'Dropdown (Legacy)',
|
||||
description: 'Single item selector',
|
||||
defaultSize: {
|
||||
width: 8,
|
||||
|
|
|
|||
326
frontend/src/Editor/WidgetManager/configs/dropdownV2.js
Normal file
326
frontend/src/Editor/WidgetManager/configs/dropdownV2.js
Normal 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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const multiselectConfig = {
|
||||
name: 'Multiselect',
|
||||
displayName: 'Multiselect',
|
||||
name: 'MultiselectLegacy',
|
||||
displayName: 'Multiselect (Legacy)',
|
||||
description: 'Multiple item selector',
|
||||
defaultSize: {
|
||||
width: 12,
|
||||
|
|
|
|||
351
frontend/src/Editor/WidgetManager/configs/multiselectV2.js
Normal file
351
frontend/src/Editor/WidgetManager/configs/multiselectV2.js
Normal 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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -213,7 +213,7 @@ export const passinputConfig = {
|
|||
},
|
||||
exposedVariables: {
|
||||
value: '',
|
||||
mandatory: { value: '{{false}}' },
|
||||
isMandatory: false,
|
||||
isVisible: true,
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const radiobuttonConfig = {
|
||||
name: 'Radio-button',
|
||||
name: 'RadioButton',
|
||||
displayName: 'Radio Button',
|
||||
description: 'Select one from multiple choices',
|
||||
component: 'RadioButton',
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
1
frontend/src/Editor/WidgetManager/constants.js
Normal file
1
frontend/src/Editor/WidgetManager/constants.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy'];
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue