diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index cf40bd2e05..5ece3710de 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -22,6 +22,8 @@ import { handleActivateTargets, handleDeactivateTargets, handleActivateNonDraggingComponents, + computeScrollDelta, + computeScrollDeltaOnDrag, } from './gridUtils'; import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd'; import useStore from '@/AppBuilder/_stores/store'; @@ -56,6 +58,7 @@ export default function Grid({ gridWidth, currentLayout }) { const canvasWidth = NO_OF_GRIDS * gridWidth; const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow); const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow); + const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow); const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS); const draggingComponentId = useStore((state) => state.draggingComponentId, shallow); const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); @@ -345,6 +348,7 @@ export default function Grid({ gridWidth, currentLayout }) { const handleDragEnd = useCallback( (boxPositions) => { let newParent = null; + let oldParent = null; const updatedLayouts = boxPositions.reduce((layouts, { id, x, y, parent }) => { const currentWidget = boxList.find((box) => box.id === id); const containerWidth = parent ? useGridStore.getState().subContainerWidths[parent] : gridWidth; @@ -389,7 +393,7 @@ export default function Grid({ gridWidth, currentLayout }) { } } newParent = parent ? parent : null; - + oldParent = currentWidget.component?.parent; layouts[id] = { width: _width, height: _height, @@ -400,6 +404,11 @@ export default function Grid({ gridWidth, currentLayout }) { return layouts; }, {}); setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true }); + + // const currentWidget = boxList.find((box) => box.id === id); + updateContainerAutoHeight(newParent); + updateContainerAutoHeight(oldParent); + toggleCanvasUpdater(); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -870,20 +879,19 @@ export default function Grid({ gridWidth, currentLayout }) { const targetSlotId = target?.slotId; const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth; - - // const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || []; - // const draggedWidgetType = dragged.widgetType; const isParentChangeAllowed = dragContext.isDroppable; // Compute new position let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged); const isModalToCanvas = source.isModal && target.slotId === 'real-canvas'; + let scrollDelta = computeScrollDelta({ source }); if (isParentChangeAllowed && !isModalToCanvas) { - const parent = target.slotId === 'real-canvas' ? null : target.slotId; // Special case for Modal; If source widget is modal, prevent drops to canvas - handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]); + const parent = target.slotId === 'real-canvas' ? null : target.slotId; + + handleDragEnd([{ id: e.target.id, x: left, y: top + scrollDelta, parent }]); } else { const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth; @@ -892,9 +900,8 @@ export default function Grid({ gridWidth, currentLayout }) { !isModalToCanvas ?? toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`); } - // Apply transform for smooth transition - e.target.style.transform = `translate(${left}px, ${top}px)`; + e.target.style.transform = `translate(${left}px, ${top + scrollDelta}px)`; // Force reordering of conatiner if the parent has not changed const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId; @@ -962,12 +969,6 @@ export default function Grid({ gridWidth, currentLayout }) { setCanvasBounds({ ...relativePosition }); } - e.target.style.transform = `translate(${left}px, ${top}px)`; - e.target.setAttribute( - 'widget-pos2', - `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` - ); - // This block is to show grid lines on the canvas when the dragged element is over a new canvas if (document.elementFromPoint(e.clientX, e.clientY)) { const targetElems = document.elementsFromPoint(e.clientX, e.clientY); @@ -995,6 +996,17 @@ export default function Grid({ gridWidth, currentLayout }) { handleActivateTargets(newParentId); } } + + // Build the drag context from the event + const source = { slotId: oldParentId }; + let scrollDelta = computeScrollDeltaOnDrag({ source }); + + e.target.style.transform = `translate(${left}px, ${top - scrollDelta}px)`; + e.target.setAttribute( + 'widget-pos2', + `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` + ); + // Postion ghost element exactly as same at dragged element if (document.getElementById(`moveable-drag-ghost`)) { document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`; @@ -1084,6 +1096,7 @@ export default function Grid({ gridWidth, currentLayout }) { } }} snapGridAll={true} + scrollable={true} /> > ); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index da179bc11d..f36f921be9 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -415,6 +415,20 @@ export function hideGridLines() { document.getElementById('real-canvas')?.classList.add('hide-grid'); } +export function showGridLinesOnSlot(slotId) { + var canvasElm = document.getElementById(`canvas-${slotId}`); + + canvasElm.classList.remove('hide-grid'); + canvasElm.classList.add('show-grid'); +} + +export function hideGridLinesOnSlot(slotId) { + var canvasElm = document.getElementById(`canvas-${slotId}`); + + canvasElm.classList.remove('show-grid'); + canvasElm.classList.add('hide-grid'); +} + // Track previously active elements for efficient cleanup let previousActiveWidgets = null; let previousActiveCanvas = null; @@ -488,3 +502,18 @@ export const handleDeactivateTargets = () => { component.classList.remove('non-dragging-component'); }); }; +export const computeScrollDelta = ({ source }) => { + // Only need to calculate scroll delta when moving from a sub-container + if (source.slotId !== 'real-canvas') { + const subContainerWrap = document + .querySelector(`#canvas-${source.slotId}`) + ?.closest('.sub-container-overflow-wrap'); + + return subContainerWrap?.scrollTop || 0; + } + + // Default case: No scroll adjustment needed + return 0; +}; + +export const computeScrollDeltaOnDrag = computeScrollDelta; diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js index a9405d043e..da5a8341bf 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js @@ -175,7 +175,6 @@ export class DragContext { const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot]; return !restrictedWidgets.includes(dragged.widgetType); - ß; } } diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 9507370e13..6610ae5fb4 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -33,6 +33,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [ 'Divider', 'VerticalDivider', 'Link', + 'Form', ]; const RenderWidget = ({ diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx index 690b0ebd10..6b0bc05422 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx @@ -182,25 +182,6 @@ export const baseComponentProperties = ( }); } - items.push({ - title: `${i18next.t('widget.common.general', 'General')}`, - isOpen: true, - children: ( - <> - {renderElement( - component, - componentMeta, - layoutPropertyChanged, - dataQueries, - 'tooltip', - 'general', - currentState, - allComponents - )} - > - ), - }); - items.push({ title: `${i18next.t('widget.common.devices', 'Devices')}`, isOpen: true, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/container.js b/frontend/src/AppBuilder/WidgetManager/widgets/container.js index 04eb035abf..6dc9a679a4 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/container.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/container.js @@ -3,7 +3,7 @@ export const containerConfig = { displayName: 'Container', description: 'Group components', defaultSize: { - width: 5, + width: 10, height: 200, }, component: 'Container', @@ -47,10 +47,16 @@ export const containerConfig = { defaultValue: true, }, }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, }, defaultChildren: [ { componentName: 'Text', + slotName: 'header', layout: { top: 20, left: 1, @@ -98,15 +104,6 @@ export const containerConfig = { }, accordian: 'container', }, - headerHeight: { - type: 'numberInput', - displayName: 'Height', - validation: { - schema: { type: 'number' }, - defaultValue: 80, - }, - accordian: 'header', - }, borderRadius: { type: 'numberInput', displayName: 'Border', @@ -158,6 +155,7 @@ export const containerConfig = { loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: `{{80}}` }, }, events: [], styles: { diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index d299ec2a6f..129322cf73 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -4,7 +4,7 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 480, + height: 450, }, defaultChildren: [ { @@ -19,8 +19,8 @@ export const formConfig = { accessorKey: 'text', styles: ['fontWeight', 'textSize', 'textColor'], defaultValue: { - text: 'Form title', - textSize: 20, + text: 'Form', + textSize: 16, textColor: '#000', }, }, @@ -34,203 +34,68 @@ export const formConfig = { }, properties: ['text'], defaultValue: { - text: 'Button2', + text: 'Submit', padding: 'none', }, }, - { - componentName: 'Text', - layout: { - top: 40, - left: 10, - height: 30, - width: 17, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'User Details', - fontWeight: 'bold', - textSize: 18, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 90, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Name', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 160, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Age', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, { componentName: 'TextInput', layout: { - top: 120, - left: 10, - height: 30, - width: 25, + top: 20, + left: 5, + height: 40, + width: 31, }, properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Enter your name', - label: '', + label: 'Name', + width: '{{60}}', + direction: 'left', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { componentName: 'NumberInput', layout: { - top: 190, - left: 10, - height: 30, - width: 25, + top: 80, + left: 5, + height: 40, + width: 31, }, - properties: ['value', 'label'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { - value: 24, - label: '', + placeholder: 'Age', + label: 'Age', + width: '{{60}}', + direction: 'left', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { - componentName: 'Button', + componentName: 'TextInput', layout: { - top: 240, - left: 10, - height: 30, - width: 10, + top: 140, + left: 5, + height: 40, + width: 31, }, - properties: ['text'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { - text: 'Submit', + placeholder: 'Tomy', + label: 'Pet name', + width: '{{60}}', + alignment: 'side', + direction: 'left', + auto: '{{false}}', + padding: 'default', }, }, ], @@ -276,6 +141,24 @@ export const formConfig = { }, showHeader: { type: 'toggle', displayName: 'Header' }, showFooter: { type: 'toggle', displayName: 'Footer' }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + canvasHeight: { + type: 'numberInput', + displayName: 'Canvas height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, visibility: { type: 'toggle', displayName: 'Visibility', @@ -294,6 +177,13 @@ export const formConfig = { defaultValue: false, }, }, + tooltip: { + type: 'code', + displayName: 'Tooltip', + validation: { schema: { type: 'string' } }, + section: 'additionalActions', + placeholder: 'Enter tooltip text', + }, }, events: { onSubmit: { displayName: 'On submit' }, @@ -316,22 +206,6 @@ export const formConfig = { defaultValue: '#ffffffff', }, }, - headerHeight: { - type: 'code', - displayName: 'Header height', - validation: { - schema: { type: 'string' }, - defaultValue: '80px', - }, - }, - footerHeight: { - type: 'code', - displayName: 'Footer height', - validation: { - schema: { type: 'string' }, - defaultValue: '80px', - }, - }, backgroundColor: { type: 'colorSwatches', displayName: 'Background color', @@ -403,18 +277,18 @@ export const formConfig = { value: "{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}", }, - showHeader: { value: '{{false}}' }, - showFooter: { value: '{{false}}' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: 60 }, + footerHeight: { value: 60 }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - headerHeight: { value: '60px' }, - footerHeight: { value: '60px' }, }, }, }; diff --git a/frontend/src/AppBuilder/Widgets/Container/Container.jsx b/frontend/src/AppBuilder/Widgets/Container/Container.jsx index 4978427370..a706d29069 100644 --- a/frontend/src/AppBuilder/Widgets/Container/Container.jsx +++ b/frontend/src/AppBuilder/Widgets/Container/Container.jsx @@ -33,7 +33,8 @@ export const Container = ({ shallow ); - const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles; + const { borderRadius, borderColor, boxShadow } = styles; + const { headerHeight = 80 } = properties; const contentBgColor = useMemo(() => { return { backgroundColor: diff --git a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx new file mode 100644 index 0000000000..888b91d3b7 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx @@ -0,0 +1,88 @@ +import React, { useEffect } from 'react'; +import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; +import { showGridLinesOnSlot, hideGridLinesOnSlot } from '@/AppBuilder/AppCanvas/Grid/gridUtils'; +import { useResizable } from '@/AppBuilder/_hooks/useMoveable'; + +export const HorizontalSlot = React.memo( + ({ + id, + height = 0, + width, + darkMode, + isDisabled, + isActive, + slotName = 'header', // 'header' or 'footer' + slotStyle = {}, + onResize, + isEditing, + maxHeight, + }) => { + const parsedHeight = parseInt(height, 10); + + const { getRootProps, getHandleProps, getResizeState } = useResizable({ + initialHeight: parsedHeight, + initialWidth: '100%', // Now respects parent's width + minHeight: 10, + maxHeight: maxHeight || 400, + maxWidth: '100%', + stepHeight: 10, // Height will change in steps of 10px + onResize: () => {}, + onDragEnd: (values) => { + onResize(values); + }, + isReverseVerticalDrag: slotName === 'footer', // Reverse dragging for Footer + }); + + const { height: resizedHeight, isDragging } = getResizeState(); + + useEffect(() => { + if (isDragging) { + showGridLinesOnSlot(id); + } else { + hideGridLinesOnSlot(id); + } + }, [isDragging, id]); + + const canvasHeight = parseInt(resizedHeight, 10) / 10; + + const resizeStyle = { + backgroundColor: darkMode ? '#1F2837' : '#fff', + }; + + return ( +