diff --git a/.version b/.version index a35c54823f..c9e290188f 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.0.5-ce +3.1.0-ce diff --git a/frontend/.version b/frontend/.version index a35c54823f..c9e290188f 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.0.5-ce +3.1.0-ce diff --git a/frontend/assets/images/icons/widgets/index.jsx b/frontend/assets/images/icons/widgets/index.jsx index ebbb50eee0..8d6997b4c2 100644 --- a/frontend/assets/images/icons/widgets/index.jsx +++ b/frontend/assets/images/icons/widgets/index.jsx @@ -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 ; case 'qrscanner': return ; - case 'radiobutton': + case 'radiobuttonlegacy': return ; + case 'radiobutton': + return ; case 'rangeslider': return ; case 'rating': diff --git a/frontend/assets/images/icons/widgets/radiobuttonV2.jsx b/frontend/assets/images/icons/widgets/radiobuttonV2.jsx new file mode 100644 index 0000000000..28b6349eb7 --- /dev/null +++ b/frontend/assets/images/icons/widgets/radiobuttonV2.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +const RadioButtonV2 = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 49 48' }) => ( + + + + +); + +export default RadioButtonV2; diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss index 4298cb9439..1f88e79baa 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss @@ -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; -} \ No newline at end of file +} diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index a8a10caf55..c8f1311033 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -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; diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index de89652d18..fe9c98b10d 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -18,6 +18,7 @@ const shouldAddBoxShadowAndVisibility = [ 'ToggleSwitchV2', 'DropdownV2', 'MultiselectV2', + 'RadioButtonV2', ]; const RenderWidget = ({ diff --git a/frontend/src/AppBuilder/AppCanvas/Selecto.jsx b/frontend/src/AppBuilder/AppCanvas/Selecto.jsx index 6fcc85ea67..dae6fed99a 100644 --- a/frontend/src/AppBuilder/AppCanvas/Selecto.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Selecto.jsx @@ -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; } diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js index 6d15fea9ad..a96dc9f9c7 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js @@ -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; diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js index 6171473174..c4e4ec6402 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js @@ -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; +}; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx index a699b3d97f..aca9eff7cb 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx @@ -124,7 +124,7 @@ export const ComponentsManagerTab = ({ darkMode }) => { 'MultiselectV2', 'RichTextEditor', 'Checkbox', - 'RadioButton', + 'RadioButtonV2', 'Datepicker', 'DateRangePicker', 'FilePicker', diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js index 82bfbfc4d2..bc7d91d4cb 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js +++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js @@ -1 +1 @@ -export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect']; +export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton']; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Chart.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Chart.jsx index ebe03675e7..fe6c33341b 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Chart.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Chart.jsx @@ -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', diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx index 74ae7c6e47..8ab5c9356a 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx @@ -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', diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx index 09c813fc7c..c3add3f1cc 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx @@ -352,6 +352,7 @@ export const EventManager = ({ actionId: 'show-alert', message: 'Hello world!', alertType: 'info', + component: eventMetaDefinition.name, ...customEventRefs, }, eventType: eventSourceType, diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx index e09862bc38..fdebdaaab5 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx @@ -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 { + onSelect(option.value); + fireEvent('onSelectionChange'); + }} + disabled={option.isDisabled} + /> + + + ); + })} + + )} + + +
+ {!isValid && validationError} +
+ + ); +}; diff --git a/frontend/src/Editor/Components/RadioButtonV2/radioButtonV2.scss b/frontend/src/Editor/Components/RadioButtonV2/radioButtonV2.scss new file mode 100644 index 0000000000..d7552b7bdd --- /dev/null +++ b/frontend/src/Editor/Components/RadioButtonV2/radioButtonV2.scss @@ -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); + } + \ No newline at end of file diff --git a/frontend/src/Editor/Components/RichTextEditor.jsx b/frontend/src/Editor/Components/RichTextEditor.jsx index 4115d8deb8..2a591afba0 100644 --- a/frontend/src/Editor/Components/RichTextEditor.jsx +++ b/frontend/src/Editor/Components/RichTextEditor.jsx @@ -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 (
); diff --git a/frontend/src/Editor/Inspector/Elements/Components/ToolTip.jsx b/frontend/src/Editor/Inspector/Elements/Components/ToolTip.jsx index 218bbe5e24..57e653ae4c 100644 --- a/frontend/src/Editor/Inspector/Elements/Components/ToolTip.jsx +++ b/frontend/src/Editor/Inspector/Elements/Components/ToolTip.jsx @@ -18,8 +18,12 @@ export const ToolTip = ({ label, meta, labelClass, bold = false }) => { if (meta?.tip) { return ( - -