diff --git a/frontend/assets/images/icons/widgets/index.jsx b/frontend/assets/images/icons/widgets/index.jsx index 058bf838e1..7ecb678d1b 100644 --- a/frontend/assets/images/icons/widgets/index.jsx +++ b/frontend/assets/images/icons/widgets/index.jsx @@ -137,6 +137,7 @@ const WidgetIcon = (props) => { case 'map': return ; case 'modal': + case 'modallegacy': return ; case 'multiselect': case 'multiselectv2': diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index 63e37e8fd1..b76668a062 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -284,7 +284,7 @@ "userGroups": "User Groups", "createNewGroup": "Create new group", "updateGroup": "Update group", - "addNewGroup": "Create new group", + "addNewGroup": "Add new group", "enterName": "Enter group name", "createGroup": "Create Group", "name": "Name" diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index 5fcedc2237..00fde7c9f2 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -131,9 +131,13 @@ class AppComponent extends React.Component { } return ''; }; - render() { const { updateAvailable, darkMode, isEditorOrViewer } = this.state; + const mergedProps = { + ...this.props, + switchDarkMode: this.switchDarkMode, + darkMode: darkMode, + }; let toastOptions = { style: { wordBreak: 'break-all', @@ -256,7 +260,7 @@ class AppComponent extends React.Component { } /> )} - }> + }> }> }> @@ -270,7 +274,7 @@ class AppComponent extends React.Component { } /> - {getDataSourcesRoutes(this.props)} + {getDataSourcesRoutes(mergedProps)} { const shouldFreeze = useStore((state) => state.getShouldFreeze()); const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow); @@ -28,16 +29,27 @@ export const ConfigHandle = ({ ); const setComponentToInspect = useStore((state) => state.setComponentToInspect); + const isModal = componentType === 'Modal' || componentType === 'ModalV2'; + const _showHandle = useStore((state) => { + const isWidgetHovered = state.getHoveredComponentForGrid() === id || state.hoveredComponentBoundaryId === id; + const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== ''; + // If one component is hovered and one is selected, show the handle for the hovered component + return ( + isWidgetHovered || + (showHandle && + (!isMultipleComponentsSelected || (isModal && isModalOpen)) && + !anyComponentHovered) + ); + }, shallow); + let height = visibility === false ? 10 : widgetHeight; + return (
{ @@ -51,7 +63,7 @@ export const ConfigHandle = ({ > diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss index 7f20210c10..e7322959e5 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss @@ -65,9 +65,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..fc163939f6 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -56,6 +56,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 +94,15 @@ 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; + return canvasWidth - 2; } 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 +143,7 @@ export const Container = React.memo( }} style={{ height: id === 'canvas' ? `${canvasHeight}` : '100%', - // backgroundSize: '25.3953px 10px', - backgroundSize: `${gridWidth}px 10px`, + backgroundSize: `${gridWidth}px ${10}px`, backgroundColor: currentMode === 'view' ? computeViewerBackgroundColor(darkMode, canvasBgColor) @@ -169,6 +174,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 = useGridStore((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 +141,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 +164,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 +366,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 +401,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 +422,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 +484,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 +521,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 +538,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,7 +576,7 @@ 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'); } @@ -584,9 +603,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 +628,12 @@ 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; } useGridStore.getState().actions.setResizingComponentId(e.target.id); - e.setMin([gridWidth, 10]); + e.setMin([gridWidth, GRID_HEIGHT]); }} onResizeEnd={(e) => { try { @@ -629,7 +645,7 @@ export default function Grid({ gridWidth, currentLayout }) { document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid'); 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 +670,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,12 +696,11 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('ResizeEnd error ->', error); } - useGridStore.getState().actions.setDragTarget(); + setDragParentId(null); toggleCanvasUpdater(); }} onResizeGroupStart={({ events }) => { - const parentElm = events[0].target.closest('.real-canvas'); - parentElm.classList.add('show-grid'); + showGridLines(); }} onResizeGroup={({ events }) => { const parentElm = events[0].target.closest('.real-canvas'); @@ -710,8 +723,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 +734,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 +764,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)`; @@ -767,6 +779,11 @@ export default function Grid({ gridWidth, currentLayout }) { }} 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 @@ -779,7 +796,7 @@ export default function Grid({ gridWidth, currentLayout }) { // to handle their own interactions like column resizing or card dragging let isDragOnInnerElement = false; - /* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works. + /* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works. Also user dont need to drag an calender from using popup */ if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) { return false; @@ -809,10 +826,6 @@ 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; - } }} onDragEnd={(e) => { try { @@ -820,141 +833,103 @@ export default function Grid({ gridWidth, currentLayout }) { useGridStore.getState().actions.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); + 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 +938,32 @@ 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; + } + } // 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 +978,26 @@ 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)`; }); updateNewPosition(events); }} onDragGroupStart={({ events }) => { - const parentElm = events[0]?.target?.closest('.real-canvas'); - parentElm?.classList?.add('show-grid'); + showGridLines(); + setIsGroupDragging(true); }} onDragGroupEnd={(e) => { handleDragGroupEnd(e); 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 +1013,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..2889fc06db 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -291,6 +291,7 @@ export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) { ? document.getElementById(id) : id : document.getElementsByClassName('real-canvas')[0]; + parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id); if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') { parentDiv = document.getElementById('canvas-' + id); } @@ -391,3 +392,25 @@ 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'); +} 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/WidgetWrapper.jsx b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx index 34c172292d..f583ebb53a 100644 --- a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx +++ b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx @@ -89,6 +89,7 @@ const WidgetWrapper = memo( widgetHeight={layoutData.height} showHandle={isWidgetActive} componentType={componentType} + visibility={visibility} /> )} { - const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child; + const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles, slotName } = + child; const componentMeta = deepClone(componentTypes.find((component) => component.component === componentName)); const componentData = JSON.parse(JSON.stringify(componentMeta)); @@ -139,7 +140,12 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou } const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop'; - const _parent = getParentComponentIdByType(child, parentMeta.component, parentId); + const _parent = getParentComponentIdByType({ + child, + parentComponent: parentMeta.component, + parentId, + slotName, + }); const newChildComponent = { id: uuidv4(), @@ -199,7 +205,8 @@ export const getAllChildComponents = (allComponents, parentId) => { allComponents[parentId]?.component?.component === 'Tabs' || allComponents[parentId]?.component?.component === 'Calendar' || allComponents[parentId]?.component?.component === 'Kanban' || - allComponents[parentId]?.component?.component === 'Container'; + allComponents[parentId]?.component?.component === 'Container' || + allComponents[parentId]?.component?.component === 'ModalV2'; if (componentParentId && isParentTabORCalendar) { let childComponent = deepClone(allComponents[componentId]); @@ -249,7 +256,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 +326,8 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI return ( parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar' || - parentComponent.component.component === 'Container' + parentComponent.component.component === 'Container' || + parentComponent.component.component === 'ModalV2' ); } @@ -483,11 +490,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 +665,23 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => { return canvasBgColor; }; -export const getParentComponentIdByType = (child, parentComponent, parentId) => { +export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => { const { tab } = child; if (parentComponent === 'Tabs') return `${parentId}-${tab}`; - else if (parentComponent === 'Container') return `${parentId}-header`; + else if (parentComponent === 'Container' || parentComponent === 'ModalV2') { + return `${parentId}-${slotName}`; + } return parentId; }; + +export const getParentWidgetFromId = (parentType, parentId) => { + const isAddingToSlot = parentId?.includes('-header') || parentId?.includes('-footer'); + + if (parentType === 'ModalV2' && isAddingToSlot) { + return 'ModalSlot'; + } else if (parentType === 'Kanban') { + return 'Kanban_card'; + } + return parentType; +}; diff --git a/frontend/src/AppBuilder/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/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/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/TJDBHinter.jsx b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx index 4edd8a0fda..625b11dedd 100644 --- a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx +++ b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx @@ -150,7 +150,7 @@ const TJDBCodeEditor = (props) => { className="cm-codehinter position-relative" style={{ width: '100%', - height: isOpen ? '350p' : 'auto', + height: isOpen ? '350px' : 'auto', }} >
@@ -167,14 +167,14 @@ 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} > -
+
{ // 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/QueryManagerBody.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx index d40dc44f74..9e6737f41c 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx @@ -45,15 +45,15 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () = const queryName = selectedQuery?.name ?? ''; const sourcecomponentName = selectedDataSource?.kind?.charAt(0).toUpperCase() + selectedDataSource?.kind?.slice(1); - const ElementToRender = selectedDataSource?.pluginId ? source : allSources[sourcecomponentName]; + const ElementToRender = selectedDataSource?.plugin_id ? source : allSources[sourcecomponentName]; const defaultOptions = useRef({}); const isFreezed = useStore((state) => 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 = () = { +export const BaseUrl = ({ dataSourceURL, theme, className = 'col-auto', style = {} }) => { return ( - {dataSourceURL} + + {dataSourceURL} + ); }; diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx index feaeadcb23..4c89ad849e 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx @@ -28,7 +28,10 @@ class Restapi extends React.Component { this.state = { options, + codeHinterHeight: 32, // Default height }; + this.codeHinterRef = React.createRef(); + this.resizeObserver = null; } componentDidUpdate(prevProps) { @@ -40,21 +43,95 @@ class Restapi extends React.Component { }, }); } + // Setup resize observer if it's not already set up + if (this.codeHinterRef.current && !this.resizeObserver) { + this.setupResizeObserver(); + } } componentDidMount() { try { + if (isEmpty(this.state.options['headers'])) { + this.addNewKeyValuePair('headers'); + } + if (isEmpty(this.state.options['cookies'])) { + this.addNewKeyValuePair('cookies'); + } if (isEmpty(this.state.options['method'])) { changeOption(this, 'method', 'get'); } + setTimeout(() => { + 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/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/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/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 a57c879121..db52205467 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx @@ -37,7 +37,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') { @@ -202,9 +201,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) { } }); setOptions(_options); - updateAllOptionsParams(_options); setMarkedAsDefault(_value); - paramUpdated({ name: 'value' }, 'value', _value, 'properties'); + updateAllOptionsParams(_options); } }; 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..b11a675e80 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx @@ -624,6 +624,8 @@ class TableComponent extends React.Component { return 'Select'; case 'newMultiSelect': return 'Multiselect'; + case 'json': + return 'JSON'; default: capitalize(text ?? ''); } 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/dropdownV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js index 8e5355f789..588bc655c6 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js @@ -299,7 +299,6 @@ export const dropdownV2Config = { ], }, label: { value: 'Select' }, - value: { value: '{{"2"}}' }, optionsLoadingState: { value: '{{false}}' }, placeholder: { value: 'Select an option' }, visibility: { value: '{{true}}' }, 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 fbd3c76a59..dc1835253d 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js @@ -48,8 +48,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 c0a0844607..3443c73c9b 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js @@ -1,6 +1,6 @@ export const modalConfig = { - name: 'Modal', - displayName: 'Modal', + name: 'ModalLegacy', + displayName: 'Modal (Legacy)', description: 'Show pop-up windows', component: 'Modal', defaultSize: { diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js new file mode 100644 index 0000000000..e7e96c4398 --- /dev/null +++ b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js @@ -0,0 +1,277 @@ +export const modalV2Config = { + name: 'Modal', + displayName: 'Modal', + description: 'Show pop-up windows', + component: 'ModalV2', + defaultSize: { + width: 10, + height: 34, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + loadingState: { + type: 'toggle', + displayName: 'Loading state', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, + visibility: { + type: 'toggle', + displayName: 'Modal trigger visibility', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + }, + disabledTrigger: { + type: 'toggle', + displayName: 'Disable modal trigger', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + }, + disabledModal: { + type: 'toggle', + displayName: 'Disable modal window', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, + useDefaultButton: { + type: 'toggle', + displayName: 'Use default trigger button', + validation: { + schema: { + type: 'boolean', + }, + defaultValue: true, + }, + }, + triggerButtonLabel: { + type: 'code', + displayName: 'Trigger button label', + validation: { + schema: { + type: 'string', + }, + defaultValue: 'Launch Modal', + }, + }, + + // Data Accordion + showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' }, + showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' }, + + size: { + type: 'select', + displayName: 'Width', + accordian: 'Data', + options: [ + { name: 'small', value: 'sm' }, + { name: 'medium', value: 'lg' }, + { name: 'large', value: 'xl' }, + { name: 'fullscreen', value: 'fullscreen' }, + ], + validation: { + schema: { type: 'string' }, + defaultValue: 'lg', + }, + }, + modalHeight: { + type: 'numberInput', + displayName: 'Height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 }, + }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + accordian: 'Data', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' }, + closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' }, + hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' }, + }, + events: { + onOpen: { displayName: 'On open' }, + onClose: { displayName: 'On close' }, + }, + defaultChildren: [ + { + componentName: 'Text', + slotName: 'header', + layout: { + top: 21, + left: 1, + height: 40, + }, + displayName: 'ModalHeaderTitle', + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor'], + defaultValue: { + text: 'Modal title', + textSize: 20, + textColor: '#000', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 24, + left: 22, + height: 36, + }, + displayName: 'ModalFooterCancel', + properties: ['text'], + styles: ['type', 'borderColor', 'padding'], + defaultValue: { + text: 'Button1', + type: 'outline', + borderColor: '#CCD1D5', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 24, + left: 32, + height: 36, + }, + displayName: 'ModalFooterConfirm', + properties: ['text'], + defaultValue: { + text: 'Button2', + padding: 'none', + }, + }, + ], + styles: { + headerBackgroundColor: { + type: 'color', + displayName: 'Header background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + footerBackgroundColor: { + type: 'color', + displayName: 'Footer background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + bodyBackgroundColor: { + type: 'color', + displayName: 'Body background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + triggerButtonBackgroundColor: { + type: 'color', + displayName: 'Trigger button background color', + validation: { + schema: { type: 'string' }, + defaultValue: false, + }, + }, + triggerButtonTextColor: { + type: 'color', + displayName: 'Trigger button text color', + validation: { + schema: { type: 'string' }, + defaultValue: false, + }, + }, + }, + exposedVariables: { + show: false, + isDisabledModal: false, + isDisabledTrigger: false, + isVisible: true, + isLoading: false, + }, + actions: [ + { + handle: 'open', + displayName: 'Open', + }, + { + handle: 'close', + displayName: 'Close', + }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'setDisableTrigger', + displayName: 'Set disable trigger', + params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setDisableModal', + displayName: 'Set disable modal', + params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set loading', + params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + ], + definition: { + others: { + showOnDesktop: { value: '{{true}}' }, + showOnMobile: { value: '{{false}}' }, + }, + properties: { + loadingState: { value: `{{false}}` }, + visibility: { value: '{{true}}' }, + disabledTrigger: { value: '{{false}}' }, + disabledModal: { value: '{{false}}' }, + useDefaultButton: { value: `{{true}}` }, + triggerButtonLabel: { value: `Launch Modal` }, + size: { value: 'lg' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, + hideCloseButton: { value: '{{false}}' }, + hideOnEsc: { value: '{{true}}' }, + closeOnClickingOutside: { value: '{{false}}' }, + modalHeight: { value: 400 }, + headerHeight: { value: 80 }, + footerHeight: { value: 80 }, + }, + events: [], + styles: { + headerBackgroundColor: { value: '#ffffffff' }, + footerBackgroundColor: { value: '#ffffffff' }, + bodyBackgroundColor: { value: '#ffffffff' }, + triggerButtonBackgroundColor: { value: '#4D72FA' }, + triggerButtonTextColor: { value: '#ffffffff' }, + }, + }, +}; diff --git a/frontend/src/AppBuilder/Widgets/Container.jsx b/frontend/src/AppBuilder/Widgets/Container.jsx index 3ccedd869b..1334098423 100644 --- a/frontend/src/AppBuilder/Widgets/Container.jsx +++ b/frontend/src/AppBuilder/Widgets/Container.jsx @@ -45,7 +45,7 @@ export const Container = ({ border: `1px solid ${borderColor}`, height, display: isVisible ? 'flex' : 'none', - overflow: 'hidden auto', + flexDirection: 'column', position: 'relative', boxShadow, }; @@ -66,9 +66,7 @@ export const Container = ({ return (
state.setModalOpenOnCanvas); const [activeId, setActiveId] = useState(null); const cardMovementRef = useRef(null); const shouldUpdateData = useRef(false); @@ -117,6 +118,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { } /**** End - Logic to reduce the zIndex of modal control box ****/ } + setModalOpenOnCanvas(`${id}-modal`, showModal); }, [showModal]); useEffect(() => { diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx index e63118a4e6..128c98aed2 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 && ( +
-
+
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/WidgetManager/configs/dropdownV2.js b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js index 247af3ccef..de90dbd0bf 100644 --- a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js +++ b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js @@ -299,7 +299,6 @@ export const dropdownV2Config = { ], }, label: { value: 'Select' }, - value: { value: '{{"2"}}' }, optionsLoadingState: { value: '{{false}}' }, placeholder: { value: 'Select an option' }, visibility: { value: '{{true}}' }, 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..25da8f73c6 100644 --- a/frontend/src/Editor/WidgetManager/configs/listview.js +++ b/frontend/src/Editor/WidgetManager/configs/listview.js @@ -49,7 +49,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/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/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 996e382805..ab50cbb05d 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -38,7 +38,6 @@ import { useLicenseStore } from '@/_stores/licenseStore'; import { shallow } from 'zustand/shallow'; import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling'; import HeaderSkeleton from '@/_ui/FolderSkeleton/HeaderSkeleton'; -import { UserGroupMigrationModal } from './MigrationModal/UserGroupMigrationModal'; import { ImportAppMenu, AppActionModal, @@ -136,13 +135,7 @@ class HomePageComponent extends React.Component { this.fetchWorkflowsWorkspaceLimit(); this.fetchOrgGit(); this.setQueryParameter(); - const hasShownModal = localStorage.getItem('hasShownUserGroupMigrationModal'); const hasClosedBanner = localStorage.getItem('hasClosedGroupMigrationBanner'); - //Only show the modal once - if (!hasShownModal) { - this.setState({ showUserGroupMigrationModal: true }); - localStorage.setItem('hasShownUserGroupMigrationModal', 'true'); - } //Only show the banner once if (hasClosedBanner) { @@ -790,6 +783,16 @@ class HomePageComponent extends React.Component { handleCommitChange = (commitEnabled) => { this.setState({ commitEnabled: commitEnabled }); }; + shouldShowMigrationBanner = () => { + const { currentSessionValue } = authenticationService; + const { appType } = this.props; + return ( + currentSessionValue?.admin && + this.state.showGroupMigrationBanner && + new Date(currentSessionValue?.current_user?.created_at) < new Date('2025-02-01') && + appType !== 'workflow' + ); + }; render() { const { apps, @@ -885,15 +888,6 @@ class HomePageComponent extends React.Component { return (
- {authenticationService.currentSessionValue?.admin && - showUserGroupMigrationModal && - this.props.appType !== 'workflow' && ( - this.setShowUserGroupMigrationModal()} - darkMode={this.props.darkMode} - /> - )} )} - {authenticationService.currentSessionValue?.admin && - showGroupMigrationBanner && - this.props.appType !== 'workflow' && ( - - )} + {this.shouldShowMigrationBanner() && ( + + )}
diff --git a/frontend/src/OrganizationSettingsPage/constant.js b/frontend/src/OrganizationSettingsPage/constant.js index f2d4b4e2df..503533a657 100644 --- a/frontend/src/OrganizationSettingsPage/constant.js +++ b/frontend/src/OrganizationSettingsPage/constant.js @@ -3,7 +3,6 @@ export const workspaceSettingsLinks = [ { id: 'groups', name: 'Groups', route: 'groups', conditions: ['admin'] }, { id: 'workspacelogin', name: 'Workspace login', route: 'workspace-login', conditions: ['admin', 'wsLoginEnabled'] }, { id: 'workspace-variables', name: 'Workspace variables', route: 'workspace-variables', conditions: ['admin'] }, - { id: 'copilot', name: 'Copilot', route: 'copilot', conditions: ['admin'] }, { id: 'custom-styles', name: 'Custom styles', route: 'custom-styles', conditions: ['admin'] }, { id: 'configure-git', name: 'Configure Git', route: 'configure-git', conditions: ['admin'] }, ]; diff --git a/frontend/src/TooljetDatabase/Drawers/CreateTableDrawer/index.jsx b/frontend/src/TooljetDatabase/Drawers/CreateTableDrawer/index.jsx index 19e5d62182..e8a1ad551c 100644 --- a/frontend/src/TooljetDatabase/Drawers/CreateTableDrawer/index.jsx +++ b/frontend/src/TooljetDatabase/Drawers/CreateTableDrawer/index.jsx @@ -8,11 +8,10 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import { BreadCrumbContext } from '@/App/App'; import LicenseBanner from '@/modules/common/components/LicenseBanner'; -export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) { +export default function CreateTableDrawer({ bannerVisible, setBannerVisible, tablesLimit, setTablesLimit }) { const { organizationId, setSelectedTable, setTables, tables } = useContext(TooljetDatabaseContext); const [isCreateTableDrawerOpen, setIsCreateTableDrawerOpen] = useState(false); const { updateSidebarNAV } = useContext(BreadCrumbContext); - const [tablesLimit, setTablesLimit] = useState({}); setBannerVisible(tablesLimit?.current >= tablesLimit?.total - 1 || false); useEffect(() => { @@ -42,15 +41,6 @@ export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) { Create new table
- - setIsCreateTableDrawerOpen(false)} 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/TooljetDatabase/Sidebar/index.jsx b/frontend/src/TooljetDatabase/Sidebar/index.jsx index bd143f11ac..437f7c52a0 100644 --- a/frontend/src/TooljetDatabase/Sidebar/index.jsx +++ b/frontend/src/TooljetDatabase/Sidebar/index.jsx @@ -3,18 +3,40 @@ import List from '../TableList'; import CreateTableDrawer from '../Drawers/CreateTableDrawer'; import { OrganizationList } from '@/modules/dashboard/components'; import cx from 'classnames'; +import LicenseBanner from '@/modules/common/components/LicenseBanner'; +import { authenticationService } from '@/_services'; export default function Sidebar({ collapseSidebar }) { const [bannerVisible, setBannerVisible] = useState(false); + const [tablesLimit, setTablesLimit] = useState({}); + const isAdmin = authenticationService.currentSessionValue?.admin === true; + const isResourceLimitReached = tablesLimit?.percentage === 100; return (
- +
-
+
+
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/_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/_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/license.scss b/frontend/src/_styles/license.scss index eb5d85c0af..14ab98ceab 100644 --- a/frontend/src/_styles/license.scss +++ b/frontend/src/_styles/license.scss @@ -198,7 +198,7 @@ .message-wrapper { display: flex; flex-direction: column; - margin-left: 10px; + margin-left: 5px; .heading { display: unset !important; @@ -246,9 +246,9 @@ .upgrade-btn-new{ white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: auto; + overflow: hidden; + text-overflow: ellipsis; + width: auto; margin-left: auto; border: 1px solid var(--border-default, #CCD1D5); background: var(--background-surface-layer-01); @@ -262,7 +262,9 @@ padding: 8px 16px 8px 16px; } - + .demo-btn-bg{ + border: 1px solid var(--border-accent-weak, #97AEFC); + } .start-trial-btn { min-width: 131px; } @@ -318,16 +320,14 @@ max-width: fit-content; .upgrade-link { - background: var(--upgrade-default); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; + margin-left: 2px; + color: var(--text-default, #1B1F24); + font-family: "IBM Plex Sans"; font-size: 12px; font-style: normal; - line-height: 20px; font-weight: 500; - width: 67px; - height: 20px; + line-height: 20px; + /* 166.667% */ } } @@ -345,16 +345,13 @@ max-width: fit-content; .upgrade-link { - background: linear-gradient(to right, #ff5f6d, #ffc371); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; + color: var(--text-default, #1B1F24); + font-family: "IBM Plex Sans"; font-size: 12px; font-style: normal; - line-height: 20px; font-weight: 500; - width: 67px; - height: 20px; + line-height: 20px; + /* 166.667% */ } } @@ -372,16 +369,13 @@ max-width: fit-content; .upgrade-link { - background: linear-gradient(to right, #ff5f6d, #ffc371); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; + color: var(--text-default, #1B1F24); + font-family: "IBM Plex Sans"; font-size: 12px; font-style: normal; - line-height: 20px; font-weight: 500; - width: 67px; - height: 20px; + line-height: 20px; + /* 166.667% */ } } @@ -399,16 +393,13 @@ max-width: fit-content; .upgrade-link { - background: var(--upgrade-default); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; + color: var(--text-default, #1B1F24); + font-family: "IBM Plex Sans"; font-size: 12px; font-style: normal; - line-height: 20px; font-weight: 500; - width: 67px; - height: 20px; + line-height: 20px; + /* 166.667% */ } } diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss index 8a9eaf972a..7b256c3812 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 { 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 8927cf416e..1533bad5c6 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; @@ -19140,4 +19151,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 3d14fb05aa..239037a852 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -2665,10 +2665,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; + } } @@ -7935,7 +7943,7 @@ tbody { } .sidebar-container-with-banner { - height: 140px !important; + height: 40px !important; padding-top: 1px !important; margin: 0 auto; display: flex; @@ -12096,13 +12104,19 @@ tbody { .sidebar-list-wrap-with-banner { margin-top: 24px; padding: 0px 20px 20px 20px; - height: calc(100vh - 280px); + height: calc(100vh - 408px); overflow: auto; span { letter-spacing: -0.02em; } } +.sidebar-list-wrap.sidebar-list-wrap-with-banner.isAdmin { + height: calc(100vh - 371px); + &.resource-limit-reached { + height: calc(100vh - 371px); + } +} .drawer-footer-btn-wrap, .variable-form-footer { @@ -14027,8 +14041,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; @@ -18755,4 +18769,52 @@ section.ai-message-prompt-input-wrapper { #inspector-tabpane-properties .accordion-header { height:32px; -} \ No newline at end of file +} +.workspace-constant-value { + position: relative; + + .fromEnv { + content: '.env'; + border-radius: 6px; + background: var(--Indigo-50, #EEF4FF); + padding: 0px 8px; + width: 40px; + align-items: center; + position: absolute; + color: var(--Indigo-700, #3538CD); + text-align: center; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; + margin-left: 24px; + } + + .isDuplicate { + padding: 0px 8px; + border-radius: 6px; + background: var(--Error-50, #FEF3F2); + color: var(--Error-700, #B42318); + text-align: center; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; + /* 166.667% */ + margin-left: 24px; + } + + .env-secret-hidden-message { + border-radius: 16px; + background: var(--Warning-50, #FFFAEB); + padding: 4px 12px; + color: var(--Warning-700, #B54708); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; + &.dark { + background: #FFFAEB !important; + } + } +} diff --git a/frontend/src/_ui/Icon/solidIcons/AICrown.jsx b/frontend/src/_ui/Icon/solidIcons/AICrown.jsx new file mode 100644 index 0000000000..47776e319a --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/AICrown.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const AICrown = ({ className = '', fill = '#FCA23F', width = '40', height = '41', ...props }) => { + return ( + + + + ); +}; + +export default AICrown; diff --git a/frontend/src/_ui/Icon/solidIcons/AppLimitSvg.jsx b/frontend/src/_ui/Icon/solidIcons/AppLimitSvg.jsx new file mode 100644 index 0000000000..e9490c038c --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/AppLimitSvg.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +const AppLimitSvg = () => ( + + + + + + +); + +export default AppLimitSvg; diff --git a/frontend/src/_ui/Icon/solidIcons/BookDemo.jsx b/frontend/src/_ui/Icon/solidIcons/BookDemo.jsx new file mode 100644 index 0000000000..8716ebdd48 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/BookDemo.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +const BookDemo = ({ className = '', fill = '#4368E3', width = '16', height = '17', ...props }) => { + return ( + + + + ); +}; + +export default BookDemo; diff --git a/frontend/src/_ui/Icon/solidIcons/CalendarIcon.jsx b/frontend/src/_ui/Icon/solidIcons/CalendarIcon.jsx new file mode 100644 index 0000000000..cf6b4b288c --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/CalendarIcon.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const CalendarIcon = ({ fill = '#FFAF41', width = '25', className = '', viewBox = '0 0 25 25' }) => ( + + + +); + +export default CalendarIcon; diff --git a/frontend/src/_ui/Icon/solidIcons/CalendarSmall.jsx b/frontend/src/_ui/Icon/solidIcons/CalendarSmall.jsx new file mode 100644 index 0000000000..1dd4793842 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/CalendarSmall.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const CalendarSmall = () => ( + + + +); + +export default CalendarSmall; diff --git a/frontend/src/_ui/Icon/solidIcons/Contactv3.jsx b/frontend/src/_ui/Icon/solidIcons/Contactv3.jsx new file mode 100644 index 0000000000..066c056822 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/Contactv3.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +const Contactv3 = ({ className = '', fill = '#CCD1D5', width = '16', height = '16', ...props }) => { + return ( + + + + ); +}; + +export default Contactv3; diff --git a/frontend/src/_ui/Icon/solidIcons/NewTabSmall.jsx b/frontend/src/_ui/Icon/solidIcons/NewTabSmall.jsx new file mode 100644 index 0000000000..8bb3c435fe --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/NewTabSmall.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const NewTabSmall = ({ fill = '#6A727C', width = '25', className = '', viewBox = '0 0 25 25' }) => ( + + + +); + +export default NewTabSmall; diff --git a/frontend/src/_ui/Icon/solidIcons/PremiumLogo.jsx b/frontend/src/_ui/Icon/solidIcons/PremiumLogo.jsx new file mode 100644 index 0000000000..1728165fa6 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/PremiumLogo.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const PremiumLogo = ({ width = '41', height = '41' }) => ( + + + +); + +export default PremiumLogo; diff --git a/frontend/src/_ui/Icon/solidIcons/StudentIcon.jsx b/frontend/src/_ui/Icon/solidIcons/StudentIcon.jsx new file mode 100644 index 0000000000..ed12fa7d6b --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/StudentIcon.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +const StudentIcon = ({ fill = '#FFAF41', width = '25', className = '', viewBox = '0 0 25 25' }) => ( + + + + + + +); + +export default StudentIcon; diff --git a/frontend/src/_ui/Icon/solidIcons/UserGroupsGrey.jsx b/frontend/src/_ui/Icon/solidIcons/UserGroupsGrey.jsx new file mode 100644 index 0000000000..debccd0039 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/UserGroupsGrey.jsx @@ -0,0 +1,32 @@ +import React from 'react'; + +const UserGroupsGrey = () => ( + + + + + + + + +); + +export default UserGroupsGrey; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index d036919cd9..34a2410e1d 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -219,6 +219,16 @@ import Replace from './Replace.jsx'; import ReplaceAll from './ReplaceAll.jsx'; import Remove02 from './Remove02.jsx'; import TooljetAi from './TooljetAI.jsx'; +import AICrown from './AICrown.jsx'; +import BookDemo from './BookDemo.jsx'; +import Contactv3 from './Contactv3.jsx'; +import PremiumLogo from './PremiumLogo.jsx'; +import StudentIcon from './StudentIcon.jsx'; +import CalendarIcon from './CalendarIcon.jsx'; +import CalendarSmall from './CalendarSmall.jsx'; +import UserGroupsGrey from './UserGroupsGrey.jsx'; +import AppLimitSvg from './AppLimitSvg.jsx'; +import NewTabSmall from './NewTabSmall.jsx'; const Icon = (props) => { switch (props.name) { @@ -662,6 +672,26 @@ const Icon = (props) => { return ; case 'remove02': return ; + case 'bookdemo': + return ; + case 'contactv3': + return ; + case 'premium-logo': + return ; + case 'calendar-icon': + return ; + case 'calendar-small': + return ; + case 'user-groups-grey': + return ; + case 'app-limit': + return ; + case 'new-tab-small': + return ; + case 'student-icon': + return ; + case 'ai-crown': + return ; default: return ; } diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx index 65a4ece91d..b13e88d767 100644 --- a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx +++ b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx @@ -53,7 +53,7 @@ export const JSONNode = ({ data, ...restProps }) => { React.useEffect(() => { if (typeof shouldExpandNode === 'function') { - set(shouldExpandNode(path, data)); + set(shouldExpandNode(path, data, currentNode)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathToBeInspected]); @@ -268,7 +268,15 @@ export const JSONNode = ({ data, ...restProps }) => { }; return ( -
+
{enableCopyToClipboard && ( { 'group-object-container': shouldDisplayIntendedBlock, 'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array', })} + id={`inspector-node-${String(currentNode).toLowerCase()}`} data-cy={`inspector-node-${String(currentNode).toLowerCase()}`} > {$NODEIcon &&
{$NODEIcon}
} diff --git a/frontend/src/_ui/Label.jsx b/frontend/src/_ui/Label.jsx index 3cd7f886cb..721ac05203 100644 --- a/frontend/src/_ui/Label.jsx +++ b/frontend/src/_ui/Label.jsx @@ -13,6 +13,7 @@ function Label({ label, width, labelRef, color, defaultAlignment, direction, aut justifyContent: direction == 'right' ? 'flex-end' : 'flex-start', fontSize: '12px', height: defaultAlignment === 'top' && '20px', + overflow: 'hidden', }} >

+

diff --git a/frontend/src/modules/WorkspaceSettings/components/ManageOrgConstantsSettings/ConstantTable.jsx b/frontend/src/modules/WorkspaceSettings/components/ManageOrgConstantsSettings/ConstantTable.jsx index 704941729c..64e836f976 100644 --- a/frontend/src/modules/WorkspaceSettings/components/ManageOrgConstantsSettings/ConstantTable.jsx +++ b/frontend/src/modules/WorkspaceSettings/components/ManageOrgConstantsSettings/ConstantTable.jsx @@ -1,9 +1,17 @@ import React, { useState } from 'react'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; -import { Tooltip } from 'react-tooltip'; +import { ToolTip } from '@/_components'; import EyeHide from '@/../assets/images/onboardingassets/Icons/EyeHide'; import EyeShow from '@/../assets/images/onboardingassets/Icons/EyeShow'; +const WithTooltip = ({ children, message, placement, show = false }) => { + return ( + + {children} + + ); +}; + const ConstantTable = ({ constants = [], canUpdateDeleteConstant = true, @@ -88,94 +96,134 @@ const ConstantTable = ({ ) : ( - {constants.map((constant) => ( - - - - {String(constant.name).length > 30 - ? String(constant.name).substring(0, 30) + '...' - : constant.name} - - - - - {!showValues[constant.id] ? '*'.repeat(displayValue(constant).length) : displayValue(constant)} - - - - {canUpdateDeleteConstant && ( + {constants.map((constant) => { + return ( + -
-
toggleShowValue(constant.id)} - data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-constant-visibility`} - > - {!showValues[constant.id] ? ( - - ) : ( - - )} -
- onEditBtnClicked(constant)} - data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-edit-button`} - > - Edit - - - onDeleteBtnClicked(constant)} - data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-delete-button`} - > - Delete - -
+ {String(constant.name).length > 30 + ? String(constant.name).substring(0, 30) + '...' + : constant.name} + - )} - - ))} + + + {!showValues[constant.id] ? ( + '*'.repeat(displayValue(constant).length) + ) : constant.type === 'Secret' && constant.fromEnv ? ( + + Values fetched at runtime, not stored in ToolJet + + ) : ( + displayValue(constant) + )} + + {constant.fromEnv && .env} + {constant.isDuplicate && Duplicate} + + + {canUpdateDeleteConstant && ( + +
+
toggleShowValue(constant.id)} + data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-constant-visibility`} + > + {!showValues[constant.id] ? ( + + ) : ( + + )} +
+ { + + Constants created from
environment variables
cannot be edited or + deleted +

+ ), + placement: 'left', + })} + > +
+ onEditBtnClicked(constant)} + data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-edit-button`} + > + Edit + + + onDeleteBtnClicked(constant)} + data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-delete-button`} + > + Delete + +
+
+ } +
+ + )} + + ); + })} )} diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx index 637a792b3e..928f45cf4c 100644 --- a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx @@ -549,10 +549,7 @@ class BaseManageGroupPermissions extends React.Component { fill={'#FDFDFE'} disabled={!isFeatureEnabled} > - {this.props.t( - 'header.organization.menus.manageGroups.permissions.createNewGroup', - 'Create new group' - )} + {this.props.t('header.organization.menus.manageGroups.permissions.addNewGroup', 'Add new group')} )} @@ -805,6 +802,7 @@ class BaseManageGroupPermissions extends React.Component { size="xsmall" type={featureAccess?.licenseStatus?.licenseType} customMessage={'Custom groups & permissions are available in our paid plans.'} + showCustomGroupBanner={true} /> )}
diff --git a/frontend/src/modules/WorkspaceSettings/pages/WorkspaceLogin/WorkspaceLoginSettings.jsx b/frontend/src/modules/WorkspaceSettings/pages/WorkspaceLogin/WorkspaceLoginSettings.jsx index 367de75afc..429490cda4 100644 --- a/frontend/src/modules/WorkspaceSettings/pages/WorkspaceLogin/WorkspaceLoginSettings.jsx +++ b/frontend/src/modules/WorkspaceSettings/pages/WorkspaceLogin/WorkspaceLoginSettings.jsx @@ -31,6 +31,7 @@ class OrganizationLogin extends React.Component { instanceSSO: [], featureAccess: {}, isBasicPlan: false, + isCommunity: false, canToggleAutomaticSSOLogin: false, showDisableAutoSSOModal: false, }; @@ -152,6 +153,7 @@ class OrganizationLogin extends React.Component { async setLoginConfigs() { const featureAccess = await licenseService.getFeatureAccess(); const isBasicPlan = !featureAccess?.licenseStatus?.isLicenseValid || featureAccess?.licenseStatus?.isExpired; + const isCommunity = !featureAccess?.licenseStatus?.isLicenseValid && featureAccess?.expiry == ''; const settings = await this.fetchSSOSettings(); const instanceSSOResult = await instanceSettingsService.fetchSSOConfigs(); const instanceSSO = !Array.isArray(instanceSSOResult) @@ -180,7 +182,8 @@ class OrganizationLogin extends React.Component { const enabledSSOs = combinedSSOConfigs.filter( (obj) => obj.enabled && obj.sso !== 'form' && (!this.protectedSSO.includes(obj.sso) || featureAccess?.[obj.sso]) ); - let passwordLoginEnabled = isBasicPlan ? true : ssoConfigs?.find((obj) => obj.sso === 'form')?.enabled || false; + let passwordLoginEnabled = + isBasicPlan && !isCommunity ? true : ssoConfigs?.find((obj) => obj.sso === 'form')?.enabled || false; if (enabledSSOs.length === 0) { try { @@ -211,6 +214,7 @@ class OrganizationLogin extends React.Component { instanceSSO: [...instanceSSO], featureAccess: featureAccess, isBasicPlan: isBasicPlan, + isCommunity: isCommunity, canToggleAutomaticSSOLogin: canToggleAutomaticSSOLogin, isAnySSOEnabled: ssoConfigs?.some( @@ -447,6 +451,7 @@ class OrganizationLogin extends React.Component { instanceSSO, featureAccess, isBasicPlan, + isCommunity, canToggleAutomaticSSOLogin, } = this.state; const flexContainerStyle = { @@ -588,7 +593,7 @@ class OrganizationLogin extends React.Component { onChange={() => this.handleCheckboxChange('passwordLoginEnabled')} data-cy="password-enable-toggle" checked={options?.passwordLoginEnabled === true} - disabled={isBasicPlan ? true : !isAnySSOEnabled} + disabled={isBasicPlan && !isCommunity ? true : !isAnySSOEnabled} />