Merge branch 'modularisation/v3' into Enhance/shortcuts

This commit is contained in:
devanshu052000 2025-03-07 00:29:16 +05:30
commit 191348efdc
33 changed files with 624 additions and 342 deletions

View file

@ -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 (
<div
className={`config-handle ${customClassName}`}
widget-id={id}
style={{
top: position === 'top' ? '-20px' : widgetTop + widgetHeight - (widgetTop < 10 ? 15 : 10),
visibility:
showHandle && (!isMultipleComponentsSelected || (componentType === 'Modal' && isModalOpen))
? 'visible'
: 'hidden',
top: position === 'top' ? '-20px' : widgetTop + height - (widgetTop < 10 ? 15 : 10),
visibility: _showHandle ? 'visible' : 'hidden',
left: '-1px',
}}
onClick={(e) => {

View file

@ -65,9 +65,3 @@
}
}
}
.main-editor-canvas .widget-target:hover > .config-handle {
visibility: visible !important;
}

View file

@ -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}
>
<div
className={cx('container-fluid rm-container p-0', {

View file

@ -186,5 +186,28 @@
display: none;
}
.moveable-guideline {
background: #97AEFC !important;
opacity: 0.8;
z-index: 9999;
}
/* */
.moveable-guideline.moveable-horizontal {
height: 1px !important;
width: 100% !important;
background: #97AEFC !important;
left: 0 !important;
}
.moveable-guideline.moveable-vertical {
width: 1px !important;
height: 100% !important;
background: #97AEFC !important;
top: 0 !important;
}
.moveable-guideline-group {
z-index: 9999;
}

View file

@ -17,6 +17,8 @@ import {
hasParentWithClass,
getPositionForGroupDrag,
adjustWidth,
hideGridLines,
showGridLines,
} from './gridUtils';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
@ -29,6 +31,7 @@ const RESIZABLE_CONFIG = {
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
};
export const GRID_HEIGHT = 10;
export default function Grid({ gridWidth, currentLayout }) {
const lastDraggedEventsRef = useRef(null);
@ -51,6 +54,48 @@ export default function Grid({ gridWidth, currentLayout }) {
const canvasWidth = NO_OF_GRIDS * gridWidth;
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
const 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}
/>
</>
);

View file

@ -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');
}

View file

@ -89,6 +89,7 @@ const WidgetWrapper = memo(
widgetHeight={layoutData.height}
showHandle={isWidgetActive}
componentType={componentType}
visibility={visibility}
/>
)}
<RenderWidget

View file

@ -249,7 +249,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);
@ -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;

View file

@ -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 = () =
<ElementToRender
renderCopilot={renderCopilot}
key={selectedQuery?.id}
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
pluginSchema={selectedDataSource?.plugin?.operations_file?.data}
selectedDataSource={selectedDataSource}
options={selectedQuery?.options}
optionsChanged={optionsChanged}
@ -281,7 +281,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE;
const docLink = isSampleDb
? 'https://docs.tooljet.com/docs/data-sources/sample-data-sources'
: selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
: selectedDataSource?.plugin_id && selectedDataSource.plugin_id.trim() !== ''
? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
: `https://docs.tooljet.com/docs/data-sources/${selectedDataSource?.kind}`;
return (

View file

@ -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 && <CustomDragLayer size={size} />}
@ -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 (
<div
style={{
position: 'fixed',
pointerEvents: 'none',
zIndex: -1,
left: 0,
top: 0,
zIndex: 1000,
left: canvasBounds?.left || 0,
top: canvasBounds?.top || 0,
height: `${height}px`,
width: `${width}px`,
}}
>
<div
style={{
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
transform: `translate(${x}px, ${y}px)`,
background: '#D9E2FC',
opacity: '0.7',
height: '100%',

View file

@ -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);
}
};

View file

@ -299,7 +299,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },

View file

@ -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'}]",
},
},

View file

@ -45,7 +45,6 @@ export const Container = ({
border: `1px solid ${borderColor}`,
height,
display: isVisible ? 'flex' : 'none',
overflow: 'hidden auto',
position: 'relative',
boxShadow,
};
@ -66,9 +65,7 @@ export const Container = ({
return (
<div
className={`jet-container tw-flex tw-flex-col ${isLoading && 'jet-container-loading'} ${
properties.showHeader && 'jet-container--with-header'
}`}
className={`jet-container widget-type-container ${properties.loadingState && 'jet-container-loading'}`}
id={id}
data-disabled={isDisabled}
style={computedStyles}

View file

@ -329,6 +329,11 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
setCurrentPageHandle(startingPage.handle);
updateFeatureAccess();
setCurrentPageId(startingPage.id, moduleId);
setResolvedPageConstants({
id: startingPage?.id,
handle: startingPage?.handle,
name: startingPage?.name,
});
setComponentNameIdMapping(moduleId);
updateEventsField('events', appData.events);
setCurrentVersionId(appData.editing_version?.id || appData.current_version_id);

View file

@ -1742,7 +1742,10 @@ export const createComponentsSlice = (set, get) => ({
getCustomResolvableReference: (value, parentId, moduleId) => {
const { getParentComponentType } = get();
const parentComponentType = getParentComponentType(parentId, moduleId);
if (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) {
if (
(parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) ||
value === '{{listItem}}'
) {
return { entityType: 'components', entityNameOrId: parentId, entityKey: 'listItem' };
} else if (
parentComponentType === 'Kanban' &&

View file

@ -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(() => {

View file

@ -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);

View file

@ -299,7 +299,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },

View file

@ -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'}]",
},
},

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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
}
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;
}
}

View file

@ -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',
}}
>
<p

View file

@ -299,7 +299,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },

View file

@ -49,7 +49,7 @@ 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'}]",
},
},

View file

@ -6,6 +6,7 @@ import { MODULES } from '@modules/app/constants/modules';
import { dbTransactionWrap } from '@helpers/database.helper';
import { DataSourceScopes, DataSourceTypes } from './constants';
import { GetQueryVariables } from './types';
import { decode } from 'js-base64';
@Injectable()
export class DataSourcesRepository extends Repository<DataSource> {
@ -70,6 +71,23 @@ export class DataSourcesRepository extends Repository<DataSource> {
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')

View file

@ -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: {

View file

@ -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 };

View file

@ -10,9 +10,16 @@ export class OrganizationConstantRepository extends Repository<OrganizationConst
}
// Updated function to find all organization constants by organizationId
async findAllByOrganizationId(organizationId: string) {
async findAllByOrganizationId(organizationId: string, type?: OrganizationConstantType) {
const whereCondition: any = {
organizationId,
};
// Add type filter if provided
if (type) {
whereCondition.type = type;
}
return this.find({
where: { organizationId },
where: whereCondition,
relations: ['orgEnvironmentConstantValues'],
});
}
@ -31,14 +38,21 @@ export class OrganizationConstantRepository extends Repository<OrganizationConst
});
}
async findByEnvironment(organizationId: string, environmentId: string) {
return this.find({
where: {
organizationId,
orgEnvironmentConstantValues: {
environmentId,
},
async findByEnvironment(organizationId: string, environmentId: string, type?: OrganizationConstantType) {
const whereCondition: any = {
organizationId,
orgEnvironmentConstantValues: {
environmentId,
},
};
// Add type filter if provided
if (type) {
whereCondition.type = type;
}
return this.find({
where: whereCondition,
relations: ['orgEnvironmentConstantValues'],
});
}

View file

@ -8,7 +8,7 @@ import { IOrganizationConstantsService } from './interfaces/IService';
import { OrganizationConstantsUtilService } from './util.service';
import { OrganizationConstantType } from './constants';
import { OrganizationConstantRepository } from './repository';
const secretValue = '**********';
@Injectable()
export class OrganizationConstantsService implements IOrganizationConstantsService {
constructor(
@ -23,22 +23,37 @@ export class OrganizationConstantsService implements IOrganizationConstantsServi
type?: OrganizationConstantType
): Promise<OrganizationConstant[]> {
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<any[]> {
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,
};
})
);
});
}

View file

@ -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) {

View file

@ -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<DynamicModule> {
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
@ -95,6 +96,7 @@ export class WorkflowsModule {
AppsAbilityFactory,
AppsRepository,
UserRepository,
DataSourcesRepository,
DataQueryRepository,
OrganizationConstantRepository,
VersionRepository,