diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index a40663b0f5..436c04a430 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -13,6 +13,7 @@ export const ConfigHandle = ({ customClassName = '', showHandle, componentType, + visibility, }) => { 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 _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 || (componentType === 'Modal' && isModalOpen)) && + !anyComponentHovered) + ); + }, shallow); + let height = visibility === false ? 10 : widgetHeight; + return (
{ 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 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 +139,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 +162,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 +364,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 +399,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 +420,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 +482,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 +519,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 +536,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 +574,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 +601,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 +626,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 +643,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 +668,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 +694,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 +721,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 +732,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 +762,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 +777,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 @@ -809,10 +824,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,6 +831,9 @@ 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; @@ -906,14 +920,14 @@ export default function Grid({ gridWidth, currentLayout }) { } e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${ - Math.round(top / 10) * 10 + Math.round(top / GRID_HEIGHT) * GRID_HEIGHT }px)`; if (draggedOverElemId === currentParentId || isParentChangeAllowed) { handleDragEnd([ { id: e.target.id, x: left, - y: Math.round(top / 10) * 10, + y: Math.round(top / GRID_HEIGHT) * GRID_HEIGHT, parent: isParentChangeAllowed ? draggedOverElemId : undefined, }, ]); @@ -924,28 +938,34 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.log('draggedOverElemId->error', 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'); + 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 + const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent); if (parentComponent?.component?.component === 'Modal') { const elemContainer = e.target.closest('.real-canvas'); const containerHeight = elemContainer.clientHeight; @@ -963,8 +983,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 +1023,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 +1058,42 @@ export default function Grid({ gridWidth, currentLayout }) { } } }} + //snap settgins + snappable={true} + snapGap={false} + isDisplaySnapDigit={false} + snapThreshold={GRID_HEIGHT} + // 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..4bfd030a3c 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -391,3 +391,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/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 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); @@ -483,11 +482,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; 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 = () = { 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 (
({ getCustomResolvableReference: (value, parentId, moduleId) => { const { getParentComponentType } = get(); const parentComponentType = getParentComponentType(parentId, moduleId); - if (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) { + if ( + (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) || + value === '{{listItem}}' + ) { return { entityType: 'components', entityNameOrId: parentId, entityKey: 'listItem' }; } else if ( parentComponentType === 'Kanban' && diff --git a/frontend/src/AppBuilder/_stores/slices/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js index cc80a9dbf7..1958b8bece 100644 --- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js @@ -3,6 +3,7 @@ import { debounce } from 'lodash'; const initialState = { hoveredComponentForGrid: '', + hoveredComponentBoundaryId: '', triggerCanvasUpdater: false, lastCanvasIdClick: '', lastCanvasClickPosition: null, @@ -13,6 +14,8 @@ export const createGridSlice = (set, get) => ({ setHoveredComponentForGrid: (id) => set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }), getHoveredComponentForGrid: () => get().hoveredComponentForGrid, + setHoveredComponentBoundaryId: (id) => + set(() => ({ hoveredComponentBoundaryId: id }), false, { type: 'setHoveredComponentBoundaryId', id }), toggleCanvasUpdater: () => set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }), debouncedToggleCanvasUpdater: debounce(() => { diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index ae976e54ea..72e2b39664 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -61,7 +61,6 @@ export const DropdownV2 = ({ }) => { const { label, - value, advanced, schema, placeholder, @@ -89,7 +88,7 @@ export const DropdownV2 = ({ padding, } = styles; const isInitialRender = useRef(true); - const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value)); + const [currentValue, setCurrentValue] = useState(() => findDefaultItem(schema)); const isMandatory = validation?.mandatory ?? false; const options = properties?.options; const [validationStatus, setValidationStatus] = useState(validate(currentValue)); @@ -168,18 +167,9 @@ export const DropdownV2 = ({ }; useEffect(() => { - if (advanced) { - setInputValue(findDefaultItem(schema)); - } + setInputValue(findDefaultItem(advanced ? schema : options)); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [advanced, JSON.stringify(schema)]); - - useEffect(() => { - if (!advanced) { - setInputValue(value); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [advanced, value]); + }, [advanced, JSON.stringify(schema), JSON.stringify(options)]); useEffect(() => { if (visibility !== properties.visibility) setVisibility(properties.visibility); 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/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/_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/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/_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', }} >

{ @@ -70,6 +71,23 @@ export class DataSourcesRepository extends Repository { query.andWhere('data_source_options.environmentId = :environmentId', { environmentId }); } const result = await query.getMany(); + result.forEach((dataSource) => { + if (dataSource.plugin) { + if (dataSource.plugin.iconFile) { + dataSource.plugin.iconFile.data = dataSource.plugin.iconFile.data.toString('utf8'); + } + if (dataSource.plugin.manifestFile) { + dataSource.plugin.manifestFile.data = JSON.parse( + decode(dataSource.plugin.manifestFile.data.toString('utf8')) + ); + } + if (dataSource.plugin.operationsFile) { + dataSource.plugin.operationsFile.data = JSON.parse( + decode(dataSource.plugin.operationsFile.data.toString('utf8')) + ); + } + } + }); const sampleDataSourceQuery = await manager .createQueryBuilder(DataSource, 'data_source') diff --git a/server/src/modules/group-permissions/constants/index.ts b/server/src/modules/group-permissions/constants/index.ts index d63934d84f..091041661a 100644 --- a/server/src/modules/group-permissions/constants/index.ts +++ b/server/src/modules/group-permissions/constants/index.ts @@ -38,8 +38,8 @@ export const DEFAULT_GROUP_PERMISSIONS = { appDelete: true, folderCRUD: true, orgConstantCRUD: true, - dataSourceCreate: true, - dataSourceDelete: true, + dataSourceCreate: false, + dataSourceDelete: false, isBuilderLevel: true, }, END_USER: { diff --git a/server/src/modules/organization-constants/controller.ts b/server/src/modules/organization-constants/controller.ts index 9a787ffb48..39862dee29 100644 --- a/server/src/modules/organization-constants/controller.ts +++ b/server/src/modules/organization-constants/controller.ts @@ -22,30 +22,51 @@ export class OrganizationConstantController implements IOrganizationConstantCont @InitFeature(FEATURE_KEY.GET) @Get() async get(@User() user, @Query('type') type: OrganizationConstantType) { - const result = await this.organizationConstantsService.allEnvironmentConstants(user.organizationId); + const result = await this.organizationConstantsService.allEnvironmentConstants(user.organizationId, false, type); return { constants: result }; } + @UseGuards(JwtAuthGuard, FeatureAbilityGuard) + @InitFeature(FEATURE_KEY.GET_DECRYPTED_CONSTANTS) + @Get('decrypted') + async getDecryptedConstants(@User() user, @Query('type') type: OrganizationConstantType) { + const result = await this.organizationConstantsService.allEnvironmentConstants(user.organizationId, true, type); + return { constants: result }; + } + + @UseGuards(JwtAuthGuard, FeatureAbilityGuard) + @InitFeature(FEATURE_KEY.GET_SECRETS) + @Get('secrets') + async getAllSecrets(@User() user) { + const result = await this.organizationConstantsService.allEnvironmentConstants( + user.organizationId, + false, + OrganizationConstantType.SECRET + ); + return { constants: result }; + } + + //by default, this api fetches only global constants (for public apps, need to fetch app to get orgId in the public guard) @UseGuards(AppAuthGuard) - @Get('public/:app_slug') @InitFeature(FEATURE_KEY.GET_PUBLIC) - async getConstantsFromPublicApp(@App() app, @Query('environmentId') environmentId) { - const result = await this.organizationConstantsService.getConstantsForEnvironment( + @Get('public/:slug') + async getConstantsFromPublicApp(@App() app) { + const result = await this.organizationConstantsService.allEnvironmentConstants( app.organizationId, - environmentId, + false, OrganizationConstantType.GLOBAL ); return { constants: result }; } //by default, this api fetches only global constants - @UseGuards(JwtAuthGuard, FeatureAbilityGuard) - @Get(':app_slug') + @UseGuards(JwtAuthGuard) @InitFeature(FEATURE_KEY.GET_FROM_APP) - async getConstantsFromApp(@User() user, @Query('environmentId') environmentId) { - const result = await this.organizationConstantsService.getConstantsForEnvironment( + @Get(':app_slug') + async getConstantsFromApp(@User() user) { + const result = await this.organizationConstantsService.allEnvironmentConstants( user.organizationId, - environmentId, + false, OrganizationConstantType.GLOBAL ); return { constants: result }; diff --git a/server/src/modules/organization-constants/repository.ts b/server/src/modules/organization-constants/repository.ts index 87e83cefdd..ddd1ba4b4a 100644 --- a/server/src/modules/organization-constants/repository.ts +++ b/server/src/modules/organization-constants/repository.ts @@ -10,9 +10,16 @@ export class OrganizationConstantRepository extends Repository { return await dbTransactionWrap(async (manager: EntityManager) => { - const result = await this.organizationConstantRepository.findAllByOrganizationId(organizationId); + const result = await this.organizationConstantRepository.findAllByOrganizationId(organizationId, type); const appEnvironments = await this.appEnvironmentUtilService.getAll(organizationId); const constantsWithValues = await Promise.all( result.map(async (constant) => { + // Skip processing values if type is SECRET and decryptSecretValue is false + if (constant.type === OrganizationConstantType.SECRET && !decryptSecretValue) { + return { + name: constant.constantName, + }; + } const values = await Promise.all( appEnvironments.map(async (env) => { const value = constant.orgEnvironmentConstantValues.find((value) => value.environmentId === env.id); + let resolvedValue = ''; + if (value) { + if (constant.type === OrganizationConstantType.SECRET) { + resolvedValue = decryptSecretValue + ? await this.organizationConstantsUtilService.decryptSecret(organizationId, value.value) + : secretValue; + } else { + resolvedValue = await this.organizationConstantsUtilService.decryptSecret( + organizationId, + value.value + ); // Constant type values are always decrypted + } + } return { environmentName: env.name, - value: - value && value.value.length > 0 - ? await this.organizationConstantsUtilService.decryptSecret(organizationId, value.value) - : '', - id: value.environmentId, + value: resolvedValue, }; }) ); @@ -62,22 +77,26 @@ export class OrganizationConstantsService implements IOrganizationConstantsServi environmentId: string, type?: OrganizationConstantType ): Promise { - return await dbTransactionWrap(async (manager: EntityManager) => { - const result = await this.organizationConstantRepository.findByEnvironment(organizationId, environmentId); + return dbTransactionWrap(async (manager: EntityManager) => { + const result = await this.organizationConstantRepository.findByEnvironment(organizationId, environmentId, type); - const constantsWithValues = result.map(async (constant) => { - const decryptedValue = await this.organizationConstantsUtilService.decryptSecret( - organizationId, - constant.orgEnvironmentConstantValues[0].value - ); - return { - id: constant.id, - name: constant.constantName, - value: decryptedValue, - }; - }); + return await Promise.all( + result.map(async (constant) => { + const resolvedValue = !(constant.type === OrganizationConstantType.SECRET) + ? await this.organizationConstantsUtilService.decryptSecret( + organizationId, + constant.orgEnvironmentConstantValues[0].value + ) + : secretValue; - return Promise.all(constantsWithValues); + return { + id: constant.id, + name: constant.constantName, + type: constant.type, + value: resolvedValue, + }; + }) + ); }); } diff --git a/server/src/modules/organization-users/service.ts b/server/src/modules/organization-users/service.ts index 77165b19b1..93f8a15a9e 100644 --- a/server/src/modules/organization-users/service.ts +++ b/server/src/modules/organization-users/service.ts @@ -193,138 +193,136 @@ export class OrganizationUsersService implements IOrganizationUsersService { let invalidGroups = []; const emailPattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i; const invalidRoles = []; - await dbTransactionWrap(async (manager: EntityManager) => { - const groupPermissions = ( - await this.groupPermissionsUtilService.getAllGroupByOrganization(currentUser.organizationId) - ).groupPermissions?.filter((gp) => !gp.disabled); - const existingGroups = groupPermissions.map((groupPermission) => groupPermission.name); - csv - .parseString(fileStream.toString(), { - headers: ['first_name', 'last_name', 'email', 'user_role', 'groups', 'metadata'], - renameHeaders: true, - ignoreEmpty: true, - }) - .transform((row: UserCsvRow, next) => { - const groupNames = this.organizationUsersUtilService.createGroupsList(row?.groups); - invalidGroups = [...invalidGroups, ...groupNames.filter((group) => !existingGroups.includes(group))]; - const groups = groupPermissions.filter((group) => groupNames.includes(group.name)).map((group) => group.id); - return next(null, { - ...row, - groups: groups, - user_role: this.organizationUsersUtilService.convertUserRolesCasing(row?.user_role), - userMetadata: row?.metadata ? JSON.parse(row.metadata) : null, - email: row?.email?.toLowerCase(), - }); - }) - .validate(async (data: UserCsvRow, next) => { - await dbTransactionWrap(async (manager: EntityManager) => { - //Check for existing users - let isInvalidRole = false; - - const user = await this.userRepository.findByEmail(data?.email, undefined, undefined, manager); - - if (user?.status === USER_STATUS.ARCHIVED) { - archivedUsers.push(data?.email); - } else if (user?.organizationUsers?.some((ou) => ou.organizationId === currentUser.organizationId)) { - existingUsers.push(data?.email); - } else { - const user = { - firstName: data?.first_name, - lastName: data?.last_name, - email: data?.email, - role: data?.user_role, - groups: data?.groups, - userMetadata: data?.metadata, - }; - users.push(user); - } - - //Check for invalid groups - - if (!Object.values(USER_ROLE).includes(data?.user_role as USER_ROLE)) { - invalidRoles.push(data?.user_role); - isInvalidRole = true; - } - - data.first_name = data.first_name?.trim(); - data.last_name = data.last_name?.trim(); - - const isValidName = data.first_name !== '' || data.last_name !== ''; - return next(null, isValidName && emailPattern.test(data.email) && !isInvalidRole); - }, manager); - }) - .on('data', function () {}) - .on('data-invalid', (row, rowNumber) => { - const invalidField = Object.keys(row).filter((key) => { - if (Array.isArray(row[key])) { - return row[key].length === 0; - } - return !row[key] || row[key] === ''; - }); - invalidRows.push(rowNumber); - invalidFields.add(invalidField); - }) - .on('end', async (rowCount: number) => { - try { - if (rowCount > MAX_ROW_COUNT) { - throw new BadRequestException('Row count cannot be greater than 500'); - } - - if (invalidRows.length) { - const invalidFieldsArray = invalidFields.entries().next().value[1]; - const errorMsg = `Missing ${[invalidFieldsArray.join(',')]} information in ${ - invalidRows.length - } row(s);. No users were uploaded, please update and try again.`; - throw new BadRequestException(errorMsg); - } - - if (invalidGroups.length) { - throw new BadRequestException( - `${invalidGroups.length} group${isPlural(invalidGroups)} doesn't exist. No users were uploaded` - ); - } - - if (invalidRoles.length > 0) { - throw new BadRequestException('Invalid role present for the users'); - } - - if (archivedUsers.length) { - throw new BadRequestException( - `User${isPlural(archivedUsers)} with email ${archivedUsers.join( - ', ' - )} is archived. No users were uploaded` - ); - } - - if (existingUsers.length) { - throw new BadRequestException( - `${existingUsers.length} users with same email already exist. No users were uploaded ` - ); - } - - if (users.length === 0) { - throw new BadRequestException('No users were uploaded'); - } - - if (users.length > 250) { - throw new BadRequestException(`You can only invite 250 users at a time`); - } - - await this.organizationUsersUtilService.inviteUserswrapper(users, currentUser); - res.status(201).send({ message: `${rowCount} user${isPlural(users)} are being added` }); - } catch (error) { - const { status, response } = error; - if (status === 451) { - res.status(status).send({ message: response, statusCode: status }); - return; - } - res.status(status).send(JSON.stringify(response)); - } - }) - .on('error', (error) => { - throw error.message; + const groupPermissions = ( + await this.groupPermissionsUtilService.getAllGroupByOrganization(currentUser.organizationId) + ).groupPermissions?.filter((gp) => !gp.disabled); + const existingGroups = groupPermissions.map((groupPermission) => groupPermission.name); + csv + .parseString(fileStream.toString(), { + headers: ['first_name', 'last_name', 'email', 'user_role', 'groups', 'metadata'], + renameHeaders: true, + ignoreEmpty: true, + }) + .transform((row: UserCsvRow, next) => { + const groupNames = this.organizationUsersUtilService.createGroupsList(row?.groups); + invalidGroups = [...invalidGroups, ...groupNames.filter((group) => !existingGroups.includes(group))]; + const groups = groupPermissions.filter((group) => groupNames.includes(group.name)).map((group) => group.id); + return next(null, { + ...row, + groups: groups, + user_role: this.organizationUsersUtilService.convertUserRolesCasing(row?.user_role), + userMetadata: row?.metadata ? JSON.parse(row.metadata) : null, + email: row?.email?.toLowerCase(), }); - }); + }) + .validate(async (data: UserCsvRow, next) => { + await dbTransactionWrap(async (manager: EntityManager) => { + //Check for existing users + let isInvalidRole = false; + + const user = await this.userRepository.findByEmail(data?.email, undefined, undefined, manager); + + if (user?.status === USER_STATUS.ARCHIVED) { + archivedUsers.push(data?.email); + } else if (user?.organizationUsers?.some((ou) => ou.organizationId === currentUser.organizationId)) { + existingUsers.push(data?.email); + } else { + const user = { + firstName: data?.first_name, + lastName: data?.last_name, + email: data?.email, + role: data?.user_role, + groups: data?.groups, + userMetadata: data?.metadata, + }; + users.push(user); + } + + //Check for invalid groups + + if (!Object.values(USER_ROLE).includes(data?.user_role as USER_ROLE)) { + invalidRoles.push(data?.user_role); + isInvalidRole = true; + } + + data.first_name = data.first_name?.trim(); + data.last_name = data.last_name?.trim(); + + const isValidName = data.first_name !== '' || data.last_name !== ''; + return next(null, isValidName && emailPattern.test(data.email) && !isInvalidRole); + }); + }) + .on('data', function () {}) + .on('data-invalid', (row, rowNumber) => { + const invalidField = Object.keys(row).filter((key) => { + if (Array.isArray(row[key])) { + return row[key].length === 0; + } + return !row[key] || row[key] === ''; + }); + invalidRows.push(rowNumber); + invalidFields.add(invalidField); + }) + .on('end', async (rowCount: number) => { + try { + if (rowCount > MAX_ROW_COUNT) { + throw new BadRequestException('Row count cannot be greater than 500'); + } + + if (invalidRows.length) { + const invalidFieldsArray = invalidFields.entries().next().value[1]; + const errorMsg = `Missing ${[invalidFieldsArray.join(',')]} information in ${ + invalidRows.length + } row(s);. No users were uploaded, please update and try again.`; + throw new BadRequestException(errorMsg); + } + + if (invalidGroups.length) { + throw new BadRequestException( + `${invalidGroups.length} group${isPlural(invalidGroups)} doesn't exist. No users were uploaded` + ); + } + + if (invalidRoles.length > 0) { + throw new BadRequestException('Invalid role present for the users'); + } + + if (archivedUsers.length) { + throw new BadRequestException( + `User${isPlural(archivedUsers)} with email ${archivedUsers.join( + ', ' + )} is archived. No users were uploaded` + ); + } + + if (existingUsers.length) { + throw new BadRequestException( + `${existingUsers.length} users with same email already exist. No users were uploaded ` + ); + } + + if (users.length === 0) { + throw new BadRequestException('No users were uploaded'); + } + + if (users.length > 250) { + throw new BadRequestException(`You can only invite 250 users at a time`); + } + + await this.organizationUsersUtilService.inviteUserswrapper(users, currentUser); + res.status(201).send({ message: `${rowCount} user${isPlural(users)} are being added` }); + } catch (error) { + const { status, response } = error; + if (status === 451) { + res.status(status).send({ message: response, statusCode: status }); + return; + } + res.status(status).send(JSON.stringify(response)); + } + }) + .on('error', (error) => { + throw error.message; + }); } async fetchUsersByValue(organizationId: string, searchInput: string) { diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts index 3ee0f2a02b..1e39e5123c 100644 --- a/server/src/modules/workflows/module.ts +++ b/server/src/modules/workflows/module.ts @@ -28,6 +28,7 @@ import { AppsAbilityFactory } from '@modules/casl/abilities/apps-ability.factory import { WorkflowSchedule } from '@entities/workflow_schedule.entity'; import { App } from '@entities/app.entity'; import { AiModule } from '@modules/ai/module'; +import { DataSourcesRepository } from '@modules/data-sources/repository'; export class WorkflowsModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { const importPath = await getImportPath(configs?.IS_GET_CONTEXT); @@ -95,6 +96,7 @@ export class WorkflowsModule { AppsAbilityFactory, AppsRepository, UserRepository, + DataSourcesRepository, DataQueryRepository, OrganizationConstantRepository, VersionRepository,