)}
diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss
index 7f20210c10..5cb1b94268 100644
--- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss
+++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss
@@ -31,22 +31,7 @@
.badge {
font-size: 9px;
border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-
- .delete-part {
- margin-left: 10px;
- float: right;
- }
-
- .delete-part::before {
- height: 12px;
- display: inline-block;
- width: 2px;
- background-color: rgba(255, 255, 255, 0.8);
- opacity: 0.5;
- content: "";
- vertical-align: middle;
- }
+ border-bottom-right-radius: 0
}
}
@@ -65,9 +50,3 @@
}
}
}
-
-.main-editor-canvas .widget-target:hover > .config-handle {
- visibility: visible !important;
-}
-
-
diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx
index fed1a3db34..e622e1a2cd 100644
--- a/frontend/src/AppBuilder/AppCanvas/Container.jsx
+++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx
@@ -6,7 +6,15 @@ import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { useDrop } from 'react-dnd';
import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils';
-import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants';
+import {
+ CANVAS_WIDTHS,
+ NO_OF_GRIDS,
+ WIDGETS_WITH_DEFAULT_CHILDREN,
+ GRID_HEIGHT,
+ CONTAINER_FORM_CANVAS_PADDING,
+ SUBCONTAINER_CANVAS_BORDER_WIDTH,
+ BOX_PADDING,
+} from './appCanvasConstants';
import { useGridStore } from '@/_stores/gridStore';
import NoComponentCanvasContainer from './NoComponentCanvasContainer';
import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants';
@@ -35,10 +43,10 @@ export const Container = React.memo(
canvasMaxWidth,
isViewerSidebarPinned,
pageSidebarStyle,
+ componentType,
}) => {
const realCanvasRef = useRef(null);
const components = useStore((state) => state.getContainerChildrenMapping(id), shallow);
- const componentType = useStore((state) => state.getComponentTypeFromId(id), shallow);
const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow);
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow);
const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow);
@@ -56,6 +64,11 @@ export const Container = React.memo(
const [{ isOverCurrent }, drop] = useDrop({
accept: 'box',
+ hover: (item) => {
+ item.canvasRef = realCanvasRef?.current;
+ item.canvasId = id;
+ item.canvasWidth = getContainerCanvasWidth();
+ },
drop: async ({ componentType }, monitor) => {
const didDrop = monitor.didDrop();
if (didDrop) return;
@@ -89,14 +102,19 @@ export const Container = React.memo(
function getContainerCanvasWidth() {
if (canvasWidth !== undefined) {
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
- return canvasWidth;
+ if (id === 'canvas') return canvasWidth;
+ if (componentType === 'Container' || componentType === 'Form') {
+ return (
+ canvasWidth - (2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING)
+ );
+ }
+ return canvasWidth - 2; // Need to update this 2 to correct value for other subcontainers
}
return realCanvasRef?.current?.offsetWidth;
}
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
-
useEffect(() => {
- useGridStore.getState().actions.setSubContainerWidths(id, (getContainerCanvasWidth() - 2) / NO_OF_GRIDS);
+ useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canvasWidth, listViewMode, columns]);
@@ -137,8 +155,7 @@ export const Container = React.memo(
}}
style={{
height: id === 'canvas' ? `${canvasHeight}` : '100%',
- // backgroundSize: '25.3953px 10px',
- backgroundSize: `${gridWidth}px 10px`,
+ backgroundSize: `${gridWidth}px ${GRID_HEIGHT}px`,
backgroundColor:
currentMode === 'view'
? computeViewerBackgroundColor(darkMode, canvasBgColor)
@@ -169,6 +186,7 @@ export const Container = React.memo(
data-parentId={id}
canvas-height={canvasHeight}
onClick={handleCanvasClick}
+ component-type={componentType}
>
state.getHoveredComponentForGrid, shallow);
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
+ const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
+ const draggingComponentId = useStore((state) => state.draggingComponentId, shallow);
+ const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
+ const [dragParentId, setDragParentId] = useState(null);
+ const [elementGuidelines, setElementGuidelines] = useState([]);
+ const componentsSnappedTo = useRef(null);
+ const prevDragParentId = useRef(null);
+ const newDragParentId = useRef(null);
+ const [isGroupDragging, setIsGroupDragging] = useState(false);
+
+ useEffect(() => {
+ const selectedSet = new Set(selectedComponents);
+ const draggingOrResizingId = draggingComponentId || resizingComponentId;
+ const isGrouped = findHighestLevelofSelection().length > 1;
+ const firstSelectedParent =
+ selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null;
+ const selectedParent = dragParentId || firstSelectedParent;
+
+ const guidelines = boxList
+ .filter((box) => {
+ const isVisible =
+ getResolvedValue(box?.component?.definition?.properties?.visibility?.value) ||
+ getResolvedValue(box?.component?.definition?.styles?.visibility?.value);
+
+ // Early return for non-visible elements
+ if (!isVisible) return false;
+
+ if (isGrouped) {
+ // If component is selected, don't show its guidelines
+ if (selectedSet.has(box.id)) return false;
+ return selectedParent ? box.parent === selectedParent : !box.parent;
+ }
+
+ if (draggingOrResizingId) {
+ if (box.id === draggingOrResizingId) return false;
+ return dragParentId ? box.parent === dragParentId : !box.parent;
+ }
+
+ return true;
+ })
+ .map((box) => `.ele-${box.id}`);
+ setElementGuidelines(guidelines);
+ }, [boxList, dragParentId, draggingComponentId, resizingComponentId, selectedComponents, getResolvedValue]);
useEffect(() => {
setBoxList(
@@ -94,7 +144,7 @@ export default function Grid({ gridWidth, currentLayout }) {
boxList.forEach(({ id, height, width, x, y, gw }) => {
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
- y = Math.round(y / 10) * 10;
+ y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
gw = gw ? gw : gridWidth;
const parent = transformedBoxes[id]?.component?.parent;
@@ -117,7 +167,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
setComponentLayout({
[id]: {
- height: height ? height : 10,
+ height: height ? height : GRID_HEIGHT,
width: newWidth ? newWidth : 1,
top: y,
left: Math.round(x / gw),
@@ -319,7 +369,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
// Round y position
- y = Math.max(0, Math.round(y / 10) * 10);
+ y = Math.max(0, Math.round(y / GRID_HEIGHT) * GRID_HEIGHT);
// Adjust height for certain parent components
if (parent) {
const parentElem = document.getElementById(`canvas-${parent}`);
@@ -354,17 +404,16 @@ export default function Grid({ gridWidth, currentLayout }) {
);
// Add event listeners for config handle visibility when hovering over widget boundary
+ // This is needed even though we have hovered widget state because when hovered on boundary,
+ // the hovered widget state is empty, hence created a separate state for boundary
React.useEffect(() => {
const moveableBox = document.querySelector(`.moveable-control-box`);
const showConfigHandle = (e) => {
const targetId = e.target.offsetParent.getAttribute('target-id');
- const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
- configHandle.classList.add('config-handle-visible');
+ useStore.getState().setHoveredComponentBoundaryId(targetId);
};
- const hideConfigHandle = (e) => {
- const targetId = e.target.offsetParent.getAttribute('target-id');
- const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
- configHandle.classList.remove('config-handle-visible');
+ const hideConfigHandle = () => {
+ useStore.getState().setHoveredComponentBoundaryId('');
};
if (moveableBox) {
moveableBox.addEventListener('mouseover', showConfigHandle);
@@ -376,49 +425,10 @@ export default function Grid({ gridWidth, currentLayout }) {
};
}, []);
- const handleDragGridLinesVisibility = (e, events = []) => {
- const { clientX, clientY } = e;
- if (!document.elementFromPoint(clientX, clientY)) return;
-
- const targetElems = document.elementsFromPoint(clientX, clientY);
- const draggedOverElements = targetElems.filter(
- (ele) =>
- !events.some((event) => event.target.id === ele.id) &&
- (ele.classList.contains('target') || ele.classList.contains('real-canvas'))
- );
- const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
- const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
- const appCanvas = document.getElementById('real-canvas');
-
- // Show grid line for main canvas
- draggedOverContainer?.classList.remove('hide-grid');
- draggedOverContainer?.classList.add('show-grid');
-
- // Remove 'show-grid' class from all sub-canvases
- const canvasElms = document.getElementsByClassName('sub-canvas');
- Array.from(canvasElms).forEach((element) => {
- element.classList.remove('show-grid');
- element.classList.add('hide-grid');
- });
-
- // Determine the potential new parent
- const parentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
-
- // Show grid for the appropriate canvas
- if (parentId) {
- const newParentCanvas = document.getElementById('canvas-' + parentId);
- if (newParentCanvas) {
- appCanvas?.classList?.remove('show-grid');
- newParentCanvas?.classList.remove('hide-grid');
- newParentCanvas?.classList.add('show-grid');
- }
- }
-
- useGridStore.getState().actions.setDragTarget(parentId);
- };
-
const handleDragGroupEnd = (e) => {
try {
+ hideGridLines();
+ setIsGroupDragging(false);
const { events, clientX, clientY } = e;
const initialParent = events[0].target.closest('.real-canvas');
// Get potential new parent using same logic as onDragEnd
@@ -477,7 +487,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// Apply transform to return to original position
ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${
- Math.round(_top / 10) * 10
+ Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT
}px)`;
}
});
@@ -514,7 +524,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// Apply grid snapping and bounds
const snappedX = Math.round(posX / _gridWidth) * _gridWidth;
- const snappedY = Math.round(posY / 10) * 10;
+ const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${snappedX}px, ${snappedY}px)`;
return {
@@ -531,6 +541,18 @@ export default function Grid({ gridWidth, currentLayout }) {
}
};
+ React.useEffect(() => {
+ const components = Array.from(document.querySelectorAll('.active-target')).filter(
+ (component) => !selectedComponents.includes(component.getAttribute('widgetid'))
+ );
+ const draggingOrResizing = draggingComponentId || resizingComponentId;
+ if (!draggingOrResizing && components.length > 0) {
+ for (const component of components) {
+ component?.classList?.remove('active-target');
+ }
+ }
+ }, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]);
+
if (mode !== 'edit') return null;
return (
@@ -557,11 +579,11 @@ export default function Grid({ gridWidth, currentLayout }) {
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
if (currentWidget.component?.parent) {
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
- useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent);
+ setDragParentId(currentWidget.component?.parent);
} else {
document.getElementById('real-canvas').classList.add('show-grid');
}
-
+ handleActivateTargets(currentWidget.component?.parent);
const currentWidth = currentWidget.width * _gridWidth;
const diffWidth = e.width - currentWidth;
const diffHeight = e.height - currentWidget.height;
@@ -584,9 +606,6 @@ export default function Grid({ gridWidth, currentLayout }) {
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
- transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
- transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
-
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${e.width}px`;
}
@@ -612,12 +631,13 @@ export default function Grid({ gridWidth, currentLayout }) {
// When clicked on widget boundary/resizer, select the component
setSelectedComponents([e.target.id]);
}
-
+ showGridLines();
if (!isComponentVisible(e.target.id)) {
return false;
}
+ handleActivateNonDraggingComponents();
useGridStore.getState().actions.setResizingComponentId(e.target.id);
- e.setMin([gridWidth, 10]);
+ e.setMin([gridWidth, GRID_HEIGHT]);
}}
onResizeEnd={(e) => {
try {
@@ -625,11 +645,10 @@ export default function Grid({ gridWidth, currentLayout }) {
const currentWidget = boxList.find(({ id }) => {
return id === e.target.id;
});
- document.getElementById('real-canvas')?.classList.remove('show-grid');
- document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid');
+ hideGridLines();
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth;
- const height = Math.round(e?.lastEvent?.height / 10) * 10;
+ const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT;
const currentWidth = currentWidget.width * _gridWidth;
const diffWidth = e.lastEvent?.width - currentWidth;
@@ -654,19 +673,17 @@ export default function Grid({ gridWidth, currentLayout }) {
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
- transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
- transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
- const roundedTransformY = Math.round(transformY / 10) * 10;
- transformY = transformY % 10 === 5 ? roundedTransformY - 10 : roundedTransformY;
+ const roundedTransformY = Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT;
+ transformY = transformY % GRID_HEIGHT === 5 ? roundedTransformY - GRID_HEIGHT : roundedTransformY;
e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${
- Math.round(transformY / 10) * 10
+ Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT
}px)`;
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`;
}
if (!maxHeightHit || e.height < e.target.clientHeight) {
- e.target.style.height = `${Math.round(e.lastEvent.height / 10) * 10}px`;
+ e.target.style.height = `${Math.round(e.lastEvent.height / GRID_HEIGHT) * GRID_HEIGHT}px`;
}
const resizeData = {
id: e.target.id,
@@ -682,18 +699,19 @@ export default function Grid({ gridWidth, currentLayout }) {
} catch (error) {
console.error('ResizeEnd error ->', error);
}
- useGridStore.getState().actions.setDragTarget();
+ handleDeactivateTargets();
+ setDragParentId(null);
toggleCanvasUpdater();
}}
onResizeGroupStart={({ events }) => {
- const parentElm = events[0].target.closest('.real-canvas');
- parentElm.classList.add('show-grid');
+ showGridLines();
+ handleActivateNonDraggingComponents();
}}
onResizeGroup={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
const parentWidth = parentElm?.clientWidth;
const parentHeight = parentElm?.clientHeight;
-
+ handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight);
events.forEach((ev) => {
ev.target.style.width = `${ev.width}px`;
@@ -710,8 +728,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const { events } = e;
const newBoxs = [];
- const parentElm = events[0].target.closest('.real-canvas');
- parentElm.classList.remove('show-grid');
+ hideGridLines();
// TODO: Logic needs to be relooked post go live P2
groupResizeDataRef.current.forEach((ev) => {
@@ -722,9 +739,9 @@ export default function Grid({ gridWidth, currentLayout }) {
let width = Math.round(ev.width / _gridWidth) * _gridWidth;
width = width < _gridWidth ? _gridWidth : width;
let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth;
- let posY = Math.round(ev.drag.translate[1] / 10) * 10;
- let height = Math.round(ev.height / 10) * 10;
- height = height < 10 ? 10 : height;
+ let posY = Math.round(ev.drag.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
+ let height = Math.round(ev.height / GRID_HEIGHT) * GRID_HEIGHT;
+ height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
@@ -752,7 +769,7 @@ export default function Grid({ gridWidth, currentLayout }) {
let posX = currentWidget?.layouts[currentLayout].left * _gridWidth;
let posY = currentWidget?.layouts[currentLayout].top;
let height = currentWidget?.layouts[currentLayout].height;
- height = height < 10 ? 10 : height;
+ height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
@@ -763,11 +780,18 @@ export default function Grid({ gridWidth, currentLayout }) {
} catch (error) {
console.error('Error resizing group', error);
}
+ handleDeactivateTargets();
toggleCanvasUpdater();
}}
checkInput
onDragStart={(e) => {
+ // This is to prevent parent component from being dragged and the stop the propagation of the event
+ if (getHoveredComponentForGrid() !== e.target.id) {
+ return false;
+ }
+ newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
+
const box = boxList.find((box) => box.id === e.target.id);
// Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS
if (SUBCONTAINER_WIDGETS.includes(box?.component?.component) && e.inputEvent.shiftKey) {
@@ -779,7 +803,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;
@@ -801,7 +825,6 @@ export default function Grid({ gridWidth, currentLayout }) {
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'));
@@ -809,152 +832,112 @@ export default function Grid({ gridWidth, currentLayout }) {
return false;
}
}
- // This is to prevent parent component from being dragged and the stop the propagation of the event
- if (getHoveredComponentForGrid() !== e.target.id) {
- return false;
- }
+ handleActivateNonDraggingComponents();
}}
onDragEnd={(e) => {
+ handleDeactivateTargets();
try {
if (isDraggingRef.current) {
- useGridStore.getState().actions.setDraggingComponentId(null);
+ useStore.getState().setDraggingComponentId(null);
isDraggingRef.current = false;
}
+ prevDragParentId.current = null;
+ 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 / 10) * 10
- }px)`;
- if (draggedOverElemId === currentParentId || isParentChangeAllowed) {
- handleDragEnd([
- {
- id: e.target.id,
- x: left,
- y: Math.round(top / 10) * 10,
- 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);
}
- // Hide all sub-canvases
- var canvasElms = document.getElementsByClassName('sub-canvas');
- var elementsArray = Array.from(canvasElms);
- elementsArray.forEach(function (element) {
- element.classList.remove('show-grid');
- element.classList.add('hide-grid');
- });
- document.getElementById('real-canvas')?.classList.remove('show-grid');
+ setCanvasBounds({ ...CANVAS_BOUNDS });
+ hideGridLines();
toggleCanvasUpdater();
}}
onDrag={(e) => {
// Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again
if (!isDraggingRef.current) {
- useGridStore.getState().actions.setDraggingComponentId(e.target.id);
+ useStore.getState().setDraggingComponentId(e.target.id);
+ showGridLines();
isDraggingRef.current = true;
}
- const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
+ const currentWidget = boxList.find((box) => box.id === e.target.id);
+ const currentParentId =
+ currentWidget?.component?.parent === null ? 'canvas' : currentWidget?.component?.parent;
+ const _gridWidth = useGridStore.getState().subContainerWidths[dragParentId] || gridWidth;
+ const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current;
- let top = e.translate[1];
- let left = e.translate[0];
+ // Snap to grid
+ let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
+ let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
+
+ // This logic is to handle the case when the dragged element is over a new canvas
+ if (_dragParentId !== currentParentId) {
+ left = e.translate[0];
+ top = e.translate[1];
+ }
// Special case for Modal
- 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)`;
@@ -963,8 +946,33 @@ export default function Grid({ gridWidth, currentLayout }) {
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
);
- handleDragGridLinesVisibility(e, [{ target: e.target }]);
+ // 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);
+ const draggedOverElements = targetElems.filter(
+ (ele) =>
+ (ele.id !== e.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas')
+ );
+ const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
+ const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
+ // Determine potential new parent
+ let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
+
+ if (newParentId === e.target.id) {
+ newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent;
+ } else if (parentComponent?.component?.component === 'Modal') {
+ // Never update parentId for Modal
+ newParentId = parentComponent?.id;
+ }
+
+ if (newParentId !== prevDragParentId.current) {
+ setDragParentId(newParentId === 'canvas' ? null : newParentId);
+ newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
+ prevDragParentId.current = newParentId;
+ handleActivateTargets(newParentId);
+ }
+ }
// 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)`;
@@ -979,31 +987,29 @@ export default function Grid({ gridWidth, currentLayout }) {
parentElm?.classList?.add('show-grid');
}
- handleDragGridLinesVisibility(ev, events);
-
events.forEach((ev) => {
- let left = ev.translate[0];
- let top = ev.translate[1];
+ const currentWidget = boxList.find(({ id }) => id === ev.target.id);
+ const _gridWidth =
+ useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
+
+ let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth;
+ let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${left}px, ${top}px)`;
});
+ handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
updateNewPosition(events);
}}
onDragGroupStart={({ events }) => {
- const parentElm = events[0]?.target?.closest('.real-canvas');
- parentElm?.classList?.add('show-grid');
+ showGridLines();
+ setIsGroupDragging(true);
+ handleActivateNonDraggingComponents();
}}
onDragGroupEnd={(e) => {
handleDragGroupEnd(e);
+ handleDeactivateTargets();
toggleCanvasUpdater();
}}
- //snap settgins
- snappable={true}
- snapThreshold={10}
- isDisplaySnapDigit={false}
- bounds={CANVAS_BOUNDS}
- displayAroundControls={true}
- controlPadding={20}
onClickGroup={(e) => {
const targetId =
e.inputEvent.target.id || e.inputEvent.target.closest('.moveable-box')?.getAttribute('widgetid');
@@ -1019,6 +1025,43 @@ export default function Grid({ gridWidth, currentLayout }) {
}
}
}}
+ //snap settgins
+ snappable={true}
+ snapGap={false}
+ isDisplaySnapDigit={false}
+ snapThreshold={GRID_HEIGHT}
+ bounds={canvasBounds}
+ // Guidelines configuration
+ elementGuidelines={elementGuidelines}
+ snapDirections={{
+ top: true,
+ right: true,
+ bottom: true,
+ left: true,
+ center: false,
+ middle: false,
+ }}
+ elementSnapDirections={{
+ top: true,
+ left: true,
+ bottom: true,
+ right: true,
+ center: false,
+ middle: false,
+ }}
+ onSnap={(e) => {
+ const components = e.elements;
+ if (isArray(componentsSnappedTo.current)) {
+ for (const component of componentsSnappedTo.current) {
+ component?.element?.classList?.remove('active-target');
+ }
+ }
+ componentsSnappedTo.current = components;
+ for (const component of components) {
+ component.element.classList.add('active-target');
+ }
+ }}
+ snapGridAll={true}
/>
>
);
diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
index 817f9a5ca9..da179bc11d 100644
--- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
+++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
@@ -1,7 +1,7 @@
import { useGridStore } from '@/_stores/gridStore';
import { isEmpty } from 'lodash';
import useStore from '@/AppBuilder/_stores/store';
-
+import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils';
export function correctBounds(layout, bounds) {
layout = scaleLayouts(layout);
const collidesWith = [];
@@ -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);
}
@@ -391,3 +392,99 @@ export function hasParentWithClass(child, className) {
return false;
}
+
+export function showGridLines() {
+ var canvasElms = document.getElementsByClassName('sub-canvas');
+ var elementsArray = Array.from(canvasElms);
+ elementsArray.forEach(function (element) {
+ element.classList.remove('hide-grid');
+ element.classList.add('show-grid');
+ });
+ document.getElementById('real-canvas')?.classList.remove('hide-grid');
+ document.getElementById('real-canvas')?.classList.add('show-grid');
+}
+
+export function hideGridLines() {
+ var canvasElms = document.getElementsByClassName('sub-canvas');
+ var elementsArray = Array.from(canvasElms);
+ elementsArray.forEach(function (element) {
+ element.classList.remove('show-grid');
+ element.classList.add('hide-grid');
+ });
+ document.getElementById('real-canvas')?.classList.remove('show-grid');
+ document.getElementById('real-canvas')?.classList.add('hide-grid');
+}
+
+// Track previously active elements for efficient cleanup
+let previousActiveWidgets = null;
+let previousActiveCanvas = null;
+
+export const handleActivateNonDraggingComponents = () => {
+ // Only add non-dragging class to visible components in viewport
+ document.querySelectorAll('.moveable-box:not(.active-target)').forEach((component) => {
+ // Check if element is visible in viewport
+ const rect = component.getBoundingClientRect();
+ const isVisible =
+ rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
+
+ if (isVisible) {
+ component.classList.add('non-dragging-component');
+ }
+ });
+};
+
+export const handleActivateTargets = (parentId) => {
+ const WIDGETS_WITH_CANVAS_OUTLINE = ['Container', 'Modal', 'Form', 'Listview', 'Kanban'];
+
+ const newParentType = document.getElementById('canvas-' + parentId)?.getAttribute('component-type');
+ let _parentId = parentId;
+ if (newParentType === 'Tabs') {
+ _parentId = getTabId(parentId);
+ } else if (WIDGETS_WITH_CANVAS_OUTLINE.includes(newParentType)) {
+ _parentId = getSubContainerIdWithSlots(parentId);
+ }
+
+ // Clean up previous active elements
+ if (previousActiveWidgets) {
+ previousActiveWidgets.classList.remove('dragging-component-canvas');
+ previousActiveWidgets = null;
+ }
+
+ if (previousActiveCanvas) {
+ previousActiveCanvas.classList.remove('dragging-component-canvas');
+ previousActiveCanvas = null;
+ }
+
+ const parentComponent = document.getElementById(_parentId);
+ if (!parentComponent) return;
+
+ if (WIDGETS_WITH_CANVAS_OUTLINE?.includes(newParentType)) {
+ // If it's multiple canvas in single widget, highlight the specific canvas
+ const canvasElm = document.getElementById('canvas-' + parentId);
+ if (canvasElm) {
+ canvasElm.classList.add('dragging-component-canvas');
+ previousActiveCanvas = canvasElm;
+ }
+ } else {
+ // Otherwise highlight the component box
+ parentComponent.classList.remove('non-dragging-component');
+ parentComponent.classList.add('dragging-component-canvas');
+ previousActiveWidgets = parentComponent;
+ }
+};
+
+export const handleDeactivateTargets = () => {
+ if (previousActiveWidgets) {
+ previousActiveWidgets.classList.remove('dragging-component-canvas');
+ previousActiveWidgets = null;
+ }
+
+ if (previousActiveCanvas) {
+ previousActiveCanvas.classList.remove('dragging-component-canvas');
+ previousActiveCanvas = null;
+ }
+
+ document.querySelectorAll('.non-dragging-component').forEach((component) => {
+ component.classList.remove('non-dragging-component');
+ });
+};
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/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx
index 427cced97b..b26586dc08 100644
--- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx
+++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx
@@ -6,7 +6,7 @@ import { OverlayTrigger } from 'react-bootstrap';
import { renderTooltip } from '@/_helpers/appUtils';
import { useTranslation } from 'react-i18next';
import ErrorBoundary from '@/_ui/ErrorBoundary';
-
+import { BOX_PADDING } from './appCanvasConstants';
const shouldAddBoxShadowAndVisibility = [
'Table',
'TextInput',
@@ -164,7 +164,7 @@ const RenderWidget = ({
state.getComponentDefinition(id)?.layouts?.[currentLayout], shallow);
const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow);
- const isDragging = useGridStore((state) => state.draggingComponentId === id);
+ const isDragging = useStore((state) => state.draggingComponentId === id);
const isResizing = useGridStore((state) => state.resizingComponentId === id);
const componentType = useStore((state) => state.getComponentDefinition(id)?.component?.component, shallow);
const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow);
@@ -52,7 +52,9 @@ const WidgetWrapper = memo(
height: visibility === false ? '10px' : `${height}px`,
transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`,
WebkitFontSmoothing: 'antialiased',
+ border: visibility === false ? `1px solid var(--border-default)` : 'none',
};
+
if (!componentType) return null;
return (
<>
@@ -67,8 +69,8 @@ const WidgetWrapper = memo(
data-id={`${id}`}
id={id}
widgetid={id}
+ component-type={componentType}
style={{
- // transform: `translate(332px, -134px)`,
// zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null,
...styles,
}}
@@ -84,11 +86,12 @@ const WidgetWrapper = memo(
{mode == 'edit' && (
)}
{
- 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,9 @@ 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 === 'Form' ||
+ allComponents[parentId]?.component?.component === 'ModalV2';
if (componentParentId && isParentTabORCalendar) {
let childComponent = deepClone(allComponents[componentId]);
@@ -240,6 +248,12 @@ const getSelectedText = () => {
// TODO: Move this function to componentSlice
export const copyComponents = ({ isCut = false, isCloning = false }) => {
+ const selectedText = window.getSelection()?.toString().trim();
+ if (selectedText) {
+ navigator.clipboard.writeText(selectedText);
+ return;
+ }
+
const selectedComponents = useStore.getState().getSelectedComponentsDefinition();
if (selectedComponents.length < 1) return getSelectedText();
const allComponents = useStore.getState().getCurrentPageComponents();
@@ -249,7 +263,6 @@ export const copyComponents = ({ isCut = false, isCloning = false }) => {
const parentComponentId = isChildOfTabsOrCalendar(selectedComponent, allComponents)
? selectedComponent.component.parent.split('-').slice(0, -1).join('-')
: selectedComponent?.component?.parent;
-
if (parentComponentId) {
// Check if the parent component is also selected
const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId);
@@ -320,7 +333,9 @@ 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 === 'Form' ||
+ parentComponent.component.component === 'ModalV2'
);
}
@@ -483,11 +498,14 @@ export function pasteComponents(targetParentId, copiedComponentObj) {
// Prevent pasting if the parent subcontainer was deleted during a cut operation
if (
targetParentId &&
+ // Check if targetParentId is deleted from the components
!Object.keys(components).find(
(key) =>
targetParentId === key ||
(components?.[key]?.component.component === 'Tabs' &&
- targetParentId?.split('-')?.slice(0, -1)?.join('-') === key)
+ targetParentId?.split('-')?.slice(0, -1)?.join('-') === key) ||
+ (['Container', 'Form', 'Modal'].includes(components?.[key]?.component.component) &&
+ ['header', 'footer'].some((section) => targetParentId.includes(section)))
)
) {
return;
@@ -655,10 +673,42 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
return canvasBgColor;
};
-export const getParentComponentIdByType = (child, parentComponent, parentId) => {
+export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName }) => {
const { tab } = child;
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
- else if (parentComponent === 'Container') return `${parentId}-header`;
+ else if (
+ slotName &&
+ (parentComponent === 'Form' || 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;
+};
+
+export const getTabId = (parentId) => {
+ return parentId.split('-').slice(0, -1).join('-');
+};
+
+export const getSubContainerIdWithSlots = (parentId) => {
+ let cleanParentId = parentId;
+ if (parentId) {
+ if (parentId.includes('header')) {
+ cleanParentId = parentId.replace('-header', '');
+ } else if (parentId.includes('footer')) {
+ cleanParentId = parentId.replace('-footer', '');
+ }
+ }
+ return cleanParentId;
+};
diff --git a/frontend/src/AppBuilder/AppCanvas/selecto.scss b/frontend/src/AppBuilder/AppCanvas/selecto.scss
index 9ca8a37f41..5602b35d5a 100644
--- a/frontend/src/AppBuilder/AppCanvas/selecto.scss
+++ b/frontend/src/AppBuilder/AppCanvas/selecto.scss
@@ -3,15 +3,18 @@
}
.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover {
- z-index: 4 !important;
-}
-
-.main-editor-canvas .widget-target:has(.nested-target:hover):hover {
- outline: 0px solid #4af;
-}
-
-.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
outline: 1px solid #4af;
z-index: 4 !important;
}
+.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
+ // outline: 1px solid #4af;
+ z-index: 4 !important;
+}
+
+// .main-editor-canvas .widget-target:hover {
+// outline: 1px solid #4af;
+// }
+
+
+
diff --git a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx
index b9fb85fcae..099cd3763a 100644
--- a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx
+++ b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx
@@ -5,19 +5,13 @@ const MIN_TABLE_ROW_HEIGHT_DEFAULT = 45;
const TableRowHeightInput = ({ value, onChange, cyLabel, staticText, styleDefinition }) => {
const [inputValue, setInputValue] = useState(value);
+ const minValue =
+ styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT;
+
useEffect(() => {
setInputValue(value < minValue ? minValue : value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, styleDefinition.cellSize?.value]);
- useEffect(() => {
- onChange(
- styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT
- );
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const minValue =
- styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT;
const handleBlur = () => {
const newValue = Math.max(inputValue, minValue);
diff --git a/frontend/src/AppBuilder/CodeBuilder/utils.js b/frontend/src/AppBuilder/CodeBuilder/utils.js
index 7a75bdb3b8..be3674aa1f 100644
--- a/frontend/src/AppBuilder/CodeBuilder/utils.js
+++ b/frontend/src/AppBuilder/CodeBuilder/utils.js
@@ -74,6 +74,7 @@ export function getSuggestionKeys(refState) {
'setVariable',
'getVariable',
'unSetVariable',
+ 'unsetAllVariables',
'showAlert',
'logout',
'showModal',
@@ -85,6 +86,7 @@ export function getSuggestionKeys(refState) {
'setPageVariable',
'getPageVariable',
'unsetPageVariable',
+ 'unsetAllPageVariables',
'switchPage',
];
diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx
index 033d266e03..f95baaa328 100644
--- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx
+++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx
@@ -300,10 +300,10 @@ const MultiLineCodeEditor = (props) => {
editable={editable} //for transformations in query manager
onCreateEditor={(view) => setEditorView(view)}
onUpdate={(view) => {
- const icon = document.querySelector('.codehinter-search-btn-wrapper');
+ const icon = document.querySelector('.codehinter-search-btn');
if (searchPanelOpen(view.state)) {
- icon.style.top = '44px';
- } else icon.style.top = '0px';
+ icon.style.display = 'none';
+ } else icon.style.display = 'block';
}}
/>
diff --git a/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx b/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx
index 2429973c25..6c28bdbb21 100644
--- a/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx
+++ b/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx
@@ -294,13 +294,9 @@ const PreviewContainer = ({
...restProps
}) => {
const { validationSchema, isWorkspaceVariable, errorStateActive, previewPlacement, validationFn } = restProps;
-
const [errorMessage, setErrorMessage] = useState('');
-
const typeofError = getCurrentNodeType(errorMessage);
-
const errorMsg = typeofError === 'Array' ? errorMessage[0] : errorMessage;
-
const darkMode = localStorage.getItem('darkMode') === 'true';
const popover = (
{!isPortalOpen && (
{
+ // Force position update on first render
+ // This is done to avoid scroll issue
+ if (state.elements.popper) {
+ state.elements.popper.style.position = 'fixed';
+ }
+ },
}}
>
{(props) => React.cloneElement(popover, props)}
diff --git a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx
index 2b807f718b..28f7451b95 100644
--- a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx
+++ b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable import/no-unresolved */
import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import {
@@ -9,12 +10,13 @@ import {
replaceNext,
replaceAll,
openSearchPanel,
- // eslint-disable-next-line import/no-unresolved
} from '@codemirror/search';
import './SearchBox.scss';
import InputComponent from '@/components/ui/Input/Index.jsx';
import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
import { ToolTip } from '@/_components/ToolTip';
+import { SelectionRange } from '@codemirror/state';
+import { useHotkeys } from 'react-hotkeys-hook';
export const handleSearchPanel = (view) => {
const dom = document.createElement('div');
@@ -35,6 +37,11 @@ function SearchPanel({ view }) {
replace: replaceTerm,
});
view.dispatch({ effects: setSearchQuery.of(query) });
+
+ const currentPos = view.state.selection.main.head;
+ view.dispatch({
+ selection: SelectionRange.create(currentPos, currentPos),
+ });
};
useEffect(() => {
@@ -44,12 +51,28 @@ function SearchPanel({ view }) {
return () => clearTimeout(handler);
}, [searchText, replaceText]);
+ const [shortcutEnabled, setShortcutEnabled] = useState(false);
+
+ // Shortcuts for search input field
+ useHotkeys(
+ ['shift+enter', 'enter'],
+ (event, handler) => {
+ if (handler.shift && handler.keys[0] === 'enter') findPrevious(view);
+ else if (handler.keys[0] === 'enter') findNext(view);
+ },
+ {
+ enabled: shortcutEnabled,
+ enableOnFormTags: true,
+ }
+ );
+
const displaySearchField = () => (
setSearchText(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && findNext(view)}
+ onFocus={() => setShortcutEnabled(true)}
+ onBlur={() => setShortcutEnabled(false)}
placeholder="Find"
size="small"
value={searchText}
diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx
index 1243f26f43..7f8765e287 100644
--- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx
+++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx
@@ -1,5 +1,5 @@
/* eslint-disable import/no-unresolved */
-import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { PreviewBox } from './PreviewBox';
import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip';
import { useTranslation } from 'react-i18next';
@@ -31,6 +31,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
const [currentValue, setCurrentValue] = useState('');
const [errorStateActive, setErrorStateActive] = useState(false);
const [cursorInsidePreview, setCursorInsidePreview] = useState(false);
+ const [showSuggestions, setShowSuggestions] = useState(true);
const validationFn = restProps?.validationFn;
const componentDefinition = useStore((state) => state.getComponentDefinition(componentId), shallow);
const parentId = componentDefinition?.component?.parent;
@@ -38,6 +39,30 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
const customVariables = customResolvables?.[parentId]?.[0] || {};
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.intersectionRatio < 1) {
+ setShowPreview(false);
+ setShowSuggestions(false);
+ } else {
+ setShowSuggestions(true);
+ }
+ },
+ { root: null, threshold: [1] } // Fires when any part of the element is out of view
+ );
+
+ if (wrapperRef.current) {
+ observer.observe(wrapperRef.current);
+ }
+
+ return () => {
+ if (wrapperRef.current) {
+ observer.unobserve(wrapperRef.current);
+ }
+ };
+ }, []);
+
const isPreviewFocused = useRef(false);
const wrapperRef = useRef(null);
@@ -136,6 +161,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
componentName={componentName}
setShowPreview={setShowPreview}
showPreview={showPreview}
+ showSuggestions={showSuggestions}
{...restProps}
/>
@@ -168,6 +194,7 @@ const EditorInput = ({
previewRef,
setShowPreview,
onInputChange,
+ showSuggestions,
}) => {
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
function autoCompleteExtensionConfig(context) {
@@ -223,7 +250,7 @@ const EditorInput = ({
defaultKeymap: true,
positionInfo: () => {
return {
- class: 'cm-completionInfo-top cm-custom-completion-info',
+ class: 'cm-completionInfo-top cm-custom-completion-info cm-custom-singleline-completion-info',
};
},
maxRenderedOptions: 10,
@@ -286,7 +313,7 @@ const EditorInput = ({
const isInsideQueryPane = !!currentEditorHeightRef?.current?.closest('.query-details');
const showLineNumbers = lang == 'jsx' || type === 'extendedSingleLine' || false;
- const customClassNames = cx('codehinter-input', {
+ const customClassNames = cx('codehinter-input single-line-codehinter-input', {
'border-danger': error,
focused: isFocused,
'focus-box-shadow-active': firstTimeFocus,
@@ -336,18 +363,9 @@ const EditorInput = ({
{/* sticky element to position the preview box correctly on top without flowing out of container */}
-
{usePortalEditor && (
- {
- setFirstTimeFocus(false);
- handleOnChange(val);
- onInputChange && onInputChange(val);
+ handleFocus()}
- onBlur={() => handleOnBlur()}
- className={customClassNames}
- theme={theme}
- indentWithTab={false}
- readOnly={disabled}
- />
+ className="check-here"
+ ref={previewRef}
+ >
+ {
+ setFirstTimeFocus(false);
+ handleOnChange(val);
+ onInputChange && onInputChange(val);
+ }}
+ basicSetup={{
+ lineNumbers: showLineNumbers,
+ syntaxHighlighting: true,
+ bracketMatching: true,
+ foldGutter: false,
+ highlightActiveLine: false,
+ autocompletion: showSuggestions,
+ completionKeymap: true,
+ searchKeymap: false,
+ }}
+ onMouseDown={() => handleFocus()}
+ onBlur={() => handleOnBlur()}
+ className={customClassNames}
+ theme={theme}
+ indentWithTab={false}
+ readOnly={disabled}
+ />
+
diff --git a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx
index 4edd8a0fda..dab57d0f20 100644
--- a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx
+++ b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx
@@ -56,8 +56,8 @@ const TJDBCodeEditor = (props) => {
const handleOnChange = (value) => {
if (value === '') {
- setErrorState(true);
- setError('JSON cannot be empty');
+ setErrorState(false);
+ setError(null);
setCurrentValue(value);
return;
}
@@ -167,18 +167,18 @@ const TJDBCodeEditor = (props) => {
componentName={componentName}
key={componentName}
forceUpdate={forceUpdate}
- optionalProps={{ styles: { height: 300 }, cls: '' }}
+ optionalProps={{ styles: { height: 300 }, cls: 'tjdb-hinter-portal' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal tjdb-portal-codehinter' }}
dragResizePortal={true}
callgpt={null}
>
-
+
[
state.isLeftSideBarPinned,
@@ -46,6 +47,7 @@ export const BaseLeftSidebar = ({
state.debugger.resetUnreadErrorCount,
state.toggleLeftSidebar,
state.isSidebarOpen,
+ state.queryPanel.isDraggingQueryPane,
],
shallow
);
@@ -68,11 +70,15 @@ export const BaseLeftSidebar = ({
};
useEffect(() => {
- setPopoverContentHeight(
- ((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100
- );
+ if (!isDraggingQueryPane) {
+ setPopoverContentHeight(
+ ((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100
+ );
+ } else {
+ setPopoverContentHeight(100);
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [queryPanelHeight]);
+ }, [queryPanelHeight, isDraggingQueryPane]);
const renderPopoverContent = () => {
if (selectedSidebarItem === null || !isSidebarOpen) return null;
diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx
index dc4b2cf7af..3adca4be98 100644
--- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx
+++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx
@@ -88,9 +88,19 @@ const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]);
- const handleNodeExpansion = (path) => {
+ const handleNodeExpansion = (path, data, currentNode) => {
if (pathToBeInspected && path?.length > 0) {
- return pathToBeInspected.includes(path[path.length - 1]);
+ const shouldExpand = pathToBeInspected.includes(path[path.length - 1]);
+
+ // Scroll to the component in the inspector
+ if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) {
+ const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`);
+ if (target) {
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ }
+
+ return shouldExpand;
} else return false;
};
diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js
index 7067cd540d..937fe45b2c 100644
--- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js
+++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js
@@ -9,6 +9,7 @@ const useCallbackActions = () => {
const currentPageComponents = useStore((state) => state?.getCurrentPageComponents(), shallow);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const runQuery = useStore((state) => state.queryPanel.runQuery);
+ const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll);
const handleRemoveComponent = (component) => {
deleteComponents([component.id]);
@@ -30,30 +31,22 @@ const useCallbackActions = () => {
return toast.success('Copied to the clipboard', { position: 'top-center' });
};
- const autoScrollTo = (id) => {
- setSelectedComponents([id]);
- const target = document.getElementById(id);
- target.scrollIntoView({ behavior: 'smooth', block: 'center' });
- };
-
const handleAutoScrollToComponent = (data) => {
- const currentPageComponents = useStore.getState().getCurrentPageComponents();
- const component = currentPageComponents?.[data.id];
-
- let parentId = component?.component?.parent;
- if (parentId) {
- const regex = /-\d+$/;
- if (regex.test(parentId)) {
- parentId = parentId.replace(regex, ''); // To get parentId without tab index if parent type is Tab
- }
- const parentType = currentPageComponents?.[parentId]?.component?.component;
- if (parentType && (parentType === 'Modal' || parentType === 'Tabs')) {
- autoScrollTo(parentId); // To scroll to parent component if parent type is Modal or Tabs
- return;
- }
+ const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(data.id);
+ if (!isAccessible) {
+ if (isOnCanvas) {
+ toast.success(
+ `This component can't be opened because it's on the main canvas. Close ${computedComponentId} and click "Go to component" to view it there`
+ );
+ } else
+ toast.success(
+ `This component can't be opened because it's inside ${computedComponentId}. Open ${computedComponentId} and click "Go to component"to view it.`
+ );
+ return;
}
-
- autoScrollTo(data.id);
+ setSelectedComponents([computedComponentId]);
+ const target = document.getElementById(computedComponentId);
+ target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
};
const callbackActions = [
diff --git a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx
index 0cdcdb844b..f0a3bbe811 100644
--- a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx
+++ b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx
@@ -7,7 +7,7 @@ import { getWorkspaceId, decodeEntities } from '@/_helpers/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
-import { staticDataSources as staticDatasources } from '../constants';
+import { defaultSources, staticDataSources as staticDatasources } from '../constants';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import Search from '@/_ui/Icon/solidIcons/Search';
import { Tooltip } from 'react-tooltip';
@@ -135,7 +135,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
{' '}
- {source.name}
+ {defaultSources[cleanWord(source.name)].name}
),
@@ -178,6 +178,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
}
};
+ function cleanWord(word) {
+ return word.replace(/default/g, '');
+ }
+
return (
state.getShouldFreeze());
useEffect(() => {
setDataSourceMeta(
- selectedQuery?.pluginId
- ? selectedQuery?.manifestFile?.data?.source
+ selectedQuery?.plugin_id
+ ? selectedQuery?.manifest_file?.data?.source
: DataSourceTypes.find((source) => source.kind === selectedQuery?.kind)
);
setSelectedQueryId(selectedQuery?.id);
@@ -188,7 +188,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
{
if (lang !== (options.transformationLanguage ?? 'javascript')) {
changeOption('transformationLanguage', lang);
- changeOption('transformation', state[lang]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lang]);
@@ -127,20 +127,24 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
useEffect(() => {
if (prevQueryId.current === queryId) {
lang !== (options.transformationLanguage ?? 'javascript') && changeOption('transformationLanguage', lang);
- setState({ ...state, [lang]: options.transformation ?? state[lang] ?? defaultValue[lang] });
+ setState((prevState) => {
+ return {
+ ...prevState,
+ ...(options?.transformation
+ ? { [options.transformationLanguage ?? 'javascript']: options?.transformation }
+ : {}),
+ ...options?.transformations,
+ };
+ });
}
prevQueryId.current = queryId;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(options.transformation)]);
+ }, [JSON.stringify(options?.transformation || {}), JSON.stringify(options.transformations)]);
useEffect(() => {
if (selectedQueryId !== queryId) {
- const nonLangdefaultCode = getNonActiveTransformations(options?.transformationLanguage ?? 'javascript');
- const finalState = _.merge(
- {},
- { [options?.transformationLanguage ?? lang]: options.transformation ?? defaultValue[lang] },
- nonLangdefaultCode
- );
+ const olderTransformation = options?.transformation ? { [lang]: options?.transformation } : {};
+ const finalState = _.merge({}, defaultValue, olderTransformation, options?.transformations);
setState(finalState);
}
@@ -206,8 +210,6 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
activeKey={lang}
onSelect={(value) => {
setLang(value);
- changeOption('transformationLanguage', value);
- changeOption('transformation', state[value]);
}}
defaultActiveKey="javascript"
>
@@ -250,7 +252,7 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
height={400}
className="query-hinter"
onChange={(value) => {
- changeOption('transformation', value);
+ changeOption('transformations', { ...state, [lang]: value });
}}
renderCopilot={renderCopilot}
componentName={`transformation`}
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/BaseUrl.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/BaseUrl.jsx
index 858f6b3aa6..43a6b672df 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/BaseUrl.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/BaseUrl.jsx
@@ -1,21 +1,31 @@
import React from 'react';
+import OverflowTooltip from '@/_components/OverflowTooltip';
-export const BaseUrl = ({ dataSourceURL, theme }) => {
+export const BaseUrl = ({ dataSourceURL, theme, className = 'col-auto', style = {} }) => {
return (
- {dataSourceURL}
+
+ {dataSourceURL}
+
);
};
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/TabContent.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/TabContent.jsx
index cf47ded427..ee55937b1c 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/TabContent.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/TabContent.jsx
@@ -30,7 +30,7 @@ export default ({
return (
<>
-
+
{
+ if (isEmpty(this.state.options['url_params'])) {
+ this.addNewKeyValuePair('url_params');
+ }
+ }, 1000);
+ setTimeout(() => {
+ if (isEmpty(this.state.options['body'])) {
+ this.addNewKeyValuePair('body');
+ }
+ }, 1000);
setTimeout(() => {
this.initizalizeRetryNetworkErrorsToggle();
}, 1000);
+
+ this.setupResizeObserver();
} catch (error) {
console.log(error);
}
}
+ componentWillUnmount() {
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ }
+ }
+
+ setupResizeObserver() {
+ if (!this.codeHinterRef.current) return;
+
+ // Try to find the editor element, checking multiple possible selectors
+ const findEditorElement = () => {
+ const element =
+ this.codeHinterRef.current.querySelector('.cm-editor') ||
+ this.codeHinterRef.current.querySelector('.codehinter-input') ||
+ this.codeHinterRef.current.querySelector('.code-hinter-wrapper');
+ return element;
+ };
+
+ // Initial attempt to find editor
+ let editorElement = findEditorElement();
+
+ // If not found immediately, try again after a short delay
+ if (!editorElement) {
+ setTimeout(() => {
+ editorElement = findEditorElement();
+ if (editorElement) {
+ this.setupObserverForElement(editorElement);
+ }
+ }, 100);
+ return;
+ }
+
+ this.setupObserverForElement(editorElement);
+ }
+
+ setupObserverForElement(element) {
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ }
+
+ this.resizeObserver = new ResizeObserver((entries) => {
+ for (let entry of entries) {
+ const height = Math.max(32, Math.min(entry.contentRect.height, 220));
+ if (height !== this.state.codeHinterHeight) {
+ this.setState({ codeHinterHeight: height });
+ }
+ }
+ });
+
+ this.resizeObserver.observe(element);
+ }
+
initizalizeRetryNetworkErrorsToggle = () => {
const isRetryNetworkErrorToggleUnused = this.props.options.retry_network_errors === null;
if (isRetryNetworkErrorToggleUnused) {
@@ -212,13 +289,30 @@ class Restapi extends React.Component {
useCustomStyles={true}
/>
-
+
URL
-
+
{dataSourceURL && (
-
+
)}
-
+
{
>
{
{
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
index 95d2ab56e0..23d1c7f7cf 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
@@ -214,4 +214,9 @@
.input-value-padding {
box-sizing: border-box;
padding-right: 30px !important;
+}
+
+.react-datepicker__navigation{
+ overflow: visible !important;
+ height: inherit !important;
}
\ No newline at end of file
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx
index 3b32706f67..20f499fbe8 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx
@@ -298,7 +298,7 @@ const DropDownSelect = ({
) : (
-
+
{index > 0 && (
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx
index 4734addb26..863c826b8a 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx
@@ -340,10 +340,7 @@ const JsonBfieldsForSelect = ({ selectedJsonbColumns, handleJSonChange, table })
handleRemove(colDetails.id, colDetails.name, colDetails.table)}
>
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSort.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSort.jsx
index 2d5a2de518..9e129e9eb4 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSort.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinSort.jsx
@@ -164,10 +164,7 @@ export default function JoinSort({ darkMode }) {
setJoinOrderByOptions(joinOrderByOptions.filter((opt, idx) => idx !== i))}
>
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx
index eba36f37a3..cf14448967 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx
@@ -535,12 +535,11 @@ const RenderFilterSection = ({ darkMode }) => {
removeFilterConditionEntry(index)}
>
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderColumnUI.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderColumnUI.jsx
index 1766691d66..89323c1c3c 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderColumnUI.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderColumnUI.jsx
@@ -54,10 +54,7 @@ const RenderColumnUI = ({
removeColumnOptionsPair(id)}
>
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderFilterSectionUI.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderFilterSectionUI.jsx
index 983019697a..cd7f94abf3 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderFilterSectionUI.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderFilterSectionUI.jsx
@@ -117,10 +117,7 @@ const RenderFilterSectionUI = ({
removeFilterConditionPair(id)}
>
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderSortUI.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderSortUI.jsx
index b5021974ba..a349b9e5ed 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderSortUI.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/RenderSortUI.jsx
@@ -86,10 +86,7 @@ const RenderSortUI = ({
removeSortConditionPair(id)}
>
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx
index e3f31c974d..e873228888 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx
@@ -677,6 +677,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
}}
componentName="TooljetDatabase"
delayOnChange={false}
+ className="w-100"
/>
)}
diff --git a/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx b/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx
index 827efe6d33..ea8623b0c1 100644
--- a/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx
+++ b/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx
@@ -185,6 +185,7 @@ export const QueryPanel = ({ darkMode }) => {
id="query-manager"
style={{
height: `calc(100% - ${isExpanded ? height : 100}%)`,
+ maxHeight: '93.5%',
cursor: isDraggingQueryPane || isTopOfQueryPanel ? 'row-resize' : 'default',
...(!isExpanded && {
border: 'none',
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/DragLayer.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
index 7450011b93..77274cd658 100644
--- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
@@ -2,12 +2,14 @@ import React, { useEffect } from 'react';
import { WidgetBox } from '../WidgetBox';
import { useDrag, useDragLayer } from 'react-dnd';
import { getEmptyImage } from 'react-dnd-html5-backend';
+import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils';
+import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants';
export const DragLayer = ({ index, component }) => {
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: 'box',
- item: { componentType: component.component },
+ item: { componentType: component.component, component },
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
}),
[component.component]
@@ -18,7 +20,6 @@ export const DragLayer = ({ index, component }) => {
}, []);
const size = component.defaultSize || { width: 30, height: 40 };
-
return (
<>
{isDragging &&
}
@@ -30,32 +31,39 @@ export const DragLayer = ({ index, component }) => {
};
const CustomDragLayer = ({ size }) => {
- const { currentOffset } = useDragLayer((monitor) => ({
+ const { currentOffset, item } = useDragLayer((monitor) => ({
currentOffset: monitor.getSourceClientOffset(),
+ item: monitor.getItem(),
}));
if (!currentOffset) return null;
- const canvasWidth = document.getElementsByClassName('real-canvas')[0]?.getBoundingClientRect()?.width;
-
+ const canvasWidth = item?.canvasWidth;
+ const canvasBounds = item?.canvasRef?.getBoundingClientRect();
const height = size.height;
- const width = (canvasWidth * size.width) / 43;
+ const width = (canvasWidth * size.width) / NO_OF_GRIDS;
+
+ // Calculate position relative to the current canvas (parent or child)
+ const left = currentOffset.x - (canvasBounds?.left || 0);
+ const top = currentOffset.y - (canvasBounds?.top || 0);
+
+ const [x, y] = snapToGrid(canvasWidth, left, top);
return (
{
@@ -151,7 +153,8 @@ export const baseComponentProperties = (
'properties',
currentState,
allComponents,
- darkMode
+ darkMode,
+ ''
)
),
});
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx
index d4676ad4b6..b39924854e 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx
@@ -19,16 +19,34 @@ export const Form = ({
allComponents,
pages,
}) => {
- const properties = Object.keys(componentMeta.properties);
+ const tempComponentMeta = deepClone(componentMeta);
+
+ let properties = [];
+ let additionalActions = [];
+ let dataProperties = [];
+
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
- const tempComponentMeta = deepClone(componentMeta);
+
+ 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 { id } = component;
const newOptions = [{ name: 'None', value: 'none' }];
- Object.entries(allComponents).forEach(([componentId, component]) => {
- if (component.component.parent === id && component?.component?.component === 'Button') {
- newOptions.push({ name: component.component.name, value: componentId });
+ Object.entries(allComponents).forEach(([componentId, _component]) => {
+ const validParent =
+ _component.component.parent === id ||
+ _component.component.parent === `${id}-footer` ||
+ _component.component.parent === `${id}-header`;
+ if (validParent && _component?.component?.component === 'Button') {
+ newOptions.push({ name: _component.component.name, value: componentId });
}
});
@@ -48,7 +66,8 @@ export const Form = ({
allComponents,
validations,
darkMode,
- pages
+ pages,
+ additionalActions
);
return
;
@@ -68,7 +87,8 @@ export const baseComponentProperties = (
allComponents,
validations,
darkMode,
- pages
+ pages,
+ additionalActions
) => {
let items = [];
if (properties.length > 0) {
@@ -90,6 +110,24 @@ export const baseComponentProperties = (
});
}
+ items.push({
+ title: 'Additional actions',
+ isOpen: true,
+ children: additionalActions?.map((property) =>
+ renderElement(
+ component,
+ componentMeta,
+ paramUpdated,
+ dataQueries,
+ property,
+ 'properties',
+ currentState,
+ allComponents,
+ darkMode
+ )
+ ),
+ });
+
if (events.length > 0) {
items.push({
title: `${i18next.t('widget.common.events', 'Events')}`,
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/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
index a99ce25ac6..095deeccaa 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
@@ -41,7 +41,6 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
if (!Array.isArray(optionsValue)) {
optionsValue = Object.values(optionsValue);
}
- const valuesToResolve = ['label', 'value'];
let options = [];
if (isDynamicOptionsEnabled || typeof optionsValue === 'string') {
@@ -216,7 +215,6 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
});
updateOptions(_options);
setMarkedAsDefault(_value);
- paramUpdated({ name: 'value' }, 'value', _value, 'properties');
}
};
@@ -315,7 +313,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
{options?.map((item, index) => {
return (
-
+
{(provided, snapshot) => (
{
+ if (!isOpen) {
+ document.activeElement?.blur(); // Manually trigger blur when popover closes
+ }
+ }}
>
-
+
setHoveredOptionIndex(index)}
@@ -425,7 +428,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
- {getResolvedValue(item.label)}
+ {getResolvedValue(item?.label)}
{index === hoveredOptionIndex && (
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
index 27ba100f52..2e975109d2 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
@@ -52,6 +52,7 @@ export const PropertiesTabElements = ({
{ label: 'Boolean', value: 'boolean' },
{ label: 'Image', value: 'image' },
{ label: 'Link', value: 'link' },
+ { label: 'JSON', value: 'json' },
// Following column types are deprecated
{ label: 'Default', value: 'default' },
{ label: 'Dropdown', value: 'dropdown' },
@@ -266,6 +267,24 @@ export const PropertiesTabElements = ({
)}
)}
+ {column.columnType === 'json' && (
+
+ )}
)}
- {['string', 'default', undefined, 'number', 'boolean', 'select', 'text', 'newMultiSelect', 'datepicker'].includes(
- column.columnType
- ) && (
+ {[
+ 'string',
+ 'default',
+ undefined,
+ 'number',
+ 'json',
+ 'boolean',
+ 'select',
+ 'text',
+ 'newMultiSelect',
+ 'datepicker',
+ ].includes(column.columnType) && (
<>
{column.columnType !== 'boolean' && (
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx
index e559369396..c3fb47d612 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx
@@ -50,6 +50,8 @@ export const ProgramaticallyHandleProperties = ({
return props?.parseInUnixTimestamp;
case 'isDateSelectionEnabled':
return props?.isDateSelectionEnabled;
+ case 'jsonIndentation':
+ return props?.jsonIndentation;
default:
return;
}
@@ -81,6 +83,9 @@ export const ProgramaticallyHandleProperties = ({
if (property === 'linkColor') {
return definitionObj?.value ?? '#1B1F24';
}
+ if (property === 'jsonIndentation') {
+ return definitionObj?.value ?? `{{true}}`;
+ }
return definitionObj?.value ?? `{{false}}`;
};
@@ -111,7 +116,9 @@ export const ProgramaticallyHandleProperties = ({
const fxActiveFieldsPropExists = props?.hasOwnProperty('fxActiveFields') ?? false;
//to support backward compatibility, when fxActive is true for a particular column, we are passing all possible combinations which should render codehinter
const fxActive =
- props?.fxActive && resolveReferences(props.fxActive) ? ['isEditable', 'columnVisibility', 'linkTarget'] : [];
+ props?.fxActive && resolveReferences(props.fxActive)
+ ? ['isEditable', 'columnVisibility', 'jsonIndentation', 'linkTarget']
+ : [];
const checkFxActiveFieldIsArrray = (fxActiveFieldsProperty) => {
// adding error handling mechanism for fxActiveFieldsProperty , if props.fxActiveFields is array , then return props.fxActiveFields or else return [], this will make sure, fxActiveFields wil always be array
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
index 5a2175cfe1..3aca83b046 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
@@ -177,7 +177,7 @@ class TableComponent extends React.Component {
style={{
width: '280px',
maxHeight: resolveReferences(column.isEditable) ? '100vh' : 'inherit',
- overflowY: 'auto',
+ // overflowY: 'auto',
zIndex: '9999',
}}
>
@@ -327,7 +327,12 @@ class TableComponent extends React.Component {
placement="left"
rootClose={this.state.actionPopOverRootClose}
overlay={this.actionPopOver(action, index)}
- onToggle={(showing) => this.setState({ showPopOver: showing })}
+ onToggle={(showing) => {
+ if (!showing) {
+ document.activeElement?.blur(); // Manually trigger blur when popover closes
+ }
+ this.setState({ showPopOver: showing });
+ }}
>
@@ -624,6 +629,8 @@ class TableComponent extends React.Component {
return 'Select';
case 'newMultiSelect':
return 'Multiselect';
+ case 'json':
+ return 'JSON';
default:
capitalize(text ?? '');
}
@@ -647,6 +654,7 @@ class TableComponent extends React.Component {
if (show) {
this.handleToggleColumnPopover(index);
} else {
+ document.activeElement?.blur(); // Manually trigger blur when popover closes
this.handleToggleColumnPopover(null);
}
}}
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/buttonGroup.js b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js
index c0fa889dd5..ab3eb40c2c 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js
@@ -127,6 +127,13 @@ export const buttonGroupConfig = {
exposedVariables: {
selected: [1],
},
+ actions: [
+ {
+ handle: 'setSelected',
+ displayName: 'Select option',
+ params: [{ handle: 'selected', displayName: 'Value' }],
+ },
+ ],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
index b4672c6afe..308aff1f36 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
@@ -311,7 +311,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
- value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select an option' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js
index 0e9f5f4ce3..2d8eb7f0a8 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js
@@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
- height: 330,
+ height: 480,
},
defaultChildren: [
+ {
+ componentName: 'Text',
+ slotName: 'header',
+ layout: {
+ top: 10,
+ left: 1,
+ height: 40,
+ },
+ properties: ['text'],
+ accessorKey: 'text',
+ styles: ['fontWeight', 'textSize', 'textColor'],
+ defaultValue: {
+ text: 'Form title',
+ textSize: 20,
+ textColor: '#000',
+ },
+ },
+ {
+ componentName: 'Button',
+ slotName: 'footer',
+ layout: {
+ top: 12,
+ left: 32,
+ height: 36,
+ },
+ properties: ['text'],
+ defaultValue: {
+ text: 'Button2',
+ padding: 'none',
+ },
+ },
{
componentName: 'Text',
layout: {
@@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
+ section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
+ showHeader: { type: 'toggle', displayName: 'Header' },
+ showFooter: { type: 'toggle', displayName: 'Footer' },
+ visibility: {
+ type: 'toggle',
+ displayName: 'Visibility',
+ section: 'additionalActions',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: true,
+ },
+ },
+ disabledState: {
+ type: 'toggle',
+ displayName: 'Disable',
+ section: 'additionalActions',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ },
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
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',
+ },
+ },
+ 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: 'color',
displayName: 'Background color',
@@ -274,26 +358,13 @@ export const formConfig = {
defaultValue: '#fff',
},
},
- visibility: {
- type: 'toggle',
- displayName: 'Visibility',
- validation: {
- schema: { type: 'boolean' },
- defaultValue: true,
- },
- },
- disabledState: {
- type: 'toggle',
- displayName: 'Disable',
- validation: {
- schema: { type: 'boolean' },
- defaultValue: false,
- },
- },
},
exposedVariables: {
data: {},
isValid: true,
+ isVisible: true,
+ isDisabled: false,
+ isLoading: false,
},
actions: [
{
@@ -304,6 +375,21 @@ export const formConfig = {
handle: 'resetForm',
displayName: 'Reset Form',
},
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisable',
+ displayName: 'Set Disable',
+ params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setLoading',
+ displayName: 'Set Loading',
+ params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }],
+ },
],
definition: {
others: {
@@ -317,15 +403,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'}}} }}",
},
- buttonToSubmit: { value: '{{"none"}}' },
+ showHeader: { value: '{{false}}' },
+ showFooter: { value: '{{false}}' },
+ visibility: { value: '{{true}}' },
+ disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
- visibility: { value: '{{true}}' },
- disabledState: { value: '{{false}}' },
+ headerHeight: { value: '60px' },
+ footerHeight: { value: '60px' },
},
},
};
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/listview.js b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
index a813bb5a0b..62b55a7fea 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
@@ -13,6 +13,7 @@ export const listviewConfig = {
top: 15,
left: 3,
height: 100,
+ width: 7,
},
properties: ['source'],
accessorKey: 'imageURL',
@@ -48,8 +49,12 @@ export const listviewConfig = {
data: {
type: 'code',
displayName: 'List data',
- validation: {
- schema: { type: 'array', element: { type: 'object' } },
+ schema: {
+ type: 'union',
+ schemas: [
+ { type: 'array', element: { type: 'object' } },
+ { type: 'array', element: { type: 'string' } },
+ ],
defaultValue: "[{text: 'Sample text 1'}]",
},
},
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/WidgetManager/widgets/tabs.js b/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js
index a397979a3e..0ed1e2a320 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js
@@ -13,6 +13,7 @@ export const tabsConfig = {
top: 60,
left: 17,
height: 100,
+ width: 7,
},
tab: 0,
properties: ['source'],
diff --git a/frontend/src/AppBuilder/Widgets/Container.jsx b/frontend/src/AppBuilder/Widgets/Container.jsx
deleted file mode 100644
index 3ccedd869b..0000000000
--- a/frontend/src/AppBuilder/Widgets/Container.jsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React, { useMemo } from 'react';
-import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container';
-import Spinner from '@/_ui/Spinner';
-import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
-
-export const Container = ({
- id,
- properties,
- styles,
- darkMode,
- height,
- width,
- setExposedVariables,
- setExposedVariable,
-}) => {
- const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles;
-
- const { isDisabled, isVisible, isLoading } = useExposeState(
- properties.loadingState,
- properties.visibility,
- properties.disabledState,
- setExposedVariables,
- setExposedVariable
- );
-
- const contentBgColor = useMemo(() => {
- return {
- backgroundColor:
- ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor,
- };
- }, [styles.backgroundColor, darkMode]);
-
- const headerBgColor = useMemo(() => {
- return {
- backgroundColor:
- ['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode
- ? '#232E3C'
- : styles.headerBackgroundColor,
- };
- }, [styles.headerBackgroundColor, darkMode]);
-
- const computedStyles = {
- backgroundColor: contentBgColor.backgroundColor,
- borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
- border: `1px solid ${borderColor}`,
- height,
- display: isVisible ? 'flex' : 'none',
- overflow: 'hidden auto',
- position: 'relative',
- boxShadow,
- };
-
- const computedHeaderStyles = {
- ...headerBgColor,
- height: `${headerHeight}px`,
- flexShrink: 0,
- flexGrow: 0,
- borderBottom: `1px solid var(--border-weak)`,
- };
-
- const computedContentStyles = {
- ...contentBgColor,
- flex: 1,
- overflow: 'auto',
- };
-
- return (
-
- {properties.showHeader && (
-
- )}
- {isLoading ? (
-
-
-
- ) : (
-
- )}
-
- );
-};
diff --git a/frontend/src/AppBuilder/Widgets/Container/Container.jsx b/frontend/src/AppBuilder/Widgets/Container/Container.jsx
new file mode 100644
index 0000000000..4978427370
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/Container/Container.jsx
@@ -0,0 +1,119 @@
+import React, { useMemo } from 'react';
+import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container';
+import Spinner from '@/_ui/Spinner';
+import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
+import { shallow } from 'zustand/shallow';
+import {
+ CONTAINER_FORM_CANVAS_PADDING,
+ SUBCONTAINER_CANVAS_BORDER_WIDTH,
+} from '@/AppBuilder/AppCanvas/appCanvasConstants';
+import useStore from '@/AppBuilder/_stores/store';
+import './container.scss';
+
+export const Container = ({
+ id,
+ properties,
+ styles,
+ darkMode,
+ height,
+ width,
+ setExposedVariables,
+ setExposedVariable,
+}) => {
+ const { isDisabled, isVisible, isLoading } = useExposeState(
+ properties.loadingState,
+ properties.visibility,
+ properties.disabledState,
+ setExposedVariables,
+ setExposedVariable
+ );
+
+ const isWidgetInContainerDragging = useStore(
+ (state) => state.containerChildrenMapping?.[id]?.includes(state?.draggingComponentId),
+ shallow
+ );
+
+ const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles;
+ const contentBgColor = useMemo(() => {
+ return {
+ backgroundColor:
+ ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor,
+ };
+ }, [styles.backgroundColor, darkMode]);
+
+ const headerBgColor = useMemo(() => {
+ return {
+ backgroundColor:
+ ['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode
+ ? '#232E3C'
+ : styles.headerBackgroundColor,
+ };
+ }, [styles.headerBackgroundColor, darkMode]);
+
+ const computedStyles = {
+ backgroundColor: contentBgColor.backgroundColor,
+ borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
+ border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`,
+ height,
+ display: isVisible ? 'flex' : 'none',
+ flexDirection: 'column',
+ position: 'relative',
+ boxShadow,
+ };
+
+ const containerHeaderStyles = {
+ flexShrink: 0,
+ padding: `${CONTAINER_FORM_CANVAS_PADDING}px ${CONTAINER_FORM_CANVAS_PADDING}px 3px ${CONTAINER_FORM_CANVAS_PADDING}px`,
+ ...headerBgColor,
+ };
+
+ const containerContentStyles = {
+ overflow: 'hidden auto',
+ display: 'flex',
+ height: '100%',
+ padding: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ };
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+ <>
+ {properties.showHeader && (
+
+
+
+ )}
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/Container/container.scss b/frontend/src/AppBuilder/Widgets/Container/container.scss
new file mode 100644
index 0000000000..323f5e8c9a
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/Container/container.scss
@@ -0,0 +1,13 @@
+.wj-container-header {
+ position: relative;
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: -7px;
+ right: -7px;
+ height: 1px;
+ background-color: var(--border-weak);
+ }
+ }
+
\ No newline at end of file
diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx
index 674d707f03..afeb4cf844 100644
--- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx
+++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx
@@ -1,17 +1,25 @@
-import React, { useRef, useState, useEffect, useMemo } from 'react';
+import React, { useRef, useState, useEffect } from 'react';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
// eslint-disable-next-line import/no-unresolved
-import { diff } from 'deep-object-diff';
import _, { debounce, omit } from 'lodash';
-import { Box } from '@/Editor/Box';
import { generateUIComponents } from './FormUtils';
import { useMounted } from '@/_hooks/use-mount';
import { onComponentClick, removeFunctionObjects } from '@/_helpers/appUtils';
-import { useAppInfo } from '@/_stores/appDataStore';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import RenderSchema from './RenderSchema';
import useStore from '@/AppBuilder/_stores/store';
+import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
import { shallow } from 'zustand/shallow';
+import {
+ CONTAINER_FORM_CANVAS_PADDING,
+ SUBCONTAINER_CANVAS_BORDER_WIDTH,
+} from '@/AppBuilder/AppCanvas/appCanvasConstants';
+import './form.scss';
+
+const getCanvasHeight = (height) => {
+ const parsedHeight = height.includes('px') ? parseInt(height, 10) : height;
+ return Math.ceil(parsedHeight);
+};
export const Form = function Form(props) {
const {
@@ -29,27 +37,71 @@ export const Form = function Form(props) {
dataCy,
} = props;
const childComponents = useStore((state) => state.getChildComponents(id), shallow);
- const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles;
- const { buttonToSubmit, loadingState, advanced, JSONSchema } = properties;
+ const {
+ borderRadius,
+ borderColor,
+ boxShadow,
+ headerHeight,
+ footerHeight,
+ footerBackgroundColor,
+ headerBackgroundColor,
+ } = styles;
+ const {
+ buttonToSubmit,
+ loadingState,
+ advanced,
+ JSONSchema,
+ showHeader = false,
+ showFooter = false,
+ visibility,
+ disabledState,
+ } = properties;
+ const { isDisabled, isVisible, isLoading } = useExposeState(
+ properties.loadingState,
+ properties.visibility,
+ properties.disabledState,
+ setExposedVariables,
+ setExposedVariable
+ );
const backgroundColor =
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor;
const computedStyles = {
backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
- border: `1px solid ${borderColor}`,
+ border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`,
height,
- display: visibility ? 'flex' : 'none',
+ display: isVisible ? 'flex' : 'none',
position: 'relative',
- overflow: 'hidden auto',
boxShadow,
+ flexDirection: 'column',
};
- const childIdNameMap = useMemo(() => {
- return Object.keys(childComponents).reduce((acc, id) => {
- const component = childComponents[id]?.component?.component;
- return { ...acc, [id]: component?.name };
- }, {});
- }, [childComponents]);
+ const formHeader = {
+ flexShrink: 0,
+ paddingBottom: '3px',
+ paddingTop: '7px',
+ paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ backgroundColor:
+ ['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
+ };
+
+ const formContent = {
+ overflow: 'hidden auto',
+ display: 'flex',
+ height: '100%',
+ paddingTop: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ paddingBottom: showFooter ? '3px' : '7px',
+ paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ };
+
+ const formFooter = {
+ flexShrink: 0,
+ padding: `${CONTAINER_FORM_CANVAS_PADDING}px`,
+ backgroundColor:
+ ['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
+ };
const parentRef = useRef(null);
const childDataRef = useRef({});
@@ -58,6 +110,8 @@ export const Form = function Form(props) {
const [isValid, setValidation] = useState(true);
const [uiComponents, setUIComponents] = useState([]);
const mounted = useMounted();
+ const canvasHeaderHeight = getCanvasHeight(headerHeight) / 10;
+ const canvasFooterHeight = getCanvasHeight(footerHeight) / 10;
useEffect(() => {
const exposedVariables = {
@@ -155,7 +209,7 @@ export const Form = function Form(props) {
};
setExposedVariables(exposedVariables);
setValidation(childValidation);
- }, [childrenData, advanced, JSON.stringify(childIdNameMap)]);
+ }, [childrenData, advanced]);
useEffect(() => {
document.addEventListener('submitForm', handleFormSubmission);
@@ -245,105 +299,113 @@ export const Form = function Form(props) {
if (e.target.className === 'real-canvas') onComponentClick(id, component);
}} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked
>
- {loadingState ? (
-
- ) : (
-
- {!advanced && (
-
-
- {/*
- */}
-
+ {showHeader && (
+
+
+ {isDisabled && (
+
diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx
index 5543ce4ee0..e0f099205f 100644
--- a/frontend/src/AppBuilder/Widgets/Modal.jsx
+++ b/frontend/src/AppBuilder/Widgets/Modal.jsx
@@ -49,6 +49,7 @@ export const Modal = function Modal({
const size = properties.size ?? 'lg';
const [modalWidth, setModalWidth] = useState();
const mode = useStore((state) => state.currentMode, shallow);
+ const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas);
/**** Start - Logic to reset the zIndex of modal control box ****/
useEffect(() => {
@@ -63,6 +64,7 @@ export const Modal = function Modal({
useGridStore.getState().actions.setOpenModalWidgetId(null);
}
}
+ setModalOpenOnCanvas(id, showModal);
}, [showModal, id, mode]);
/**** End - Logic to reset the zIndex of modal control box ****/
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 && (
+
);
diff --git a/frontend/src/AppBuilder/_helpers/editorHelpers.js b/frontend/src/AppBuilder/_helpers/editorHelpers.js
index 9abeb9a56b..5e817c7f7e 100644
--- a/frontend/src/AppBuilder/_helpers/editorHelpers.js
+++ b/frontend/src/AppBuilder/_helpers/editorHelpers.js
@@ -59,12 +59,13 @@ import { BoundedBox } from '@/Editor/Components/BoundedBox/BoundedBox';
import { isPDFSupported } from '@/_helpers/appUtils';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { useEditorStore } from '@/_stores/editorStore';
-import { Container } from '@/AppBuilder/Widgets/Container';
+import { Container } from '@/AppBuilder/Widgets/Container/Container';
import { Listview } from '@/AppBuilder/Widgets/Listview';
import { Tabs } from '@/AppBuilder/Widgets/Tabs';
import { Kanban } from '@/AppBuilder/Widgets/Kanban/Kanban';
import { Form } from '@/AppBuilder/Widgets/Form/Form';
import { Modal } from '@/AppBuilder/Widgets/Modal';
+import { ModalV2 } from '@/AppBuilder/Widgets/ModalV2/ModalV2';
import { Calendar } from '@/AppBuilder/Widgets/Calendar/Calendar';
// import './requestIdleCallbackPolyfill';
@@ -106,6 +107,7 @@ export const AllComponents = {
Multiselect,
MultiselectV2,
Modal,
+ ModalV2,
Chart,
Map: MapComponent,
QrScanner,
diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js
index a8fac45c48..1b664cfa21 100644
--- a/frontend/src/AppBuilder/_hooks/useAppData.js
+++ b/frontend/src/AppBuilder/_hooks/useAppData.js
@@ -329,6 +329,11 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
setCurrentPageHandle(startingPage.handle);
updateFeatureAccess();
setCurrentPageId(startingPage.id, moduleId);
+ setResolvedPageConstants({
+ id: startingPage?.id,
+ handle: startingPage?.handle,
+ name: startingPage?.name,
+ });
setComponentNameIdMapping(moduleId);
updateEventsField('events', appData.events);
setCurrentVersionId(appData.editing_version?.id || appData.current_version_id);
diff --git a/frontend/src/AppBuilder/_stores/slices/appSlice.js b/frontend/src/AppBuilder/_stores/slices/appSlice.js
index 0132b78d8b..39cc7a4986 100644
--- a/frontend/src/AppBuilder/_stores/slices/appSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/appSlice.js
@@ -7,6 +7,7 @@ import { getWorkspaceId } from '@/_helpers/utils';
import { navigate } from '@/AppBuilder/_utils/misc';
import queryString from 'query-string';
import { replaceEntityReferencesWithIds } from '../utils';
+import _ from 'lodash';
const initialState = {
app: {},
@@ -124,10 +125,14 @@ export const createAppSlice = (set, get) => ({
setComponentNameIdMapping('canvas');
setQueryMapping('canvas');
+ const isLicenseValid =
+ !_.get(license, 'featureAccess.licenseStatus.isExpired', true) &&
+ _.get(license, 'featureAccess.licenseStatus.isLicenseValid', false);
+
const appId = get().app.appId;
const filteredQueryParams = queryParams.filter(([key, value]) => {
if (!value) return false;
- if (key === 'env' && !license.isLicenseValid()) return false;
+ if (key === 'env' && isLicenseValid) return false;
return true;
});
diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
index 1f6ce18405..8f325dd614 100644
--- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
@@ -10,7 +10,11 @@ import {
import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { cloneDeep, merge, set as lodashSet } from 'lodash';
-import { computeComponentName, getAllChildComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils';
+import {
+ computeComponentName,
+ getAllChildComponents,
+ getParentWidgetFromId,
+} from '@/AppBuilder/AppCanvas/appCanvasUtils';
import { pageConfig } from '@/AppBuilder/RightSideBar/PageSettingsTab/pageConfig';
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
import { DEFAULT_COMPONENT_STRUCTURE } from './resolvedSlice';
@@ -40,6 +44,7 @@ const initialState = {
currentPageHandle: null,
showWidgetDeleteConfirmation: false,
focusedParentId: null,
+ modalsOpenOnCanvas: [],
};
export const createComponentsSlice = (set, get) => ({
@@ -502,7 +507,7 @@ export const createComponentsSlice = (set, get) => ({
const resolvedMandatory = getResolvedValue(mandatory, customResolveObjects) || false;
- if (resolvedMandatory == true && !widgetValue) {
+ if (resolvedMandatory == true && !widgetValue && widgetValue !== 0) {
return {
isValid: false,
validationError: `Field cannot be empty`,
@@ -761,7 +766,7 @@ export const createComponentsSlice = (set, get) => ({
const { getComponentTypeFromId } = get();
const transformedParentId = parentId?.length > 36 ? parentId.slice(0, 36) : parentId;
let parentType = getComponentTypeFromId(transformedParentId, moduleId);
- const parentWidget = parentType === 'Kanban' ? 'Kanban_card' : parentType;
+ const parentWidget = getParentWidgetFromId(parentType, parentId);
const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
if (!isParentChangeAllowed)
@@ -1742,7 +1747,10 @@ export const createComponentsSlice = (set, get) => ({
getCustomResolvableReference: (value, parentId, moduleId) => {
const { getParentComponentType } = get();
const parentComponentType = getParentComponentType(parentId, moduleId);
- if (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) {
+ if (
+ (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) ||
+ value === '{{listItem}}'
+ ) {
return { entityType: 'components', entityNameOrId: parentId, entityKey: 'listItem' };
} else if (
parentComponentType === 'Kanban' &&
@@ -1860,4 +1868,17 @@ export const createComponentsSlice = (set, get) => ({
const currentPage = getCurrentPage(moduleId);
return currentPage?.autoComputeLayout;
},
+ setModalOpenOnCanvas: (modalId, isOpen) => {
+ const { modalsOpenOnCanvas } = get();
+ let newModalOpenOnCanvas = [];
+
+ if (isOpen) {
+ newModalOpenOnCanvas = [...modalsOpenOnCanvas, modalId];
+ } else {
+ newModalOpenOnCanvas = modalsOpenOnCanvas.filter((id) => id !== modalId);
+ }
+ set((state) => {
+ state.modalsOpenOnCanvas = newModalOpenOnCanvas;
+ });
+ },
});
diff --git a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js
index 0d444272a3..1edd3994c5 100644
--- a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js
@@ -136,7 +136,6 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
updateVersionNameAction: async (appId, versionId, versionName, onSuccess, onFailure) => {
try {
await appVersionService.save(appId, versionId, { name: versionName });
- console.log('happening');
set((state) => {
if (state.selectedVersion && state.selectedVersion.id === versionId) {
@@ -177,7 +176,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
appVersionsLazyLoaded: false,
selectedEnvironment: response.editorEnvironment,
appVersionEnvironment: response.appVersionEnvironment,
- environments: response.environments,
+ environments: response?.environments?.length ? response.environments : get().environments,
};
if (state.selectedVersion?.id === versionId) {
@@ -241,7 +240,6 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
useStore.getState()?.license?.featureAccess
),
};
- console.log({ environment, get: get().appVersionEnvironment });
const versionIsAvailableInEnvironment = environment?.priority <= get().currentAppVersionEnvironment?.priority;
if (!versionIsAvailableInEnvironment) {
@@ -252,7 +250,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
});
selectedVersion = response.editorVersion;
const appVersionEnvironment = get().environments.find(
- (environment) => environment.id === selectedVersion.current_environment_id
+ (environment) => environment.id === selectedVersion.currentEnvironmentId
);
//TODO: need to check if this is needed
diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
index 948ac39b39..995050d023 100644
--- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
@@ -690,6 +690,12 @@ export const createEventsSlice = (set, get) => ({
return getVariable(key);
}
+ case 'unset-all-custom-variables': {
+ const { unsetAllVariables } = get();
+ unsetAllVariables();
+ return Promise.resolve();
+ }
+
case 'unset-custom-variable': {
const { unsetVariable } = get();
const key = getResolvedValue(event.key, customVariables);
@@ -746,6 +752,12 @@ export const createEventsSlice = (set, get) => ({
return getPageVariable(key);
}
+ case 'unset-all-page-variables': {
+ const { unsetAllPageVariables } = get();
+ unsetAllPageVariables();
+ return Promise.resolve();
+ }
+
case 'unset-page-variable': {
const { unsetPageVariable } = get();
const key = getResolvedValue(event.key, customVariables);
@@ -953,6 +965,13 @@ export const createEventsSlice = (set, get) => ({
}
};
+ const unsetAllVariables = () => {
+ const event = {
+ actionId: 'unset-all-custom-variables',
+ };
+ return executeAction(event, mode, {});
+ };
+
const unSetVariable = (key = '') => {
if (key) {
const event = {
@@ -1066,6 +1085,13 @@ export const createEventsSlice = (set, get) => ({
return executeAction(event, mode, {});
};
+ const unsetAllPageVariables = () => {
+ const event = {
+ actionId: 'unset-all-page-variables',
+ };
+ return executeAction(event, mode, {});
+ };
+
const unsetPageVariable = (key = '') => {
const event = {
actionId: 'unset-page-variable',
@@ -1133,6 +1159,7 @@ export const createEventsSlice = (set, get) => ({
runQuery,
setVariable,
getVariable,
+ unsetAllVariables,
unSetVariable,
showAlert,
logout,
@@ -1144,6 +1171,7 @@ export const createEventsSlice = (set, get) => ({
generateFile,
setPageVariable,
getPageVariable,
+ unsetAllPageVariables,
unsetPageVariable,
switchPage,
logInfo,
diff --git a/frontend/src/AppBuilder/_stores/slices/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js
index cc80a9dbf7..642266a32b 100644
--- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js
@@ -3,9 +3,11 @@ import { debounce } from 'lodash';
const initialState = {
hoveredComponentForGrid: '',
+ hoveredComponentBoundaryId: '',
triggerCanvasUpdater: false,
lastCanvasIdClick: '',
lastCanvasClickPosition: null,
+ draggingComponentId: null,
};
export const createGridSlice = (set, get) => ({
@@ -13,11 +15,14 @@ export const createGridSlice = (set, get) => ({
setHoveredComponentForGrid: (id) =>
set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }),
getHoveredComponentForGrid: () => get().hoveredComponentForGrid,
+ setHoveredComponentBoundaryId: (id) =>
+ set(() => ({ hoveredComponentBoundaryId: id }), false, { type: 'setHoveredComponentBoundaryId', id }),
toggleCanvasUpdater: () =>
set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }),
debouncedToggleCanvasUpdater: debounce(() => {
get().toggleCanvasUpdater();
}, 200),
+ setDraggingComponentId: (id) => set(() => ({ draggingComponentId: id })),
moveComponentPosition: (direction) => {
const { setComponentLayout, currentLayout, getSelectedComponentsDefinition, debouncedToggleCanvasUpdater } = get();
let layouts = {};
diff --git a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js
index 98decac629..367ca4cf0c 100644
--- a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js
@@ -37,4 +37,85 @@ export const createLeftSideBarSlice = (set, get) => ({
toggleLeftSidebar(true);
}
},
+ getComponentIdToAutoScroll: (componentId) => {
+ const { getCurrentPageComponents, getAllExposedValues, modalsOpenOnCanvas } = get();
+ const currentPageComponents = getCurrentPageComponents();
+
+ let targetComponentId = componentId;
+ let current = componentId;
+ const visited = new Set();
+ let isInsideOpenModal = false;
+
+ // Bubble up to the outermost parent to find the target component
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (visited.has(current)) break;
+ visited.add(current);
+
+ const parentId = currentPageComponents?.[current]?.component?.parent;
+ if (!parentId) break;
+
+ let isComponentVisibleInParent = true;
+ let nextPossibleCandidate = parentId;
+
+ // If the component exists inside a tab component
+ const regForTabs = /-(?!\d{12}$)\d+$/; // Parent id for tabs follow the format 'id-index' and index is not UUIDv4 id segment
+ if (regForTabs.test(parentId)) {
+ const reg = /-(\d+)$/;
+ const tabIndex = Number(parentId.match(reg)[1]); // Tab index inside which the component exists
+
+ const tabId = parentId.replace(regForTabs, ''); // Extract tab id from parent id
+
+ const { currentTab } = getAllExposedValues().components?.[tabId] || {};
+ const activeTabIndex = Number(currentTab);
+
+ nextPossibleCandidate = tabId;
+ if (tabIndex !== activeTabIndex) {
+ isComponentVisibleInParent = false;
+ }
+ }
+
+ // If the component exists inside a modal component
+ if (currentPageComponents?.[parentId]?.component?.component === 'Modal') {
+ nextPossibleCandidate = parentId;
+ if (!modalsOpenOnCanvas.includes(parentId)) {
+ isComponentVisibleInParent = false;
+ }
+ }
+
+ // If the component exists inside the kanban component's modal
+ if (parentId.endsWith('-modal')) {
+ nextPossibleCandidate = parentId.replace(/-modal$/, ''); // Extract kanban id from parent id
+ if (!modalsOpenOnCanvas.includes(parentId)) {
+ isComponentVisibleInParent = false;
+ }
+ }
+
+ // If the open modal contains the component
+ if (modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] === parentId) {
+ isInsideOpenModal = true;
+ }
+
+ if (!isComponentVisibleInParent) {
+ targetComponentId = nextPossibleCandidate;
+ }
+ current = nextPossibleCandidate;
+ }
+
+ if (modalsOpenOnCanvas.length > 0 && !isInsideOpenModal) {
+ const targetId = visited.size === 1 ? modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] : current;
+ const componentName = currentPageComponents?.[targetId]?.component?.name;
+
+ return {
+ isAccessible: false,
+ computedComponentId: componentName,
+ isOnCanvas: visited.size === 1,
+ };
+ }
+
+ return {
+ isAccessible: true,
+ computedComponentId: targetComponentId,
+ };
+ },
});
diff --git a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js
index ebbd524fd1..b76c8b072d 100644
--- a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js
@@ -140,6 +140,21 @@ export const createResolvedSlice = (set, get) => ({
get().updateDependencyValues(`variables.${key}`);
},
+ unsetAllVariables: (moduleId = 'canvas') => {
+ const variables = get().resolvedStore.modules[moduleId].exposedValues.variables;
+ set(
+ (state) => {
+ state.resolvedStore.modules[moduleId].exposedValues.variables = {};
+ },
+ false,
+ 'unsetAllVariables'
+ );
+ Object.keys(variables).forEach((key) => {
+ get().removeNode(`variables.${key}`);
+ get().updateDependencyValues(`variables.${key}`);
+ });
+ },
+
// page.variables
setPageVariable: (key, value, moduleId = 'canvas') => {
set(
@@ -167,6 +182,21 @@ export const createResolvedSlice = (set, get) => ({
get().updateDependencyValues(`page.variables.${key}`);
},
+ unsetAllPageVariables: (moduleId = 'canvas') => {
+ const pageVariables = get().resolvedStore.modules[moduleId].exposedValues.page.variables;
+ set(
+ (state) => {
+ state.resolvedStore.modules[moduleId].exposedValues.page.variables = {};
+ },
+ false,
+ 'unsetAllPageVariables'
+ );
+ Object.keys(pageVariables).forEach((key) => {
+ get().removeNode(`page.variables.${key}`);
+ get().updateDependencyValues(`page.variables.${key}`);
+ });
+ },
+
setResolvedQuery: (queryId, details, moduleId = 'canvas') => {
set(
(state) => {
diff --git a/frontend/src/AppBuilder/_stores/utils.js b/frontend/src/AppBuilder/_stores/utils.js
index bb08c620da..33e50eb9cc 100644
--- a/frontend/src/AppBuilder/_stores/utils.js
+++ b/frontend/src/AppBuilder/_stores/utils.js
@@ -473,6 +473,7 @@ export function createReferencesLookup(currentState, forQueryParams = false, ini
const actions = [
'runQuery',
'setVariable',
+ 'unsetAllVariables',
'unSetVariable',
'showAlert',
'logout',
@@ -483,6 +484,7 @@ export function createReferencesLookup(currentState, forQueryParams = false, ini
'goToApp',
'generateFile',
'setPageVariable',
+ 'unsetAllPageVariables',
'unsetPageVariable',
'switchPage',
'logInfo',
diff --git a/frontend/src/Editor/ActionTypes.js b/frontend/src/Editor/ActionTypes.js
index 01b0a34759..0bad71b3ac 100644
--- a/frontend/src/Editor/ActionTypes.js
+++ b/frontend/src/Editor/ActionTypes.js
@@ -78,6 +78,10 @@ export const ActionTypes = [
{ name: 'value', type: 'code', default: '' },
],
},
+ {
+ name: 'Unset all variables',
+ id: 'unset-all-custom-variables',
+ },
{
name: 'Unset variable',
id: 'unset-custom-variable',
@@ -96,6 +100,10 @@ export const ActionTypes = [
{ name: 'value', type: 'code', default: '' },
],
},
+ {
+ name: 'Unset all page variables',
+ id: 'unset-all-page-variables',
+ },
{
name: 'Unset page variable',
id: 'unset-page-variable',
@@ -104,6 +112,7 @@ export const ActionTypes = [
{ name: 'value', type: 'code', default: '' },
],
},
+
{
name: 'Control component',
id: 'control-component',
diff --git a/frontend/src/Editor/Components/ButtonGroup.jsx b/frontend/src/Editor/Components/ButtonGroup.jsx
index 7364348271..67618a61ab 100644
--- a/frontend/src/Editor/Components/ButtonGroup.jsx
+++ b/frontend/src/Editor/Components/ButtonGroup.jsx
@@ -66,6 +66,34 @@ export const ButtonGroup = function Button({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [multiSelection]);
+ const setSelected = (selected) => {
+ if (multiSelection) {
+ if (Array.isArray(selected)) {
+ const filteredItems = selected.filter((item) => values.includes(item));
+ setDefaultActive(filteredItems);
+ setExposedVariable('selected', filteredItems.join(','));
+ } else if ((typeof selected === 'string' || typeof selected === 'number') && values.includes(selected)) {
+ setDefaultActive([selected]);
+ setExposedVariable('selected', String(selected));
+ }
+ } else {
+ if (Array.isArray(selected)) {
+ const filteredItems = selected.filter((item) => values.includes(item));
+ if (filteredItems?.length >= 1) {
+ setDefaultActive([filteredItems[0]]);
+ setExposedVariable('selected', String(filteredItems[0]));
+ }
+ } else if ((typeof selected === 'string' || typeof selected === 'number') && values.includes(selected)) {
+ setDefaultActive([selected]);
+ setExposedVariable('selected', String(selected));
+ }
+ }
+ };
+
+ useEffect(() => {
+ setExposedVariable('setSelected', setSelected);
+ }, [multiSelection, values]);
+
const buttonClick = (index) => {
if (defaultActive?.includes(values[index]) && multiSelection) {
const copyDefaultActive = [...defaultActive];
diff --git a/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx b/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx
index 5bfe9818e5..4d4b7a2e48 100644
--- a/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx
+++ b/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx
@@ -42,7 +42,7 @@ export const CustomComponent = (props) => {
setCustomProps({ ...customPropRef.current, ...e.data.updatedObj });
} else if (e.data.message === 'RUN_QUERY') {
const options = {
- parameters: e.data.parameters,
+ parameters: JSON.parse(e.data.parameters),
queryName: e.data.queryName,
};
onEvent('onTrigger', [], options);
diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
index ce286d7fb8..216e8caa12 100644
--- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
+++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
@@ -61,7 +61,6 @@ export const DropdownV2 = ({
}) => {
const {
label,
- value,
advanced,
schema,
placeholder,
@@ -90,7 +89,7 @@ export const DropdownV2 = ({
padding,
} = styles;
const isInitialRender = useRef(true);
- const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value));
+ const [currentValue, setCurrentValue] = useState(() => findDefaultItem(schema));
const isMandatory = validation?.mandatory ?? false;
const options = properties?.options;
const [validationStatus, setValidationStatus] = useState(validate(currentValue));
@@ -170,18 +169,9 @@ export const DropdownV2 = ({
};
useEffect(() => {
- if (advanced) {
- setInputValue(findDefaultItem(schema));
- }
+ setInputValue(findDefaultItem(advanced ? schema : options));
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [advanced, JSON.stringify(schema)]);
-
- useEffect(() => {
- if (!advanced) {
- setInputValue(value);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [advanced, value]);
+ }, [advanced, JSON.stringify(schema), JSON.stringify(options)]);
useEffect(() => {
if (visibility !== properties.visibility) setVisibility(properties.visibility);
@@ -448,6 +438,7 @@ export const DropdownV2 = ({
onChange={(selectedOption, actionProps) => {
if (actionProps.action === 'clear') {
setInputValue(null);
+ fireEvent('onSelect');
}
if (actionProps.action === 'select-option') {
setInputValue(selectedOption.value);
diff --git a/frontend/src/Editor/Components/TextInput.jsx b/frontend/src/Editor/Components/TextInput.jsx
index 29fbaaa625..ce482947a9 100644
--- a/frontend/src/Editor/Components/TextInput.jsx
+++ b/frontend/src/Editor/Components/TextInput.jsx
@@ -236,8 +236,6 @@ export const TextInput = function TextInput({
value: properties.value,
isMandatory: isMandatory,
isLoading: loading,
- isVisible: visibility,
- isDisabled: disable,
};
setExposedVariables(exposedVariables);
@@ -245,6 +243,17 @@ export const TextInput = function TextInput({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ useEffect(() => {
+ // Fix for "visibility is not defined" in production because there's a naming conflict in the code.
+ // The issue is in the exposedVariables object where we had both a function named visibility and a property isVisible that depends on the state variable with the same name.
+ const exposedVariables = {
+ isVisible: visibility,
+ isDisabled: disable,
+ };
+ setExposedVariables(exposedVariables);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const setInputValue = (value) => {
setValue(value);
setExposedVariable('value', value);
diff --git a/frontend/src/Editor/ControlledComponentToRender.jsx b/frontend/src/Editor/ControlledComponentToRender.jsx
index 4765d0d69f..54a451188b 100644
--- a/frontend/src/Editor/ControlledComponentToRender.jsx
+++ b/frontend/src/Editor/ControlledComponentToRender.jsx
@@ -10,7 +10,7 @@ function deepEqualityCheckusingLoDash(obj1, obj2) {
export const shouldUpdate = (prevProps, nextProps) => {
const listToRender = getComponentsToRenders();
// evaluate change in exposedVariables only for Modal component, because open/close in modal relies on exposedVariables
- const compareExposedVariables = nextProps.componentName === 'Modal';
+ const compareExposedVariables = nextProps.componentName === 'Modal' || nextProps.componentName === 'ModalV2';
let needToRender = false;
diff --git a/frontend/src/Editor/DraggableBox.jsx b/frontend/src/Editor/DraggableBox.jsx
index ae2740bc84..3cdad272b7 100644
--- a/frontend/src/Editor/DraggableBox.jsx
+++ b/frontend/src/Editor/DraggableBox.jsx
@@ -10,7 +10,7 @@ import { resolveWidgetFieldValue } from '@/_helpers/utils';
import ErrorBoundary from './ErrorBoundary';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
-import { useNoOfGrid, useGridStore } from '@/_stores/gridStore';
+import { useGridStore } from '@/_stores/gridStore';
import WidgetBox from './WidgetBox';
import * as Sentry from '@sentry/react';
import { findHighestLevelofSelection } from './DragContainer';
@@ -61,7 +61,7 @@ const DraggableBox = React.memo(
}) => {
const isResizing = useGridStore((state) => state.resizingComponentId === id);
const [canDrag, setCanDrag] = useState(true);
- const noOfGrid = useNoOfGrid();
+ const noOfGrid = 43;
const {
currentLayout,
setHoveredComponent,
diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx
index 66548cbef8..aba2ca6af1 100644
--- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx
+++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx
@@ -5,6 +5,7 @@ import JSONTreeViewer from '@/_ui/JSONTreeViewer';
import cx from 'classnames';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import useStore from '@/AppBuilder/_stores/store';
+import { toast } from 'react-hot-toast';
function Logs({ logProps, idx }) {
const [open, setOpen] = React.useState(false);
@@ -52,10 +53,19 @@ function Logs({ logProps, idx }) {
}
};
+ const copyToClipboard = (data) => {
+ const stringified = JSON.stringify(data, null, 2).replace(/\\/g, '');
+ navigator.clipboard.writeText(stringified);
+ return toast.success('Value copied to clipboard', { position: 'top-center' });
+ };
+
const callbackActions = [
{
for: 'all',
- actions: [{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }],
+ actions: [
+ { name: 'Copy value', dispatchAction: copyToClipboard, icon: false },
+ { name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true },
+ ],
enableForAllChildren: true,
enableFor1stLevelChildren: true,
},
diff --git a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx
index 7e8865cdfe..d1afd378f4 100644
--- a/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx
+++ b/frontend/src/Editor/QueryManager/Components/DataSourceSelect.jsx
@@ -7,7 +7,6 @@ import { getWorkspaceId, decodeEntities } from '@/_helpers/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
-import { staticDataSources as staticDatasources } from '../constants';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import Search from '@/_ui/Icon/solidIcons/Search';
import { Tooltip } from 'react-tooltip';
@@ -16,7 +15,7 @@ import { canCreateDataSource } from '@/_helpers';
import './../queryManager.theme.scss';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
-function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, defaultDataSources }) {
+function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, staticDataSources }) {
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const sampleDataSource = useSampleDataSource();
@@ -33,11 +32,6 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
closePopup();
};
- const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
- const staticDataSources = workflowsEnabled
- ? staticDatasources
- : staticDatasources.filter((ds) => ds?.kind !== 'workflows');
-
useEffect(() => {
const shouldAddSampleDataSource = !!sampleDataSource;
const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter(
@@ -148,7 +142,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
),
isDisabled: true,
- options: defaultDataSources?.map((source) => ({
+ options: staticDataSources?.map((source) => ({
label: (
{' '}
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx
index 40818a3bb9..7a4a0b0bce 100644
--- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx
+++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx
@@ -163,7 +163,7 @@ export const DateTimePicker = ({
Save Changes
-
+
Esc
Discard Changes
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
index 95d2ab56e0..55d0e7f3ed 100644
--- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
+++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
@@ -214,4 +214,24 @@
.input-value-padding {
box-sizing: border-box;
padding-right: 30px !important;
+}
+
+.datepicker-widget.theme-tjdb{
+ .react-datepicker__navigation{
+ overflow: visible !important;
+ height: inherit !important;
+ }
+}
+
+.esc-btn-datepicker{
+ height: 18px ;
+ align-items: center;
+}
+
+.tjdb-td-wrapper{
+ .react-datepicker-time__input{
+ input{
+ line-height: normal !important;
+ }
+ }
}
\ No newline at end of file
diff --git a/frontend/src/Editor/SubContainer.jsx b/frontend/src/Editor/SubContainer.jsx
index 01335af55a..fc78875819 100644
--- a/frontend/src/Editor/SubContainer.jsx
+++ b/frontend/src/Editor/SubContainer.jsx
@@ -23,7 +23,7 @@ import { useEditorStore } from '@/_stores/editorStore';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
-import { useGridStore, useResizingComponentId } from '@/_stores/gridStore';
+import { useGridStore } from '@/_stores/gridStore';
import GhostWidget from './GhostWidget';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
@@ -68,7 +68,7 @@ export const SubContainer = ({
shallow
);
- const resizingComponentId = useResizingComponentId();
+ const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
const noOfGrids = 43;
const { isGridActive } = useGridStore((state) => ({ isGridActive: state.activeGrid === parent }), shallow);
diff --git a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js
index b4672c6afe..308aff1f36 100644
--- a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js
+++ b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js
@@ -311,7 +311,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
- value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select an option' },
diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js
index ac82fcb171..2d8eb7f0a8 100644
--- a/frontend/src/Editor/WidgetManager/configs/form.js
+++ b/frontend/src/Editor/WidgetManager/configs/form.js
@@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
- height: 330,
+ height: 480,
},
defaultChildren: [
+ {
+ componentName: 'Text',
+ slotName: 'header',
+ layout: {
+ top: 10,
+ left: 1,
+ height: 40,
+ },
+ properties: ['text'],
+ accessorKey: 'text',
+ styles: ['fontWeight', 'textSize', 'textColor'],
+ defaultValue: {
+ text: 'Form title',
+ textSize: 20,
+ textColor: '#000',
+ },
+ },
+ {
+ componentName: 'Button',
+ slotName: 'footer',
+ layout: {
+ top: 12,
+ left: 32,
+ height: 36,
+ },
+ properties: ['text'],
+ defaultValue: {
+ text: 'Button2',
+ padding: 'none',
+ },
+ },
{
componentName: 'Text',
layout: {
@@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
+ section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
+ showHeader: { type: 'toggle', displayName: 'Header' },
+ showFooter: { type: 'toggle', displayName: 'Footer' },
+ visibility: {
+ type: 'toggle',
+ displayName: 'Visibility',
+ section: 'additionalActions',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: true,
+ },
+ },
+ disabledState: {
+ type: 'toggle',
+ displayName: 'Disable',
+ section: 'additionalActions',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ },
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
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',
+ },
+ },
+ 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: 'color',
displayName: 'Background color',
@@ -274,26 +358,13 @@ export const formConfig = {
defaultValue: '#fff',
},
},
- visibility: {
- type: 'toggle',
- displayName: 'Visibility',
- validation: {
- schema: { type: 'boolean' },
- defaultValue: true,
- },
- },
- disabledState: {
- type: 'toggle',
- displayName: 'Disable',
- validation: {
- schema: { type: 'boolean' },
- defaultValue: false,
- },
- },
},
exposedVariables: {
data: {},
isValid: true,
+ isVisible: true,
+ isDisabled: false,
+ isLoading: false,
},
actions: [
{
@@ -304,6 +375,21 @@ export const formConfig = {
handle: 'resetForm',
displayName: 'Reset Form',
},
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisable',
+ displayName: 'Set Disable',
+ params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setLoading',
+ displayName: 'Set Loading',
+ params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }],
+ },
],
definition: {
others: {
@@ -317,14 +403,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}}' },
+ visibility: { value: '{{true}}' },
+ disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
- visibility: { value: '{{true}}' },
- disabledState: { value: '{{false}}' },
+ headerHeight: { value: '60px' },
+ footerHeight: { value: '60px' },
},
},
};
diff --git a/frontend/src/Editor/WidgetManager/configs/index.js b/frontend/src/Editor/WidgetManager/configs/index.js
index d73cd8934f..93e45fd06c 100644
--- a/frontend/src/Editor/WidgetManager/configs/index.js
+++ b/frontend/src/Editor/WidgetManager/configs/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';
@@ -59,7 +60,8 @@ export {
buttonConfig,
tableConfig,
chartConfig,
- modalConfig,
+ modalConfig, //!Depreciated
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
diff --git a/frontend/src/Editor/WidgetManager/configs/listview.js b/frontend/src/Editor/WidgetManager/configs/listview.js
index a813bb5a0b..86825142eb 100644
--- a/frontend/src/Editor/WidgetManager/configs/listview.js
+++ b/frontend/src/Editor/WidgetManager/configs/listview.js
@@ -13,6 +13,7 @@ export const listviewConfig = {
top: 15,
left: 3,
height: 100,
+ width: 7,
},
properties: ['source'],
accessorKey: 'imageURL',
@@ -49,7 +50,13 @@ export const listviewConfig = {
type: 'code',
displayName: 'List data',
validation: {
- schema: { type: 'array', element: { type: 'object' } },
+ schema: {
+ type: 'union',
+ schemas: [
+ { type: 'array', element: { type: 'object' } },
+ { type: 'array', element: { type: 'string' } },
+ ],
+ },
defaultValue: "[{text: 'Sample text 1'}]",
},
},
diff --git a/frontend/src/Editor/WidgetManager/configs/modal.js b/frontend/src/Editor/WidgetManager/configs/modal.js
index 60f791831e..7716ac8e2c 100644
--- a/frontend/src/Editor/WidgetManager/configs/modal.js
+++ b/frontend/src/Editor/WidgetManager/configs/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/Editor/WidgetManager/configs/modalV2.js b/frontend/src/Editor/WidgetManager/configs/modalV2.js
new file mode 100644
index 0000000000..e7e96c4398
--- /dev/null
+++ b/frontend/src/Editor/WidgetManager/configs/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/Editor/WidgetManager/configs/tabs.js b/frontend/src/Editor/WidgetManager/configs/tabs.js
index a397979a3e..0ed1e2a320 100644
--- a/frontend/src/Editor/WidgetManager/configs/tabs.js
+++ b/frontend/src/Editor/WidgetManager/configs/tabs.js
@@ -13,6 +13,7 @@ export const tabsConfig = {
top: 60,
left: 17,
height: 100,
+ width: 7,
},
tab: 0,
properties: ['source'],
diff --git a/frontend/src/Editor/WidgetManager/constants.js b/frontend/src/Editor/WidgetManager/constants.js
index 35ea9d5252..8c593455e1 100644
--- a/frontend/src/Editor/WidgetManager/constants.js
+++ b/frontend/src/Editor/WidgetManager/constants.js
@@ -1 +1,7 @@
-export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy', 'RadioButtonLegacy'];
+export const LEGACY_ITEMS = [
+ 'ToggleSwitchLegacy',
+ 'DropdownLegacy',
+ 'MultiselectLegacy',
+ 'RadioButtonLegacy',
+ 'ModalLegacy',
+];
diff --git a/frontend/src/Editor/WidgetManager/widgetConfig.js b/frontend/src/Editor/WidgetManager/widgetConfig.js
index 30bfdfc7f2..2b03f5c3f5 100644
--- a/frontend/src/Editor/WidgetManager/widgetConfig.js
+++ b/frontend/src/Editor/WidgetManager/widgetConfig.js
@@ -3,6 +3,7 @@ import {
tableConfig,
chartConfig,
modalConfig,
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
@@ -62,6 +63,7 @@ export const widgets = [
buttonConfig,
chartConfig,
modalConfig,
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
diff --git a/frontend/src/TooljetDatabase/Filter/index.jsx b/frontend/src/TooljetDatabase/Filter/index.jsx
index 84b0110fe4..6c8f7c811d 100644
--- a/frontend/src/TooljetDatabase/Filter/index.jsx
+++ b/frontend/src/TooljetDatabase/Filter/index.jsx
@@ -251,11 +251,7 @@ const Filter = ({
}
/>
- {filterCount > 0 ? (
-
{pluralize(validFilterCountRef.current, 'filter')}
- ) : (
-
Filter
- )}
+ {filterCount > 0 ?
{pluralize(filterCount, 'filter')} :
Filter
}
{/* {areFiltersApplied && (
ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')}
diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx
index 141bd5c927..0f5db30e9b 100644
--- a/frontend/src/_components/DynamicForm.jsx
+++ b/frontend/src/_components/DynamicForm.jsx
@@ -572,6 +572,7 @@ const DynamicForm = ({
'd-flex': isHorizontalLayout,
'dynamic-form-row': isHorizontalLayout,
})}
+ data-cy={`${key.replace(/_/g, '-')}-section`}
key={key}
>
{!isSpecificComponent && (
@@ -628,6 +629,7 @@ const DynamicForm = ({
{...getElementProps(obj[key])}
{...computedProps[propertyKey]}
data-cy={`${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}-text-field`}
+ dataCy={obj[key].key.replace(/_/g, '-')}
//to be removed after whole ui is same
isHorizontalLayout={isHorizontalLayout}
/>
@@ -669,7 +671,7 @@ const DynamicForm = ({
)}
-
+
-
{label}
+
{label}
handleEncryptedFieldsToggle(name)}
+ data-cy={`button-${(disabled ? 'Edit' : 'Cancel').toLowerCase().replace(/\s+/g, '-')}`}
>
{disabled ? 'Edit' : 'Cancel'}
diff --git a/frontend/src/_components/OverflowTooltip.jsx b/frontend/src/_components/OverflowTooltip.jsx
index 297f17d236..1523ae449a 100644
--- a/frontend/src/_components/OverflowTooltip.jsx
+++ b/frontend/src/_components/OverflowTooltip.jsx
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { ToolTip } from '@/_components';
-export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', ...rest }) {
+export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', placement = 'bottom', ...rest }) {
const [isOverflowed, setIsOverflow] = useState(false);
const textElementRef = useRef();
@@ -17,7 +17,7 @@ export default function OverflowTooltip({ children, className, whiteSpace = 'now
className={className}
delay={{ show: '0', hide: '0' }}
tooltipClassName="overflow-tooltip"
- placement="bottom"
+ placement={placement}
message={children}
show={isOverflowed}
width={rest?.width}
diff --git a/frontend/src/_helpers/authorizeWorkspace.js b/frontend/src/_helpers/authorizeWorkspace.js
index 540d4f9b73..a03d7b986d 100644
--- a/frontend/src/_helpers/authorizeWorkspace.js
+++ b/frontend/src/_helpers/authorizeWorkspace.js
@@ -226,7 +226,7 @@ export const authorizeUserAndHandleErrors = (workspace_id, workspace_slug, callb
const unauthorized_organization_slug = workspace_slug;
/* get current session's workspace id */
- authenticationService
+ sessionService
.validateSession()
.then(({ current_organization_id, ...restSessionData }) => {
/* change current organization id to valid one [current logged in organization] */
diff --git a/frontend/src/_helpers/constants.js b/frontend/src/_helpers/constants.js
index 1c3c3d48b6..79275fc6df 100644
--- a/frontend/src/_helpers/constants.js
+++ b/frontend/src/_helpers/constants.js
@@ -129,6 +129,7 @@ export const DATA_SOURCE_TYPE = {
LOCAL: 'local',
GLOBAL: 'global',
STATIC: 'static',
+ DEFAULT: 'default',
};
export const SAMPLE_DB_KIND = {
diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js
index 07f0b6dccb..7373260691 100644
--- a/frontend/src/_helpers/utils.js
+++ b/frontend/src/_helpers/utils.js
@@ -242,18 +242,20 @@ export function resolveReferences(
} else {
const dynamicVariables = getDynamicVariables(object);
- for (const dynamicVariable of dynamicVariables) {
- const value = resolveString(
- dynamicVariable,
- state,
- customObjects,
- reservedKeyword,
- withError,
- forPreviewBox
- );
+ if (dynamicVariables) {
+ for (const dynamicVariable of dynamicVariables) {
+ const value = resolveString(
+ dynamicVariable,
+ state,
+ customObjects,
+ reservedKeyword,
+ withError,
+ forPreviewBox
+ );
- if (typeof value !== 'function') {
- object = object.replace(dynamicVariable, value);
+ if (typeof value !== 'function') {
+ object = object.replace(dynamicVariable, value);
+ }
}
}
}
diff --git a/frontend/src/_services/custom_styles.service.js b/frontend/src/_services/custom_styles.service.js
index d3c98d6382..73de321466 100644
--- a/frontend/src/_services/custom_styles.service.js
+++ b/frontend/src/_services/custom_styles.service.js
@@ -3,13 +3,13 @@ import { authHeader, handleResponse, handleResponseWithoutValidation } from '@/_
function save(body) {
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
- return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleResponse);
+ return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleResponse);
}
function get(validateResponse = true) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
const handleOutput = validateResponse ? handleResponse : handleResponseWithoutValidation;
- return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleOutput);
+ return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleOutput);
}
function getForAppViewerEditor(validateResponse = true) {
diff --git a/frontend/src/_services/organization_constants.service.js b/frontend/src/_services/organization_constants.service.js
index 0ab57eb776..cd85a6fe15 100644
--- a/frontend/src/_services/organization_constants.service.js
+++ b/frontend/src/_services/organization_constants.service.js
@@ -15,7 +15,7 @@ export const orgEnvironmentConstantService = {
function getAll(type = null) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
const queryParams = type ? `?type=${type}` : '';
- return fetch(`${config.apiUrl}/organization-constants${queryParams}`, requestOptions).then(handleResponse);
+ return fetch(`${config.apiUrl}/organization-constants/decrypted${queryParams}`, requestOptions).then(handleResponse);
}
function create(name, value, type, environments) {
diff --git a/frontend/src/_stores/gridStore.js b/frontend/src/_stores/gridStore.js
index aa070e4a89..213f07ac16 100644
--- a/frontend/src/_stores/gridStore.js
+++ b/frontend/src/_stores/gridStore.js
@@ -7,7 +7,6 @@ const initialState = {
noOfGrid: 43,
draggedSubContainer: false,
resizingComponentId: null,
- draggingComponentId: null,
dragTarget: null,
isGroupHandleHoverd: false,
idGroupDragged: false,
@@ -20,11 +19,7 @@ export const useGridStore = create(
(set) => ({
...initialState,
actions: {
- setActiveGrid: (gridId) => set({ activeGrid: gridId }),
- setNoOfGrid: (noOfGrid) => set({ noOfGrid }),
- setDraggedSubContainer: (draggedSubContainer) => set({ draggedSubContainer }),
setResizingComponentId: (id) => set({ resizingComponentId: id }),
- setDraggingComponentId: (id) => set({ draggingComponentId: id }),
setDragTarget: (dragTarget) => set({ dragTarget }),
setIsGroupHandleHoverd: (isGroupHandleHoverd) => set({ isGroupHandleHoverd }),
setIdGroupDragged: (idGroupDragged) => set({ idGroupDragged }),
@@ -46,21 +41,5 @@ useGridStore.subscribe(({ draggingComponentId }) => {
}
});
-// useEditorStore.subscribe(({ hoveredComponent }) => {
-// console.log('hoveredComponent--', hoveredComponent);
-// if (hoveredComponent) {
-// document.querySelector(`[data-hovered-control]`)?.removeAttribute('data-hovered-control');
-// document.querySelector(`[target-id='${hoveredComponent}']`)?.setAttribute('data-hovered-control', true);
-// } else if (document.querySelector(`[data-hovered-control]`)) {
-// document.querySelector(`[data-hovered-control]`)?.removeAttribute('data-hovered-control');
-// }
-// });
-
-export const useActiveGrid = () => useGridStore((state) => state.activeGrid, shallow);
-export const useNoOfGrid = () => useGridStore((state) => state.noOfGrid, shallow);
-export const useDraggedSubContainer = () => useGridStore((state) => state.draggedSubContainer, shallow);
-export const useGridStoreActions = () => useGridStore((state) => state.actions, shallow);
-export const useDragTarget = () => useGridStore((state) => state.dragTarget, shallow);
-export const useResizingComponentId = () => useGridStore((state) => state.resizingComponentId, shallow);
export const useIsGroupHandleHoverd = () => useGridStore((state) => state.isGroupHandleHoverd, shallow);
export const useOpenModalWidgetId = () => useGridStore((state) => state.openModalWidgetId, shallow);
diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss
index 5f742b2153..e84756dca7 100644
--- a/frontend/src/_styles/components.scss
+++ b/frontend/src/_styles/components.scss
@@ -493,4 +493,13 @@ $btn-dark-color: #FFFFFF;
}
}
}
+}
+
+//[Container-widget]Show scrollbar only on hover
+.widget-type-container {
+ overflow: hidden auto;
+ scrollbar-width: none;
+ &:hover {
+ scrollbar-width: auto;
+ }
}
\ No newline at end of file
diff --git a/frontend/src/_styles/left-sidebar.scss b/frontend/src/_styles/left-sidebar.scss
index 3f68e1486b..82b6762b54 100644
--- a/frontend/src/_styles/left-sidebar.scss
+++ b/frontend/src/_styles/left-sidebar.scss
@@ -701,6 +701,7 @@
.sidebar-h-100-popover {
position: relative;
height: 100vh;
+ overflow-y:scroll !important;
margin-top: 0px;
border-radius: 0px !important;
diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss
index 8a9eaf972a..1c63a111e9 100644
--- a/frontend/src/_styles/queryManager.scss
+++ b/frontend/src/_styles/queryManager.scss
@@ -1250,6 +1250,11 @@ $border-radius: 4px;
color: var(--slate12) !important;
}
}
+ &.data-source-exists {
+ .cm-editor {
+ border-radius: 0 4px 4px 0 !important;
+ }
+ }
}
.rest-api-methods-select-element-container {
@@ -1851,8 +1856,9 @@ $border-radius: 4px;
.tjdb-codhinter-wrapper{
.codehinter-input{
.cm-editor{
- height: 30px !important;
+ // height: 30px !important;
min-height: 30px !important;
+ max-height:100px !important;
border-radius: 0 !important;
border-right: 0 ;
}
@@ -1860,8 +1866,9 @@ $border-radius: 4px;
}
.tjdb-limit-offset-codehinter{
.cm-editor{
- height: 30px !important;
+ // height: 30px !important;
min-height: 30px !important;
+ max-height:100px !important;
}
}
@@ -1899,4 +1906,19 @@ $border-radius: 4px;
line-height: 18px;
}
}
+}
+
+
+.qm-delete-btn {
+ min-height: 30px;
+ height: 100% !important;
+ align-items: flex-start !important;
+ padding-top: 6px;
+}
+
+.restapi-key-value {
+ .code-hinter-wrapper, .code-editor-basic-wrapper, .codehinter-container, .cm-codehinter, .code-editor-query-panel{
+ height:100%;
+ max-height: 100px;
+ }
}
\ No newline at end of file
diff --git a/frontend/src/_styles/table-component.scss b/frontend/src/_styles/table-component.scss
index 5e0edd5aff..82f6b3de71 100644
--- a/frontend/src/_styles/table-component.scss
+++ b/frontend/src/_styles/table-component.scss
@@ -1467,5 +1467,59 @@
}
.tj-table-tag-col-readonly {
- margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
-}
\ No newline at end of file
+ margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
+}
+
+.jet-data-table {
+ .table-bordered {
+ th,
+ td {
+ border-bottom: 1px solid var(--interactive-overlay-border-pressed) !important;
+ border-right: 1px solid var(--interactive-overlay-border-pressed) !important;
+
+ &:first-child {
+ border-left: none !important;
+ }
+
+ &:last-child {
+ border-right: none !important;
+ }
+ }
+
+ thead th {
+ border-top: none !important;
+
+ &:first-child {
+ border-left: none !important;
+ }
+
+ &:last-child {
+ border-right: none !important;
+ }
+ }
+ }
+
+ .table-striped {
+ tbody {
+ div[data-index]:nth-child(odd) {
+ background-color: transparent !important;
+ }
+
+ div[data-index]:nth-child(even) {
+ background-color: var(--slate2) !important;
+ }
+ }
+ }
+}
+
+@media (hover: none) and (pointer: coarse) {
+ .jet-data-table {
+ overflow: auto;
+ }
+ // hide scrollbar on touch devices
+ .jet-data-table::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ background: transparent;
+ }
+}
diff --git a/frontend/src/_styles/tabler.scss b/frontend/src/_styles/tabler.scss
index 192915bec9..6ea0021701 100644
--- a/frontend/src/_styles/tabler.scss
+++ b/frontend/src/_styles/tabler.scss
@@ -5277,7 +5277,8 @@ fieldset:disabled .btn {
width: 100%;
height: 100%;
overflow: hidden;
- outline: 0
+ outline: 0;
+ padding-left: 0 !important;
}
.modal-dialog {
@@ -5311,7 +5312,7 @@ fieldset:disabled .btn {
}
.modal-dialog-scrollable .modal-content {
- max-height: 100%;
+ max-height: 88%;
overflow: hidden
}
@@ -5447,6 +5448,10 @@ fieldset:disabled .btn {
margin: 0
}
+.real-canvas .modal-dialog.modal-fullscreen {
+ width: 100%;
+}
+
.modal-fullscreen .modal-content {
height: 100%;
border: 0;
@@ -5465,6 +5470,12 @@ fieldset:disabled .btn {
border-radius: 0
}
+.modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
+ // Modal header height
+ padding-bottom: 56px;
+ max-height: 100%;
+}
+
@media (max-width:575.98px) {
.modal-fullscreen-sm-down {
width: 100vw;
@@ -19129,4 +19140,4 @@ img {
background: #1f2936;
border-color: #dadcde
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index 2d1f28be3d..6c9346724d 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -628,7 +628,6 @@ button {
.inspector {
- padding: 0px !important;
.form-control-plaintext {
padding: 0;
color: var(--slate12);
@@ -1544,7 +1543,7 @@ button {
.tab-content {
overflow-y: auto;
// TAB HEADER HEIGHT + FOOTER HEIGHT + Extra padding = 120px
- height: calc(100vh - 7.5rem);
+ height: calc(100vh - 10.4rem);
// Hide scrollbar
-ms-overflow-style: none;
/* IE and Edge */
@@ -2665,10 +2664,18 @@ hr {
overflow-y: initial !important
}
- .modal-dialog-scrollable .modal-content {
+ .modal-dialog-scrollable:not(.modal-fullscreen) .modal-content {
max-height: 88% !important;
}
+ .modal-dialog-scrollable.modal-fullscreen .modal-content {
+ max-height: 100% !important;
+ }
+
+ .modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
+ // Modal header height
+ padding-bottom: 0;
+ }
}
@@ -14022,8 +14029,8 @@ tbody {
}
/*
-* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
-* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
+* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
+* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
*/
.tj-dark-mode {
background-color: var(--base) !important;
@@ -15816,6 +15823,7 @@ tbody {
}
.rest-api-options-codehinter {
+ height: 100%;
.cm-content>.cm-line {
// max-width: 357px !important;
}
@@ -18569,6 +18577,12 @@ section.ai-message-prompt-input-wrapper {
flex-grow: 1;
}
}
+
+
+.cm-tooltip {
+ z-index: 9999 !important;
+}
+
.workspace-constant-value {
position: relative;
@@ -18616,4 +18630,11 @@ section.ai-message-prompt-input-wrapper {
background: #FFFAEB !important;
}
}
+}
+
+
+.single-line-codehinter-input {
+ .cm-editor {
+ max-height: 100px !important;
+ }
}
\ No newline at end of file
diff --git a/frontend/src/_ui/Accordion/AccordionItem.js b/frontend/src/_ui/Accordion/AccordionItem.js
index 22fe54512d..79fd86a41f 100644
--- a/frontend/src/_ui/Accordion/AccordionItem.js
+++ b/frontend/src/_ui/Accordion/AccordionItem.js
@@ -1,5 +1,5 @@
-import React, { useEffect } from 'react';
-import cx from 'classnames';
+import React, { useEffect } from "react";
+import cx from "classnames";
const AccordionItem = ({ open = true, index, title, children }) => {
const [show, setShow] = React.useState(open);
@@ -13,7 +13,7 @@ const AccordionItem = ({ open = true, index, title, children }) => {
function isNotEmpty(item) {
if (Array.isArray(item)) {
return item.some(isNotEmpty); // Check if any element in the array is not empty
- } else if (typeof item === 'object') {
+ } else if (typeof item === "object") {
return Object.keys(item).some((key) => isNotEmpty(item[key])); // Check if any key in the object is not empty
} else {
return Boolean(item); // Check if the item itself is truthy
@@ -23,7 +23,7 @@ const AccordionItem = ({ open = true, index, title, children }) => {
function removeEmptyItems(input) {
if (Array.isArray(input)) {
return input.filter(isNotEmpty);
- } else if (typeof input === 'object') {
+ } else if (typeof input === "object") {
return isNotEmpty(input) ? input : null;
} else {
return input;
@@ -31,22 +31,38 @@ const AccordionItem = ({ open = true, index, title, children }) => {
}
return (