diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx
index d27a66b0ec..6821f43ce2 100644
--- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx
+++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx
@@ -22,6 +22,7 @@ import {
} from './gridUtils';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
+import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
import useStore from '@/AppBuilder/_stores/store';
import './Grid.css';
import { NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
@@ -54,6 +55,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 [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow);
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
const [dragParentId, setDragParentId] = useState(null);
@@ -794,7 +796,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// 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.
+ /* 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 */
if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) {
return false;
@@ -835,109 +837,47 @@ export default function Grid({ gridWidth, currentLayout }) {
newDragParentId.current = null;
setDragParentId(null);
- if (!e.lastEvent) {
- return;
+ if (!e.lastEvent) return;
+
+ // Build the drag context from the event
+ const dragContext = dragContextBuilder({ event: e, widgets: boxList });
+ const { target, source, dragged } = dragContext;
+
+ 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';
+
+ 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 }]);
+ } else {
+ const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth;
+
+ left = dragged.left * sourcegridWidth;
+ top = dragged.top;
+
+ !isModalToCanvas ??
+ toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
}
- let draggedOverElemId = boxList.find((box) => box.id === e.target.id)?.parent;
- let draggedOverElemIdType;
- const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
- let draggedOverElem;
- if (document.elementFromPoint(e.clientX, e.clientY) && parentComponent?.component?.component !== 'Modal') {
- const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
- draggedOverElem = targetElems.find((ele) => {
- const isOwnChild = e.target.contains(ele); // if the hovered element is a child of actual draged element its not considered
- if (isOwnChild) return false;
+ // Apply transform for smooth transition
+ e.target.style.transform = `translate(${left}px, ${top}px)`;
- let isDroppable = ele.id !== e.target.id && ele.classList.contains('drag-container-parent');
- if (isDroppable) {
- let widgetId = ele?.getAttribute('component-id') || ele.id;
- let widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
- if (!widgetType) {
- widgetId = widgetId.split('-').slice(0, -1).join('-');
- widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
- }
- if (
- !['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(
- widgetType
- )
- ) {
- isDroppable = false;
- }
- }
- return isDroppable;
- });
- draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id;
- draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type');
- }
-
- const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
- const currentParentId = boxList.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent;
- let left = e.lastEvent?.translate[0];
- let top = e.lastEvent?.translate[1];
- if (
- ['Listview', 'Kanban', 'Container'].includes(
- boxList.find((box) => box.id === draggedOverElemId)?.component?.component
- )
- ) {
- const elemContainer = e.target.closest('.real-canvas');
- const containerHeight = elemContainer.clientHeight;
- const maxY = containerHeight - e.target.clientHeight;
- top = top > maxY ? maxY : top;
- }
-
- const currentWidget = boxList.find(({ id }) => id === e.target.id)?.component?.component;
- const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId;
- draggedOverElemIdType = getComponentTypeFromId(parentId);
- const parentWidget = draggedOverElemIdType === 'Kanban' ? 'Kanban_card' : draggedOverElemIdType;
- const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
- const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
- 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,
- parentWidgetType
- );
- left = _left;
- top = _top;
- } else {
- const currBox = boxList.find((l) => l.id === e.target.id);
- left = currBox.left * gridWidth;
- top = currBox.top;
- toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`);
- }
- }
-
- e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${
- Math.round(top / GRID_HEIGHT) * GRID_HEIGHT
- }px)`;
- if (draggedOverElemId === currentParentId || isParentChangeAllowed) {
- handleDragEnd([
- {
- id: e.target.id,
- x: left,
- y: Math.round(top / GRID_HEIGHT) * GRID_HEIGHT,
- parent: isParentChangeAllowed ? draggedOverElemId : undefined,
- },
- ]);
- }
- const box = boxList.find((box) => box.id === e.target.id);
- //
- setTimeout(() => setSelectedComponents([box.id]));
+ // Select the dragged component after drop
+ setTimeout(() => setSelectedComponents([dragged.id]));
} catch (error) {
- console.log('draggedOverElemId->error', error);
+ console.error('Error in onDragEnd:', error);
}
+ setCanvasBounds({ ...CANVAS_BOUNDS });
hideGridLines();
toggleCanvasUpdater();
}}
@@ -965,16 +905,31 @@ export default function Grid({ gridWidth, currentLayout }) {
}
// Special case for Modal
- const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
- if (parentComponent?.component?.component === 'Modal') {
- const elemContainer = e.target.closest('.real-canvas');
- const containerHeight = elemContainer.clientHeight;
- const containerWidth = elemContainer.clientWidth;
- const maxY = containerHeight - e.target.clientHeight;
- const maxLeft = containerWidth - e.target.clientWidth;
+ const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
+ const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
+ const parentComponent = boxList.find((box) => box.id === parentId);
+ const parentWidgetType = parentComponent?.component?.component;
+ const isOnHeaderOrFooter = oldParentId
+ ? oldParentId.includes('-header') || oldParentId.includes('-footer')
+ : false;
+ const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter;
+ const isParentNewModal = parentComponent?.component?.component === 'ModalV2';
+ const isParentLegacyModal = parentComponent?.component?.component === 'Modal';
+ const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
- top = top < 0 ? 0 : top > maxY ? maxY : top;
- left = left < 0 ? 0 : left > maxLeft ? maxLeft : left;
+ if (isParentModal) {
+ const modalContainer = e.target.closest('.tj-modal-widget-content');
+ const mainCanvas = document.getElementById('real-canvas');
+
+ const mainRect = mainCanvas.getBoundingClientRect();
+ const modalRect = modalContainer.getBoundingClientRect();
+ const relativePosition = {
+ top: modalRect.top - mainRect.top,
+ right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
+ bottom: modalRect.height + (modalRect.top - mainRect.top),
+ left: modalRect.left - mainRect.left,
+ };
+ setCanvasBounds({ ...relativePosition });
}
e.target.style.transform = `translate(${left}px, ${top}px)`;
@@ -1063,6 +1018,7 @@ export default function Grid({ gridWidth, currentLayout }) {
snapGap={false}
isDisplaySnapDigit={false}
snapThreshold={GRID_HEIGHT}
+ bounds={canvasBounds}
// Guidelines configuration
elementGuidelines={elementGuidelines}
snapDirections={{
diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
index 4bfd030a3c..2889fc06db 100644
--- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
+++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
@@ -291,6 +291,7 @@ export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
? document.getElementById(id)
: id
: document.getElementsByClassName('real-canvas')[0];
+ parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id);
if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') {
parentDiv = document.getElementById('canvas-' + id);
}
diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
new file mode 100644
index 0000000000..a9405d043e
--- /dev/null
+++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
@@ -0,0 +1,266 @@
+/**
+ * Drag Context Breakdown:
+ *
+ * This object encapsulates all relevant details about a drag event,
+ * grouping the **source (where the widget came from)** and **target (where it's being dropped)**.
+ *
+ * Core Concepts:
+ * - `draggedWidget` → The widget being dragged (`e.target`).
+ * - `sourceSlot` → The original parent container of `draggedWidget`.
+ * - This could be a **header, footer, or a sub-container (like a container body)**.
+ * - `targetSlot` → The new parent container where `draggedWidget` is dropped.
+ * - `sourceWidget` → The **widget that owns** `sourceSlot` (its direct parent).
+ * - `targetWidget` → The **widget that owns** `targetSlot` (its direct parent).
+ *
+ * These entities are structured into a **contextual grouping**, allowing for easy access:
+ *
+ * {
+ * source: {
+ * widget: sourceWidget, // The original widget that holds the source slot.
+ * slot: sourceSlot, // The slot where the widget was initially located.
+ * id: sourceWidget.id, // Unique identifier of the source widget.
+ * slotId: sourceSlot.id, // Unique identifier of the source slot.
+ *
+ * isModal: computed function, // Checks if sourceWidget is a Modal.
+ * slotType: computed function, // Determines if the slot is a header, footer, or body.
+ * widgetType: computed function, // Returns the type of the widget (e.g., Table, Form, etc.).
+ * },
+ *
+ * target: {
+ * widget: targetWidget, // The new widget where the dragged widget is being placed.
+ * slot: targetSlot, // The slot inside `targetWidget` where the drop is happening.
+ * id: targetWidget.id, // Unique identifier of the target widget.
+ * slotId: targetSlot.id, // Unique identifier of the target slot.
+ *
+ * isModal: computed function, // Checks if targetWidget is a Modal.
+ * slotType: computed function, // Determines if the slot is a header, footer, or body.
+ * widgetType: computed function, // Returns the type of the target widget.
+ * }
+ * }
+ *
+ * Additional Checks:
+ * - `isSourceModal` → **Is the source inside a modal?**
+ * - `isTargetModal` → **Is the target inside a modal?**
+ * - `isDraggingToModalSlots` → **Is the widget being dragged into a modal slot (header/footer)?**
+ * - `targetSlotType` → **Determines whether the drop is happening in a header, footer, or body.**
+ *
+ * Why This Matters?
+ * - This structure helps **validate and restrict movements**, ensuring widgets follow UI constraints.
+ * - Prevents invalid drops (e.g., putting a button inside a Table component).
+ * - Enables **modular and flexible** widget movement across different UI sections.
+ */
+import { getMouseDistanceFromParentDiv } from '../gridUtils';
+import {
+ RESTRICTED_WIDGETS_CONFIG,
+ RESTRICTED_WIDGET_SLOTS_CONFIG,
+} from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig';
+
+const CANVAS_ID = 'canvas';
+const REAL_CANVAS_ID = 'real-canvas';
+
+/**
+ * Represents the widget being dragged.
+ *
+ * This class encapsulates all necessary information about the dragged widget,
+ * including its type, position, and whether it is allowed to move into certain areas.
+ */
+export class DragEntity {
+ constructor(widget) {
+ this.widget = widget; // The widget object being dragged
+ this.id = widget?.id || null; // Unique ID of the dragged widget
+ this.left = widget.left; // Initial X position (relative to grid)
+ this.top = widget.top; // Initial Y position (relative to grid)
+ }
+
+ get widgetType() {
+ return this.widget?.component?.component || null;
+ }
+}
+
+/**
+ * Defines a **droppable area** in the canvas.
+ *
+ * A droppable area is a container that can accept dragged widgets.
+ * This class helps determine if a slot is valid and handles various properties like modals.
+ */
+export class DropAreaEntity {
+ static dropAreaWidgets = ['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'ModalV2', 'Listview', 'Container', 'Table'];
+
+ constructor(widget, slotId) {
+ this.widget = widget; // The widget that owns this slot
+ this.id = widget?.id || CANVAS_ID; // ID of the widget
+ this.slotId = slotId || REAL_CANVAS_ID; // ID of the slot where the widget is located
+ }
+
+ // Checks if the widget is a modal
+ get isModal() {
+ return ['Modal', 'ModalV2'].includes(this.widget?.component?.component);
+ }
+
+ // Checks if the widget is the new version of modal
+ get isNewModal() {
+ return this.widget?.component?.component === 'ModalV2';
+ }
+
+ // Checks if the widget is the legacy modal
+ get isLegacyModal() {
+ return this.widget?.component?.component === 'Modal';
+ }
+
+ // Determines if the slot belongs to a modal's header/footer
+ get isInModalSlot() {
+ return this.isNewModal && this.isOnCustomSlot;
+ }
+
+ // Identifies if the slot is a custom slot (e.g., modal header/footer)
+ get isOnCustomSlot() {
+ return this.slotId.includes('-header') || this.slotId.includes('-footer');
+ }
+
+ // Determines if the slot is a valid drop target
+ get isDroppable() {
+ return DropAreaEntity.dropAreaWidgets.includes(this.widgetType);
+ }
+
+ // Returns the type of slot (header, footer, body, etc.)
+ get slotType() {
+ return this.slotId ? this.slotId.split('-').pop() : CANVAS_ID;
+ }
+
+ // Returns the type of the widget inside the slot
+ get widgetType() {
+ return this.widget?.component?.component || CANVAS_ID;
+ }
+}
+
+/**
+ * Represents the **dragging context**, encapsulating information
+ * about the source, target, and the dragged widget.
+ *
+ * This helps determine:
+ * - Whether the move is valid
+ * - Where the widget should be placed
+ * - Any restrictions based on parent-child relationships
+ */
+export class DragContext {
+ constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets }) {
+ const sourceWidgetId = sourceSlotId?.slice(0, 36);
+ const sourceWidget = getWidgetById(widgets, sourceWidgetId);
+
+ const targetWidgetId = targetSlotId?.slice(0, 36);
+ const targetWidget = getWidgetById(widgets, targetWidgetId);
+
+ const draggedWidget = getWidgetById(widgets, draggedWidgetId);
+
+ this.source = new DropAreaEntity(sourceWidget, sourceSlotId);
+ this.target = new DropAreaEntity(targetWidget, targetSlotId);
+ this.dragged = new DragEntity(draggedWidget);
+ this.widgets = widgets;
+ }
+
+ /**
+ * Updates the **target slot** dynamically as the drag event progresses.
+ */
+ updateTarget(targetSlotId) {
+ const targetWidgetId = targetSlotId?.slice(0, 36);
+ const targetWidget = getWidgetById(this.widgets, targetWidgetId);
+ this.target = new DropAreaEntity(targetWidget, targetSlotId);
+ }
+
+ get isDroppable() {
+ const { dragged, target } = this;
+
+ const restrictedWidgetsOnTarget = RESTRICTED_WIDGETS_CONFIG?.[target.widgetType] || [];
+ const restrictedWidgetsOnTargetSlot = RESTRICTED_WIDGET_SLOTS_CONFIG?.[target.slotType] || [];
+
+ const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot];
+ return !restrictedWidgets.includes(dragged.widgetType);
+ ß;
+ }
+}
+
+/**
+ * Constructs the **dragging context** by gathering all relevant details from the event.
+ */
+export function dragContextBuilder({ event, widgets }) {
+ const draggedWidgetId = event.target.id;
+ const draggedWidget = getWidgetById(widgets, draggedWidgetId);
+ const sourceSlotId = draggedWidget.parent;
+
+ // Initialize drag context
+ const context = new DragContext({ widgets, draggedWidgetId, sourceSlotId, targetSlotId: sourceSlotId });
+
+ // Determine the potential drop target
+ const targetSlotId = getDroppableSlotIdOnScreen(event, widgets);
+ context.updateTarget(targetSlotId);
+
+ return context;
+}
+
+/**
+ * Given an event, finds the **nearest valid droppable slot**.
+ */
+export const getDroppableSlotIdOnScreen = (event, widgets) => {
+ const [slotId] = document
+ .elementsFromPoint(event.clientX, event.clientY)
+ .filter(
+ (ele) =>
+ !event.target.contains(ele) && ele.id !== event.target.id && ele.classList.contains('drag-container-parent')
+ )
+ .map((ele) => extractSlotId(ele))
+ .filter((slotId) => {
+ const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID;
+ return DropAreaEntity.dropAreaWidgets.includes(widgetType);
+ });
+
+ return slotId;
+};
+
+/**
+ * Finds a widget by its ID.
+ */
+export function getWidgetById(boxList, targetId) {
+ return boxList.find((box) => box.id === targetId) ?? null;
+}
+
+/**
+ * Extracts the **slot ID** from a given DOM element.
+ */
+const extractSlotId = (element) => {
+ return element?.getAttribute('component-id') || element.id.replace(/^canvas-/, '');
+};
+
+/**
+ * Computes the final (left, top) position for a dragged widget based on grid snapping and drop conditions.
+ *
+ * @param {Object} event - Drag event object containing movement data.
+ * @param {DropAreaEntity} target - The target drop area entity (where widget is dropped).
+ * @param {boolean} isParentChangeAllowed - Whether the widget can move to the target.
+ * @param {number} gridWidth - The width of the grid for alignment.
+ * @param {DragEntity} dragged - The entity being dragged.
+ * @returns {Object} { left, top } - The computed position.
+ */
+export const getAdjustedDropPosition = (event, target, isParentChangeAllowed, gridWidth, dragged) => {
+ let left = event.lastEvent?.translate[0];
+ let top = event.lastEvent?.translate[1];
+
+ if (isParentChangeAllowed) {
+ // Compute the relative position inside the new container
+ const { left: adjustedLeft, top: adjustedTop } = getMouseDistanceFromParentDiv(
+ event,
+ target.slotId,
+ target.widgetType
+ );
+
+ return {
+ left: Math.round(adjustedLeft / gridWidth) * gridWidth, // Snap to the nearest grid column
+ top: Math.round(adjustedTop / 10) * 10, // Snap to the nearest 10px
+ };
+ }
+
+ // If movement is restricted, revert to original position
+ return {
+ left: dragged.left * gridWidth,
+ top: dragged.top,
+ };
+};
diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js
index 6ca8f97e2f..75b3402331 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', 'Container'];
+export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container', 'ModalV2'];
export const DEFAULT_CANVAS_WIDTH = 1292;
diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js
index 2c4259c22f..4e7b56ea70 100644
--- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js
+++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js
@@ -89,7 +89,8 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
const defaultChildren = deepClone(parentMeta)['defaultChildren'];
defaultChildren.forEach((child) => {
- const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child;
+ const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles, slotName } =
+ child;
const componentMeta = deepClone(componentTypes.find((component) => component.component === componentName));
const componentData = JSON.parse(JSON.stringify(componentMeta));
@@ -139,7 +140,12 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
}
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
- const _parent = getParentComponentIdByType(child, parentMeta.component, parentId);
+ const _parent = getParentComponentIdByType({
+ child,
+ parentComponent: parentMeta.component,
+ parentId,
+ slotName,
+ });
const newChildComponent = {
id: uuidv4(),
@@ -199,7 +205,8 @@ export const getAllChildComponents = (allComponents, parentId) => {
allComponents[parentId]?.component?.component === 'Tabs' ||
allComponents[parentId]?.component?.component === 'Calendar' ||
allComponents[parentId]?.component?.component === 'Kanban' ||
- allComponents[parentId]?.component?.component === 'Container';
+ allComponents[parentId]?.component?.component === 'Container' ||
+ allComponents[parentId]?.component?.component === 'ModalV2';
if (componentParentId && isParentTabORCalendar) {
let childComponent = deepClone(allComponents[componentId]);
@@ -319,7 +326,8 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
return (
parentComponent.component.component === 'Tabs' ||
parentComponent.component.component === 'Calendar' ||
- parentComponent.component.component === 'Container'
+ parentComponent.component.component === 'Container' ||
+ parentComponent.component.component === 'ModalV2'
);
}
@@ -657,10 +665,23 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
return canvasBgColor;
};
-export const getParentComponentIdByType = (child, parentComponent, parentId) => {
+export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => {
const { tab } = child;
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
- else if (parentComponent === 'Container') return `${parentId}-header`;
+ else if (parentComponent === 'Container' || parentComponent === 'ModalV2') {
+ return `${parentId}-${slotName}`;
+ }
return parentId;
};
+
+export const getParentWidgetFromId = (parentType, parentId) => {
+ const isAddingToSlot = parentId?.includes('-header') || parentId?.includes('-footer');
+
+ if (parentType === 'ModalV2' && isAddingToSlot) {
+ return 'ModalSlot';
+ } else if (parentType === 'Kanban') {
+ return 'Kanban_card';
+ }
+ return parentType;
+};
diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
index ef70343140..2ad7977496 100644
--- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
@@ -133,7 +133,7 @@ export const ComponentsManagerTab = ({ darkMode }) => {
'StarRating',
];
const integrationItems = ['Map'];
- const layoutItems = ['Container', 'Listview', 'Tabs', 'Modal'];
+ const layoutItems = ['Container', 'Listview', 'Tabs', 'ModalV2'];
filteredComponents.forEach((f) => {
if (commonItems.includes(f)) commonSection.items.push(f);
diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js
index cc66756eff..fda26656ca 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', 'RadioButton', 'Datepicker'];
+export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx
index acd1908bba..d680f21d1e 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx
@@ -23,6 +23,7 @@ const SHOW_ADDITIONAL_ACTIONS = [
'Button',
'RichTextEditor',
'Image',
+ 'ModalV2',
];
const PROPERTIES_VS_ACCORDION_TITLE = {
Text: 'Data',
@@ -34,6 +35,7 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
Button: 'Data',
Image: 'Data',
Container: 'Data',
+ ModalV2: 'Data',
};
export const DefaultComponent = ({ componentMeta, darkMode, ...restProps }) => {
@@ -151,7 +153,8 @@ export const baseComponentProperties = (
'properties',
currentState,
allComponents,
- darkMode
+ darkMode,
+ ''
)
),
});
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx
new file mode 100644
index 0000000000..4131217386
--- /dev/null
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import Accordion from '@/_ui/Accordion';
+import { renderElement } from '../Utils';
+import { baseComponentProperties } from './DefaultComponent';
+import { resolveReferences } from '@/_helpers/utils';
+
+const INDEX_OF_TRIGGER = 2;
+
+export const ModalV2 = ({ componentMeta, darkMode, ...restProps }) => {
+ const {
+ layoutPropertyChanged,
+ component,
+ paramUpdated,
+ dataQueries,
+ currentState,
+ eventsChanged,
+ apps,
+ allComponents,
+ } = restProps;
+
+ let properties = [];
+ let additionalActions = [];
+ let dataProperties = [];
+
+ const events = Object.keys(componentMeta.events);
+ const validations = Object.keys(componentMeta.validation || {});
+
+ for (const [key] of Object.entries(componentMeta?.properties)) {
+ if (componentMeta?.properties[key]?.section === 'additionalActions') {
+ additionalActions.push(key);
+ } else if (componentMeta?.properties[key]?.accordian === 'Data') {
+ dataProperties.push(key);
+ } else {
+ properties.push(key);
+ }
+ }
+
+ const renderCustomElement = (param, paramType = 'properties') => {
+ return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
+ };
+ const conditionalAccordionItems = (component) => {
+ const useDefaultButton = resolveReferences(
+ component.component.definition.properties.useDefaultButton?.value ?? false
+ );
+ const accordionItems = [];
+ let renderOptions = [];
+ const options = ['visibility', 'disabledTrigger', 'useDefaultButton'];
+
+ options.map((option) => renderOptions.push(renderCustomElement(option)));
+
+ const conditionalOptions = [{ name: 'triggerButtonLabel', condition: useDefaultButton }];
+
+ conditionalOptions.map(({ name, condition }) => {
+ if (condition) renderOptions.push(renderCustomElement(name));
+ });
+
+ accordionItems.push({
+ title: 'Trigger',
+ children: renderOptions,
+ });
+
+ return accordionItems;
+ };
+
+ if (component.component.definition.properties.size.value === 'fullscreen') {
+ component.component.properties.modalHeight = {
+ ...component.component.properties.modalHeight,
+ isHidden: true,
+ };
+ }
+
+ if (component.component.definition.properties.showHeader.value === '{{false}}') {
+ component.component.properties.headerHeight = {
+ ...component.component.properties.headerHeight,
+ isHidden: true,
+ };
+ }
+
+ if (component.component.definition.properties.showFooter.value === '{{false}}') {
+ component.component.properties.footerHeight = {
+ ...component.component.properties.footerHeight,
+ isHidden: true,
+ };
+ }
+
+ const accordionItems = baseComponentProperties(
+ dataProperties,
+ events,
+ component,
+ componentMeta,
+ layoutPropertyChanged,
+ paramUpdated,
+ dataQueries,
+ currentState,
+ eventsChanged,
+ apps,
+ allComponents,
+ validations,
+ darkMode,
+ [],
+ additionalActions
+ );
+
+ const [optionsItems] = conditionalAccordionItems(component);
+
+ // Insert the Trigger option as the third item
+ accordionItems.splice(INDEX_OF_TRIGGER, 0, optionsItems);
+
+ return ;
+};
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx
index 7ee512ee20..c74e023c0b 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx
@@ -19,6 +19,7 @@ export const Code = ({
accordian,
placeholder,
validationFn,
+ isHidden = false,
}) => {
const currentState = useCurrentState();
@@ -43,6 +44,7 @@ export const Code = ({
onChange({ name: 'iconVisibility' }, 'value', value, 'styles');
}
+ if (isHidden) return null;
return (
{
@@ -703,6 +705,9 @@ const GetAccordion = React.memo(
case 'FilePicker':
return ;
+ case 'ModalV2':
+ return ;
+
case 'Modal':
return ;
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
index 0ddda9f572..62ee032172 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
@@ -50,7 +50,8 @@ export function renderCustomStyles(
componentConfig.component == 'MultiselectV2' ||
componentConfig.component == 'RadioButtonV2' ||
componentConfig.component == 'Button' ||
- componentConfig.component == 'Image'
+ componentConfig.component == 'Image' ||
+ componentConfig.component == 'ModalV2'
) {
const paramTypeConfig = componentMeta[paramType] || {};
const paramConfig = paramTypeConfig[param] || {};
@@ -131,6 +132,7 @@ export function renderElement(
const paramTypeDefinition = componentDefinition[paramType] || {};
const definition = paramTypeDefinition[param] || {};
const meta = componentMeta[paramType][param];
+ const isHidden = component.component.properties[param]?.isHidden ?? false;
if (
componentConfig.component == 'DropDown' ||
@@ -170,6 +172,7 @@ export function renderElement(
component={component}
placeholder={placeholder}
validationFn={validationFn}
+ isHidden={isHidden}
/>
);
}
diff --git a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
index 1226bfba3e..69ded14971 100644
--- a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import WidgetIcon from '@/../assets/images/icons/widgets';
import { useTranslation } from 'react-i18next';
-const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
+const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
const NEW_WIDGETS = [
'ToggleSwitchV2',
'DropdownV2',
@@ -12,6 +12,7 @@ const NEW_WIDGETS = [
'DaterangePicker',
'DatePickerV2',
'TimePicker',
+ 'ModalV2',
];
export const WidgetBox = ({ component, darkMode }) => {
diff --git a/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js
index 4ad52da372..6933f4a5a5 100644
--- a/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js
+++ b/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js
@@ -4,7 +4,14 @@ export const RESTRICTED_WIDGETS_CONFIG = {
Calendar: ['Calendar', 'Kanban'],
Container: ['Calendar', 'Kanban'],
Modal: ['Calendar', 'Kanban'],
+ ModalV2: ['Calendar', 'Kanban'],
+ ModalSlot: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
Tabs: ['Calendar', 'Kanban'],
Kanban_popout: ['Calendar', 'Kanban'],
Listview: ['Calendar', 'Kanban'],
};
+
+export const RESTRICTED_WIDGET_SLOTS_CONFIG = {
+ header: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
+ footer: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
+};
diff --git a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
index 13832857fb..be2855c476 100644
--- a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
+++ b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
@@ -3,6 +3,7 @@ import {
tableConfig,
chartConfig,
modalConfig,
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
@@ -64,6 +65,7 @@ export const widgets = [
buttonConfig,
chartConfig,
modalConfig,
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/index.js b/frontend/src/AppBuilder/WidgetManager/widgets/index.js
index 2540cdeeef..ce0e73fdf5 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/index.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/index.js
@@ -2,6 +2,7 @@ import { buttonConfig } from './button';
import { tableConfig } from './table';
import { chartConfig } from './chart';
import { modalConfig } from './modal';
+import { modalV2Config } from './modalV2';
import { formConfig } from './form';
import { textinputConfig } from './textinput';
import { numberinputConfig } from './numberinput';
@@ -62,7 +63,8 @@ export {
buttonConfig,
tableConfig,
chartConfig,
- modalConfig,
+ modalConfig, //Deprecated
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
index 42740ad9c1..8f0c34b566 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
@@ -1,6 +1,6 @@
export const modalConfig = {
- name: 'Modal',
- displayName: 'Modal',
+ name: 'ModalLegacy',
+ displayName: 'Modal (Legacy)',
description: 'Show pop-up windows',
component: 'Modal',
defaultSize: {
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
new file mode 100644
index 0000000000..e7e96c4398
--- /dev/null
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
@@ -0,0 +1,277 @@
+export const modalV2Config = {
+ name: 'Modal',
+ displayName: 'Modal',
+ description: 'Show pop-up windows',
+ component: 'ModalV2',
+ defaultSize: {
+ width: 10,
+ height: 34,
+ },
+ others: {
+ showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
+ showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
+ },
+ properties: {
+ loadingState: {
+ type: 'toggle',
+ displayName: 'Loading state',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ section: 'additionalActions',
+ },
+ visibility: {
+ type: 'toggle',
+ displayName: 'Modal trigger visibility',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: true,
+ },
+ },
+ disabledTrigger: {
+ type: 'toggle',
+ displayName: 'Disable modal trigger',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ },
+ disabledModal: {
+ type: 'toggle',
+ displayName: 'Disable modal window',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ section: 'additionalActions',
+ },
+ useDefaultButton: {
+ type: 'toggle',
+ displayName: 'Use default trigger button',
+ validation: {
+ schema: {
+ type: 'boolean',
+ },
+ defaultValue: true,
+ },
+ },
+ triggerButtonLabel: {
+ type: 'code',
+ displayName: 'Trigger button label',
+ validation: {
+ schema: {
+ type: 'string',
+ },
+ defaultValue: 'Launch Modal',
+ },
+ },
+
+ // Data Accordion
+ showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
+ showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
+
+ size: {
+ type: 'select',
+ displayName: 'Width',
+ accordian: 'Data',
+ options: [
+ { name: 'small', value: 'sm' },
+ { name: 'medium', value: 'lg' },
+ { name: 'large', value: 'xl' },
+ { name: 'fullscreen', value: 'fullscreen' },
+ ],
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'lg',
+ },
+ },
+ modalHeight: {
+ type: 'numberInput',
+ displayName: 'Height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
+ },
+ headerHeight: {
+ type: 'numberInput',
+ displayName: 'Header height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
+ },
+ footerHeight: {
+ type: 'numberInput',
+ displayName: 'Footer height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
+ },
+ hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
+ closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
+ hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
+ },
+ events: {
+ onOpen: { displayName: 'On open' },
+ onClose: { displayName: 'On close' },
+ },
+ defaultChildren: [
+ {
+ componentName: 'Text',
+ slotName: 'header',
+ layout: {
+ top: 21,
+ left: 1,
+ height: 40,
+ },
+ displayName: 'ModalHeaderTitle',
+ properties: ['text'],
+ accessorKey: 'text',
+ styles: ['fontWeight', 'textSize', 'textColor'],
+ defaultValue: {
+ text: 'Modal title',
+ textSize: 20,
+ textColor: '#000',
+ },
+ },
+ {
+ componentName: 'Button',
+ slotName: 'footer',
+ layout: {
+ top: 24,
+ left: 22,
+ height: 36,
+ },
+ displayName: 'ModalFooterCancel',
+ properties: ['text'],
+ styles: ['type', 'borderColor', 'padding'],
+ defaultValue: {
+ text: 'Button1',
+ type: 'outline',
+ borderColor: '#CCD1D5',
+ },
+ },
+ {
+ componentName: 'Button',
+ slotName: 'footer',
+ layout: {
+ top: 24,
+ left: 32,
+ height: 36,
+ },
+ displayName: 'ModalFooterConfirm',
+ properties: ['text'],
+ defaultValue: {
+ text: 'Button2',
+ padding: 'none',
+ },
+ },
+ ],
+ styles: {
+ headerBackgroundColor: {
+ type: 'color',
+ displayName: 'Header background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ footerBackgroundColor: {
+ type: 'color',
+ displayName: 'Footer background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ bodyBackgroundColor: {
+ type: 'color',
+ displayName: 'Body background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ triggerButtonBackgroundColor: {
+ type: 'color',
+ displayName: 'Trigger button background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: false,
+ },
+ },
+ triggerButtonTextColor: {
+ type: 'color',
+ displayName: 'Trigger button text color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: false,
+ },
+ },
+ },
+ exposedVariables: {
+ show: false,
+ isDisabledModal: false,
+ isDisabledTrigger: false,
+ isVisible: true,
+ isLoading: false,
+ },
+ actions: [
+ {
+ handle: 'open',
+ displayName: 'Open',
+ },
+ {
+ handle: 'close',
+ displayName: 'Close',
+ },
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisableTrigger',
+ displayName: 'Set disable trigger',
+ params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisableModal',
+ displayName: 'Set disable modal',
+ params: [{ handle: 'setDisableModal', 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: {
+ loadingState: { value: `{{false}}` },
+ visibility: { value: '{{true}}' },
+ disabledTrigger: { value: '{{false}}' },
+ disabledModal: { value: '{{false}}' },
+ useDefaultButton: { value: `{{true}}` },
+ triggerButtonLabel: { value: `Launch Modal` },
+ size: { value: 'lg' },
+ showHeader: { value: '{{true}}' },
+ showFooter: { value: '{{true}}' },
+ hideCloseButton: { value: '{{false}}' },
+ hideOnEsc: { value: '{{true}}' },
+ closeOnClickingOutside: { value: '{{false}}' },
+ modalHeight: { value: 400 },
+ headerHeight: { value: 80 },
+ footerHeight: { value: 80 },
+ },
+ events: [],
+ styles: {
+ headerBackgroundColor: { value: '#ffffffff' },
+ footerBackgroundColor: { value: '#ffffffff' },
+ bodyBackgroundColor: { value: '#ffffffff' },
+ triggerButtonBackgroundColor: { value: '#4D72FA' },
+ triggerButtonTextColor: { value: '#ffffffff' },
+ },
+ },
+};
diff --git a/frontend/src/AppBuilder/Widgets/Container.jsx b/frontend/src/AppBuilder/Widgets/Container.jsx
index 16623280a5..1334098423 100644
--- a/frontend/src/AppBuilder/Widgets/Container.jsx
+++ b/frontend/src/AppBuilder/Widgets/Container.jsx
@@ -45,6 +45,7 @@ export const Container = ({
border: `1px solid ${borderColor}`,
height,
display: isVisible ? 'flex' : 'none',
+ flexDirection: 'column',
position: 'relative',
boxShadow,
};
diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx
new file mode 100644
index 0000000000..e25027ce33
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { default as BootstrapModal } from 'react-bootstrap/Modal';
+import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
+import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
+
+export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode, width, footerHeight, onClick }) => {
+ const canvasFooterHeight = getCanvasHeight(footerHeight);
+ return (
+
+
+ {isDisabled && (
+