@@ -382,30 +381,18 @@ export const Form = function Form(props) {
)}
-
- {isDisabled && (
-
+
)}
);
diff --git a/frontend/src/AppBuilder/Widgets/Form/form.scss b/frontend/src/AppBuilder/Widgets/Form/form.scss
index 530e837eb2..1758e1d0d1 100644
--- a/frontend/src/AppBuilder/Widgets/Form/form.scss
+++ b/frontend/src/AppBuilder/Widgets/Form/form.scss
@@ -1,3 +1,7 @@
+.jet-form-body {
+ background-color: inherit;
+}
+
.wj-form-header {
position: relative;
&::after {
@@ -38,3 +42,44 @@
box-sizing: content-box;
padding: 4px 0;
}
+
+.resizable-slot {
+ position: relative;
+ height: auto;
+ box-shadow: 0 0 0 1px transparent; /* Acts as a border */
+ transition: box-shadow 0.15s ease-in-out;
+
+ &:hover {
+ box-shadow: 0 0 0 1px var(--border-weak);
+ }
+
+ &.active {
+ box-shadow: 0 0 0 1px var(--border-accent-strong);
+ }
+
+ .resize-handle {
+ position: absolute;
+ bottom: -4px;
+ left: 50%; /* Center horizontally */
+ transform: translateX(-50%); /* Ensure proper centering */
+ width: 24px;
+ height: 8px;
+ border-radius: 4px;
+ background-color: var(--background-accent-strong);
+ cursor: ns-resize;
+ z-index: 1;
+ visibility: hidden;
+ transition: visibility 0.15s ease-in-out;
+ }
+
+ &.active .resize-handle {
+ visibility: visible;
+ }
+}
+.only-bottom {
+}
+
+.jet-form-footer .resize-handle {
+ top: -4px;
+ bottom: unset;
+}
diff --git a/frontend/src/AppBuilder/_hooks/useActiveSlot.js b/frontend/src/AppBuilder/_hooks/useActiveSlot.js
new file mode 100644
index 0000000000..bc3269a7ca
--- /dev/null
+++ b/frontend/src/AppBuilder/_hooks/useActiveSlot.js
@@ -0,0 +1,46 @@
+import { useState, useEffect } from 'react';
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
+
+const useIsWidgetSelected = (id) => {
+ // Get selected components from store using shallow comparison
+ const selectedComponents = useStore((state) => state.selectedComponents, shallow);
+
+ // Check if the only selected component is the provided `id`
+ return selectedComponents.length === 1 && selectedComponents[0] === id;
+};
+
+
+export const useActiveSlot = (widgetId) => {
+ const [activeSlot, setActiveSlot] = useState(''); // Default to widget ID
+ const isSelected = useIsWidgetSelected(widgetId); // Check if widget is selected
+ useEffect(() => {
+ const handleClick = (event) => {
+ let target = event.target;
+
+ // Traverse up to find a slot with an id
+ while (target && target !== document.body) {
+ if (target.id && target.id.startsWith('canvas-')) {
+ const slotId = target.id.replace(/^canvas-/, ''); // ✅ Strip "canvas-"
+ setActiveSlot(slotId);
+ return;
+ }
+ target = target.parentElement;
+ }
+
+ // If no slot is found, reset to widget ID
+ setActiveSlot(widgetId);
+ };
+
+ // Attach single click if the widget is selected, otherwise listen for double-click
+ const eventType = isSelected ? 'click' : 'dblclick';
+
+ document.addEventListener(eventType, handleClick);
+
+ return () => {
+ document.removeEventListener(eventType, handleClick);
+ };
+ }, [widgetId, isSelected]); // Re-run when widgetId or selection state changes
+
+ return activeSlot;
+};
diff --git a/frontend/src/AppBuilder/_hooks/useMoveable.js b/frontend/src/AppBuilder/_hooks/useMoveable.js
new file mode 100644
index 0000000000..ea2687e473
--- /dev/null
+++ b/frontend/src/AppBuilder/_hooks/useMoveable.js
@@ -0,0 +1,135 @@
+import { useRef, useState } from 'react';
+
+const defaultProps = {
+ minHeight: 50,
+ maxHeight: 600,
+ minWidth: 50,
+ maxWidth: 600,
+ lockHorizontal: false,
+ lockVertical: false,
+ stepHeight: 10, // Default step size for height
+ stepWidth: 10, // Default step size for width
+ onResize: null,
+ onDragStart: null,
+ onDragEnd: null,
+ isReverseVerticalDrag: false,
+};
+
+export const useResizable = (options = {}) => {
+ const props = { ...defaultProps, ...options };
+ const parentRef = useRef(null);
+ const [isDragging, setIsDragging] = useState(false); // ✅ Track dragging state
+
+ const [height, setHeight] = useState(
+ typeof props.initialHeight === 'string' ? props.initialHeight : `${props.initialHeight || 200}px`
+ );
+ const [width, setWidth] = useState(
+ typeof props.initialWidth === 'string' ? props.initialWidth : `${props.initialWidth || 200}px`
+ );
+
+ const getRootProps = () => ({
+ ref: parentRef,
+ style: { height, width },
+ });
+
+ const getResizeState = () => ({
+ height,
+ width,
+ isDragging,
+ });
+
+ const getHandleProps = () => {
+ const handleMouseDown = (e) => {
+ // Prevent right-click drag activation (button === 2)
+ if (e.button === 2) return;
+
+ if (!parentRef.current) return;
+ e.stopPropagation();
+ e.preventDefault();
+ const startHeight = parseInt(parentRef.current.clientHeight);
+ const startWidth = parseInt(parentRef.current.clientWidth);
+ const parentWidth = parentRef.current.parentElement ? parentRef.current.parentElement.clientWidth : startWidth;
+ const startY = e.clientY;
+ const startX = e.clientX;
+ const isPercentage = typeof props.initialWidth === 'string' && props.initialWidth.includes('%');
+
+ setIsDragging(true); // ✅ Set dragging state to true
+
+ if (props.onDragStart) {
+ props.onDragStart({ newHeight: startHeight, newWidth: startWidth });
+ }
+
+ const handleMouseMove = (moveEvent) => {
+ moveEvent.stopPropagation();
+ moveEvent.preventDefault();
+ let newHeight = startHeight;
+ let newWidth = startWidth;
+
+ if (!props.lockVertical) {
+ const deltaY = props.isReverseVerticalDrag ? startY - moveEvent.clientY : moveEvent.clientY - startY;
+ newHeight = startHeight + deltaY;
+ newHeight = Math.max(props.minHeight, Math.min(props.maxHeight, newHeight));
+ newHeight = Math.round(newHeight / props.stepHeight) * props.stepHeight; // Snap to stepHeight
+ }
+
+ if (!props.lockHorizontal) {
+ newWidth = startWidth + (moveEvent.clientX - startX);
+ newWidth = Math.max(props.minWidth, Math.min(props.maxWidth, newWidth));
+ newWidth = Math.round(newWidth / props.stepWidth) * props.stepWidth; // Snap to stepWidth
+
+ if (isPercentage) {
+ newWidth = (newWidth / parentWidth) * 100; // Convert to percentage
+ newWidth = `${newWidth.toFixed(2)}%`;
+ } else {
+ newWidth = `${newWidth}px`;
+ }
+ }
+
+ setHeight(`${newHeight}px`);
+ setWidth(newWidth);
+
+ if (parentRef.current) {
+ parentRef.current.style.height = `${newHeight}px`;
+ parentRef.current.style.width = newWidth;
+ }
+
+ if (props.onResize) {
+ props.onResize({
+ newHeight,
+ newWidth,
+ heightDiff: newHeight - startHeight,
+ widthDiff: isPercentage
+ ? parseInt(newWidth) - (startWidth / parentWidth) * 100
+ : parseInt(newWidth) - startWidth,
+ });
+ }
+ };
+
+ const handleMouseUp = () => {
+ setIsDragging(false); // ✅ Set dragging state to false
+
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+
+ if (props.onDragEnd) {
+ // Get the updated height and width from the DOM instead of relying on state
+ const finalHeight = parentRef.current ? parseInt(parentRef.current.clientHeight) : parseInt(height);
+ const finalWidth = parentRef.current ? parseInt(parentRef.current.clientWidth) : parseInt(width);
+
+ props.onDragEnd({ newHeight: finalHeight, newWidth: finalWidth });
+ }
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ };
+
+ return {
+ onMouseDown: handleMouseDown,
+ };
+ };
+
+ return { rootRef: parentRef, getRootProps, getHandleProps, getResizeState };
+};
+
+export default useResizable;
diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js
index 2d8eb7f0a8..c5194822b6 100644
--- a/frontend/src/Editor/WidgetManager/configs/form.js
+++ b/frontend/src/Editor/WidgetManager/configs/form.js
@@ -294,6 +294,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' },
diff --git a/server/src/modules/apps/services/widget-config/form.js b/server/src/modules/apps/services/widget-config/form.js
index 2d8eb7f0a8..c5194822b6 100644
--- a/server/src/modules/apps/services/widget-config/form.js
+++ b/server/src/modules/apps/services/widget-config/form.js
@@ -294,6 +294,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' },
From 7a50c41103128ef312406d5b78e510528acada0d Mon Sep 17 00:00:00 2001
From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Date: Tue, 25 Mar 2025 21:52:37 +0530
Subject: [PATCH 02/12] Fixes interaction bugs with slot resizing
---
.../Form/Components/HorizontalSlot.jsx | 15 ++++---
frontend/src/AppBuilder/Widgets/Form/Form.jsx | 10 +++--
.../src/AppBuilder/Widgets/Form/form.scss | 25 ++++++++++-
.../src/AppBuilder/_hooks/useActiveSlot.js | 41 ++++++++++++++++---
4 files changed, 75 insertions(+), 16 deletions(-)
diff --git a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx
index 0e15e4a058..e892dc4f9f 100644
--- a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx
+++ b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx
@@ -13,15 +13,16 @@ export const HorizontalSlot = React.memo(
isActive,
slotName = 'header', // 'header' or 'footer'
slotStyle = {},
- onResize
+ onResize,
+ maxHeight,
}) => {
const parsedHeight = parseInt(height, 10);
const { getRootProps, getHandleProps, getResizeState } = useResizable({
initialHeight: parsedHeight,
initialWidth: '100%', // Now respects parent's width
- minHeight: 40,
- maxHeight: 400,
+ minHeight: 10,
+ maxHeight: maxHeight || 400,
maxWidth: '100%',
stepHeight: 10, // Height will change in steps of 10px
onResize: () => {},
@@ -33,8 +34,6 @@ export const HorizontalSlot = React.memo(
const { height: resizedHeight, isDragging } = getResizeState();
-
-
useEffect(() => {
if (isDragging) {
showGridLinesOnSlot(id);
@@ -47,7 +46,10 @@ export const HorizontalSlot = React.memo(
return (
-
+
diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx
index ffa2373a96..1328fc195d 100644
--- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx
+++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx
@@ -268,21 +268,24 @@ export const Form = function Form(props) {
const setComponentProperty = useStore((state) => state.setComponentProperty, shallow);
const updateHeaderSizeInStore = ({ newHeight }) => {
const heightInPx = `${parseInt(newHeight, 10)}px`;
- console.log('newHeight', newHeight);
setComponentProperty(id, `headerHeight`, heightInPx, 'properties', 'value', false);
};
const updateFooterSizeInStore = ({ newHeight }) => {
const heightInPx = `${parseInt(newHeight, 10)}px`;
- console.log('newHeight', newHeight);
setComponentProperty(id, `footerHeight`, heightInPx, 'properties', 'value', false);
};
+
+ // debugger;
+ const headerMaxHeight = parseInt(height, 10) - parseInt(footerHeight, 10) - 100 - 10;
+ const footerMaxHeight = parseInt(height, 10) - parseInt(headerHeight, 10) - 100 - 10;
const formFooter = {
flexShrink: 0,
paddingTop: '3px',
paddingBottom: '7px',
paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ maxHeight: `${footerMaxHeight}px`,
backgroundColor:
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
};
@@ -292,13 +295,14 @@ export const Form = function Form(props) {
paddingTop: '7px',
paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ maxHeight: `${headerMaxHeight}px`,
backgroundColor:
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
};
return (