Merge pull request #11403 from ToolJet/release/app-builder-s-4

Release appbuilder S4
This commit is contained in:
Johnson Cherian 2024-12-10 11:07:33 +05:30 committed by GitHub
commit bc102a3e93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2503 additions and 364 deletions

View file

@ -1 +1 @@
3.0.5-ce
3.1.0-ce

View file

@ -1 +1 @@
3.0.5-ce
3.1.0-ce

View file

@ -39,6 +39,7 @@ import Passwordinput from './passwordinput.jsx';
import Pdf from './pdf.jsx';
import Qrscanner from './qrscanner.jsx';
import RadioButton from './radio-button.jsx';
import RadioButtonV2 from './radiobuttonV2.jsx';
import Rangeslider from './rangeslider.jsx';
import Rating from './rating.jsx';
import Spinner from './spinner.jsx';
@ -140,8 +141,10 @@ const WidgetIcon = (props) => {
return <Pdf {...props} />;
case 'qrscanner':
return <Qrscanner {...props} />;
case 'radiobutton':
case 'radiobuttonlegacy':
return <RadioButton {...props} />;
case 'radiobutton':
return <RadioButtonV2 {...props} />;
case 'rangeslider':
return <Rangeslider {...props} />;
case 'rating':

View file

@ -0,0 +1,17 @@
import React from 'react';
const RadioButtonV2 = ({ 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} d="M24.889 46c12.15 0 22-9.85 22-22s-9.85-22-22-22-22 9.85-22 22 9.85 22 22 22z"></path>
<path fill="#3E63DD" d="M24.889 33a9 9 0 100-18 9 9 0 000 18z"></path>
</svg>
);
export default RadioButtonV2;

View file

@ -78,10 +78,6 @@
visibility: visible !important;
}
.main-editor-canvas .widget-target:hover .widget-target:hover > .config-handle {
visibility: visible !important;
}
.main-editor-canvas .widget-target:hover .widget-target:hover > .widget-target > .config-handle {
visibility: hidden !important;
}
}

View file

@ -353,6 +353,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
[boxList, currentLayout, gridWidth]
);
if (mode !== 'edit') return null;
return (
@ -425,6 +426,22 @@ export default function Grid({ gridWidth, currentLayout }) {
document.getElementById('resize-ghost-widget').style.height = `${e.target.clientHeight}px`;
}
}}
onResizeStart={(e) => {
if (
e.target.id &&
useGridStore.getState().resizingComponentId !== e.target.id &&
!e.target.classList.contains('delete-icon')
) {
// When clicked on widget boundary/resizer, select the component
setSelectedComponents([e.target.id]);
}
if (!isComponentVisible(e.target.id)) {
return false;
}
useGridStore.getState().actions.setResizingComponentId(e.target.id);
e.setMin([gridWidth, 10]);
}}
onResizeEnd={(e) => {
try {
useGridStore.getState().actions.setResizingComponentId(null);
@ -491,13 +508,6 @@ export default function Grid({ gridWidth, currentLayout }) {
useGridStore.getState().actions.setDragTarget();
toggleCanvasUpdater();
}}
onResizeStart={(e) => {
if (!isComponentVisible(e.target.id)) {
return false;
}
useGridStore.getState().actions.setResizingComponentId(e.target.id);
e.setMin([gridWidth, 10]);
}}
onResizeGroupStart={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
parentElm.classList.add('show-grid');
@ -582,7 +592,12 @@ export default function Grid({ gridWidth, currentLayout }) {
onDragStart={(e) => {
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
const box = boxList.find((box) => box.id === e.target.id);
let isDragOnTableORCalendar = false;
// This flag indicates whether the drag event originated on a child element within a component
// (e.g., inside a Table's columns, Calendar's dates, or Kanban's cards).
// When true, it prevents the parent component from being dragged, allowing the inner elements
// to handle their own interactions like column resizing or card dragging
let isDragOnInnerElement = false;
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
Also user dont need to drag an calender from using popup */
@ -593,20 +608,24 @@ export default function Grid({ gridWidth, currentLayout }) {
/* Checking if the dragged elemenent is a table. If its a table drag is disabled since it will affect column resizing and reordering */
if (box?.component?.component === 'Table') {
const tableElem = e.target.querySelector('.jet-data-table');
isDragOnTableORCalendar = tableElem.contains(e.inputEvent.target);
isDragOnInnerElement = tableElem.contains(e.inputEvent.target);
}
if (box?.component?.component === 'Calendar') {
const calenderElem =
e.target.querySelector('.rbc-month-view') ||
e.target.querySelector('.rbc-time-view') ||
e.target.querySelector('.rbc-day-view');
isDragOnTableORCalendar = calenderElem.contains(e.inputEvent.target);
isDragOnInnerElement = calenderElem.contains(e.inputEvent.target);
}
if (
['RangeSlider', 'Container', 'BoundedBox', 'Kanban'].includes(box?.component?.component) ||
isDragOnTableORCalendar
) {
if (box?.component?.component === 'Kanban') {
const handleContainers = e.target.querySelectorAll('.handle-container');
isDragOnInnerElement = Array.from(handleContainers).some((container) =>
container.contains(e.inputEvent.target)
);
}
if (['RangeSlider', 'BoundedBox'].includes(box?.component?.component) || isDragOnInnerElement) {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
const isHandle = targetElems.find((ele) => ele.classList.contains('handle-content'));
if (!isHandle) {
@ -666,7 +685,9 @@ export default function Grid({ gridWidth, currentLayout }) {
let left = e.lastEvent?.translate[0];
let top = e.lastEvent?.translate[1];
if (
['Listview', 'Kanban'].includes(boxList.find((box) => box.id === draggedOverElemId)?.component?.component)
['Listview', 'Kanban', 'Container'].includes(
boxList.find((box) => box.id === draggedOverElemId)?.component?.component
)
) {
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
@ -683,10 +704,19 @@ export default function Grid({ gridWidth, currentLayout }) {
if (draggedOverElemId !== currentParentId) {
if (isParentChangeAllowed) {
const draggedOverWidget = boxList.find((box) => box.id === draggedOverElemId);
let parentWidgetType = boxList.find((box) => box.id === draggedOverElemId)?.component?.component;
// @TODO - When dropping back to container from canvas, the boxList doesn't have canvas header,
// boxList will return null. But we need to tell getMouseDistanceFromParentDiv parentWidgetType is container
// As container id is like 'canvas-2375e23765e-123234'
if (parentId && !parentWidgetType && draggedOverElemId.includes('-header')) {
parentWidgetType = 'Container';
}
let { left: _left, top: _top } = getMouseDistanceFromParentDiv(
e,
draggedOverWidget?.component?.component === 'Kanban' ? draggedOverElem : draggedOverElemId,
boxList.find((box) => box.id === draggedOverElemId)?.component?.component
parentWidgetType
);
left = _left;
top = _top;

View file

@ -18,6 +18,7 @@ const shouldAddBoxShadowAndVisibility = [
'ToggleSwitchV2',
'DropdownV2',
'MultiselectV2',
'RadioButtonV2',
];
const RenderWidget = ({

View file

@ -61,9 +61,8 @@ export const EditorSelecto = () => {
if (selection) {
selection.removeAllRanges();
}
const target = e.inputEvent.target;
// This condition is to ensure selection happens only on main app canvas and not on child containers
// This condition is to ensure selection happens only on main app canvas and not on subcontainers
if (target.getAttribute('component-id') === 'canvas') {
return true;
}

View file

@ -6,7 +6,7 @@ export const CANVAS_WIDTHS = Object.freeze({
rightSideBarWidth: 300,
});
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban'];
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container'];
export const DEFAULT_CANVAS_WIDTH = 1292;

View file

@ -3,7 +3,7 @@ import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { componentTypes } from '../WidgetManager';
import useStore from '@/AppBuilder/_stores/store';
import { toast } from 'react-hot-toast';
import { CANVAS_WIDTHS, NO_OF_GRIDS } from './appCanvasConstants';
import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants';
import _ from 'lodash';
export function snapToGrid(canvasWidth, x, y) {
@ -42,8 +42,6 @@ export const addNewWidgetToTheEditor = (componentType, eventMonitorObject, curre
componentData.definition.others.showOnMobile.value = `{{true}}`;
}
const widgetsWithDefaultComponents = ['Listview', 'Tabs', 'Form', 'Kanban'];
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
const newComponent = {
id: uuidv4(),
@ -66,7 +64,7 @@ export const addNewWidgetToTheEditor = (componentType, eventMonitorObject, curre
height: defaultHeight,
},
},
withDefaultChildren: widgetsWithDefaultComponents.includes(componentData.component),
withDefaultChildren: WIDGETS_WITH_DEFAULT_CHILDREN.includes(componentData.component),
};
return newComponent;
@ -136,13 +134,14 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
}
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
const _parent = getParentComponentIdByType(child, parentMeta.component, parentId);
const newChildComponent = {
id: uuidv4(),
name: widgetName,
component: {
...componentData,
parent: parentMeta.component === 'Tabs' ? parentId + '-' + tab : parentId,
parent: getParentComponentIdByType(child, parentMeta.component, parentId),
},
layouts: {
[currentLayout]: {
@ -193,7 +192,8 @@ export const getAllChildComponents = (allComponents, parentId) => {
const isParentTabORCalendar =
allComponents[parentId]?.component?.component === 'Tabs' ||
allComponents[parentId]?.component?.component === 'Calendar' ||
allComponents[parentId]?.component?.component === 'Kanban';
allComponents[parentId]?.component?.component === 'Kanban' ||
allComponents[parentId]?.component?.component === 'Container';
if (componentParentId && isParentTabORCalendar) {
let childComponent = deepClone(allComponents[componentId]);
@ -336,7 +336,11 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
const parentComponent = allComponents?.[parentId];
if (parentComponent) {
return parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar';
return (
parentComponent.component.component === 'Tabs' ||
parentComponent.component.component === 'Calendar' ||
parentComponent.component.component === 'Container'
);
}
return false;
@ -457,3 +461,11 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
}
return canvasBgColor;
};
export const getParentComponentIdByType = (child, parentComponent, parentId) => {
const { tab } = child;
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
else if (parentComponent === 'Container') return `${parentId}-header`;
return parentId;
};

View file

@ -124,7 +124,7 @@ export const ComponentsManagerTab = ({ darkMode }) => {
'MultiselectV2',
'RichTextEditor',
'Checkbox',
'RadioButton',
'RadioButtonV2',
'Datepicker',
'DateRangePicker',
'FilePicker',

View file

@ -1 +1 @@
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect'];
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton'];

View file

@ -181,22 +181,25 @@ class Chart extends React.Component {
),
});
}
}
items.push({
title: 'Options',
children: (
<>
{renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'loadingState',
'properties',
currentState
)}
{renderElement(component, componentMeta, paramUpdated, dataQueries, 'showAxes', 'properties', currentState)}
{renderElement(
items.push({
title: 'Options',
children: (
<>
{renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'loadingState',
'properties',
currentState
)}
{chartType !== 'pie' &&
renderElement(component, componentMeta, paramUpdated, dataQueries, 'showAxes', 'properties', currentState)}
{chartType !== 'pie' &&
renderElement(
component,
componentMeta,
paramUpdated,
@ -205,10 +208,9 @@ class Chart extends React.Component {
'properties',
currentState
)}
</>
),
});
}
</>
),
});
items.push({
title: 'Events',

View file

@ -12,6 +12,7 @@ import { shallow } from 'zustand/shallow';
const SHOW_ADDITIONAL_ACTIONS = [
'Text',
'Container',
'TextInput',
'NumberInput',
'PasswordInput',
@ -20,6 +21,7 @@ const SHOW_ADDITIONAL_ACTIONS = [
'DropdownV2',
'MultiselectV2',
'Button',
'RichTextEditor',
];
const PROPERTIES_VS_ACCORDION_TITLE = {
Text: 'Data',

View file

@ -352,6 +352,7 @@ export const EventManager = ({
actionId: 'show-alert',
message: 'Hello world!',
alertType: 'info',
component: eventMetaDefinition.name,
...customEventRefs,
},
eventType: eventSourceType,

View file

@ -66,6 +66,7 @@ const NEW_REVAMPED_COMPONENTS = [
'Checkbox',
'DropdownV2',
'MultiselectV2',
'RadioButtonV2',
'Button',
];
@ -702,6 +703,7 @@ const GetAccordion = React.memo(
case 'DropdownV2':
case 'MultiselectV2':
case 'RadioButtonV2':
return <Select {...restProps} />;
default: {

View file

@ -48,6 +48,7 @@ export function renderCustomStyles(
componentConfig.component == 'Table' ||
componentConfig.component == 'DropdownV2' ||
componentConfig.component == 'MultiselectV2' ||
componentConfig.component == 'RadioButtonV2' ||
componentConfig.component == 'Button'
) {
const paramTypeConfig = componentMeta[paramType] || {};

View file

@ -2,8 +2,8 @@ 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 LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton'];
const NEW_WIDGETS = ['ToggleSwitchV2', 'DropdownV2', 'MultiselectV2', 'RadioButtonV2'];
export const WidgetBox = ({ component, darkMode }) => {
const { t } = useTranslation();

View file

@ -10,6 +10,7 @@ import {
datepickerConfig,
checkboxConfig,
radiobuttonConfig,
radiobuttonV2Config,
toggleswitchConfig,
toggleSwitchV2Config,
textareaConfig,
@ -67,6 +68,7 @@ export const widgets = [
datepickerConfig,
checkboxConfig,
radiobuttonConfig,
radiobuttonV2Config,
toggleswitchConfig,
toggleSwitchV2Config,
textareaConfig,

View file

@ -132,10 +132,7 @@ export const buttonConfig = {
borderRadius: {
type: 'numberInput',
displayName: 'Border radius',
validation: {
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } },
defaultValue: false,
},
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: false },
accordian: 'button',
},
boxShadow: {

View file

@ -15,22 +15,92 @@ export const containerConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
showHeader: {
type: 'toggle',
displayName: 'Show header',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
accordian: 'field',
},
},
defaultChildren: [
{
componentName: 'Text',
layout: {
top: 20,
left: 1,
height: 40,
},
displayName: 'ContainerText',
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Container title',
fontWeight: 'bold',
textSize: 16,
textColor: '#000',
},
},
],
events: {},
styles: {
backgroundColor: {
type: 'color',
displayName: 'Background color',
displayName: 'Background',
validation: {
schema: { type: 'string' },
defaultValue: '#fff',
},
},
headerBackgroundColor: {
type: 'color',
displayName: 'Header',
validation: {
schema: { type: 'string' },
defaultValue: '#fff',
},
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
validation: {
schema: { type: 'number' },
defaultValue: 80,
},
accordian: 'field',
},
borderRadius: {
type: 'code',
displayName: 'Border radius',
@ -50,40 +120,46 @@ export const containerConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
},
defaultValue: false,
},
},
exposedVariables: {},
exposedVariables: {
isVisible: true,
isDisabled: false,
isLoading: false,
},
actions: [
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set disable',
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set loading',
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
properties: {
visible: { value: '{{true}}' },
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
headerBackgroundColor: { value: '#fff' },
borderRadius: { value: '4' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '80' },
},
},
};

View file

@ -82,10 +82,7 @@ export const imageConfig = {
padding: {
type: 'code',
displayName: 'Padding',
validation: {
schema: { type: 'number' },
defaultValue: 0,
},
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 0 },
},
visibility: {
type: 'toggle',

View file

@ -9,6 +9,7 @@ import { passinputConfig } from './passwordinput';
import { datepickerConfig } from './datepicker';
import { checkboxConfig } from './checkbox';
import { radiobuttonConfig } from './radiobutton';
import { radiobuttonV2Config } from './radioButtonV2';
import { toggleswitchConfig } from './toggleswitch';
import { toggleSwitchV2Config } from './toggleswitchv2';
import { textareaConfig } from './textarea';
@ -65,7 +66,8 @@ export {
passinputConfig,
datepickerConfig,
checkboxConfig,
radiobuttonConfig,
radiobuttonConfig, //!Depreciated
radiobuttonV2Config,
toggleswitchConfig, //!Depreciated
toggleSwitchV2Config,
textareaConfig,

View file

@ -20,10 +20,7 @@ export const numberinputConfig = {
value: {
type: 'code',
displayName: 'Default value',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 0,
},
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 0 },
},
placeholder: {
type: 'code',
@ -236,7 +233,7 @@ export const numberinputConfig = {
},
],
exposedVariables: {
value: 99,
value: 0,
isMandatory: false,
isVisible: true,
isDisabled: false,

View file

@ -0,0 +1,295 @@
export const radiobuttonV2Config = {
name: 'RadioButton',
displayName: 'Radio Button',
description: 'Select one from multiple choices',
component: 'RadioButtonV2',
defaultSize: {
width: 12,
height: 43,
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
validation: {
customRule: {
type: 'code',
displayName: 'Custom validation',
placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
},
mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
},
properties: {
label: {
type: 'code',
displayName: 'Label',
validation: {
schema: { type: 'string' },
defaultValue: 'Select',
},
accordian: 'Data',
},
advanced: {
type: 'toggle',
displayName: 'Dynamic options',
validation: {
schema: { 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: {
onSelectionChange: { displayName: 'On select' },
},
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',
},
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,
},
borderColor: {
type: 'color',
displayName: 'Border',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
switchOnBackgroundColor: {
type: 'color',
displayName: 'Checked background',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
tip: 'Checked background',
tooltipStyle: {},
tooltipPlacement: 'bottom',
},
switchOffBackgroundColor: {
type: 'color',
displayName: 'Unchecked background',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
tip: 'Unchecked background',
tooltipStyle: {},
tooltipPlacement: 'bottom',
},
handleColor: {
type: 'color',
displayName: 'Handle color',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
optionsTextColor: {
type: 'color',
displayName: 'Text',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'container',
},
},
actions: [
{
handle: 'selectOption',
displayName: 'Select option',
params: [{ handle: 'option', displayName: 'Option' }],
},
{
handle: 'deselectOption',
displayName: 'Deselect option',
params: [{ handle: 'option', displayName: 'Option' }],
},
{
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' }],
},
],
exposedVariables: {
label: 'Select',
},
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
validation: {
mandatory: { value: '{{false}}' },
},
properties: {
label: { value: 'Select' },
value: { value: '{{"2"}}' },
advanced: { value: `{{false}}` },
options: {
value: [
{
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 },
},
],
},
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },
optionsLoadingState: { value: '{{false}}' },
optionVisibility: { value: '{{[true, true, true]}}' },
optionDisable: { value: '{{[false, false, false]}}' },
schema: {
value:
"{{[\t{label: 'option1',value: '1',disable: false,visible: true,default: true},{label: 'option2',value: '2',disable: false,visible: true},{label: 'option3',value: '3',disable: false,visible: true}\t]}}",
},
},
events: [],
styles: {
labelColor: { value: '#11181C' },
direction: { value: 'left' },
alignment: { value: 'side' },
auto: { value: '{{false}}' },
labelWidth: { value: '20' },
borderColor: { value: '#FFFFFF' },
switchOffBackgroundColor: { value: '#FFFFFF' },
switchOnBackgroundColor: { value: '#4368E3' },
handleColor: { value: '#FFFFFF' },
optionsTextColor: { value: '#11181C' },
padding: { value: 'default' },
},
},
};

View file

@ -1,6 +1,6 @@
export const radiobuttonConfig = {
name: 'RadioButton',
displayName: 'Radio Button',
name: 'RadioButtonLegacy',
displayName: 'Radio Button (Legacy)',
description: 'Select one from multiple choices',
component: 'RadioButton',
defaultSize: {

View file

@ -28,6 +28,15 @@ export const richtextareaConfig = {
defaultValue: 'Default text',
},
},
loadingState: {
type: 'toggle',
displayName: 'Show loading state',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
section: 'additionalActions',
},
},
events: {},
styles: {
@ -55,6 +64,28 @@ export const richtextareaConfig = {
exposedVariables: {
value: '',
},
actions: [
{
handle: 'setValue',
displayName: 'Set value',
params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }],
},
{
handle: 'setDisable',
displayName: 'Set disable',
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
},
{
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' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
@ -63,6 +94,7 @@ export const richtextareaConfig = {
properties: {
placeholder: { value: 'Placeholder text' },
defaultValue: { value: '' },
loadingState: { value: `{{false}}` },
},
events: [],
styles: {

View file

@ -289,6 +289,7 @@ export const tableConfig = {
onCellValueChanged: { displayName: 'Cell value changed' },
onFilterChanged: { displayName: 'Filter changed' },
onNewRowsAdded: { displayName: 'Add new rows' },
onTableDataDownload: { displayName: 'Download data' },
},
styles: {
textColor: {
@ -310,6 +311,16 @@ export const tableConfig = {
{ displayName: 'Wrap', value: 'wrap' },
],
},
headerCasing: {
type: 'switch',
displayName: 'Header casing',
validation: { schema: { type: 'string' } },
accordian: 'Data',
options: [
{ displayName: 'AA', value: 'uppercase' },
{ displayName: 'As typed', value: 'none' },
],
},
tableType: {
type: 'select',
displayName: 'Row style',
@ -529,6 +540,7 @@ export const tableConfig = {
value: [
{
name: 'id',
key: 'id',
id: 'e3ecbf7fa52c4d7210a93edb8f43776267a489bad52bd108be9588f790126737',
autogenerated: true,
fxActiveFields: [],
@ -548,6 +560,7 @@ export const tableConfig = {
},
{
name: 'name',
key: 'name',
id: '5d2a3744a006388aadd012fcc15cc0dbcb5f9130e0fbb64c558561c97118754a',
autogenerated: true,
fxActiveFields: [],
@ -556,6 +569,7 @@ export const tableConfig = {
},
{
name: 'email',
key: 'email',
id: 'afc9a5091750a1bd4760e38760de3b4be11a43452ae8ae07ce2eebc569fe9a7f',
autogenerated: true,
fxActiveFields: [],
@ -564,6 +578,7 @@ export const tableConfig = {
},
{
name: 'date',
key: 'date',
id: '27b75c8af9d34d1eaa1f9bb7f8f9f7b0abf1823e799748c8bb57e74f53b2c1dc',
autogenerated: true,
fxActiveFields: [],
@ -576,6 +591,7 @@ export const tableConfig = {
},
{
name: 'mobile_number',
key: 'mobile_number',
id: '9c2e3c40572a4aefb8e179ee39a0e1ac9dc2b2e6634be56e1c05be13c3d1de56',
autogenerated: true,
fxActiveFields: [],
@ -652,6 +668,7 @@ export const tableConfig = {
styles: {
textColor: { value: '#000' },
columnHeaderWrap: { value: 'fixed' },
headerCasing: { value: 'uppercase' },
actionButtonRadius: { value: '0' },
cellSize: { value: 'regular' },
borderRadius: { value: '8' },

View file

@ -1,37 +1,99 @@
import React, { useMemo } from 'react';
import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container';
import Spinner from '@/_ui/Spinner';
import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
export const Container = ({ id, properties, styles, darkMode, height, width }) => {
const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles;
const bgColor = useMemo(() => {
export const Container = ({
id,
properties,
styles,
darkMode,
height,
width,
setExposedVariables,
setExposedVariable,
}) => {
const { isDisabled, isVisible, isLoading } = useExposeState(
properties.loadingState,
properties.visibility,
properties.disabledState,
setExposedVariables,
setExposedVariable
);
const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles;
const contentBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor,
};
}, [styles.backgroundColor, darkMode]);
const headerBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode
? '#232E3C'
: styles.headerBackgroundColor,
};
}, [styles.headerBackgroundColor, darkMode]);
const computedStyles = {
backgroundColor: bgColor.backgroundColor,
backgroundColor: contentBgColor.backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
border: `1px solid ${borderColor}`,
height,
display: visibility ? 'flex' : 'none',
display: isVisible ? 'flex' : 'none',
overflow: 'hidden auto',
position: 'relative',
boxShadow,
};
const computedHeaderStyles = {
...headerBgColor,
height: `${headerHeight}px`,
flexShrink: 0,
flexGrow: 0,
borderBottom: `1px solid var(--border-weak)`,
};
const computedContentStyles = {
...contentBgColor,
flex: 1,
overflow: 'auto',
};
return (
<div
className={`jet-container ${properties.loadingState && 'jet-container-loading'}`}
className={`jet-container tw-flex tw-flex-col ${isLoading ? 'jet-container-loading' : ''} ${
properties.showHeader && 'jet-container--with-header'
}`}
id={id}
data-disabled={disabledState}
data-disabled={isDisabled}
style={computedStyles}
>
{properties.loadingState ? (
{isLoading ? (
<Spinner />
) : (
<ContainerComponent id={id} styles={bgColor} canvasHeight={height} canvasWidth={width} darkMode={darkMode} />
<>
{properties.showHeader && (
<ContainerComponent
id={`${id}-header`}
styles={computedHeaderStyles}
canvasHeight={headerHeight / 10}
canvasWidth={width}
allowContainerSelect={true}
darkMode={darkMode}
/>
)}
<ContainerComponent
id={id}
styles={computedContentStyles}
canvasHeight={height}
canvasWidth={width}
darkMode={darkMode}
/>
</>
)}
</div>
);

View file

@ -290,8 +290,19 @@ const Component = ({ children, ...restProps }) => {
} = restProps['modalProps'];
const setSelectedComponentAsModal = useStore((state) => state.setSelectedComponentAsModal, shallow);
// When the modal body is clicked capture it and use the callback to set the selected component as modal
const handleModalBodyClick = (event) => {
const clickedComponentId = event.target.getAttribute('component-id');
// Check if the clicked element is part of the modal canvas & same widget with id
if (id === clickedComponentId) {
setSelectedComponentAsModal(id);
}
};
return (
<BootstrapModal {...restProps}>
<BootstrapModal {...restProps} onClick={handleModalBodyClick}>
{showConfigHandler && (
<ConfigHandle
id={id}

View file

@ -58,6 +58,7 @@ export const Footer = React.memo(
showAddNewRowPopup,
downlaodPopover,
getToggleHideAllColumnsProps,
isDownloadTableDataEventAssociated,
}) => {
function hideColumnsPopover() {
return (
@ -223,32 +224,57 @@ export const Footer = React.memo(
)}
{!loadingState && showDownloadButton && (
<div>
<Tooltip id="tooltip-for-download" className="tooltip" />
<OverlayTriggerComponent
trigger="click"
overlay={downlaodPopover()}
rootClose={true}
placement={'top-end'}
>
<ButtonSolid
variant="ghostBlack"
className={`tj-text-xsm `}
customStyles={{
minWidth: '32px',
}}
leftIcon="filedownload"
fill={`var(--icons-default)`}
iconWidth="16"
size="md"
data-tooltip-id="tooltip-for-download"
data-tooltip-content="Download"
onClick={(e) => {
if (document.activeElement === e.currentTarget) {
e.currentTarget.blur();
}
}}
></ButtonSolid>
</OverlayTriggerComponent>
{
// if server side pagination is enabled and download event is associated with the table, then directly fire download event without displaying popover
isDownloadTableDataEventAssociated && !clientSidePagination ? (
<>
<Tooltip id="tooltip-for-download-serverside-pagingation" className="tooltip" />
<ButtonSolid
variant="ghostBlack"
className={`tj-text-xsm `}
customStyles={{
minWidth: '32px',
}}
leftIcon="filedownload"
fill={`var(--icons-default)`}
iconWidth="16"
size="md"
data-tooltip-id="tooltip-for-download-serverside-pagingation"
data-tooltip-content="Download"
onClick={() => fireEvent('onTableDataDownload')}
></ButtonSolid>
</>
) : (
<>
<Tooltip id="tooltip-for-download" className="tooltip" />
<OverlayTriggerComponent
trigger="click"
overlay={downlaodPopover()}
rootClose={true}
placement={'top-end'}
>
<ButtonSolid
variant="ghostBlack"
className={`tj-text-xsm `}
customStyles={{
minWidth: '32px',
}}
leftIcon="filedownload"
fill={`var(--icons-default)`}
iconWidth="16"
size="md"
data-tooltip-id="tooltip-for-download"
data-tooltip-content="Download"
onClick={(e) => {
if (document.activeElement === e.currentTarget) {
e.currentTarget.blur();
}
}}
></ButtonSolid>
</OverlayTriggerComponent>
</>
)
}
</div>
)}
{!loadingState && !hideColumnSelectorButton && (

View file

@ -17,6 +17,7 @@ export const TableHeader = ({
columnHeaderWrap,
setResizingColumnId,
resizingColumnId,
headerCasing,
}) => {
const calculateWidthOfActionColumnHeader = (position) => {
let totalWidth = null;
@ -193,6 +194,7 @@ export const TableHeader = ({
'text-truncate': getResolvedValue(columnHeaderWrap) === 'fixed',
'wrap-wrapper': getResolvedValue(columnHeaderWrap) === 'wrap',
})}
style={{ textTransform: headerCasing === 'uppercase' ? 'uppercase' : 'none' }}
>
{column.render('Header')}
</div>

View file

@ -135,6 +135,7 @@ export const Table = React.memo(
borderColor,
isMaxRowHeightAuto,
columnHeaderWrap,
headerCasing,
} = loadPropertiesAndStyles(properties, styles, darkMode);
const updatedDataReference = useRef([]);
const preSelectRow = useRef(false);
@ -153,6 +154,7 @@ export const Table = React.memo(
const [tableDetails, dispatch] = useReducer(reducer, initialState());
const [hoverAdded, setHoverAdded] = useState(false);
const [generatedColumn, setGeneratedColumn] = useState([]);
const [isDownloadTableDataEventAssociated, setIsDownloadTableDataEventAssociated] = useState(false);
const mergeToTableDetails = useCallback((payload) => dispatch(reducerActions.mergeToTableDetails(payload)), []);
const mergeToFilterDetails = (payload) => dispatch(reducerActions.mergeToFilterDetails(payload));
@ -174,6 +176,9 @@ export const Table = React.memo(
useEffect(() => mergeToTableDetails({ columnProperties: properties?.columns }), [properties?.columns]);
useEffect(() => {
const isDownloadTableDataEventAssociated = tableEvents.some((event) => event?.name === 'onTableDataDownload');
if (isDownloadTableDataEventAssociated) setIsDownloadTableDataEventAssociated(true);
else setIsDownloadTableDataEventAssociated(false);
const hoverEvent = tableEvents?.find(({ event }) => {
return event?.eventId == 'onRowHovered';
});
@ -1096,6 +1101,7 @@ export const Table = React.memo(
columnHeaderWrap={columnHeaderWrap}
setResizingColumnId={setResizingColumnId}
resizingColumnId={resizingColumnId}
headerCasing={headerCasing}
/>
{page.length > 0 && !loadingState && (
<tbody
@ -1165,6 +1171,7 @@ export const Table = React.memo(
{loadingState ? <LoadingState /> : page.length === 0 ? <EmptyState /> : null}
</div>
<Footer
isDownloadTableDataEventAssociated={isDownloadTableDataEventAssociated}
enablePagination={enablePagination}
tableDetails={tableDetails}
loadingState={loadingState}

View file

@ -83,6 +83,7 @@ export default function loadPropertiesAndStyles(properties, styles, darkMode) {
const contentWrapProperty = styles?.contentWrap ?? true;
const borderColor = styles?.borderColor ?? 'var(--borders-weak-disabled)';
const columnHeaderWrap = styles?.columnHeaderWrap ?? 'fixed';
const headerCasing = styles?.headerCasing ?? 'uppercase';
return {
color,
@ -122,5 +123,6 @@ export default function loadPropertiesAndStyles(properties, styles, darkMode) {
boxShadow,
borderColor,
columnHeaderWrap,
headerCasing,
};
}

View file

@ -21,8 +21,8 @@ import { Map as MapComponent } from '@/Editor/Components/Map/Map';
import { QrScanner } from '@/Editor/Components/QrScanner/QrScanner';
import { ToggleSwitch } from '@/Editor/Components/Toggle';
import { ToggleSwitchV2 } from '@/Editor/Components/ToggleV2';
import { RadioButton } from '@/Editor/Components/RadioButton';
import { RadioButtonV2 } from '@/Editor/Components/RadioButtonV2/RadioButtonV2';
import { StarRating } from '@/Editor/Components/StarRating';
import { Divider } from '@/Editor/Components/Divider';
import { FilePicker } from '@/Editor/Components/FilePicker';
@ -106,6 +106,7 @@ export const AllComponents = {
QrScanner,
ToggleSwitch,
RadioButton,
RadioButtonV2,
StarRating,
Divider,
FilePicker,

View file

@ -0,0 +1,51 @@
import { useEffect, useState } from 'react';
export const useExposeState = (loadingState, visibleState, disabledState, setExposedVariables, setExposedVariable) => {
const [isDisabled, setDisable] = useState(disabledState || false);
const [isVisible, setVisibility] = useState(visibleState || true);
const [isLoading, setLoading] = useState(loadingState || false);
// Effect to conditionally update state from properties passed to widget
useEffect(() => {
setDisable(disabledState);
}, [disabledState]);
useEffect(() => {
setVisibility(visibleState);
}, [visibleState]);
useEffect(() => {
setLoading(loadingState);
}, [loadingState]);
// exposed variables with state and async setters, happens on first time load
useEffect(() => {
setExposedVariables({
setDisable: async (value) => setDisable(value),
setVisibility: async (value) => setVisibility(value),
setLoading: async (value) => setLoading(value),
});
}, [setExposedVariables]);
//Side effect to state variables, these will run after the state is set and the values will be exposed
useEffect(() => {
setExposedVariable('isDisabled', isDisabled);
}, [isDisabled, setExposedVariable]);
useEffect(() => {
setExposedVariable('isVisible', isVisible);
}, [isVisible, setExposedVariable]);
useEffect(() => {
setExposedVariable('isLoading', isLoading);
}, [isLoading, setExposedVariable]);
return {
isDisabled,
setDisable,
isVisible,
setVisibility,
isLoading,
setLoading,
};
};

View file

@ -1822,7 +1822,11 @@ export const createComponentsSlice = (set, get) => ({
const label = componentDefinition?.component?.definition?.properties?.label;
const getAllExposedValues = get().getAllExposedValues;
// Early return for non input components
if (!['TextInput', 'PasswordInput', 'NumberInput', 'DropdownV2', 'MultiselectV2'].includes(componentType)) {
if (
!['TextInput', 'PasswordInput', 'NumberInput', 'DropdownV2', 'MultiselectV2', 'RadioButtonV2'].includes(
componentType
)
) {
return layoutData?.height;
}
const { alignment = { value: null }, width = { value: null }, auto = { value: null } } = stylesDefinition ?? {};

View file

@ -149,6 +149,7 @@ export const createDebuggerSlice = (set, get) => ({
componentId: id,
},
logLevel: 'error',
errorTarget: 'Component Property',
timestamp: moment().toISOString(),
}));
@ -191,6 +192,7 @@ export const createDebuggerSlice = (set, get) => ({
effectiveProperty: { [property]: defaultValue },
componentId,
},
errorTarget: 'Component Property',
logLevel: 'error',
timestamp: moment().toISOString(),
});

View file

@ -15,6 +15,7 @@ import generateFile from '@/_lib/generate-file';
import urlJoin from 'url-join';
import { useCallback } from 'react';
import { navigate } from '@/AppBuilder/_utils/misc';
import moment from 'moment';
// To unsubscribe from the changes when no longer needed
// unsubscribe();
@ -211,32 +212,46 @@ export const createEventsSlice = (set, get) => ({
state.eventsSlice.module[moduleId].events = newEvents;
});
},
setTablePageIndex: (tableId, index = 1) => {
const { getExposedValueOfComponent } = get();
if (_.isEmpty(tableId)) {
console.log('No table is associated with this event.');
setTablePageIndex: (tableId, index, eventObj) => {
try {
const { getExposedValueOfComponent } = get();
if (typeof index !== 'number' && index !== undefined) {
throw new Error('Invalid page index.');
}
const exposedValue = getExposedValueOfComponent(tableId);
if (!exposedValue) {
throw new Error('No table is associated with this event.');
}
exposedValue.setPage(index);
return Promise.resolve();
} catch (error) {
get().eventsSlice.logError('set_table_page_index', 'set-table-page-index', error, eventObj, {
eventId: eventObj.eventType,
});
}
const exposedValue = getExposedValueOfComponent(tableId);
if (!exposedValue) {
console.log('No table is associated with this event.');
return Promise.resolve();
}
exposedValue.setPage(index);
return Promise.resolve();
},
showModal: (modal, show) => {
const { getExposedValueOfComponent } = get();
const modalId = modal?.id ?? modal;
console.log('modalId', modalId);
if (_.isEmpty(modalId)) {
console.log('No modal is associated with this event.');
return Promise.resolve();
}
const exposedValue = getExposedValueOfComponent(modalId);
show ? exposedValue.open() : exposedValue.close();
showModal: (modal, show, eventObj) => {
try {
const { getExposedValueOfComponent } = get();
const modalId = modal?.id ?? modal;
if (_.isEmpty(modalId)) {
throw new Error('No modal is associated with this event.');
}
const exposedValue = getExposedValueOfComponent(modalId);
show ? exposedValue.open() : exposedValue.close();
return Promise.resolve();
return Promise.resolve();
} catch (error) {
get().eventsSlice.logError(
show ? 'show_modal' : 'close_modal',
show ? 'show-modal' : 'close_modal',
error,
eventObj,
{
eventId: eventObj.eventType,
}
);
}
},
handleEvent: (eventName, events, options, moduleId = 'canvas') => {
const latestEvents = get().eventsSlice.getModuleEvents(moduleId);
@ -361,6 +376,7 @@ export const createEventsSlice = (set, get) => ({
'onSubmit',
'onInvalid',
'onNewRowsAdded',
'onTableDataDownload',
].includes(eventName)
) {
executeActionsForEventId(eventName, events, mode, customVariables);
@ -381,13 +397,84 @@ export const createEventsSlice = (set, get) => ({
?.sort((a, b) => a.index - b.index);
for (const event of filteredEvents) {
await get().eventsSlice.executeAction(event.event, mode, customVariables);
await get().eventsSlice.executeAction(event, mode, customVariables);
}
},
executeAction: debounce(async (event, mode, customVariables = {}) => {
logError(errorType, errorKind, error, eventObj = '', options = {}, logLevel) {
const { event = eventObj } = eventObj;
const pages = get().modules.canvas.pages;
const currentPageId = get().currentPageId;
const currentPage = pages.find((page) => page.id === currentPageId);
const componentIdMapping = get().modules['canvas'].componentNameIdMapping;
const componentName = Object.keys(componentIdMapping).find(
(key) => componentIdMapping[key] === eventObj?.sourceId
);
const componentId = eventObj?.sourceId;
const getSource = () => {
if (eventObj.eventType) {
return eventObj.eventType === 'data_query' ? 'query' : eventObj.eventType;
}
const sourceMap = {
onDataQueryFailure: 'query',
onDataQuerySuccess: 'query',
onPageLoad: 'page',
};
return sourceMap[event.eventId] || 'component';
};
const getQueryName = () => {
const queries = get().dataQuery.queries.modules.canvas;
return queries.find((query) => query.id === eventObj?.sourceId || '')?.name || '';
};
const constructErrorHeader = () => {
const source = getSource();
const pageName = currentPage.name;
const headerMap = {
component: `[Page ${pageName}] [Component ${componentName}] [Event ${event?.eventId}] [Action ${event.actionId}]`,
page: `[Page ${pageName}] [Event ${event.eventId}] [Action ${event.actionId}]`,
query: `[Query ${getQueryName()}] [Event ${event.eventId}] [Action ${event.actionId}]`,
};
return headerMap[source] || '';
};
const constructErrorTarget = () => {
const source = getSource();
const errorTargetMap = {
page: 'Event Errors with page',
component: 'Component Event',
query: 'Event Errors with query',
};
return errorTargetMap[source];
};
useStore.getState().debugger.log({
logLevel: logLevel ? logLevel : 'error',
type: errorType ? errorType : 'event',
kind: errorKind,
key: constructErrorHeader(),
error: {
message: error.message,
description: JSON.stringify(error.message, null, 2),
...(event.component && componentId && { componentId: componentId }),
},
errorTarget: constructErrorTarget(),
options: options,
strace: 'app_level',
timestamp: moment().toISOString(),
});
},
executeAction: debounce(async (eventObj, mode, customVariables = {}) => {
const { event = eventObj } = eventObj;
const { getExposedValueOfComponent, getResolvedValue } = get();
if (event.runOnlyIf) {
if (event?.runOnlyIf) {
const shouldRun = getResolvedValue(event.runOnlyIf, customVariables);
if (!shouldRun) {
return false;
@ -418,23 +505,37 @@ export const createEventsSlice = (set, get) => ({
return Promise.resolve();
}
case 'run-query': {
const { queryId, queryName } = event;
const params = event['parameters'];
const resolvedParams = {};
if (params) {
Object.keys(params).map((param) => (resolvedParams[param] = getResolvedValue(params[param], undefined)));
try {
const { queryId, queryName, component, eventId } = event;
const params = event['parameters'];
if (!queryId && !queryName) {
throw new Error('No query selected');
}
const resolvedParams = {};
if (params) {
Object.keys(params).map(
(param) => (resolvedParams[param] = getResolvedValue(params[param], undefined))
);
}
// !Todo tackle confirm query part once done
return get().queryPanel.runQuery(
queryId,
queryName,
undefined,
undefined,
resolvedParams,
component,
eventId,
false,
false,
'canvas'
);
} catch (error) {
get().eventsSlice.logError('run_query', 'run-query', error, eventObj, {
eventId: event.eventId,
});
return Promise.reject(error);
}
// !Todo tackle confirm query part once done
return get().queryPanel.runQuery(
queryId,
queryName,
undefined,
undefined,
resolvedParams,
false,
false,
'canvas'
);
}
case 'logout': {
return logoutAction();
@ -447,39 +548,47 @@ export const createEventsSlice = (set, get) => ({
return Promise.resolve();
}
case 'go-to-app': {
const resolvedValue = getResolvedValue(event.slug, customVariables);
const slug = resolvedValue;
const queryParams = event.queryParams?.reduce(
(result, queryParam) => ({
...result,
...{
[getResolvedValue(queryParam[0])]: getResolvedValue(queryParam[1], undefined, customVariables),
},
}),
{}
);
let url = `/applications/${slug}`;
if (queryParams) {
const queryPart = serializeNestedObjectToQueryParams(queryParams);
if (queryPart.length > 0) url = url + `?${queryPart}`;
}
if (mode === 'view') {
navigate(url);
} else {
if (confirm('The app will be opened in a new tab as the action is triggered from the editor.')) {
window.open(urlJoin(window.public_config?.TOOLJET_HOST, url));
try {
if (!event.slug) {
throw new Error('No application slug provided');
}
const resolvedValue = getResolvedValue(event.slug, customVariables);
const slug = resolvedValue;
const queryParams = event.queryParams?.reduce(
(result, queryParam) => ({
...result,
...{
[getResolvedValue(queryParam[0])]: getResolvedValue(queryParam[1], undefined, customVariables),
},
}),
{}
);
let url = `/applications/${slug}`;
if (queryParams) {
const queryPart = serializeNestedObjectToQueryParams(queryParams);
if (queryPart.length > 0) url = url + `?${queryPart}`;
}
if (mode === 'view') {
navigate(url);
} else {
if (confirm('The app will be opened in a new tab as the action is triggered from the editor.')) {
window.open(urlJoin(window.public_config?.TOOLJET_HOST, url));
}
}
return Promise.resolve();
} catch (error) {
get().eventsSlice.logError('go_to_app', 'go-to-app', error, eventObj, { eventId: event.eventId });
return Promise.reject();
}
return Promise.resolve();
}
case 'show-modal':
return get().eventsSlice.showModal(event.modal, true);
return get().eventsSlice.showModal(event.modal, true, eventObj);
case 'close-modal':
return get().eventsSlice.showModal(event.modal, false);
return get().eventsSlice.showModal(event.modal, false, eventObj);
case 'copy-to-clipboard': {
const contentToCopy = getResolvedValue(event.contentToCopy, customVariables);
copyToClipboard(contentToCopy);
@ -508,7 +617,7 @@ export const createEventsSlice = (set, get) => ({
}
case 'set-table-page': {
get().eventsSlice.setTablePageIndex(event.table, getResolvedValue(event.pageIndex));
get().eventsSlice.setTablePageIndex(event.table, getResolvedValue(event.pageIndex), eventObj);
break;
}
@ -631,85 +740,110 @@ export const createEventsSlice = (set, get) => ({
// return;
}
case 'control-component': {
// let component = Object.values(getCurrentState()?.components ?? {}).filter(
// (component) => component.id === event.componentId
// )[0];
const component = getExposedValueOfComponent(event.componentId);
const action = component?.[event.componentSpecificActionHandle];
// let action = '';
// let actionArguments = '';
// check if component id not found then try to find if its available as child widget else continue
// with normal flow finding action
// if (component == undefined) {
// component = _ref.appDefinition.pages[getCurrentState()?.page?.id].components[event.componentId].component;
// const parent = Object.values(getCurrentState()?.components ?? {}).find(
// (item) => item.id === component.parent
// );
// const child = Object.values(parent?.children).find((item) => item.id === event.componentId);
// if (child) {
// action = child[event.componentSpecificActionHandle];
// }
// } else {
// //normal component outside a container ex : form
// action = component?.[event.componentSpecificActionHandle];
// }
// actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
// ...param,
// value: resolveReferences(param.value, undefined, customVariables),
// }));
// console.log('actionArguments', event.componentSpecificActionParams);
const actionArguments = event.componentSpecificActionParams.map((param) => {
const value = getResolvedValue(param.value, customVariables);
return {
...param,
value: value,
// value: resolveCode(re.valueWithBrackets, getAllExposedValues()),
};
});
// const actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
// ...param,
// value: resolveReferences(param.value, getAllExposedValues(), customVariables),
// }));
try {
// let component = Object.values(getCurrentState()?.components ?? {}).filter(
// (component) => component.id === event.componentId
// )[0];
const { event } = eventObj;
if (!event.componentSpecificActionHandle) {
throw new Error('No component-specific action handle provided.');
}
const component = getExposedValueOfComponent(event.componentId);
if (!event.componentId || !Object.keys(component).length) {
throw new Error('No component ID provided for control-component action.');
}
const action = component?.[event.componentSpecificActionHandle];
// let action = '';
// let actionArguments = '';
// check if component id not found then try to find if its available as child widget else continue
// with normal flow finding action
// if (component == undefined) {
// component = _ref.appDefinition.pages[getCurrentState()?.page?.id].components[event.componentId].component;
// const parent = Object.values(getCurrentState()?.components ?? {}).find(
// (item) => item.id === component.parent
// );
// const child = Object.values(parent?.children).find((item) => item.id === event.componentId);
// if (child) {
// action = child[event.componentSpecificActionHandle];
// }
// } else {
// //normal component outside a container ex : form
// action = component?.[event.componentSpecificActionHandle];
// }
// actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
// ...param,
// value: resolveReferences(param.value, undefined, customVariables),
// }));
// console.log('actionArguments', event.componentSpecificActionParams);
const actionArguments = event.componentSpecificActionParams.map((param) => {
const value = getResolvedValue(param.value, customVariables);
return {
...param,
value: value,
// value: resolveCode(re.valueWithBrackets, getAllExposedValues()),
};
});
// const actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
// ...param,
// value: resolveReferences(param.value, getAllExposedValues(), customVariables),
// }));
const actionPromise = action && action(...actionArguments.map((argument) => argument.value));
return actionPromise ?? Promise.resolve();
const actionPromise = action && action(...actionArguments.map((argument) => argument.value));
return actionPromise ?? Promise.resolve();
} catch (error) {
get().eventsSlice.logError('control_component', 'control-component', error, eventObj, {
eventId: event.eventId,
});
return Promise.reject(error);
}
}
case 'switch-page': {
const { switchPage } = get();
const page = get().modules.canvas.pages.find((page) => page.id === event.pageId);
const queryParams = event.queryParams || [];
if (!page.disabled) {
const resolvedQueryParams = [];
queryParams.forEach((param) => {
resolvedQueryParams.push([
getResolvedValue(param[0], customVariables),
getResolvedValue(param[1], customVariables),
]);
});
const currentUrlParams = new URLSearchParams(window.location.search);
currentUrlParams.forEach((value, key) => {
if (key === 'version' || key === 'env') {
// if version or env is in current url query param but not in resolved params then add it to resolvedQueryParams
const exists = resolvedQueryParams.some(([resolvedKey]) => resolvedKey === key);
if (!exists) {
resolvedQueryParams.unshift([key, value]);
try {
const { pageId } = event;
if (!pageId) {
throw new Error('No page ID provided');
}
const { switchPage } = get();
const page = get().modules.canvas.pages.find((page) => page.id === event.pageId);
const queryParams = event.queryParams || [];
if (!page.disabled) {
const resolvedQueryParams = [];
queryParams.forEach((param) => {
resolvedQueryParams.push([
getResolvedValue(param[0], customVariables),
getResolvedValue(param[1], customVariables),
]);
});
const currentUrlParams = new URLSearchParams(window.location.search);
currentUrlParams.forEach((value, key) => {
if (key === 'version' || key === 'env') {
// if version or env is in current url query param but not in resolved params then add it to resolvedQueryParams
const exists = resolvedQueryParams.some(([resolvedKey]) => resolvedKey === key);
if (!exists) {
resolvedQueryParams.unshift([key, value]);
}
}
}
});
switchPage(page.id, page.handle, resolvedQueryParams);
} else {
toast.error('Page is disabled');
//!TODO push to debugger
get().debugger.log({
logLevel: 'error',
type: 'navToDisablePage',
kind: 'page',
message: `Attempt to switch to disabled page ${page.name} blocked.`,
error: 'Page is disabled',
});
switchPage(page.id, page.handle, resolvedQueryParams);
} else {
toast.error('Page is disabled');
//!TODO push to debugger
get().debugger.log({
logLevel: 'error',
type: 'navToDisablePage',
kind: 'page',
message: `Attempt to switch to disabled page ${page.name} blocked.`,
error: 'Page is disabled',
});
}
return Promise.resolve();
} catch (error) {
get().eventsSlice.logError('switch_page', 'switch-page', error, eventObj, {
eventId: event.eventId,
});
}
return Promise.resolve();
}
}
}

View file

@ -201,6 +201,8 @@ export const createQueryPanelSlice = (set, get) => ({
confirmed = undefined,
mode = 'edit',
userSuppliedParameters = {},
component,
eventId,
shouldSetPreviewData = false,
isOnLoad = false,
moduleId = 'canvas'
@ -247,8 +249,7 @@ export const createQueryPanelSlice = (set, get) => ({
if (query) {
dataQuery = JSON.parse(JSON.stringify(query));
} else {
toast.error('No query has been associated with the action.');
return;
throw new Error('No query selected');
}
if (_.isEmpty(parameters)) {
@ -298,6 +299,7 @@ export const createQueryPanelSlice = (set, get) => ({
isLoading: true,
data: [],
rawData: [],
id: queryId,
});
let queryExecutionPromise = null;
@ -367,6 +369,7 @@ export const createQueryPanelSlice = (set, get) => ({
kind: query.kind,
key: query.name,
message: errorData?.description,
errorTarget: 'Queries',
error:
query.kind === 'restapi'
? {
@ -435,6 +438,7 @@ export const createQueryPanelSlice = (set, get) => ({
key: query.name,
message: 'Query executed successfully',
isQuerySuccessLog: true,
errorTarget: 'Queries',
});
setResolvedQuery(queryId, {
@ -760,6 +764,7 @@ export const createQueryPanelSlice = (set, get) => ({
error: result,
isTransformation: true,
isQuerySuccessLog: result?.status === 'failed' ? false : true,
errorTarget: 'Queries',
});
return result;
},

View file

@ -340,11 +340,11 @@ const DynamicEditorBridge = (props) => {
<div className={cx({ 'codeShow-active': codeShow }, 'wrapper-div-code-editor')}>
<div className={cx('d-flex align-items-center justify-content-between')}>
{paramLabel !== ' ' && !HIDDEN_CODE_HINTER_LABELS.includes(paramLabel) && (
<div className={`field ${className}`} data-cy={`${cyLabel}-widget-parameter-label`}>
<div className={`field text-truncate d-flex ${className}`} data-cy={`${cyLabel}-widget-parameter-label`}>
<ToolTip
label={t(`widget.commonProperties.${camelCase(paramLabel)}`, paramLabel)}
meta={fieldMeta}
labelClass={`tj-text-xsm color-slate12 ${codeShow ? 'mb-2' : 'mb-0'} ${
labelClass={`tj-text-xsm color-slate12 w-100 text-truncate ${codeShow ? 'mb-2' : 'mb-0'} ${
darkMode && 'color-whitish-darkmode'
}`}
/>

View file

@ -1,8 +1,10 @@
/* eslint-disable react/no-string-refs */
import React from 'react';
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState } from 'draft-js';
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState, convertFromHTML } from 'draft-js';
import 'draft-js/dist/Draft.css';
import { stateToHTML } from 'draft-js-export-html';
import Loader from '@/ToolJetUI/Loader/Loader';
import DOMPurify from 'dompurify';
// Custom overrides for "code" style.
const styleMap = {
@ -148,10 +150,16 @@ const InlineStyleControls = (props) => {
class DraftEditor extends React.Component {
constructor(props) {
super(props);
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
this.state = {
editorState: EditorState.createWithContent(ContentState.createFromText(this.props.defaultValue)),
editorState: EditorState.createWithContent(
ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap)
),
};
this.editorContainerRef = React.createRef();
this.controlsRef = React.createRef();
this.focus = () => this.refs.editor.focus();
this.onChange = (editorState) => {
let html = stateToHTML(editorState.getCurrentContent());
@ -165,9 +173,63 @@ class DraftEditor extends React.Component {
this.toggleInlineStyle = this._toggleInlineStyle.bind(this);
}
componentDidMount() {
//For resizing the editor container based on the height of rich text editor controls
this.resizeObserver = new ResizeObserver(() => {
if (this.controlsRef.current && this.editorContainerRef.current) {
const controlsHeight = this.controlsRef.current.offsetHeight;
const editorHeight = this.props.height - 46 - controlsHeight;
this.editorContainerRef.current.style.height = `${editorHeight}px`;
}
});
if (this.controlsRef.current) {
this.resizeObserver.observe(this.controlsRef.current);
}
const exposedVariables = {
value: this.props.defaultValue,
isDisabled: this.props.isDisabled,
isVisible: this.props.isVisible,
isLoading: this.props.isLoading,
setValue: async (text) => {
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(text));
const newContentState = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap
);
const newEditorState = EditorState.createWithContent(newContentState);
const html = stateToHTML(newContentState);
this.props.handleChange(html);
this.setState({ editorState: newEditorState });
},
setDisable: async (value) => {
this.props.setExposedVariable('isDisabled', value);
this.props.setIsDisabled(value);
},
setVisibility: async (value) => {
this.props.setExposedVariable('isVisible', value);
this.props.setIsVisible(value);
},
setLoading: async (value) => {
this.props.setExposedVariable('isLoading', value);
this.props.setIsLoading(value);
},
};
this.props.setExposedVariables(exposedVariables);
this.props.isInitialRender.current = false;
}
componentWillUnmount() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
componentDidUpdate(prevProps) {
if (prevProps.defaultValue !== this.props.defaultValue) {
const newContentState = ContentState.createFromText(this.props.defaultValue);
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
const newContentState = ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap);
const newEditorState = EditorState.createWithContent(newContentState);
const html = stateToHTML(newContentState);
@ -218,13 +280,19 @@ class DraftEditor extends React.Component {
}
}
return (
<div className="RichEditor-root">
<div className="RichEditor-controls">
return this.props.isLoading ? (
<div style={{ height: '100%', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<center>
<Loader width="16" absolute={false} />
</center>
</div>
) : (
<div className="RichEditor-root" style={{ overflowY: 'scroll', scrollbarWidth: 'none' }}>
<div className="RichEditor-controls" ref={this.controlsRef}>
<BlockStyleControls editorState={editorState} onToggle={this.toggleBlockType} />
<InlineStyleControls editorState={editorState} onToggle={this.toggleInlineStyle} />
</div>
<div className={className} style={{ height: `${this.props.height - 60}px` }} onClick={this.focus}>
<div className={className} ref={this.editorContainerRef} onClick={this.focus}>
<Editor
blockStyleFn={getBlockStyle}
customStyleMap={styleMap}
@ -232,7 +300,13 @@ class DraftEditor extends React.Component {
handleKeyCommand={this.handleKeyCommand}
keyBindingFn={this.mapKeyToEditorCommand}
onChange={this.onChange}
placeholder={this.props.placeholder}
placeholder={
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(this.props.placeholder || ''),
}}
/>
}
ref="editor"
spellCheck={true}
/>

View file

@ -72,6 +72,9 @@ export const NumberInput = function NumberInput({
useEffect(() => {
setInputValue(Number(parseFloat(properties.value).toFixed(properties.decimalPlaces)));
if (isNaN(Number(parseFloat(properties.value).toFixed(properties.decimalPlaces)))) {
setExposedVariable('value', null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.value]);
@ -144,6 +147,7 @@ export const NumberInput = function NumberInput({
},
clear: async function () {
setInputValue('');
setExposedVariable('value', null);
fireEvent('onChange');
},
setLoading: async function (loading) {
@ -167,7 +171,8 @@ export const NumberInput = function NumberInput({
};
if (!isNaN(value)) {
exposedVariables.value = value;
}
} else exposedVariables.value = null;
setExposedVariables(exposedVariables);
isInitialRender.current = false;

View file

@ -0,0 +1,295 @@
import React, { useEffect, useMemo, useState, useRef } from 'react';
import Label from '@/_ui/Label';
import cx from 'classnames';
import './radioButtonV2.scss';
import Loader from '@/ToolJetUI/Loader/Loader';
import { has, isObject } from 'lodash';
export const RadioButtonV2 = ({
properties,
styles,
fireEvent,
setExposedVariable,
setExposedVariables,
darkMode,
componentName,
validate,
validation,
}) => {
const { label, value, options, disabledState, advanced, schema, optionsLoadingState, loadingState } = properties;
const {
activeColor,
direction,
auto: labelAutoWidth,
labelWidth,
optionsTextColor,
borderColor,
switchOffBackgroundColor,
handleColor,
switchOnBackgroundColor,
labelColor,
alignment,
} = styles;
const isInitialRender = useRef(true);
const [checkedValue, setCheckedValue] = useState(advanced ? findDefaultItem(schema) : value);
const [visibility, setVisibility] = useState(properties.visibility);
const [isLoading, setIsLoading] = useState(loadingState);
const [isDisabled, setIsDisabled] = useState(disabledState);
const isMandatory = validation?.mandatory ?? false;
const [validationStatus, setValidationStatus] = useState(validate(checkedValue));
const { isValid, validationError } = validationStatus;
const labelRef = useRef();
const radioBtnRef = useRef();
const selectOptions = useMemo(() => {
let _options = advanced ? schema : options;
if (Array.isArray(_options)) {
let _selectOptions = _options
.filter((data) => data?.visible ?? true)
.map((data) => ({
...data,
label: data?.label,
value: data?.value,
isDisabled: data?.disable ?? false,
}));
return _selectOptions;
} else {
return [];
}
}, [advanced, schema, options]);
function findDefaultItem(optionSchema) {
if (!Array.isArray(optionSchema)) {
return undefined;
}
const foundItem = optionSchema?.find((item) => item?.default === true && item?.visible === true);
return foundItem?.value;
}
function onSelect(value) {
const _value = isObject(value) && has(value, 'value') ? value?.value : value;
setCheckedValue(_value);
setExposedVariable('value', _value);
const validationStatus = validate(_value);
setValidationStatus(validationStatus);
setExposedVariable('isValid', validationStatus?.isValid);
}
useEffect(() => {
if (isInitialRender.current) return;
if (advanced) {
onSelect(findDefaultItem(schema));
} else onSelect(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, JSON.stringify(schema), value]);
useEffect(() => {
if (visibility !== properties.visibility) setVisibility(properties.visibility);
if (isLoading !== loadingState) setIsLoading(loadingState);
if (isDisabled !== disabledState) setIsDisabled(disabledState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.visibility, loadingState, disabledState]);
useEffect(() => {
if (isInitialRender.current) return;
const _options = selectOptions?.map(({ label, value }) => ({ label, value }));
setExposedVariable('options', _options);
setExposedVariable('selectOption', async function (value) {
onSelect(value);
fireEvent('onSelectionChange');
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(selectOptions)]);
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('label', label);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [label]);
useEffect(() => {
if (isInitialRender.current) return;
const validationStatus = validate(checkedValue);
setValidationStatus(validationStatus);
setExposedVariable('isValid', validationStatus?.isValid);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [validate]);
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('isMandatory', isMandatory);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMandatory]);
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('isLoading', loadingState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadingState]);
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('isVisible', properties.visibility);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.visibility]);
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('isDisabled', disabledState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disabledState]);
useEffect(() => {
const _options = selectOptions?.map(({ label, value }) => ({ label, value }));
const exposedVariables = {
value: checkedValue,
label: label,
options: _options,
isValid: isValid,
isMandatory: isMandatory,
isLoading: loadingState,
isVisible: properties.visibility,
isDisabled: disabledState,
selectOption: async function (value) {
onSelect(value);
fireEvent('onSelectionChange');
},
deselectOption: async function () {
onSelect(null);
fireEvent('onSelectionChange');
},
setVisibility: async function (value) {
setVisibility(value);
setExposedVariable('isVisible', value);
},
setDisable: async function (value) {
setIsDisabled(value);
setExposedVariable('isDisabled', value);
},
setLoading: async function (value) {
setIsLoading(value);
setExposedVariable('isLoading', value);
},
};
setExposedVariables(exposedVariables);
isInitialRender.current = false;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
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(componentName).toLowerCase()} `}
data-disabled={isDisabled}
id={String(componentName)}
className={cx('radio-button,', 'd-flex', {
[alignment === 'top' &&
((labelWidth != 0 && label?.length != 0) ||
(labelAutoWidth && labelWidth == 0 && label && label?.length != 0))
? 'flex-column'
: '']: true,
'flex-row-reverse': direction === 'right' && alignment === 'side',
'text-right': direction === 'right' && alignment === 'top',
invisible: !visibility,
visibility: visibility,
})}
style={{
position: 'relative',
width: '100%',
paddingLeft: '0px',
}}
>
<Label
label={label}
width={labelWidth}
labelRef={labelRef}
darkMode={darkMode}
color={labelColor}
defaultAlignment={alignment}
direction={direction}
auto={labelAutoWidth}
isMandatory={isMandatory}
_width={_width}
top={alignment !== 'top' && '2px'}
/>
<div className="px-0 h-100 w-100" ref={radioBtnRef}>
{isLoading || optionsLoadingState ? (
<Loader style={{ right: '50%', zIndex: 3, position: 'absolute' }} width="20" />
) : (
<div className="">
{selectOptions.map((option, index) => {
const isChecked = checkedValue == option.value;
return (
<label key={index} className="radio-button-container">
<span
style={{
color:
optionsTextColor !== '#1B1F24'
? optionsTextColor
: isDisabled || isLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
}}
>
{option.label}
</span>
<input
style={{
marginTop: '1px',
backgroundColor: checkedValue === option.value ? `${activeColor}` : 'white',
}}
checked={checkedValue == option.value}
type="radio"
value={option.value}
onChange={() => {
onSelect(option.value);
fireEvent('onSelectionChange');
}}
disabled={option.isDisabled}
/>
<span
className="checkmark"
style={{
backgroundColor:
!isChecked && (option.isDisabled ? 'var(--surfaces-surface-03)' : switchOffBackgroundColor),
'--selected-background-color': option.isDisabled
? 'var(--surfaces-surface-03)'
: switchOnBackgroundColor,
'--selected-border-color': borderColor,
'--selected-handle-color': option.isDisabled ? 'var(--icons-default)' : handleColor,
border:
!isChecked && (option.isDisabled ? 'var(--surfaces-surface-03)' : `1px solid ${borderColor}`),
}}
></span>
</label>
);
})}
</div>
)}
</div>
</div>
<div
className={`${isValid ? '' : visibility ? 'd-flex' : 'none'}`}
style={{
color: 'var(--status-error-strong)',
justifyContent: direction === 'right' ? 'flex-start' : 'flex-end',
fontSize: '11px',
fontWeight: '400',
lineHeight: '16px',
}}
>
{!isValid && validationError}
</div>
</>
);
};

View file

@ -0,0 +1,66 @@
/* label container */
.radio-button-container {
position: relative;
padding-left: 22px;
margin-bottom: 8px;
margin-right: 20px;
font-size: 14px;
line-height: 20px;
font-weight: 400;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
// /* Hide the browser's default radio button */
.radio-button-container input {
position: absolute;
opacity: 0;
cursor: pointer;
}
/* Create a custom radio button */
.checkmark {
position: absolute;
top: 2px;
left: 0;
height: 16px;
width: 16px;
border-radius: 50%;
}
// // /* On mouse-over, add a grey background color */
// .radio-button-container:focus input ~ .checkmark {
// border-color: red
// }
// /* When the radio button is checked */
.radio-button-container input:checked ~ .checkmark {
background-color: var(--selected-background-color);
border-color: var(--selected-border-color);
}
// /* Create the indicator (the dot/circle - hidden when not checked) */
.checkmark:after {
content: "";
position: absolute;
display: none;
}
// /* Show the indicator (dot/circle) when checked */
.radio-button-container input:checked ~ .checkmark:after {
display: block;
}
// /* Style the indicator (dot/circle) */
.radio-button-container .checkmark:after {
transform: translate(50%, 50%);
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--selected-handle-color);
}

View file

@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useRef, useState } from 'react';
import 'draft-js/dist/Draft.css';
import { DraftEditor } from './DraftEditor';
@ -8,17 +9,38 @@ export const RichTextEditor = function RichTextEditor({
properties,
styles,
setExposedVariable,
setExposedVariables,
dataCy,
}) {
const isInitialRender = useRef(true);
const { visibility, disabledState, boxShadow } = styles;
const placeholder = properties.placeholder;
const defaultValue = properties?.defaultValue ?? '';
// exposing the default value at first
const [isDisabled, setIsDisabled] = useState(disabledState);
const [isVisible, setIsVisible] = useState(visibility);
const [isLoading, setIsLoading] = useState(properties?.loadingState);
useEffect(() => {
setExposedVariable('value', defaultValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (isDisabled !== disabledState) setIsDisabled(disabledState);
if (isVisible !== visibility) setIsVisible(visibility);
if (isLoading !== properties.loadingState) setIsLoading(properties.loadingState);
}, [properties.loadingState, styles.visibility, styles.disabledState]);
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('isDisabled', disabledState);
}, [disabledState]);
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('isVisible', visibility);
}, [visibility]);
useEffect(() => {
if (isInitialRender.current) return;
setExposedVariable('isLoading', isLoading);
}, [isLoading]);
function handleChange(html) {
setExposedVariable('value', html);
@ -26,16 +48,25 @@ export const RichTextEditor = function RichTextEditor({
return (
<div
data-disabled={disabledState}
style={{ height: `${height}px`, display: visibility ? '' : 'none', boxShadow }}
data-disabled={isDisabled}
style={{ height: `${height}px`, display: isVisible ? '' : 'none', boxShadow }}
data-cy={dataCy}
>
<DraftEditor
isInitialRender={isInitialRender}
handleChange={handleChange}
height={height}
width={width}
placeholder={placeholder}
defaultValue={defaultValue}
isLoading={isLoading}
isVisible={visibility}
isDisabled={disabledState}
setExposedVariable={setExposedVariable}
setExposedVariables={setExposedVariables}
setIsDisabled={setIsDisabled}
setIsVisible={setIsVisible}
setIsLoading={setIsLoading}
></DraftEditor>
</div>
);

View file

@ -18,8 +18,12 @@ export const ToolTip = ({ label, meta, labelClass, bold = false }) => {
if (meta?.tip) {
return (
<OverlayTrigger placement="left" delay={{ show: 250, hide: 400 }} overlay={renderTooltip}>
<label style={tooltipStyle} className={labelClass || 'form-label'}>
<OverlayTrigger
placement={meta?.tooltipPlacement || 'left'}
delay={{ show: 250, hide: 400 }}
overlay={renderTooltip}
>
<label style={meta?.tooltipStyle || tooltipStyle} className={labelClass || 'form-label'}>
{label}
</label>
</OverlayTrigger>

View file

@ -8,12 +8,11 @@ import { useEditorActions, useEditorStore } from '@/_stores/editorStore';
function Logs({ logProps, idx }) {
const [open, setOpen] = React.useState(false);
let titleLogType = logProps?.type;
// need to change the titleLogType to query for transformations because if transformation fails, it is eventually a query failure
let titleLogType = logProps?.type !== 'event' ? logProps?.type : '';
if (titleLogType === 'transformations') {
titleLogType = 'query';
}
const title = ` [${capitalize(titleLogType)} ${logProps?.key}]`;
const title = logProps?.key;
const message =
logProps?.type === 'navToDisablePage'
? logProps?.message
@ -21,19 +20,21 @@ function Logs({ logProps, idx }) {
? 'Completed'
: logProps?.type === 'component'
? `Invalid property detected: ${logProps?.message}.`
: logProps?.type === 'Custom Log'
? logProps?.description
: `${startCase(logProps?.type)} failed: ${
logProps?.description ||
logProps?.message ||
(isString(logProps?.message) && logProps?.message) ||
(isString(logProps?.error?.description) && logProps?.error?.description) || //added string check since description can be an object. eg: runpy
logProps?.error?.message
logProps?.error?.message.trim()
}`;
const defaultStyles = {
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
transform: open ? 'rotate(0deg)' : 'rotate(-90deg)',
transition: '0.2s all',
display: logProps?.isQuerySuccessLog || logProps.type === 'navToDisablePage' ? 'none' : 'inline-block',
cursor: 'pointer',
paddingTop: '8px',
top: '8px',
pointerEvents: logProps?.isQuerySuccessLog || logProps.type === 'navToDisablePage' ? 'none' : 'default',
};
@ -85,20 +86,25 @@ function Logs({ logProps, idx }) {
onClick={(e) => {
setOpen((prev) => !prev);
}}
style={{ pointerEvents: logProps?.isQuerySuccessLog ? 'none' : 'default' }}
style={{ pointerEvents: logProps?.isQuerySuccessLog ? 'none' : 'default', position: 'relative' }}
>
<span className={cx('position-absolute')} style={defaultStyles}>
<SolidIcon name="cheveronright" width="16" />
<SolidIcon name="rightarrrow" fill={`var(--icons-strong)`} width="16" />
</span>
<span className="w-100" style={{ paddingTop: '8px', paddingBottom: '8px', paddingLeft: '20px' }}>
{logProps.type === 'navToDisablePage' ? (
renderNavToDisabledPageMessage()
) : (
<>
<span className="d-flex justify-content-between align-items-center text-truncate">
<span className="text-truncate text-slate-12">{title}</span>
<div className="d-flex align-items-center justify-content-between">
<div className="error-target cursor-pointer">{logProps?.errorTarget}</div>
<small className="text-slate-10 text-right ">{moment(logProps?.timestamp).fromNow()}</small>
</span>
</div>
<div className={`d-flex justify-content-between align-items-center ${!open && 'text-truncate'}`}>
<span className={` cursor-pointer debugger-error-title ${!open && 'text-truncate'}`}>
<HighlightSecondWord text={title} />
</span>
</div>
<span
className={cx('mx-1', {
'text-tomato-9': !logProps?.isQuerySuccessLog,
@ -134,3 +140,22 @@ function Logs({ logProps, idx }) {
let isString = (value) => typeof value === 'string' || value instanceof String;
export default Logs;
const HighlightSecondWord = ({ text }) => {
const processedText = text.split(/(\[.*?\])/).map((segment, index) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const content = segment.slice(1, -1).split(' ');
const firstWord = content[0];
const secondWord = content[1];
return (
<span key={index}>
[{firstWord} <b>{secondWord}</b>]
</span>
);
}
return segment;
});
return <span>{processedText}</span>;
};

View file

@ -15,22 +15,86 @@ export const containerConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
section: 'additionalActions',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
showHeader: {
type: 'toggle',
displayName: 'Show header',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
defaultChildren: [
{
componentName: 'Text',
layout: {
top: 20,
left: 1,
height: 40,
},
displayName: 'ContainerText',
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Container title',
fontWeight: 'bold',
textSize: 16,
textColor: '#000',
},
},
],
events: {},
styles: {
backgroundColor: {
type: 'color',
displayName: 'Background color',
displayName: 'Background',
validation: {
schema: { type: 'string' },
defaultValue: '#fff',
},
},
headerBackgroundColor: {
type: 'color',
displayName: 'Header',
validation: {
schema: { type: 'string' },
defaultValue: '#fff',
},
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
validation: {
schema: { type: 'number' },
defaultValue: 80,
},
accordian: 'field',
},
borderRadius: {
type: 'code',
displayName: 'Border radius',
@ -50,40 +114,48 @@ export const containerConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
},
defaultValue: false,
},
},
exposedVariables: {},
exposedVariables: {
isVisible: true,
isDisabled: false,
isLoading: false,
},
actions: [
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set disable',
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set loading',
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
properties: {
visible: { value: '{{true}}' },
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
headerBackgroundColor: { value: '#fff' },
borderRadius: { value: '4' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '80' },
},
},
};

View file

@ -9,6 +9,7 @@ import { passinputConfig } from './passwordinput';
import { datepickerConfig } from './datepicker';
import { checkboxConfig } from './checkbox';
import { radiobuttonConfig } from './radiobutton';
import { radiobuttonV2Config } from './radioButtonV2';
import { toggleswitchConfig } from './toggleswitch';
import { toggleSwitchV2Config } from './toggleswitchv2';
import { textareaConfig } from './textarea';
@ -65,7 +66,8 @@ export {
passinputConfig,
datepickerConfig,
checkboxConfig,
radiobuttonConfig,
radiobuttonConfig, //!Depreciated
radiobuttonV2Config,
toggleswitchConfig, //!Depreciated
toggleSwitchV2Config,
textareaConfig,

View file

@ -236,7 +236,7 @@ export const numberinputConfig = {
},
],
exposedVariables: {
value: 99,
value: 0,
isMandatory: false,
isVisible: true,
isDisabled: false,

View file

@ -0,0 +1,295 @@
export const radiobuttonV2Config = {
name: 'RadioButton',
displayName: 'Radio Button',
description: 'Select one from multiple choices',
component: 'RadioButtonV2',
defaultSize: {
width: 12,
height: 43,
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
validation: {
customRule: {
type: 'code',
displayName: 'Custom validation',
placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
},
mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
},
properties: {
label: {
type: 'code',
displayName: 'Label',
validation: {
schema: { type: 'string' },
defaultValue: 'Select',
},
accordian: 'Data',
},
advanced: {
type: 'toggle',
displayName: 'Dynamic options',
validation: {
schema: { 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: {
onSelectionChange: { displayName: 'On select' },
},
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',
},
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,
},
borderColor: {
type: 'color',
displayName: 'Border',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
switchOnBackgroundColor: {
type: 'color',
displayName: 'Checked background',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
tip: 'Checked background',
tooltipStyle: {},
tooltipPlacement: 'bottom',
},
switchOffBackgroundColor: {
type: 'color',
displayName: 'Unchecked background',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
tip: 'Unchecked background',
tooltipStyle: {},
tooltipPlacement: 'bottom',
},
handleColor: {
type: 'color',
displayName: 'Handle color',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
optionsTextColor: {
type: 'color',
displayName: 'Text',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'container',
},
},
actions: [
{
handle: 'selectOption',
displayName: 'Select option',
params: [{ handle: 'option', displayName: 'Option' }],
},
{
handle: 'deselectOption',
displayName: 'Deselect option',
params: [{ handle: 'option', displayName: 'Option' }],
},
{
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' }],
},
],
exposedVariables: {
label: 'Select',
},
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
validation: {
mandatory: { value: '{{false}}' },
},
properties: {
label: { value: 'Select' },
value: { value: '{{"2"}}' },
advanced: { value: `{{false}}` },
options: {
value: [
{
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 },
},
],
},
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },
optionsLoadingState: { value: '{{false}}' },
optionVisibility: { value: '{{[true, true, true]}}' },
optionDisable: { value: '{{[false, false, false]}}' },
schema: {
value:
"{{[\t{label: 'option1',value: '1',disable: false,visible: true,default: true},{label: 'option2',value: '2',disable: false,visible: true},{label: 'option3',value: '3',disable: false,visible: true}\t]}}",
},
},
events: [],
styles: {
labelColor: { value: '#11181C' },
direction: { value: 'left' },
alignment: { value: 'side' },
auto: { value: '{{false}}' },
labelWidth: { value: '20' },
borderColor: { value: '#FFFFFF' },
switchOffBackgroundColor: { value: '#FFFFFF' },
switchOnBackgroundColor: { value: '#4368E3' },
handleColor: { value: '#FFFFFF' },
optionsTextColor: { value: '#11181C' },
padding: { value: 'default' },
},
},
};

View file

@ -1,6 +1,6 @@
export const radiobuttonConfig = {
name: 'RadioButton',
displayName: 'Radio Button',
name: 'RadioButtonLegacy',
displayName: 'Radio Button (Legacy)',
description: 'Select one from multiple choices',
component: 'RadioButton',
defaultSize: {

View file

@ -28,6 +28,15 @@ export const richtextareaConfig = {
defaultValue: 'Default text',
},
},
loadingState: {
type: 'toggle',
displayName: 'Show loading state',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
section: 'additionalActions',
},
},
events: {},
styles: {
@ -55,6 +64,28 @@ export const richtextareaConfig = {
exposedVariables: {
value: '',
},
actions: [
{
handle: 'setValue',
displayName: 'Set value',
params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }],
},
{
handle: 'setDisable',
displayName: 'Set disable',
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
},
{
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' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
@ -63,6 +94,7 @@ export const richtextareaConfig = {
properties: {
placeholder: { value: 'Placeholder text' },
defaultValue: { value: '' },
loadingState: { value: `{{false}}` },
},
events: [],
styles: {

View file

@ -289,6 +289,7 @@ export const tableConfig = {
onCellValueChanged: { displayName: 'Cell value changed' },
onFilterChanged: { displayName: 'Filter changed' },
onNewRowsAdded: { displayName: 'Add new rows' },
onTableDataDownload: { displayName: 'Download data' },
},
styles: {
textColor: {
@ -310,6 +311,16 @@ export const tableConfig = {
{ displayName: 'Wrap', value: 'wrap' },
],
},
headerCasing: {
type: 'switch',
displayName: 'Header casing',
validation: { schema: { type: 'string' } },
accordian: 'Data',
options: [
{ displayName: 'AA', value: 'uppercase' },
{ displayName: 'As typed', value: 'none' },
],
},
tableType: {
type: 'select',
displayName: 'Row style',
@ -529,6 +540,7 @@ export const tableConfig = {
value: [
{
name: 'id',
key: 'id',
id: 'e3ecbf7fa52c4d7210a93edb8f43776267a489bad52bd108be9588f790126737',
autogenerated: true,
fxActiveFields: [],
@ -548,6 +560,7 @@ export const tableConfig = {
},
{
name: 'name',
key: 'name',
id: '5d2a3744a006388aadd012fcc15cc0dbcb5f9130e0fbb64c558561c97118754a',
autogenerated: true,
fxActiveFields: [],
@ -556,6 +569,7 @@ export const tableConfig = {
},
{
name: 'email',
key: 'email',
id: 'afc9a5091750a1bd4760e38760de3b4be11a43452ae8ae07ce2eebc569fe9a7f',
autogenerated: true,
fxActiveFields: [],
@ -564,6 +578,7 @@ export const tableConfig = {
},
{
name: 'date',
key: 'date',
id: '27b75c8af9d34d1eaa1f9bb7f8f9f7b0abf1823e799748c8bb57e74f53b2c1dc',
autogenerated: true,
fxActiveFields: [],
@ -576,6 +591,7 @@ export const tableConfig = {
},
{
name: 'mobile_number',
key: 'mobile_number',
id: '9c2e3c40572a4aefb8e179ee39a0e1ac9dc2b2e6634be56e1c05be13c3d1de56',
autogenerated: true,
fxActiveFields: [],
@ -652,6 +668,7 @@ export const tableConfig = {
styles: {
textColor: { value: '#000' },
columnHeaderWrap: { value: 'fixed' },
headerCasing: { value: 'uppercase' },
actionButtonRadius: { value: '0' },
cellSize: { value: 'regular' },
borderRadius: { value: '8' },

View file

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

View file

@ -10,6 +10,7 @@ import {
datepickerConfig,
checkboxConfig,
radiobuttonConfig,
radiobuttonV2Config,
toggleswitchConfig,
toggleSwitchV2Config,
textareaConfig,
@ -68,6 +69,7 @@ export const widgets = [
datepickerConfig,
checkboxConfig,
radiobuttonConfig,
radiobuttonV2Config,
toggleswitchConfig,
toggleSwitchV2Config,
textareaConfig,

View file

@ -232,11 +232,24 @@
}
.debugger-content {
padding: 0px 16px;
background-color: var(--base);
cursor: pointer;
hr {
margin-top: 0px !important;
margin-bottom: 16px !important;
margin: 0px !important;
margin-top: 16px !important;
}
&:hover {
background-color: var(--slate3);
}
.error-target {
background-color: var(--interactive-overlays-fill-hover) !important;
padding: 4px 7px;
border-radius: 7px;
color: var(--slate10)
}
}

View file

@ -14400,7 +14400,6 @@ color: var(--text-default);
.debugger-card-body {
margin-top: 8px;
margin-bottom: 16px;
padding: 0px 16px;
}
.left-sidebar-header-btn {
@ -14743,7 +14742,6 @@ color: var(--text-default);
.debugger-card-body {
margin-top: 8px;
margin-bottom: 16px;
padding: 0px 16px;
}
.left-sidebar-header-btn {
@ -15103,7 +15101,6 @@ color: var(--text-default);
.debugger-card-body {
margin-top: 8px;
margin-bottom: 16px;
padding: 0px 16px;
}
.left-sidebar-header-btn {

View file

@ -1 +1 @@
3.0.5-ce
3.1.0-ce

View file

@ -7,6 +7,7 @@
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"postbuild": "npm run copy-schemas",
"lint": "eslint . '**/*.ts'",
"format": "eslint . --fix '**/*.ts'",
"start": "nest start",
@ -32,7 +33,8 @@
"db:setup": "npm run db:create && npm run db:migrate",
"db:setup:prod": "npm run db:create:prod && npm run db:migrate:prod",
"db:reset": "npm run db:drop && npm run db:setup",
"typeorm": "typeorm-ts-node-commonjs"
"typeorm": "typeorm-ts-node-commonjs",
"copy-schemas": "copyfiles -u 4 src/dto/validators/schemas/**/*.json dist/src/dto/validators/schemas"
},
"dependencies": {
"@casl/ability": "^5.3.1",
@ -60,6 +62,7 @@
"class-validator": "^0.14.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"copyfiles": "^2.4.1",
"dotenv": "^10.0.0",
"express-http-proxy": "^1.6.3",
"fast-csv": "^4.3.6",
@ -76,8 +79,8 @@
"lodash": "^4.17.21",
"module-from-string": "^3.3.0",
"nestjs-pino": "^1.4.0",
"node-sql-parser": "^5.3.1",
"node-mailer": "^0.1.1",
"node-sql-parser": "^5.3.1",
"nodemailer": "^6.6.3",
"passport": "^0.7.0",
"passport-jwt": "^4.0.0",
@ -152,4 +155,4 @@
"node": "18.18.2",
"npm": "9.8.1"
}
}
}

View file

@ -433,4 +433,4 @@ export const getSubpath = () => {
}
}
return subpath;
};
};

View file

@ -15,6 +15,33 @@ export const containerConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
showHeader: {
type: 'toggle',
displayName: 'Show header',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -25,12 +52,29 @@ export const containerConfig = {
styles: {
backgroundColor: {
type: 'color',
displayName: 'Background color',
displayName: 'Background',
validation: {
schema: { type: 'string' },
defaultValue: '#fff',
},
},
headerBackgroundColor: {
type: 'color',
displayName: 'Header',
validation: {
schema: { type: 'string' },
defaultValue: '#fff',
},
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
validation: {
schema: { type: 'number' },
defaultValue: 80,
},
accordian: 'field',
},
borderRadius: {
type: 'code',
displayName: 'Border radius',
@ -50,40 +94,67 @@ export const containerConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
},
defaultValue: false,
},
},
exposedVariables: {},
defaultChildren: [
{
componentName: 'Text',
layout: {
top: 20,
left: 1,
height: 40,
},
displayName: 'ContainerText',
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Container title',
fontWeight: 'bold',
textSize: 16,
textColor: '#000',
},
},
],
exposedVariables: {
isVisible: true,
isDisabled: false,
isLoading: false,
},
actions: [
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set disable',
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set loading',
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
properties: {
visible: { value: '{{true}}' },
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
headerBackgroundColor: { value: '#fff' },
borderRadius: { value: '4' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: `80`, },
},
},
};

View file

@ -9,6 +9,7 @@ import { passinputConfig } from './passwordinput';
import { datepickerConfig } from './datepicker';
import { checkboxConfig } from './checkbox';
import { radiobuttonConfig } from './radiobutton';
import { radiobuttonV2Config } from './radioButtonV2';
import { toggleswitchConfig } from './toggleswitch';
import { toggleSwitchV2Config } from './toggleswitchv2';
import { textareaConfig } from './textarea';
@ -65,7 +66,8 @@ const widgets = {
passinputConfig,
datepickerConfig,
checkboxConfig,
radiobuttonConfig,
radiobuttonConfig, //!Depreciated
radiobuttonV2Config,
toggleswitchConfig, //!Depreciated
toggleSwitchV2Config,
textareaConfig,

View file

@ -236,7 +236,7 @@ export const numberinputConfig = {
},
],
exposedVariables: {
value: 99,
value: 0,
isMandatory: false,
isVisible: true,
isDisabled: false,

View file

@ -0,0 +1,295 @@
export const radiobuttonV2Config = {
name: 'RadioButton',
displayName: 'Radio Button',
description: 'Select one from multiple choices',
component: 'RadioButtonV2',
defaultSize: {
width: 12,
height: 43,
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
validation: {
customRule: {
type: 'code',
displayName: 'Custom validation',
placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
},
mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
},
properties: {
label: {
type: 'code',
displayName: 'Label',
validation: {
schema: { type: 'string' },
defaultValue: 'Select',
},
accordian: 'Data',
},
advanced: {
type: 'toggle',
displayName: 'Dynamic options',
validation: {
schema: { 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: {
onSelectionChange: { displayName: 'On select' },
},
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',
},
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,
},
borderColor: {
type: 'color',
displayName: 'Border',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
switchOnBackgroundColor: {
type: 'color',
displayName: 'Checked background',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
tip: 'Checked background',
tooltipStyle: {},
tooltipPlacement: 'bottom',
},
switchOffBackgroundColor: {
type: 'color',
displayName: 'Unchecked background',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
tip: 'Unchecked background',
tooltipStyle: {},
tooltipPlacement: 'bottom',
},
handleColor: {
type: 'color',
displayName: 'Handle color',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
optionsTextColor: {
type: 'color',
displayName: 'Text',
validation: {
schema: { type: 'string' },
},
accordian: 'switch',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'container',
},
},
actions: [
{
handle: 'selectOption',
displayName: 'Select option',
params: [{ handle: 'option', displayName: 'Option' }],
},
{
handle: 'deselectOption',
displayName: 'Deselect option',
params: [{ handle: 'option', displayName: 'Option' }],
},
{
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' }],
},
],
exposedVariables: {
label: 'Select',
},
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
validation: {
mandatory: { value: '{{false}}' },
},
properties: {
label: { value: 'Select' },
value: { value: '{{"2"}}' },
advanced: { value: `{{false}}` },
options: {
value: [
{
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 },
},
],
},
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },
optionsLoadingState: { value: '{{false}}' },
optionVisibility: { value: '{{[true, true, true]}}' },
optionDisable: { value: '{{[false, false, false]}}' },
schema: {
value:
"{{[\t{label: 'option1',value: '1',disable: false,visible: true,default: true},{label: 'option2',value: '2',disable: false,visible: true},{label: 'option3',value: '3',disable: false,visible: true}\t]}}",
},
},
events: [],
styles: {
labelColor: { value: '#11181C' },
direction: { value: 'left' },
alignment: { value: 'side' },
auto: { value: '{{false}}' },
labelWidth: { value: '20' },
borderColor: { value: '#FFFFFF' },
switchOffBackgroundColor: { value: '#FFFFFF' },
switchOnBackgroundColor: { value: '#4368E3' },
handleColor: { value: '#FFFFFF' },
optionsTextColor: { value: '#11181C' },
padding: { value: 'default' },
},
},
};

View file

@ -28,6 +28,15 @@ export const richtextareaConfig = {
defaultValue: 'Default text',
},
},
loadingState: {
type: 'toggle',
displayName: 'Show loading state',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
section: 'additionalActions',
},
},
events: {},
styles: {
@ -55,6 +64,28 @@ export const richtextareaConfig = {
exposedVariables: {
value: '',
},
actions: [
{
handle: 'setValue',
displayName: 'Set value',
params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }],
},
{
handle: 'setDisable',
displayName: 'Set disable',
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
},
{
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' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
@ -63,6 +94,7 @@ export const richtextareaConfig = {
properties: {
placeholder: { value: 'Placeholder text' },
defaultValue: { value: '' },
loadingState: { value: `{{false}}` },
},
events: [],
styles: {

View file

@ -289,6 +289,7 @@ export const tableConfig = {
onCellValueChanged: { displayName: 'Cell value changed' },
onFilterChanged: { displayName: 'Filter changed' },
onNewRowsAdded: { displayName: 'Add new rows' },
onTableDataDownload: { displayName: 'Download data' },
},
styles: {
textColor: {
@ -310,6 +311,16 @@ export const tableConfig = {
{ displayName: 'Wrap', value: 'wrap' },
],
},
headerCasing: {
type: 'switch',
displayName: 'Header casing',
validation: { schema: { type: 'string' } },
accordian: 'Data',
options: [
{ displayName: 'AA', value: 'uppercase' },
{ displayName: 'As typed', value: 'none' },
],
},
tableType: {
type: 'select',
displayName: 'Row style',
@ -530,6 +541,7 @@ export const tableConfig = {
value: [
{
name: 'id',
key: 'id',
id: 'e3ecbf7fa52c4d7210a93edb8f43776267a489bad52bd108be9588f790126737',
autogenerated: true,
fxActiveFields: [],
@ -549,6 +561,7 @@ export const tableConfig = {
},
{
name: 'name',
key: 'name',
id: '5d2a3744a006388aadd012fcc15cc0dbcb5f9130e0fbb64c558561c97118754a',
autogenerated: true,
fxActiveFields: [],
@ -557,6 +570,7 @@ export const tableConfig = {
},
{
name: 'email',
key: 'email',
id: 'afc9a5091750a1bd4760e38760de3b4be11a43452ae8ae07ce2eebc569fe9a7f',
autogenerated: true,
fxActiveFields: [],
@ -565,6 +579,7 @@ export const tableConfig = {
},
{
name: 'date',
key: 'date',
id: '27b75c8af9d34d1eaa1f9bb7f8f9f7b0abf1823e799748c8bb57e74f53b2c1dc',
autogenerated: true,
fxActiveFields: [],
@ -577,6 +592,7 @@ export const tableConfig = {
},
{
name: 'phone',
key: 'phone',
id: '9c2e3c40572a4aefb8e179ee39a0e1ac9dc2b2e6634be56e1c05be13c3d1de56',
autogenerated: true,
fxActiveFields: [],
@ -653,6 +669,7 @@ export const tableConfig = {
styles: {
textColor: { value: '#000' },
columnHeaderWrap: { value: 'fixed' },
headerCasing: { value: 'uppercase' },
actionButtonRadius: { value: '0' },
cellSize: { value: 'regular' },
borderRadius: { value: '8' },

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"module": "commonjs",
"declaration": true,
"removeComments": true,