diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss
index 7f20210c10..e7322959e5 100644
--- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss
+++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss
@@ -65,9 +65,3 @@
}
}
}
-
-.main-editor-canvas .widget-target:hover > .config-handle {
- visibility: visible !important;
-}
-
-
diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx
index fed1a3db34..fc163939f6 100644
--- a/frontend/src/AppBuilder/AppCanvas/Container.jsx
+++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx
@@ -56,6 +56,11 @@ export const Container = React.memo(
const [{ isOverCurrent }, drop] = useDrop({
accept: 'box',
+ hover: (item) => {
+ item.canvasRef = realCanvasRef?.current;
+ item.canvasId = id;
+ item.canvasWidth = getContainerCanvasWidth();
+ },
drop: async ({ componentType }, monitor) => {
const didDrop = monitor.didDrop();
if (didDrop) return;
@@ -89,14 +94,15 @@ export const Container = React.memo(
function getContainerCanvasWidth() {
if (canvasWidth !== undefined) {
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
- return canvasWidth;
+ if (id === 'canvas') return canvasWidth;
+ return canvasWidth - 2;
}
return realCanvasRef?.current?.offsetWidth;
}
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
useEffect(() => {
- useGridStore.getState().actions.setSubContainerWidths(id, (getContainerCanvasWidth() - 2) / NO_OF_GRIDS);
+ useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canvasWidth, listViewMode, columns]);
@@ -137,8 +143,7 @@ export const Container = React.memo(
}}
style={{
height: id === 'canvas' ? `${canvasHeight}` : '100%',
- // backgroundSize: '25.3953px 10px',
- backgroundSize: `${gridWidth}px 10px`,
+ backgroundSize: `${gridWidth}px ${10}px`,
backgroundColor:
currentMode === 'view'
? computeViewerBackgroundColor(darkMode, canvasBgColor)
@@ -169,6 +174,7 @@ export const Container = React.memo(
data-parentId={id}
canvas-height={canvasHeight}
onClick={handleCanvasClick}
+ component-type={componentType}
>
state.getHoveredComponentForGrid, shallow);
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
+ const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
+ const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow);
+ const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
+ const [dragParentId, setDragParentId] = useState(null);
+ const [elementGuidelines, setElementGuidelines] = useState([]);
+ const componentsSnappedTo = useRef(null);
+ const prevDragParentId = useRef(null);
+ const newDragParentId = useRef(null);
+ const [isGroupDragging, setIsGroupDragging] = useState(false);
+
+ useEffect(() => {
+ const selectedSet = new Set(selectedComponents);
+ const draggingOrResizingId = draggingComponentId || resizingComponentId;
+ const isGrouped = findHighestLevelofSelection().length > 1;
+ const firstSelectedParent =
+ selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null;
+ const selectedParent = dragParentId || firstSelectedParent;
+
+ const guidelines = boxList
+ .filter((box) => {
+ const isVisible =
+ getResolvedValue(box?.component?.definition?.properties?.visibility?.value) ||
+ getResolvedValue(box?.component?.definition?.styles?.visibility?.value);
+
+ // Early return for non-visible elements
+ if (!isVisible) return false;
+
+ if (isGrouped) {
+ // If component is selected, don't show its guidelines
+ if (selectedSet.has(box.id)) return false;
+ return selectedParent ? box.parent === selectedParent : !box.parent;
+ }
+
+ if (draggingOrResizingId) {
+ if (box.id === draggingOrResizingId) return false;
+ return dragParentId ? box.parent === dragParentId : !box.parent;
+ }
+
+ return true;
+ })
+ .map((box) => `.ele-${box.id}`);
+ setElementGuidelines(guidelines);
+ }, [boxList, dragParentId, draggingComponentId, resizingComponentId, selectedComponents, getResolvedValue]);
useEffect(() => {
setBoxList(
@@ -94,7 +141,7 @@ export default function Grid({ gridWidth, currentLayout }) {
boxList.forEach(({ id, height, width, x, y, gw }) => {
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
- y = Math.round(y / 10) * 10;
+ y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
gw = gw ? gw : gridWidth;
const parent = transformedBoxes[id]?.component?.parent;
@@ -117,7 +164,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
setComponentLayout({
[id]: {
- height: height ? height : 10,
+ height: height ? height : GRID_HEIGHT,
width: newWidth ? newWidth : 1,
top: y,
left: Math.round(x / gw),
@@ -319,7 +366,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
// Round y position
- y = Math.max(0, Math.round(y / 10) * 10);
+ y = Math.max(0, Math.round(y / GRID_HEIGHT) * GRID_HEIGHT);
// Adjust height for certain parent components
if (parent) {
const parentElem = document.getElementById(`canvas-${parent}`);
@@ -354,17 +401,16 @@ export default function Grid({ gridWidth, currentLayout }) {
);
// Add event listeners for config handle visibility when hovering over widget boundary
+ // This is needed even though we have hovered widget state because when hovered on boundary,
+ // the hovered widget state is empty, hence created a separate state for boundary
React.useEffect(() => {
const moveableBox = document.querySelector(`.moveable-control-box`);
const showConfigHandle = (e) => {
const targetId = e.target.offsetParent.getAttribute('target-id');
- const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
- configHandle.classList.add('config-handle-visible');
+ useStore.getState().setHoveredComponentBoundaryId(targetId);
};
- const hideConfigHandle = (e) => {
- const targetId = e.target.offsetParent.getAttribute('target-id');
- const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
- configHandle.classList.remove('config-handle-visible');
+ const hideConfigHandle = () => {
+ useStore.getState().setHoveredComponentBoundaryId('');
};
if (moveableBox) {
moveableBox.addEventListener('mouseover', showConfigHandle);
@@ -376,49 +422,10 @@ export default function Grid({ gridWidth, currentLayout }) {
};
}, []);
- const handleDragGridLinesVisibility = (e, events = []) => {
- const { clientX, clientY } = e;
- if (!document.elementFromPoint(clientX, clientY)) return;
-
- const targetElems = document.elementsFromPoint(clientX, clientY);
- const draggedOverElements = targetElems.filter(
- (ele) =>
- !events.some((event) => event.target.id === ele.id) &&
- (ele.classList.contains('target') || ele.classList.contains('real-canvas'))
- );
- const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
- const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
- const appCanvas = document.getElementById('real-canvas');
-
- // Show grid line for main canvas
- draggedOverContainer?.classList.remove('hide-grid');
- draggedOverContainer?.classList.add('show-grid');
-
- // Remove 'show-grid' class from all sub-canvases
- const canvasElms = document.getElementsByClassName('sub-canvas');
- Array.from(canvasElms).forEach((element) => {
- element.classList.remove('show-grid');
- element.classList.add('hide-grid');
- });
-
- // Determine the potential new parent
- const parentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
-
- // Show grid for the appropriate canvas
- if (parentId) {
- const newParentCanvas = document.getElementById('canvas-' + parentId);
- if (newParentCanvas) {
- appCanvas?.classList?.remove('show-grid');
- newParentCanvas?.classList.remove('hide-grid');
- newParentCanvas?.classList.add('show-grid');
- }
- }
-
- useGridStore.getState().actions.setDragTarget(parentId);
- };
-
const handleDragGroupEnd = (e) => {
try {
+ hideGridLines();
+ setIsGroupDragging(false);
const { events, clientX, clientY } = e;
const initialParent = events[0].target.closest('.real-canvas');
// Get potential new parent using same logic as onDragEnd
@@ -477,7 +484,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// Apply transform to return to original position
ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${
- Math.round(_top / 10) * 10
+ Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT
}px)`;
}
});
@@ -514,7 +521,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// Apply grid snapping and bounds
const snappedX = Math.round(posX / _gridWidth) * _gridWidth;
- const snappedY = Math.round(posY / 10) * 10;
+ const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${snappedX}px, ${snappedY}px)`;
return {
@@ -531,6 +538,18 @@ export default function Grid({ gridWidth, currentLayout }) {
}
};
+ React.useEffect(() => {
+ const components = Array.from(document.querySelectorAll('.active-target')).filter(
+ (component) => !selectedComponents.includes(component.getAttribute('widgetid'))
+ );
+ const draggingOrResizing = draggingComponentId || resizingComponentId;
+ if (!draggingOrResizing && components.length > 0) {
+ for (const component of components) {
+ component?.classList?.remove('active-target');
+ }
+ }
+ }, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]);
+
if (mode !== 'edit') return null;
return (
@@ -557,7 +576,7 @@ export default function Grid({ gridWidth, currentLayout }) {
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
if (currentWidget.component?.parent) {
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
- useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent);
+ setDragParentId(currentWidget.component?.parent);
} else {
document.getElementById('real-canvas').classList.add('show-grid');
}
@@ -584,9 +603,6 @@ export default function Grid({ gridWidth, currentLayout }) {
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
- transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
- transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
-
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${e.width}px`;
}
@@ -612,12 +628,12 @@ export default function Grid({ gridWidth, currentLayout }) {
// When clicked on widget boundary/resizer, select the component
setSelectedComponents([e.target.id]);
}
-
+ showGridLines();
if (!isComponentVisible(e.target.id)) {
return false;
}
useGridStore.getState().actions.setResizingComponentId(e.target.id);
- e.setMin([gridWidth, 10]);
+ e.setMin([gridWidth, GRID_HEIGHT]);
}}
onResizeEnd={(e) => {
try {
@@ -629,7 +645,7 @@ export default function Grid({ gridWidth, currentLayout }) {
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid');
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth;
- const height = Math.round(e?.lastEvent?.height / 10) * 10;
+ const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT;
const currentWidth = currentWidget.width * _gridWidth;
const diffWidth = e.lastEvent?.width - currentWidth;
@@ -654,19 +670,17 @@ export default function Grid({ gridWidth, currentLayout }) {
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
- transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
- transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
- const roundedTransformY = Math.round(transformY / 10) * 10;
- transformY = transformY % 10 === 5 ? roundedTransformY - 10 : roundedTransformY;
+ const roundedTransformY = Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT;
+ transformY = transformY % GRID_HEIGHT === 5 ? roundedTransformY - GRID_HEIGHT : roundedTransformY;
e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${
- Math.round(transformY / 10) * 10
+ Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT
}px)`;
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`;
}
if (!maxHeightHit || e.height < e.target.clientHeight) {
- e.target.style.height = `${Math.round(e.lastEvent.height / 10) * 10}px`;
+ e.target.style.height = `${Math.round(e.lastEvent.height / GRID_HEIGHT) * GRID_HEIGHT}px`;
}
const resizeData = {
id: e.target.id,
@@ -682,12 +696,11 @@ export default function Grid({ gridWidth, currentLayout }) {
} catch (error) {
console.error('ResizeEnd error ->', error);
}
- useGridStore.getState().actions.setDragTarget();
+ setDragParentId(null);
toggleCanvasUpdater();
}}
onResizeGroupStart={({ events }) => {
- const parentElm = events[0].target.closest('.real-canvas');
- parentElm.classList.add('show-grid');
+ showGridLines();
}}
onResizeGroup={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
@@ -710,8 +723,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const { events } = e;
const newBoxs = [];
- const parentElm = events[0].target.closest('.real-canvas');
- parentElm.classList.remove('show-grid');
+ hideGridLines();
// TODO: Logic needs to be relooked post go live P2
groupResizeDataRef.current.forEach((ev) => {
@@ -722,9 +734,9 @@ export default function Grid({ gridWidth, currentLayout }) {
let width = Math.round(ev.width / _gridWidth) * _gridWidth;
width = width < _gridWidth ? _gridWidth : width;
let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth;
- let posY = Math.round(ev.drag.translate[1] / 10) * 10;
- let height = Math.round(ev.height / 10) * 10;
- height = height < 10 ? 10 : height;
+ let posY = Math.round(ev.drag.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
+ let height = Math.round(ev.height / GRID_HEIGHT) * GRID_HEIGHT;
+ height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
@@ -752,7 +764,7 @@ export default function Grid({ gridWidth, currentLayout }) {
let posX = currentWidget?.layouts[currentLayout].left * _gridWidth;
let posY = currentWidget?.layouts[currentLayout].top;
let height = currentWidget?.layouts[currentLayout].height;
- height = height < 10 ? 10 : height;
+ height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
@@ -767,6 +779,11 @@ export default function Grid({ gridWidth, currentLayout }) {
}}
checkInput
onDragStart={(e) => {
+ // This is to prevent parent component from being dragged and the stop the propagation of the event
+ if (getHoveredComponentForGrid() !== e.target.id) {
+ return false;
+ }
+ newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
const box = boxList.find((box) => box.id === e.target.id);
// Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS
@@ -779,7 +796,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// to handle their own interactions like column resizing or card dragging
let isDragOnInnerElement = false;
- /* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
+ /* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
Also user dont need to drag an calender from using popup */
if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) {
return false;
@@ -809,10 +826,6 @@ export default function Grid({ gridWidth, currentLayout }) {
return false;
}
}
- // This is to prevent parent component from being dragged and the stop the propagation of the event
- if (getHoveredComponentForGrid() !== e.target.id) {
- return false;
- }
}}
onDragEnd={(e) => {
try {
@@ -820,141 +833,103 @@ export default function Grid({ gridWidth, currentLayout }) {
useGridStore.getState().actions.setDraggingComponentId(null);
isDraggingRef.current = false;
}
+ prevDragParentId.current = null;
+ newDragParentId.current = null;
+ setDragParentId(null);
- if (!e.lastEvent) {
- return;
+ if (!e.lastEvent) return;
+
+ // Build the drag context from the event
+ const dragContext = dragContextBuilder({ event: e, widgets: boxList });
+ const { target, source, dragged } = dragContext;
+
+ const targetSlotId = target?.slotId;
+ const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth;
+
+ // const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || [];
+ // const draggedWidgetType = dragged.widgetType;
+ const isParentChangeAllowed = dragContext.isDroppable;
+
+ // Compute new position
+ let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged);
+
+ const isModalToCanvas = source.isModal && target.slotId === 'real-canvas';
+
+ if (isParentChangeAllowed && !isModalToCanvas) {
+ const parent = target.slotId === 'real-canvas' ? null : target.slotId;
+ // Special case for Modal; If source widget is modal, prevent drops to canvas
+ handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]);
+ } else {
+ const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth;
+
+ left = dragged.left * sourcegridWidth;
+ top = dragged.top;
+
+ !isModalToCanvas ??
+ toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
}
- let draggedOverElemId = boxList.find((box) => box.id === e.target.id)?.parent;
- let draggedOverElemIdType;
- const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
- let draggedOverElem;
- if (document.elementFromPoint(e.clientX, e.clientY) && parentComponent?.component?.component !== 'Modal') {
- const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
- draggedOverElem = targetElems.find((ele) => {
- const isOwnChild = e.target.contains(ele); // if the hovered element is a child of actual draged element its not considered
- if (isOwnChild) return false;
+ // Apply transform for smooth transition
+ e.target.style.transform = `translate(${left}px, ${top}px)`;
- let isDroppable = ele.id !== e.target.id && ele.classList.contains('drag-container-parent');
- if (isDroppable) {
- let widgetId = ele?.getAttribute('component-id') || ele.id;
- let widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
- if (!widgetType) {
- widgetId = widgetId.split('-').slice(0, -1).join('-');
- widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
- }
- if (
- !['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(
- widgetType
- )
- ) {
- isDroppable = false;
- }
- }
- return isDroppable;
- });
- draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id;
- draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type');
- }
-
- const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
- const currentParentId = boxList.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent;
- let left = e.lastEvent?.translate[0];
- let top = e.lastEvent?.translate[1];
- if (
- ['Listview', 'Kanban', 'Container'].includes(
- boxList.find((box) => box.id === draggedOverElemId)?.component?.component
- )
- ) {
- const elemContainer = e.target.closest('.real-canvas');
- const containerHeight = elemContainer.clientHeight;
- const maxY = containerHeight - e.target.clientHeight;
- top = top > maxY ? maxY : top;
- }
-
- const currentWidget = boxList.find(({ id }) => id === e.target.id)?.component?.component;
- const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId;
- draggedOverElemIdType = getComponentTypeFromId(parentId);
- const parentWidget = draggedOverElemIdType === 'Kanban' ? 'Kanban_card' : draggedOverElemIdType;
- const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
- const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
- if (draggedOverElemId !== currentParentId) {
- if (isParentChangeAllowed) {
- const draggedOverWidget = boxList.find((box) => box.id === draggedOverElemId);
-
- let parentWidgetType = boxList.find((box) => box.id === draggedOverElemId)?.component?.component;
- // @TODO - When dropping back to container from canvas, the boxList doesn't have canvas header,
- // boxList will return null. But we need to tell getMouseDistanceFromParentDiv parentWidgetType is container
- // As container id is like 'canvas-2375e23765e-123234'
- if (parentId && !parentWidgetType && draggedOverElemId.includes('-header')) {
- parentWidgetType = 'Container';
- }
-
- let { left: _left, top: _top } = getMouseDistanceFromParentDiv(
- e,
- draggedOverWidget?.component?.component === 'Kanban' ? draggedOverElem : draggedOverElemId,
- parentWidgetType
- );
- left = _left;
- top = _top;
- } else {
- const currBox = boxList.find((l) => l.id === e.target.id);
- left = currBox.left * gridWidth;
- top = currBox.top;
- toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`);
- }
- }
-
- e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${
- Math.round(top / 10) * 10
- }px)`;
- if (draggedOverElemId === currentParentId || isParentChangeAllowed) {
- handleDragEnd([
- {
- id: e.target.id,
- x: left,
- y: Math.round(top / 10) * 10,
- parent: isParentChangeAllowed ? draggedOverElemId : undefined,
- },
- ]);
- }
- const box = boxList.find((box) => box.id === e.target.id);
- //
- setTimeout(() => setSelectedComponents([box.id]));
+ // Select the dragged component after drop
+ setTimeout(() => setSelectedComponents([dragged.id]));
} catch (error) {
- console.log('draggedOverElemId->error', error);
+ console.error('Error in onDragEnd:', error);
}
- // Hide all sub-canvases
- var canvasElms = document.getElementsByClassName('sub-canvas');
- var elementsArray = Array.from(canvasElms);
- elementsArray.forEach(function (element) {
- element.classList.remove('show-grid');
- element.classList.add('hide-grid');
- });
- document.getElementById('real-canvas')?.classList.remove('show-grid');
+ setCanvasBounds({ ...CANVAS_BOUNDS });
+ hideGridLines();
toggleCanvasUpdater();
}}
onDrag={(e) => {
// Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again
if (!isDraggingRef.current) {
useGridStore.getState().actions.setDraggingComponentId(e.target.id);
+ showGridLines();
isDraggingRef.current = true;
}
- const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
+ const currentWidget = boxList.find((box) => box.id === e.target.id);
+ const currentParentId =
+ currentWidget?.component?.parent === null ? 'canvas' : currentWidget?.component?.parent;
+ const _gridWidth = useGridStore.getState().subContainerWidths[dragParentId] || gridWidth;
+ const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current;
- let top = e.translate[1];
- let left = e.translate[0];
+ // Snap to grid
+ let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
+ let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
+
+ // This logic is to handle the case when the dragged element is over a new canvas
+ if (_dragParentId !== currentParentId) {
+ left = e.translate[0];
+ top = e.translate[1];
+ }
// Special case for Modal
- if (parentComponent?.component?.component === 'Modal') {
- const elemContainer = e.target.closest('.real-canvas');
- const containerHeight = elemContainer.clientHeight;
- const containerWidth = elemContainer.clientWidth;
- const maxY = containerHeight - e.target.clientHeight;
- const maxLeft = containerWidth - e.target.clientWidth;
+ const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
+ const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
+ const parentComponent = boxList.find((box) => box.id === parentId);
+ const parentWidgetType = parentComponent?.component?.component;
+ const isOnHeaderOrFooter = oldParentId
+ ? oldParentId.includes('-header') || oldParentId.includes('-footer')
+ : false;
+ const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter;
+ const isParentNewModal = parentComponent?.component?.component === 'ModalV2';
+ const isParentLegacyModal = parentComponent?.component?.component === 'Modal';
+ const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
- top = top < 0 ? 0 : top > maxY ? maxY : top;
- left = left < 0 ? 0 : left > maxLeft ? maxLeft : left;
+ if (isParentModal) {
+ const modalContainer = e.target.closest('.tj-modal-widget-content');
+ const mainCanvas = document.getElementById('real-canvas');
+
+ const mainRect = mainCanvas.getBoundingClientRect();
+ const modalRect = modalContainer.getBoundingClientRect();
+ const relativePosition = {
+ top: modalRect.top - mainRect.top,
+ right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
+ bottom: modalRect.height + (modalRect.top - mainRect.top),
+ left: modalRect.left - mainRect.left,
+ };
+ setCanvasBounds({ ...relativePosition });
}
e.target.style.transform = `translate(${left}px, ${top}px)`;
@@ -963,8 +938,32 @@ export default function Grid({ gridWidth, currentLayout }) {
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
);
- handleDragGridLinesVisibility(e, [{ target: e.target }]);
+ // This block is to show grid lines on the canvas when the dragged element is over a new canvas
+ if (document.elementFromPoint(e.clientX, e.clientY)) {
+ const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
+ const draggedOverElements = targetElems.filter(
+ (ele) =>
+ (ele.id !== e.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas')
+ );
+ const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
+ const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
+ // Determine potential new parent
+ let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
+
+ if (newParentId === e.target.id) {
+ newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent;
+ } else if (parentComponent?.component?.component === 'Modal') {
+ // Never update parentId for Modal
+ newParentId = parentComponent?.id;
+ }
+
+ if (newParentId !== prevDragParentId.current) {
+ setDragParentId(newParentId === 'canvas' ? null : newParentId);
+ newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
+ prevDragParentId.current = newParentId;
+ }
+ }
// Postion ghost element exactly as same at dragged element
if (document.getElementById(`moveable-drag-ghost`)) {
document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`;
@@ -979,31 +978,26 @@ export default function Grid({ gridWidth, currentLayout }) {
parentElm?.classList?.add('show-grid');
}
- handleDragGridLinesVisibility(ev, events);
-
events.forEach((ev) => {
- let left = ev.translate[0];
- let top = ev.translate[1];
+ const currentWidget = boxList.find(({ id }) => id === ev.target.id);
+ const _gridWidth =
+ useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
+
+ let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth;
+ let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${left}px, ${top}px)`;
});
updateNewPosition(events);
}}
onDragGroupStart={({ events }) => {
- const parentElm = events[0]?.target?.closest('.real-canvas');
- parentElm?.classList?.add('show-grid');
+ showGridLines();
+ setIsGroupDragging(true);
}}
onDragGroupEnd={(e) => {
handleDragGroupEnd(e);
toggleCanvasUpdater();
}}
- //snap settgins
- snappable={true}
- snapThreshold={10}
- isDisplaySnapDigit={false}
- bounds={CANVAS_BOUNDS}
- displayAroundControls={true}
- controlPadding={20}
onClickGroup={(e) => {
const targetId =
e.inputEvent.target.id || e.inputEvent.target.closest('.moveable-box')?.getAttribute('widgetid');
@@ -1019,6 +1013,43 @@ export default function Grid({ gridWidth, currentLayout }) {
}
}
}}
+ //snap settgins
+ snappable={true}
+ snapGap={false}
+ isDisplaySnapDigit={false}
+ snapThreshold={GRID_HEIGHT}
+ bounds={canvasBounds}
+ // Guidelines configuration
+ elementGuidelines={elementGuidelines}
+ snapDirections={{
+ top: true,
+ right: true,
+ bottom: true,
+ left: true,
+ center: false,
+ middle: false,
+ }}
+ elementSnapDirections={{
+ top: true,
+ left: true,
+ bottom: true,
+ right: true,
+ center: false,
+ middle: false,
+ }}
+ onSnap={(e) => {
+ const components = e.elements;
+ if (isArray(componentsSnappedTo.current)) {
+ for (const component of componentsSnappedTo.current) {
+ component?.element?.classList?.remove('active-target');
+ }
+ }
+ componentsSnappedTo.current = components;
+ for (const component of components) {
+ component.element.classList.add('active-target');
+ }
+ }}
+ snapGridAll={true}
/>
>
);
diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
index 817f9a5ca9..2889fc06db 100644
--- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
+++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js
@@ -291,6 +291,7 @@ export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
? document.getElementById(id)
: id
: document.getElementsByClassName('real-canvas')[0];
+ parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id);
if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') {
parentDiv = document.getElementById('canvas-' + id);
}
@@ -391,3 +392,25 @@ export function hasParentWithClass(child, className) {
return false;
}
+
+export function showGridLines() {
+ var canvasElms = document.getElementsByClassName('sub-canvas');
+ var elementsArray = Array.from(canvasElms);
+ elementsArray.forEach(function (element) {
+ element.classList.remove('hide-grid');
+ element.classList.add('show-grid');
+ });
+ document.getElementById('real-canvas')?.classList.remove('hide-grid');
+ document.getElementById('real-canvas')?.classList.add('show-grid');
+}
+
+export function hideGridLines() {
+ var canvasElms = document.getElementsByClassName('sub-canvas');
+ var elementsArray = Array.from(canvasElms);
+ elementsArray.forEach(function (element) {
+ element.classList.remove('show-grid');
+ element.classList.add('hide-grid');
+ });
+ document.getElementById('real-canvas')?.classList.remove('show-grid');
+ document.getElementById('real-canvas')?.classList.add('hide-grid');
+}
diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
new file mode 100644
index 0000000000..a9405d043e
--- /dev/null
+++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
@@ -0,0 +1,266 @@
+/**
+ * Drag Context Breakdown:
+ *
+ * This object encapsulates all relevant details about a drag event,
+ * grouping the **source (where the widget came from)** and **target (where it's being dropped)**.
+ *
+ * Core Concepts:
+ * - `draggedWidget` → The widget being dragged (`e.target`).
+ * - `sourceSlot` → The original parent container of `draggedWidget`.
+ * - This could be a **header, footer, or a sub-container (like a container body)**.
+ * - `targetSlot` → The new parent container where `draggedWidget` is dropped.
+ * - `sourceWidget` → The **widget that owns** `sourceSlot` (its direct parent).
+ * - `targetWidget` → The **widget that owns** `targetSlot` (its direct parent).
+ *
+ * These entities are structured into a **contextual grouping**, allowing for easy access:
+ *
+ * {
+ * source: {
+ * widget: sourceWidget, // The original widget that holds the source slot.
+ * slot: sourceSlot, // The slot where the widget was initially located.
+ * id: sourceWidget.id, // Unique identifier of the source widget.
+ * slotId: sourceSlot.id, // Unique identifier of the source slot.
+ *
+ * isModal: computed function, // Checks if sourceWidget is a Modal.
+ * slotType: computed function, // Determines if the slot is a header, footer, or body.
+ * widgetType: computed function, // Returns the type of the widget (e.g., Table, Form, etc.).
+ * },
+ *
+ * target: {
+ * widget: targetWidget, // The new widget where the dragged widget is being placed.
+ * slot: targetSlot, // The slot inside `targetWidget` where the drop is happening.
+ * id: targetWidget.id, // Unique identifier of the target widget.
+ * slotId: targetSlot.id, // Unique identifier of the target slot.
+ *
+ * isModal: computed function, // Checks if targetWidget is a Modal.
+ * slotType: computed function, // Determines if the slot is a header, footer, or body.
+ * widgetType: computed function, // Returns the type of the target widget.
+ * }
+ * }
+ *
+ * Additional Checks:
+ * - `isSourceModal` → **Is the source inside a modal?**
+ * - `isTargetModal` → **Is the target inside a modal?**
+ * - `isDraggingToModalSlots` → **Is the widget being dragged into a modal slot (header/footer)?**
+ * - `targetSlotType` → **Determines whether the drop is happening in a header, footer, or body.**
+ *
+ * Why This Matters?
+ * - This structure helps **validate and restrict movements**, ensuring widgets follow UI constraints.
+ * - Prevents invalid drops (e.g., putting a button inside a Table component).
+ * - Enables **modular and flexible** widget movement across different UI sections.
+ */
+import { getMouseDistanceFromParentDiv } from '../gridUtils';
+import {
+ RESTRICTED_WIDGETS_CONFIG,
+ RESTRICTED_WIDGET_SLOTS_CONFIG,
+} from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig';
+
+const CANVAS_ID = 'canvas';
+const REAL_CANVAS_ID = 'real-canvas';
+
+/**
+ * Represents the widget being dragged.
+ *
+ * This class encapsulates all necessary information about the dragged widget,
+ * including its type, position, and whether it is allowed to move into certain areas.
+ */
+export class DragEntity {
+ constructor(widget) {
+ this.widget = widget; // The widget object being dragged
+ this.id = widget?.id || null; // Unique ID of the dragged widget
+ this.left = widget.left; // Initial X position (relative to grid)
+ this.top = widget.top; // Initial Y position (relative to grid)
+ }
+
+ get widgetType() {
+ return this.widget?.component?.component || null;
+ }
+}
+
+/**
+ * Defines a **droppable area** in the canvas.
+ *
+ * A droppable area is a container that can accept dragged widgets.
+ * This class helps determine if a slot is valid and handles various properties like modals.
+ */
+export class DropAreaEntity {
+ static dropAreaWidgets = ['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'ModalV2', 'Listview', 'Container', 'Table'];
+
+ constructor(widget, slotId) {
+ this.widget = widget; // The widget that owns this slot
+ this.id = widget?.id || CANVAS_ID; // ID of the widget
+ this.slotId = slotId || REAL_CANVAS_ID; // ID of the slot where the widget is located
+ }
+
+ // Checks if the widget is a modal
+ get isModal() {
+ return ['Modal', 'ModalV2'].includes(this.widget?.component?.component);
+ }
+
+ // Checks if the widget is the new version of modal
+ get isNewModal() {
+ return this.widget?.component?.component === 'ModalV2';
+ }
+
+ // Checks if the widget is the legacy modal
+ get isLegacyModal() {
+ return this.widget?.component?.component === 'Modal';
+ }
+
+ // Determines if the slot belongs to a modal's header/footer
+ get isInModalSlot() {
+ return this.isNewModal && this.isOnCustomSlot;
+ }
+
+ // Identifies if the slot is a custom slot (e.g., modal header/footer)
+ get isOnCustomSlot() {
+ return this.slotId.includes('-header') || this.slotId.includes('-footer');
+ }
+
+ // Determines if the slot is a valid drop target
+ get isDroppable() {
+ return DropAreaEntity.dropAreaWidgets.includes(this.widgetType);
+ }
+
+ // Returns the type of slot (header, footer, body, etc.)
+ get slotType() {
+ return this.slotId ? this.slotId.split('-').pop() : CANVAS_ID;
+ }
+
+ // Returns the type of the widget inside the slot
+ get widgetType() {
+ return this.widget?.component?.component || CANVAS_ID;
+ }
+}
+
+/**
+ * Represents the **dragging context**, encapsulating information
+ * about the source, target, and the dragged widget.
+ *
+ * This helps determine:
+ * - Whether the move is valid
+ * - Where the widget should be placed
+ * - Any restrictions based on parent-child relationships
+ */
+export class DragContext {
+ constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets }) {
+ const sourceWidgetId = sourceSlotId?.slice(0, 36);
+ const sourceWidget = getWidgetById(widgets, sourceWidgetId);
+
+ const targetWidgetId = targetSlotId?.slice(0, 36);
+ const targetWidget = getWidgetById(widgets, targetWidgetId);
+
+ const draggedWidget = getWidgetById(widgets, draggedWidgetId);
+
+ this.source = new DropAreaEntity(sourceWidget, sourceSlotId);
+ this.target = new DropAreaEntity(targetWidget, targetSlotId);
+ this.dragged = new DragEntity(draggedWidget);
+ this.widgets = widgets;
+ }
+
+ /**
+ * Updates the **target slot** dynamically as the drag event progresses.
+ */
+ updateTarget(targetSlotId) {
+ const targetWidgetId = targetSlotId?.slice(0, 36);
+ const targetWidget = getWidgetById(this.widgets, targetWidgetId);
+ this.target = new DropAreaEntity(targetWidget, targetSlotId);
+ }
+
+ get isDroppable() {
+ const { dragged, target } = this;
+
+ const restrictedWidgetsOnTarget = RESTRICTED_WIDGETS_CONFIG?.[target.widgetType] || [];
+ const restrictedWidgetsOnTargetSlot = RESTRICTED_WIDGET_SLOTS_CONFIG?.[target.slotType] || [];
+
+ const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot];
+ return !restrictedWidgets.includes(dragged.widgetType);
+ ß;
+ }
+}
+
+/**
+ * Constructs the **dragging context** by gathering all relevant details from the event.
+ */
+export function dragContextBuilder({ event, widgets }) {
+ const draggedWidgetId = event.target.id;
+ const draggedWidget = getWidgetById(widgets, draggedWidgetId);
+ const sourceSlotId = draggedWidget.parent;
+
+ // Initialize drag context
+ const context = new DragContext({ widgets, draggedWidgetId, sourceSlotId, targetSlotId: sourceSlotId });
+
+ // Determine the potential drop target
+ const targetSlotId = getDroppableSlotIdOnScreen(event, widgets);
+ context.updateTarget(targetSlotId);
+
+ return context;
+}
+
+/**
+ * Given an event, finds the **nearest valid droppable slot**.
+ */
+export const getDroppableSlotIdOnScreen = (event, widgets) => {
+ const [slotId] = document
+ .elementsFromPoint(event.clientX, event.clientY)
+ .filter(
+ (ele) =>
+ !event.target.contains(ele) && ele.id !== event.target.id && ele.classList.contains('drag-container-parent')
+ )
+ .map((ele) => extractSlotId(ele))
+ .filter((slotId) => {
+ const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID;
+ return DropAreaEntity.dropAreaWidgets.includes(widgetType);
+ });
+
+ return slotId;
+};
+
+/**
+ * Finds a widget by its ID.
+ */
+export function getWidgetById(boxList, targetId) {
+ return boxList.find((box) => box.id === targetId) ?? null;
+}
+
+/**
+ * Extracts the **slot ID** from a given DOM element.
+ */
+const extractSlotId = (element) => {
+ return element?.getAttribute('component-id') || element.id.replace(/^canvas-/, '');
+};
+
+/**
+ * Computes the final (left, top) position for a dragged widget based on grid snapping and drop conditions.
+ *
+ * @param {Object} event - Drag event object containing movement data.
+ * @param {DropAreaEntity} target - The target drop area entity (where widget is dropped).
+ * @param {boolean} isParentChangeAllowed - Whether the widget can move to the target.
+ * @param {number} gridWidth - The width of the grid for alignment.
+ * @param {DragEntity} dragged - The entity being dragged.
+ * @returns {Object} { left, top } - The computed position.
+ */
+export const getAdjustedDropPosition = (event, target, isParentChangeAllowed, gridWidth, dragged) => {
+ let left = event.lastEvent?.translate[0];
+ let top = event.lastEvent?.translate[1];
+
+ if (isParentChangeAllowed) {
+ // Compute the relative position inside the new container
+ const { left: adjustedLeft, top: adjustedTop } = getMouseDistanceFromParentDiv(
+ event,
+ target.slotId,
+ target.widgetType
+ );
+
+ return {
+ left: Math.round(adjustedLeft / gridWidth) * gridWidth, // Snap to the nearest grid column
+ top: Math.round(adjustedTop / 10) * 10, // Snap to the nearest 10px
+ };
+ }
+
+ // If movement is restricted, revert to original position
+ return {
+ left: dragged.left * gridWidth,
+ top: dragged.top,
+ };
+};
diff --git a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx
index 34c172292d..f583ebb53a 100644
--- a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx
+++ b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx
@@ -89,6 +89,7 @@ const WidgetWrapper = memo(
widgetHeight={layoutData.height}
showHandle={isWidgetActive}
componentType={componentType}
+ visibility={visibility}
/>
)}
{
- const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child;
+ const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles, slotName } =
+ child;
const componentMeta = deepClone(componentTypes.find((component) => component.component === componentName));
const componentData = JSON.parse(JSON.stringify(componentMeta));
@@ -139,7 +140,12 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
}
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
- const _parent = getParentComponentIdByType(child, parentMeta.component, parentId);
+ const _parent = getParentComponentIdByType({
+ child,
+ parentComponent: parentMeta.component,
+ parentId,
+ slotName,
+ });
const newChildComponent = {
id: uuidv4(),
@@ -199,7 +205,8 @@ export const getAllChildComponents = (allComponents, parentId) => {
allComponents[parentId]?.component?.component === 'Tabs' ||
allComponents[parentId]?.component?.component === 'Calendar' ||
allComponents[parentId]?.component?.component === 'Kanban' ||
- allComponents[parentId]?.component?.component === 'Container';
+ allComponents[parentId]?.component?.component === 'Container' ||
+ allComponents[parentId]?.component?.component === 'ModalV2';
if (componentParentId && isParentTabORCalendar) {
let childComponent = deepClone(allComponents[componentId]);
@@ -249,7 +256,6 @@ export const copyComponents = ({ isCut = false, isCloning = false }) => {
const parentComponentId = isChildOfTabsOrCalendar(selectedComponent, allComponents)
? selectedComponent.component.parent.split('-').slice(0, -1).join('-')
: selectedComponent?.component?.parent;
-
if (parentComponentId) {
// Check if the parent component is also selected
const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId);
@@ -320,7 +326,8 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
return (
parentComponent.component.component === 'Tabs' ||
parentComponent.component.component === 'Calendar' ||
- parentComponent.component.component === 'Container'
+ parentComponent.component.component === 'Container' ||
+ parentComponent.component.component === 'ModalV2'
);
}
@@ -483,11 +490,14 @@ export function pasteComponents(targetParentId, copiedComponentObj) {
// Prevent pasting if the parent subcontainer was deleted during a cut operation
if (
targetParentId &&
+ // Check if targetParentId is deleted from the components
!Object.keys(components).find(
(key) =>
targetParentId === key ||
(components?.[key]?.component.component === 'Tabs' &&
- targetParentId?.split('-')?.slice(0, -1)?.join('-') === key)
+ targetParentId?.split('-')?.slice(0, -1)?.join('-') === key) ||
+ (['Container', 'Form', 'Modal'].includes(components?.[key]?.component.component) &&
+ ['header', 'footer'].some((section) => targetParentId.includes(section)))
)
) {
return;
@@ -655,10 +665,23 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
return canvasBgColor;
};
-export const getParentComponentIdByType = (child, parentComponent, parentId) => {
+export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => {
const { tab } = child;
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
- else if (parentComponent === 'Container') return `${parentId}-header`;
+ else if (parentComponent === 'Container' || parentComponent === 'ModalV2') {
+ return `${parentId}-${slotName}`;
+ }
return parentId;
};
+
+export const getParentWidgetFromId = (parentType, parentId) => {
+ const isAddingToSlot = parentId?.includes('-header') || parentId?.includes('-footer');
+
+ if (parentType === 'ModalV2' && isAddingToSlot) {
+ return 'ModalSlot';
+ } else if (parentType === 'Kanban') {
+ return 'Kanban_card';
+ }
+ return parentType;
+};
diff --git a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx
index b9fb85fcae..099cd3763a 100644
--- a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx
+++ b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx
@@ -5,19 +5,13 @@ const MIN_TABLE_ROW_HEIGHT_DEFAULT = 45;
const TableRowHeightInput = ({ value, onChange, cyLabel, staticText, styleDefinition }) => {
const [inputValue, setInputValue] = useState(value);
+ const minValue =
+ styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT;
+
useEffect(() => {
setInputValue(value < minValue ? minValue : value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, styleDefinition.cellSize?.value]);
- useEffect(() => {
- onChange(
- styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT
- );
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const minValue =
- styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT;
const handleBlur = () => {
const newValue = Math.max(inputValue, minValue);
diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx
index 033d266e03..f95baaa328 100644
--- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx
+++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx
@@ -300,10 +300,10 @@ const MultiLineCodeEditor = (props) => {
editable={editable} //for transformations in query manager
onCreateEditor={(view) => setEditorView(view)}
onUpdate={(view) => {
- const icon = document.querySelector('.codehinter-search-btn-wrapper');
+ const icon = document.querySelector('.codehinter-search-btn');
if (searchPanelOpen(view.state)) {
- icon.style.top = '44px';
- } else icon.style.top = '0px';
+ icon.style.display = 'none';
+ } else icon.style.display = 'block';
}}
/>
diff --git a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx
index 2b807f718b..28f7451b95 100644
--- a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx
+++ b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable import/no-unresolved */
import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import {
@@ -9,12 +10,13 @@ import {
replaceNext,
replaceAll,
openSearchPanel,
- // eslint-disable-next-line import/no-unresolved
} from '@codemirror/search';
import './SearchBox.scss';
import InputComponent from '@/components/ui/Input/Index.jsx';
import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
import { ToolTip } from '@/_components/ToolTip';
+import { SelectionRange } from '@codemirror/state';
+import { useHotkeys } from 'react-hotkeys-hook';
export const handleSearchPanel = (view) => {
const dom = document.createElement('div');
@@ -35,6 +37,11 @@ function SearchPanel({ view }) {
replace: replaceTerm,
});
view.dispatch({ effects: setSearchQuery.of(query) });
+
+ const currentPos = view.state.selection.main.head;
+ view.dispatch({
+ selection: SelectionRange.create(currentPos, currentPos),
+ });
};
useEffect(() => {
@@ -44,12 +51,28 @@ function SearchPanel({ view }) {
return () => clearTimeout(handler);
}, [searchText, replaceText]);
+ const [shortcutEnabled, setShortcutEnabled] = useState(false);
+
+ // Shortcuts for search input field
+ useHotkeys(
+ ['shift+enter', 'enter'],
+ (event, handler) => {
+ if (handler.shift && handler.keys[0] === 'enter') findPrevious(view);
+ else if (handler.keys[0] === 'enter') findNext(view);
+ },
+ {
+ enabled: shortcutEnabled,
+ enableOnFormTags: true,
+ }
+ );
+
const displaySearchField = () => (
setSearchText(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && findNext(view)}
+ onFocus={() => setShortcutEnabled(true)}
+ onBlur={() => setShortcutEnabled(false)}
placeholder="Find"
size="small"
value={searchText}
diff --git a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx
index 4edd8a0fda..625b11dedd 100644
--- a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx
+++ b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx
@@ -150,7 +150,7 @@ const TJDBCodeEditor = (props) => {
className="cm-codehinter position-relative"
style={{
width: '100%',
- height: isOpen ? '350p' : 'auto',
+ height: isOpen ? '350px' : 'auto',
}}
>
@@ -167,14 +167,14 @@ const TJDBCodeEditor = (props) => {
componentName={componentName}
key={componentName}
forceUpdate={forceUpdate}
- optionalProps={{ styles: { height: 300 }, cls: '' }}
+ optionalProps={{ styles: { height: 300 }, cls: 'tjdb-hinter-portal' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal tjdb-portal-codehinter' }}
dragResizePortal={true}
callgpt={null}
>
-
+
{
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]);
- const handleNodeExpansion = (path) => {
+ const handleNodeExpansion = (path, data, currentNode) => {
if (pathToBeInspected && path?.length > 0) {
- return pathToBeInspected.includes(path[path.length - 1]);
+ const shouldExpand = pathToBeInspected.includes(path[path.length - 1]);
+
+ // Scroll to the component in the inspector
+ if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) {
+ const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`);
+ if (target) {
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ }
+
+ return shouldExpand;
} else return false;
};
diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js
index 7067cd540d..937fe45b2c 100644
--- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js
+++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js
@@ -9,6 +9,7 @@ const useCallbackActions = () => {
const currentPageComponents = useStore((state) => state?.getCurrentPageComponents(), shallow);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const runQuery = useStore((state) => state.queryPanel.runQuery);
+ const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll);
const handleRemoveComponent = (component) => {
deleteComponents([component.id]);
@@ -30,30 +31,22 @@ const useCallbackActions = () => {
return toast.success('Copied to the clipboard', { position: 'top-center' });
};
- const autoScrollTo = (id) => {
- setSelectedComponents([id]);
- const target = document.getElementById(id);
- target.scrollIntoView({ behavior: 'smooth', block: 'center' });
- };
-
const handleAutoScrollToComponent = (data) => {
- const currentPageComponents = useStore.getState().getCurrentPageComponents();
- const component = currentPageComponents?.[data.id];
-
- let parentId = component?.component?.parent;
- if (parentId) {
- const regex = /-\d+$/;
- if (regex.test(parentId)) {
- parentId = parentId.replace(regex, ''); // To get parentId without tab index if parent type is Tab
- }
- const parentType = currentPageComponents?.[parentId]?.component?.component;
- if (parentType && (parentType === 'Modal' || parentType === 'Tabs')) {
- autoScrollTo(parentId); // To scroll to parent component if parent type is Modal or Tabs
- return;
- }
+ const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(data.id);
+ if (!isAccessible) {
+ if (isOnCanvas) {
+ toast.success(
+ `This component can't be opened because it's on the main canvas. Close ${computedComponentId} and click "Go to component" to view it there`
+ );
+ } else
+ toast.success(
+ `This component can't be opened because it's inside ${computedComponentId}. Open ${computedComponentId} and click "Go to component"to view it.`
+ );
+ return;
}
-
- autoScrollTo(data.id);
+ setSelectedComponents([computedComponentId]);
+ const target = document.getElementById(computedComponentId);
+ target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
};
const callbackActions = [
diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx
index d40dc44f74..9e6737f41c 100644
--- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx
+++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx
@@ -45,15 +45,15 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
const queryName = selectedQuery?.name ?? '';
const sourcecomponentName = selectedDataSource?.kind?.charAt(0).toUpperCase() + selectedDataSource?.kind?.slice(1);
- const ElementToRender = selectedDataSource?.pluginId ? source : allSources[sourcecomponentName];
+ const ElementToRender = selectedDataSource?.plugin_id ? source : allSources[sourcecomponentName];
const defaultOptions = useRef({});
const isFreezed = useStore((state) => state.getShouldFreeze());
useEffect(() => {
setDataSourceMeta(
- selectedQuery?.pluginId
- ? selectedQuery?.manifestFile?.data?.source
+ selectedQuery?.plugin_id
+ ? selectedQuery?.manifest_file?.data?.source
: DataSourceTypes.find((source) => source.kind === selectedQuery?.kind)
);
setSelectedQueryId(selectedQuery?.id);
@@ -188,7 +188,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
{
+export const BaseUrl = ({ dataSourceURL, theme, className = 'col-auto', style = {} }) => {
return (
- {dataSourceURL}
+
+ {dataSourceURL}
+
);
};
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx
index feaeadcb23..4c89ad849e 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx
@@ -28,7 +28,10 @@ class Restapi extends React.Component {
this.state = {
options,
+ codeHinterHeight: 32, // Default height
};
+ this.codeHinterRef = React.createRef();
+ this.resizeObserver = null;
}
componentDidUpdate(prevProps) {
@@ -40,21 +43,95 @@ class Restapi extends React.Component {
},
});
}
+ // Setup resize observer if it's not already set up
+ if (this.codeHinterRef.current && !this.resizeObserver) {
+ this.setupResizeObserver();
+ }
}
componentDidMount() {
try {
+ if (isEmpty(this.state.options['headers'])) {
+ this.addNewKeyValuePair('headers');
+ }
+ if (isEmpty(this.state.options['cookies'])) {
+ this.addNewKeyValuePair('cookies');
+ }
if (isEmpty(this.state.options['method'])) {
changeOption(this, 'method', 'get');
}
+ setTimeout(() => {
+ if (isEmpty(this.state.options['url_params'])) {
+ this.addNewKeyValuePair('url_params');
+ }
+ }, 1000);
+ setTimeout(() => {
+ if (isEmpty(this.state.options['body'])) {
+ this.addNewKeyValuePair('body');
+ }
+ }, 1000);
setTimeout(() => {
this.initizalizeRetryNetworkErrorsToggle();
}, 1000);
+
+ this.setupResizeObserver();
} catch (error) {
console.log(error);
}
}
+ componentWillUnmount() {
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ }
+ }
+
+ setupResizeObserver() {
+ if (!this.codeHinterRef.current) return;
+
+ // Try to find the editor element, checking multiple possible selectors
+ const findEditorElement = () => {
+ const element =
+ this.codeHinterRef.current.querySelector('.cm-editor') ||
+ this.codeHinterRef.current.querySelector('.codehinter-input') ||
+ this.codeHinterRef.current.querySelector('.code-hinter-wrapper');
+ return element;
+ };
+
+ // Initial attempt to find editor
+ let editorElement = findEditorElement();
+
+ // If not found immediately, try again after a short delay
+ if (!editorElement) {
+ setTimeout(() => {
+ editorElement = findEditorElement();
+ if (editorElement) {
+ this.setupObserverForElement(editorElement);
+ }
+ }, 100);
+ return;
+ }
+
+ this.setupObserverForElement(editorElement);
+ }
+
+ setupObserverForElement(element) {
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ }
+
+ this.resizeObserver = new ResizeObserver((entries) => {
+ for (let entry of entries) {
+ const height = Math.max(32, Math.min(entry.contentRect.height, 220));
+ if (height !== this.state.codeHinterHeight) {
+ this.setState({ codeHinterHeight: height });
+ }
+ }
+ });
+
+ this.resizeObserver.observe(element);
+ }
+
initizalizeRetryNetworkErrorsToggle = () => {
const isRetryNetworkErrorToggleUnused = this.props.options.retry_network_errors === null;
if (isRetryNetworkErrorToggleUnused) {
@@ -212,13 +289,30 @@ class Restapi extends React.Component {
useCustomStyles={true}
/>
-
+
URL
-
+
{dataSourceURL && (
-
+
)}
-
+
{
>
{
{
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
index 95d2ab56e0..23d1c7f7cf 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
@@ -214,4 +214,9 @@
.input-value-padding {
box-sizing: border-box;
padding-right: 30px !important;
+}
+
+.react-datepicker__navigation{
+ overflow: visible !important;
+ height: inherit !important;
}
\ No newline at end of file
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx
index e3f31c974d..e873228888 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx
@@ -677,6 +677,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
}}
componentName="TooljetDatabase"
delayOnChange={false}
+ className="w-100"
/>
)}
diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
index ef70343140..2ad7977496 100644
--- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
@@ -133,7 +133,7 @@ export const ComponentsManagerTab = ({ darkMode }) => {
'StarRating',
];
const integrationItems = ['Map'];
- const layoutItems = ['Container', 'Listview', 'Tabs', 'Modal'];
+ const layoutItems = ['Container', 'Listview', 'Tabs', 'ModalV2'];
filteredComponents.forEach((f) => {
if (commonItems.includes(f)) commonSection.items.push(f);
diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
index 7450011b93..77274cd658 100644
--- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
@@ -2,12 +2,14 @@ import React, { useEffect } from 'react';
import { WidgetBox } from '../WidgetBox';
import { useDrag, useDragLayer } from 'react-dnd';
import { getEmptyImage } from 'react-dnd-html5-backend';
+import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils';
+import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants';
export const DragLayer = ({ index, component }) => {
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: 'box',
- item: { componentType: component.component },
+ item: { componentType: component.component, component },
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
}),
[component.component]
@@ -18,7 +20,6 @@ export const DragLayer = ({ index, component }) => {
}, []);
const size = component.defaultSize || { width: 30, height: 40 };
-
return (
<>
{isDragging && }
@@ -30,32 +31,39 @@ export const DragLayer = ({ index, component }) => {
};
const CustomDragLayer = ({ size }) => {
- const { currentOffset } = useDragLayer((monitor) => ({
+ const { currentOffset, item } = useDragLayer((monitor) => ({
currentOffset: monitor.getSourceClientOffset(),
+ item: monitor.getItem(),
}));
if (!currentOffset) return null;
- const canvasWidth = document.getElementsByClassName('real-canvas')[0]?.getBoundingClientRect()?.width;
-
+ const canvasWidth = item?.canvasWidth;
+ const canvasBounds = item?.canvasRef?.getBoundingClientRect();
const height = size.height;
- const width = (canvasWidth * size.width) / 43;
+ const width = (canvasWidth * size.width) / NO_OF_GRIDS;
+
+ // Calculate position relative to the current canvas (parent or child)
+ const left = currentOffset.x - (canvasBounds?.left || 0);
+ const top = currentOffset.y - (canvasBounds?.top || 0);
+
+ const [x, y] = snapToGrid(canvasWidth, left, top);
return (
{
@@ -151,7 +153,8 @@ export const baseComponentProperties = (
'properties',
currentState,
allComponents,
- darkMode
+ darkMode,
+ ''
)
),
});
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx
new file mode 100644
index 0000000000..4131217386
--- /dev/null
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/ModalV2.jsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import Accordion from '@/_ui/Accordion';
+import { renderElement } from '../Utils';
+import { baseComponentProperties } from './DefaultComponent';
+import { resolveReferences } from '@/_helpers/utils';
+
+const INDEX_OF_TRIGGER = 2;
+
+export const ModalV2 = ({ componentMeta, darkMode, ...restProps }) => {
+ const {
+ layoutPropertyChanged,
+ component,
+ paramUpdated,
+ dataQueries,
+ currentState,
+ eventsChanged,
+ apps,
+ allComponents,
+ } = restProps;
+
+ let properties = [];
+ let additionalActions = [];
+ let dataProperties = [];
+
+ const events = Object.keys(componentMeta.events);
+ const validations = Object.keys(componentMeta.validation || {});
+
+ for (const [key] of Object.entries(componentMeta?.properties)) {
+ if (componentMeta?.properties[key]?.section === 'additionalActions') {
+ additionalActions.push(key);
+ } else if (componentMeta?.properties[key]?.accordian === 'Data') {
+ dataProperties.push(key);
+ } else {
+ properties.push(key);
+ }
+ }
+
+ const renderCustomElement = (param, paramType = 'properties') => {
+ return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
+ };
+ const conditionalAccordionItems = (component) => {
+ const useDefaultButton = resolveReferences(
+ component.component.definition.properties.useDefaultButton?.value ?? false
+ );
+ const accordionItems = [];
+ let renderOptions = [];
+ const options = ['visibility', 'disabledTrigger', 'useDefaultButton'];
+
+ options.map((option) => renderOptions.push(renderCustomElement(option)));
+
+ const conditionalOptions = [{ name: 'triggerButtonLabel', condition: useDefaultButton }];
+
+ conditionalOptions.map(({ name, condition }) => {
+ if (condition) renderOptions.push(renderCustomElement(name));
+ });
+
+ accordionItems.push({
+ title: 'Trigger',
+ children: renderOptions,
+ });
+
+ return accordionItems;
+ };
+
+ if (component.component.definition.properties.size.value === 'fullscreen') {
+ component.component.properties.modalHeight = {
+ ...component.component.properties.modalHeight,
+ isHidden: true,
+ };
+ }
+
+ if (component.component.definition.properties.showHeader.value === '{{false}}') {
+ component.component.properties.headerHeight = {
+ ...component.component.properties.headerHeight,
+ isHidden: true,
+ };
+ }
+
+ if (component.component.definition.properties.showFooter.value === '{{false}}') {
+ component.component.properties.footerHeight = {
+ ...component.component.properties.footerHeight,
+ isHidden: true,
+ };
+ }
+
+ const accordionItems = baseComponentProperties(
+ dataProperties,
+ events,
+ component,
+ componentMeta,
+ layoutPropertyChanged,
+ paramUpdated,
+ dataQueries,
+ currentState,
+ eventsChanged,
+ apps,
+ allComponents,
+ validations,
+ darkMode,
+ [],
+ additionalActions
+ );
+
+ const [optionsItems] = conditionalAccordionItems(component);
+
+ // Insert the Trigger option as the third item
+ accordionItems.splice(INDEX_OF_TRIGGER, 0, optionsItems);
+
+ return
;
+};
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
index a57c879121..db52205467 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
@@ -37,7 +37,6 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
if (!Array.isArray(optionsValue)) {
optionsValue = Object.values(optionsValue);
}
- const valuesToResolve = ['label', 'value'];
let options = [];
if (isDynamicOptionsEnabled || typeof optionsValue === 'string') {
@@ -202,9 +201,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
});
setOptions(_options);
- updateAllOptionsParams(_options);
setMarkedAsDefault(_value);
- paramUpdated({ name: 'value' }, 'value', _value, 'properties');
+ updateAllOptionsParams(_options);
}
};
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
index 27ba100f52..2e975109d2 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
@@ -52,6 +52,7 @@ export const PropertiesTabElements = ({
{ label: 'Boolean', value: 'boolean' },
{ label: 'Image', value: 'image' },
{ label: 'Link', value: 'link' },
+ { label: 'JSON', value: 'json' },
// Following column types are deprecated
{ label: 'Default', value: 'default' },
{ label: 'Dropdown', value: 'dropdown' },
@@ -266,6 +267,24 @@ export const PropertiesTabElements = ({
)}
)}
+ {column.columnType === 'json' && (
+
+ )}
)}
- {['string', 'default', undefined, 'number', 'boolean', 'select', 'text', 'newMultiSelect', 'datepicker'].includes(
- column.columnType
- ) && (
+ {[
+ 'string',
+ 'default',
+ undefined,
+ 'number',
+ 'json',
+ 'boolean',
+ 'select',
+ 'text',
+ 'newMultiSelect',
+ 'datepicker',
+ ].includes(column.columnType) && (
<>
{column.columnType !== 'boolean' && (
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx
index e559369396..c3fb47d612 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx
@@ -50,6 +50,8 @@ export const ProgramaticallyHandleProperties = ({
return props?.parseInUnixTimestamp;
case 'isDateSelectionEnabled':
return props?.isDateSelectionEnabled;
+ case 'jsonIndentation':
+ return props?.jsonIndentation;
default:
return;
}
@@ -81,6 +83,9 @@ export const ProgramaticallyHandleProperties = ({
if (property === 'linkColor') {
return definitionObj?.value ?? '#1B1F24';
}
+ if (property === 'jsonIndentation') {
+ return definitionObj?.value ?? `{{true}}`;
+ }
return definitionObj?.value ?? `{{false}}`;
};
@@ -111,7 +116,9 @@ export const ProgramaticallyHandleProperties = ({
const fxActiveFieldsPropExists = props?.hasOwnProperty('fxActiveFields') ?? false;
//to support backward compatibility, when fxActive is true for a particular column, we are passing all possible combinations which should render codehinter
const fxActive =
- props?.fxActive && resolveReferences(props.fxActive) ? ['isEditable', 'columnVisibility', 'linkTarget'] : [];
+ props?.fxActive && resolveReferences(props.fxActive)
+ ? ['isEditable', 'columnVisibility', 'jsonIndentation', 'linkTarget']
+ : [];
const checkFxActiveFieldIsArrray = (fxActiveFieldsProperty) => {
// adding error handling mechanism for fxActiveFieldsProperty , if props.fxActiveFields is array , then return props.fxActiveFields or else return [], this will make sure, fxActiveFields wil always be array
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
index 5a2175cfe1..b11a675e80 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
@@ -624,6 +624,8 @@ class TableComponent extends React.Component {
return 'Select';
case 'newMultiSelect':
return 'Multiselect';
+ case 'json':
+ return 'JSON';
default:
capitalize(text ?? '');
}
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx
index 7ee512ee20..c74e023c0b 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx
@@ -19,6 +19,7 @@ export const Code = ({
accordian,
placeholder,
validationFn,
+ isHidden = false,
}) => {
const currentState = useCurrentState();
@@ -43,6 +44,7 @@ export const Code = ({
onChange({ name: 'iconVisibility' }, 'value', value, 'styles');
}
+ if (isHidden) return null;
return (
{
@@ -703,6 +705,9 @@ const GetAccordion = React.memo(
case 'FilePicker':
return ;
+ case 'ModalV2':
+ return ;
+
case 'Modal':
return ;
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
index 0ddda9f572..62ee032172 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
@@ -50,7 +50,8 @@ export function renderCustomStyles(
componentConfig.component == 'MultiselectV2' ||
componentConfig.component == 'RadioButtonV2' ||
componentConfig.component == 'Button' ||
- componentConfig.component == 'Image'
+ componentConfig.component == 'Image' ||
+ componentConfig.component == 'ModalV2'
) {
const paramTypeConfig = componentMeta[paramType] || {};
const paramConfig = paramTypeConfig[param] || {};
@@ -131,6 +132,7 @@ export function renderElement(
const paramTypeDefinition = componentDefinition[paramType] || {};
const definition = paramTypeDefinition[param] || {};
const meta = componentMeta[paramType][param];
+ const isHidden = component.component.properties[param]?.isHidden ?? false;
if (
componentConfig.component == 'DropDown' ||
@@ -170,6 +172,7 @@ export function renderElement(
component={component}
placeholder={placeholder}
validationFn={validationFn}
+ isHidden={isHidden}
/>
);
}
diff --git a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
index 1226bfba3e..69ded14971 100644
--- a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import WidgetIcon from '@/../assets/images/icons/widgets';
import { useTranslation } from 'react-i18next';
-const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
+const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
const NEW_WIDGETS = [
'ToggleSwitchV2',
'DropdownV2',
@@ -12,6 +12,7 @@ const NEW_WIDGETS = [
'DaterangePicker',
'DatePickerV2',
'TimePicker',
+ 'ModalV2',
];
export const WidgetBox = ({ component, darkMode }) => {
diff --git a/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js
index 4ad52da372..6933f4a5a5 100644
--- a/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js
+++ b/frontend/src/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig.js
@@ -4,7 +4,14 @@ export const RESTRICTED_WIDGETS_CONFIG = {
Calendar: ['Calendar', 'Kanban'],
Container: ['Calendar', 'Kanban'],
Modal: ['Calendar', 'Kanban'],
+ ModalV2: ['Calendar', 'Kanban'],
+ ModalSlot: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
Tabs: ['Calendar', 'Kanban'],
Kanban_popout: ['Calendar', 'Kanban'],
Listview: ['Calendar', 'Kanban'],
};
+
+export const RESTRICTED_WIDGET_SLOTS_CONFIG = {
+ header: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
+ footer: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
+};
diff --git a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
index 13832857fb..be2855c476 100644
--- a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
+++ b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
@@ -3,6 +3,7 @@ import {
tableConfig,
chartConfig,
modalConfig,
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
@@ -64,6 +65,7 @@ export const widgets = [
buttonConfig,
chartConfig,
modalConfig,
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
index 8e5355f789..588bc655c6 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
@@ -299,7 +299,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
- value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/index.js b/frontend/src/AppBuilder/WidgetManager/widgets/index.js
index 2540cdeeef..ce0e73fdf5 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/index.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/index.js
@@ -2,6 +2,7 @@ import { buttonConfig } from './button';
import { tableConfig } from './table';
import { chartConfig } from './chart';
import { modalConfig } from './modal';
+import { modalV2Config } from './modalV2';
import { formConfig } from './form';
import { textinputConfig } from './textinput';
import { numberinputConfig } from './numberinput';
@@ -62,7 +63,8 @@ export {
buttonConfig,
tableConfig,
chartConfig,
- modalConfig,
+ modalConfig, //Deprecated
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
index fbd3c76a59..dc1835253d 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
@@ -48,8 +48,12 @@ export const listviewConfig = {
data: {
type: 'code',
displayName: 'List data',
- validation: {
- schema: { type: 'array', element: { type: 'object' } },
+ schema: {
+ type: 'union',
+ schemas: [
+ { type: 'array', element: { type: 'object' } },
+ { type: 'array', element: { type: 'string' } },
+ ],
defaultValue: "[{text: 'Sample text 1'}]",
},
},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
index c0a0844607..3443c73c9b 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
@@ -1,6 +1,6 @@
export const modalConfig = {
- name: 'Modal',
- displayName: 'Modal',
+ name: 'ModalLegacy',
+ displayName: 'Modal (Legacy)',
description: 'Show pop-up windows',
component: 'Modal',
defaultSize: {
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
new file mode 100644
index 0000000000..e7e96c4398
--- /dev/null
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
@@ -0,0 +1,277 @@
+export const modalV2Config = {
+ name: 'Modal',
+ displayName: 'Modal',
+ description: 'Show pop-up windows',
+ component: 'ModalV2',
+ defaultSize: {
+ width: 10,
+ height: 34,
+ },
+ others: {
+ showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
+ showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
+ },
+ properties: {
+ loadingState: {
+ type: 'toggle',
+ displayName: 'Loading state',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ section: 'additionalActions',
+ },
+ visibility: {
+ type: 'toggle',
+ displayName: 'Modal trigger visibility',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: true,
+ },
+ },
+ disabledTrigger: {
+ type: 'toggle',
+ displayName: 'Disable modal trigger',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ },
+ disabledModal: {
+ type: 'toggle',
+ displayName: 'Disable modal window',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ section: 'additionalActions',
+ },
+ useDefaultButton: {
+ type: 'toggle',
+ displayName: 'Use default trigger button',
+ validation: {
+ schema: {
+ type: 'boolean',
+ },
+ defaultValue: true,
+ },
+ },
+ triggerButtonLabel: {
+ type: 'code',
+ displayName: 'Trigger button label',
+ validation: {
+ schema: {
+ type: 'string',
+ },
+ defaultValue: 'Launch Modal',
+ },
+ },
+
+ // Data Accordion
+ showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
+ showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
+
+ size: {
+ type: 'select',
+ displayName: 'Width',
+ accordian: 'Data',
+ options: [
+ { name: 'small', value: 'sm' },
+ { name: 'medium', value: 'lg' },
+ { name: 'large', value: 'xl' },
+ { name: 'fullscreen', value: 'fullscreen' },
+ ],
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'lg',
+ },
+ },
+ modalHeight: {
+ type: 'numberInput',
+ displayName: 'Height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
+ },
+ headerHeight: {
+ type: 'numberInput',
+ displayName: 'Header height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
+ },
+ footerHeight: {
+ type: 'numberInput',
+ displayName: 'Footer height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
+ },
+ hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
+ closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
+ hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
+ },
+ events: {
+ onOpen: { displayName: 'On open' },
+ onClose: { displayName: 'On close' },
+ },
+ defaultChildren: [
+ {
+ componentName: 'Text',
+ slotName: 'header',
+ layout: {
+ top: 21,
+ left: 1,
+ height: 40,
+ },
+ displayName: 'ModalHeaderTitle',
+ properties: ['text'],
+ accessorKey: 'text',
+ styles: ['fontWeight', 'textSize', 'textColor'],
+ defaultValue: {
+ text: 'Modal title',
+ textSize: 20,
+ textColor: '#000',
+ },
+ },
+ {
+ componentName: 'Button',
+ slotName: 'footer',
+ layout: {
+ top: 24,
+ left: 22,
+ height: 36,
+ },
+ displayName: 'ModalFooterCancel',
+ properties: ['text'],
+ styles: ['type', 'borderColor', 'padding'],
+ defaultValue: {
+ text: 'Button1',
+ type: 'outline',
+ borderColor: '#CCD1D5',
+ },
+ },
+ {
+ componentName: 'Button',
+ slotName: 'footer',
+ layout: {
+ top: 24,
+ left: 32,
+ height: 36,
+ },
+ displayName: 'ModalFooterConfirm',
+ properties: ['text'],
+ defaultValue: {
+ text: 'Button2',
+ padding: 'none',
+ },
+ },
+ ],
+ styles: {
+ headerBackgroundColor: {
+ type: 'color',
+ displayName: 'Header background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ footerBackgroundColor: {
+ type: 'color',
+ displayName: 'Footer background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ bodyBackgroundColor: {
+ type: 'color',
+ displayName: 'Body background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ triggerButtonBackgroundColor: {
+ type: 'color',
+ displayName: 'Trigger button background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: false,
+ },
+ },
+ triggerButtonTextColor: {
+ type: 'color',
+ displayName: 'Trigger button text color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: false,
+ },
+ },
+ },
+ exposedVariables: {
+ show: false,
+ isDisabledModal: false,
+ isDisabledTrigger: false,
+ isVisible: true,
+ isLoading: false,
+ },
+ actions: [
+ {
+ handle: 'open',
+ displayName: 'Open',
+ },
+ {
+ handle: 'close',
+ displayName: 'Close',
+ },
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisableTrigger',
+ displayName: 'Set disable trigger',
+ params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisableModal',
+ displayName: 'Set disable modal',
+ params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setLoading',
+ displayName: 'Set loading',
+ params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ ],
+ definition: {
+ others: {
+ showOnDesktop: { value: '{{true}}' },
+ showOnMobile: { value: '{{false}}' },
+ },
+ properties: {
+ loadingState: { value: `{{false}}` },
+ visibility: { value: '{{true}}' },
+ disabledTrigger: { value: '{{false}}' },
+ disabledModal: { value: '{{false}}' },
+ useDefaultButton: { value: `{{true}}` },
+ triggerButtonLabel: { value: `Launch Modal` },
+ size: { value: 'lg' },
+ showHeader: { value: '{{true}}' },
+ showFooter: { value: '{{true}}' },
+ hideCloseButton: { value: '{{false}}' },
+ hideOnEsc: { value: '{{true}}' },
+ closeOnClickingOutside: { value: '{{false}}' },
+ modalHeight: { value: 400 },
+ headerHeight: { value: 80 },
+ footerHeight: { value: 80 },
+ },
+ events: [],
+ styles: {
+ headerBackgroundColor: { value: '#ffffffff' },
+ footerBackgroundColor: { value: '#ffffffff' },
+ bodyBackgroundColor: { value: '#ffffffff' },
+ triggerButtonBackgroundColor: { value: '#4D72FA' },
+ triggerButtonTextColor: { value: '#ffffffff' },
+ },
+ },
+};
diff --git a/frontend/src/AppBuilder/Widgets/Container.jsx b/frontend/src/AppBuilder/Widgets/Container.jsx
index 3ccedd869b..1334098423 100644
--- a/frontend/src/AppBuilder/Widgets/Container.jsx
+++ b/frontend/src/AppBuilder/Widgets/Container.jsx
@@ -45,7 +45,7 @@ export const Container = ({
border: `1px solid ${borderColor}`,
height,
display: isVisible ? 'flex' : 'none',
- overflow: 'hidden auto',
+ flexDirection: 'column',
position: 'relative',
boxShadow,
};
@@ -66,9 +66,7 @@ export const Container = ({
return (
state.setModalOpenOnCanvas);
const [activeId, setActiveId] = useState(null);
const cardMovementRef = useRef(null);
const shouldUpdateData = useRef(false);
@@ -117,6 +118,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) {
}
/**** End - Logic to reduce the zIndex of modal control box ****/
}
+ setModalOpenOnCanvas(`${id}-modal`, showModal);
}, [showModal]);
useEffect(() => {
diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx
index e63118a4e6..128c98aed2 100644
--- a/frontend/src/AppBuilder/Widgets/Modal.jsx
+++ b/frontend/src/AppBuilder/Widgets/Modal.jsx
@@ -49,6 +49,7 @@ export const Modal = function Modal({
const size = properties.size ?? 'lg';
const [modalWidth, setModalWidth] = useState();
const mode = useStore((state) => state.currentMode, shallow);
+ const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas);
/**** Start - Logic to reset the zIndex of modal control box ****/
useEffect(() => {
@@ -63,6 +64,7 @@ export const Modal = function Modal({
useGridStore.getState().actions.setOpenModalWidgetId(null);
}
}
+ setModalOpenOnCanvas(id, showModal);
}, [showModal, id, mode]);
/**** End - Logic to reset the zIndex of modal control box ****/
diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx
new file mode 100644
index 0000000000..e25027ce33
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { default as BootstrapModal } from 'react-bootstrap/Modal';
+import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
+import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
+
+export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode, width, footerHeight, onClick }) => {
+ const canvasFooterHeight = getCanvasHeight(footerHeight);
+ return (
+
+
+ {isDisabled && (
+
-
+
Esc
Discard Changes
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
index 95d2ab56e0..55d0e7f3ed 100644
--- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
+++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
@@ -214,4 +214,24 @@
.input-value-padding {
box-sizing: border-box;
padding-right: 30px !important;
+}
+
+.datepicker-widget.theme-tjdb{
+ .react-datepicker__navigation{
+ overflow: visible !important;
+ height: inherit !important;
+ }
+}
+
+.esc-btn-datepicker{
+ height: 18px ;
+ align-items: center;
+}
+
+.tjdb-td-wrapper{
+ .react-datepicker-time__input{
+ input{
+ line-height: normal !important;
+ }
+ }
}
\ No newline at end of file
diff --git a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js
index 247af3ccef..de90dbd0bf 100644
--- a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js
+++ b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js
@@ -299,7 +299,6 @@ export const dropdownV2Config = {
],
},
label: { value: 'Select' },
- value: { value: '{{"2"}}' },
optionsLoadingState: { value: '{{false}}' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },
diff --git a/frontend/src/Editor/WidgetManager/configs/index.js b/frontend/src/Editor/WidgetManager/configs/index.js
index d73cd8934f..93e45fd06c 100644
--- a/frontend/src/Editor/WidgetManager/configs/index.js
+++ b/frontend/src/Editor/WidgetManager/configs/index.js
@@ -2,6 +2,7 @@ import { buttonConfig } from './button';
import { tableConfig } from './table';
import { chartConfig } from './chart';
import { modalConfig } from './modal';
+import { modalV2Config } from './modalV2';
import { formConfig } from './form';
import { textinputConfig } from './textinput';
import { numberinputConfig } from './numberinput';
@@ -59,7 +60,8 @@ export {
buttonConfig,
tableConfig,
chartConfig,
- modalConfig,
+ modalConfig, //!Depreciated
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
diff --git a/frontend/src/Editor/WidgetManager/configs/listview.js b/frontend/src/Editor/WidgetManager/configs/listview.js
index a813bb5a0b..25da8f73c6 100644
--- a/frontend/src/Editor/WidgetManager/configs/listview.js
+++ b/frontend/src/Editor/WidgetManager/configs/listview.js
@@ -49,7 +49,13 @@ export const listviewConfig = {
type: 'code',
displayName: 'List data',
validation: {
- schema: { type: 'array', element: { type: 'object' } },
+ schema: {
+ type: 'union',
+ schemas: [
+ { type: 'array', element: { type: 'object' } },
+ { type: 'array', element: { type: 'string' } },
+ ],
+ },
defaultValue: "[{text: 'Sample text 1'}]",
},
},
diff --git a/frontend/src/Editor/WidgetManager/configs/modal.js b/frontend/src/Editor/WidgetManager/configs/modal.js
index 60f791831e..7716ac8e2c 100644
--- a/frontend/src/Editor/WidgetManager/configs/modal.js
+++ b/frontend/src/Editor/WidgetManager/configs/modal.js
@@ -1,6 +1,6 @@
export const modalConfig = {
- name: 'Modal',
- displayName: 'Modal',
+ name: 'ModalLegacy',
+ displayName: 'Modal (Legacy)',
description: 'Show pop-up windows',
component: 'Modal',
defaultSize: {
diff --git a/frontend/src/Editor/WidgetManager/configs/modalV2.js b/frontend/src/Editor/WidgetManager/configs/modalV2.js
new file mode 100644
index 0000000000..e7e96c4398
--- /dev/null
+++ b/frontend/src/Editor/WidgetManager/configs/modalV2.js
@@ -0,0 +1,277 @@
+export const modalV2Config = {
+ name: 'Modal',
+ displayName: 'Modal',
+ description: 'Show pop-up windows',
+ component: 'ModalV2',
+ defaultSize: {
+ width: 10,
+ height: 34,
+ },
+ others: {
+ showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
+ showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
+ },
+ properties: {
+ loadingState: {
+ type: 'toggle',
+ displayName: 'Loading state',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ section: 'additionalActions',
+ },
+ visibility: {
+ type: 'toggle',
+ displayName: 'Modal trigger visibility',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: true,
+ },
+ },
+ disabledTrigger: {
+ type: 'toggle',
+ displayName: 'Disable modal trigger',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ },
+ disabledModal: {
+ type: 'toggle',
+ displayName: 'Disable modal window',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: false,
+ },
+ section: 'additionalActions',
+ },
+ useDefaultButton: {
+ type: 'toggle',
+ displayName: 'Use default trigger button',
+ validation: {
+ schema: {
+ type: 'boolean',
+ },
+ defaultValue: true,
+ },
+ },
+ triggerButtonLabel: {
+ type: 'code',
+ displayName: 'Trigger button label',
+ validation: {
+ schema: {
+ type: 'string',
+ },
+ defaultValue: 'Launch Modal',
+ },
+ },
+
+ // Data Accordion
+ showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
+ showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
+
+ size: {
+ type: 'select',
+ displayName: 'Width',
+ accordian: 'Data',
+ options: [
+ { name: 'small', value: 'sm' },
+ { name: 'medium', value: 'lg' },
+ { name: 'large', value: 'xl' },
+ { name: 'fullscreen', value: 'fullscreen' },
+ ],
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'lg',
+ },
+ },
+ modalHeight: {
+ type: 'numberInput',
+ displayName: 'Height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
+ },
+ headerHeight: {
+ type: 'numberInput',
+ displayName: 'Header height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
+ },
+ footerHeight: {
+ type: 'numberInput',
+ displayName: 'Footer height',
+ accordian: 'Data',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
+ },
+ hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
+ closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
+ hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
+ },
+ events: {
+ onOpen: { displayName: 'On open' },
+ onClose: { displayName: 'On close' },
+ },
+ defaultChildren: [
+ {
+ componentName: 'Text',
+ slotName: 'header',
+ layout: {
+ top: 21,
+ left: 1,
+ height: 40,
+ },
+ displayName: 'ModalHeaderTitle',
+ properties: ['text'],
+ accessorKey: 'text',
+ styles: ['fontWeight', 'textSize', 'textColor'],
+ defaultValue: {
+ text: 'Modal title',
+ textSize: 20,
+ textColor: '#000',
+ },
+ },
+ {
+ componentName: 'Button',
+ slotName: 'footer',
+ layout: {
+ top: 24,
+ left: 22,
+ height: 36,
+ },
+ displayName: 'ModalFooterCancel',
+ properties: ['text'],
+ styles: ['type', 'borderColor', 'padding'],
+ defaultValue: {
+ text: 'Button1',
+ type: 'outline',
+ borderColor: '#CCD1D5',
+ },
+ },
+ {
+ componentName: 'Button',
+ slotName: 'footer',
+ layout: {
+ top: 24,
+ left: 32,
+ height: 36,
+ },
+ displayName: 'ModalFooterConfirm',
+ properties: ['text'],
+ defaultValue: {
+ text: 'Button2',
+ padding: 'none',
+ },
+ },
+ ],
+ styles: {
+ headerBackgroundColor: {
+ type: 'color',
+ displayName: 'Header background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ footerBackgroundColor: {
+ type: 'color',
+ displayName: 'Footer background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ bodyBackgroundColor: {
+ type: 'color',
+ displayName: 'Body background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#ffffffff',
+ },
+ },
+ triggerButtonBackgroundColor: {
+ type: 'color',
+ displayName: 'Trigger button background color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: false,
+ },
+ },
+ triggerButtonTextColor: {
+ type: 'color',
+ displayName: 'Trigger button text color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: false,
+ },
+ },
+ },
+ exposedVariables: {
+ show: false,
+ isDisabledModal: false,
+ isDisabledTrigger: false,
+ isVisible: true,
+ isLoading: false,
+ },
+ actions: [
+ {
+ handle: 'open',
+ displayName: 'Open',
+ },
+ {
+ handle: 'close',
+ displayName: 'Close',
+ },
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisableTrigger',
+ displayName: 'Set disable trigger',
+ params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisableModal',
+ displayName: 'Set disable modal',
+ params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setLoading',
+ displayName: 'Set loading',
+ params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ ],
+ definition: {
+ others: {
+ showOnDesktop: { value: '{{true}}' },
+ showOnMobile: { value: '{{false}}' },
+ },
+ properties: {
+ loadingState: { value: `{{false}}` },
+ visibility: { value: '{{true}}' },
+ disabledTrigger: { value: '{{false}}' },
+ disabledModal: { value: '{{false}}' },
+ useDefaultButton: { value: `{{true}}` },
+ triggerButtonLabel: { value: `Launch Modal` },
+ size: { value: 'lg' },
+ showHeader: { value: '{{true}}' },
+ showFooter: { value: '{{true}}' },
+ hideCloseButton: { value: '{{false}}' },
+ hideOnEsc: { value: '{{true}}' },
+ closeOnClickingOutside: { value: '{{false}}' },
+ modalHeight: { value: 400 },
+ headerHeight: { value: 80 },
+ footerHeight: { value: 80 },
+ },
+ events: [],
+ styles: {
+ headerBackgroundColor: { value: '#ffffffff' },
+ footerBackgroundColor: { value: '#ffffffff' },
+ bodyBackgroundColor: { value: '#ffffffff' },
+ triggerButtonBackgroundColor: { value: '#4D72FA' },
+ triggerButtonTextColor: { value: '#ffffffff' },
+ },
+ },
+};
diff --git a/frontend/src/Editor/WidgetManager/constants.js b/frontend/src/Editor/WidgetManager/constants.js
index 35ea9d5252..8c593455e1 100644
--- a/frontend/src/Editor/WidgetManager/constants.js
+++ b/frontend/src/Editor/WidgetManager/constants.js
@@ -1 +1,7 @@
-export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy', 'RadioButtonLegacy'];
+export const LEGACY_ITEMS = [
+ 'ToggleSwitchLegacy',
+ 'DropdownLegacy',
+ 'MultiselectLegacy',
+ 'RadioButtonLegacy',
+ 'ModalLegacy',
+];
diff --git a/frontend/src/Editor/WidgetManager/widgetConfig.js b/frontend/src/Editor/WidgetManager/widgetConfig.js
index 30bfdfc7f2..2b03f5c3f5 100644
--- a/frontend/src/Editor/WidgetManager/widgetConfig.js
+++ b/frontend/src/Editor/WidgetManager/widgetConfig.js
@@ -3,6 +3,7 @@ import {
tableConfig,
chartConfig,
modalConfig,
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
@@ -62,6 +63,7 @@ export const widgets = [
buttonConfig,
chartConfig,
modalConfig,
+ modalV2Config,
formConfig,
textinputConfig,
numberinputConfig,
diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx
index 996e382805..ab50cbb05d 100644
--- a/frontend/src/HomePage/HomePage.jsx
+++ b/frontend/src/HomePage/HomePage.jsx
@@ -38,7 +38,6 @@ import { useLicenseStore } from '@/_stores/licenseStore';
import { shallow } from 'zustand/shallow';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
import HeaderSkeleton from '@/_ui/FolderSkeleton/HeaderSkeleton';
-import { UserGroupMigrationModal } from './MigrationModal/UserGroupMigrationModal';
import {
ImportAppMenu,
AppActionModal,
@@ -136,13 +135,7 @@ class HomePageComponent extends React.Component {
this.fetchWorkflowsWorkspaceLimit();
this.fetchOrgGit();
this.setQueryParameter();
- const hasShownModal = localStorage.getItem('hasShownUserGroupMigrationModal');
const hasClosedBanner = localStorage.getItem('hasClosedGroupMigrationBanner');
- //Only show the modal once
- if (!hasShownModal) {
- this.setState({ showUserGroupMigrationModal: true });
- localStorage.setItem('hasShownUserGroupMigrationModal', 'true');
- }
//Only show the banner once
if (hasClosedBanner) {
@@ -790,6 +783,16 @@ class HomePageComponent extends React.Component {
handleCommitChange = (commitEnabled) => {
this.setState({ commitEnabled: commitEnabled });
};
+ shouldShowMigrationBanner = () => {
+ const { currentSessionValue } = authenticationService;
+ const { appType } = this.props;
+ return (
+ currentSessionValue?.admin &&
+ this.state.showGroupMigrationBanner &&
+ new Date(currentSessionValue?.current_user?.created_at) < new Date('2025-02-01') &&
+ appType !== 'workflow'
+ );
+ };
render() {
const {
apps,
@@ -885,15 +888,6 @@ class HomePageComponent extends React.Component {
return (
- {authenticationService.currentSessionValue?.admin &&
- showUserGroupMigrationModal &&
- this.props.appType !== 'workflow' && (
-
this.setShowUserGroupMigrationModal()}
- darkMode={this.props.darkMode}
- />
- )}
)}
- {authenticationService.currentSessionValue?.admin &&
- showGroupMigrationBanner &&
- this.props.appType !== 'workflow' && (
-
- )}
+ {this.shouldShowMigrationBanner() && (
+
+ )}
diff --git a/frontend/src/OrganizationSettingsPage/constant.js b/frontend/src/OrganizationSettingsPage/constant.js
index f2d4b4e2df..503533a657 100644
--- a/frontend/src/OrganizationSettingsPage/constant.js
+++ b/frontend/src/OrganizationSettingsPage/constant.js
@@ -3,7 +3,6 @@ export const workspaceSettingsLinks = [
{ id: 'groups', name: 'Groups', route: 'groups', conditions: ['admin'] },
{ id: 'workspacelogin', name: 'Workspace login', route: 'workspace-login', conditions: ['admin', 'wsLoginEnabled'] },
{ id: 'workspace-variables', name: 'Workspace variables', route: 'workspace-variables', conditions: ['admin'] },
- { id: 'copilot', name: 'Copilot', route: 'copilot', conditions: ['admin'] },
{ id: 'custom-styles', name: 'Custom styles', route: 'custom-styles', conditions: ['admin'] },
{ id: 'configure-git', name: 'Configure Git', route: 'configure-git', conditions: ['admin'] },
];
diff --git a/frontend/src/TooljetDatabase/Drawers/CreateTableDrawer/index.jsx b/frontend/src/TooljetDatabase/Drawers/CreateTableDrawer/index.jsx
index 19e5d62182..e8a1ad551c 100644
--- a/frontend/src/TooljetDatabase/Drawers/CreateTableDrawer/index.jsx
+++ b/frontend/src/TooljetDatabase/Drawers/CreateTableDrawer/index.jsx
@@ -8,11 +8,10 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { BreadCrumbContext } from '@/App/App';
import LicenseBanner from '@/modules/common/components/LicenseBanner';
-export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) {
+export default function CreateTableDrawer({ bannerVisible, setBannerVisible, tablesLimit, setTablesLimit }) {
const { organizationId, setSelectedTable, setTables, tables } = useContext(TooljetDatabaseContext);
const [isCreateTableDrawerOpen, setIsCreateTableDrawerOpen] = useState(false);
const { updateSidebarNAV } = useContext(BreadCrumbContext);
- const [tablesLimit, setTablesLimit] = useState({});
setBannerVisible(tablesLimit?.current >= tablesLimit?.total - 1 || false);
useEffect(() => {
@@ -42,15 +41,6 @@ export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) {
Create new table
-
-
setIsCreateTableDrawerOpen(false)}
diff --git a/frontend/src/TooljetDatabase/Filter/index.jsx b/frontend/src/TooljetDatabase/Filter/index.jsx
index 84b0110fe4..6c8f7c811d 100644
--- a/frontend/src/TooljetDatabase/Filter/index.jsx
+++ b/frontend/src/TooljetDatabase/Filter/index.jsx
@@ -251,11 +251,7 @@ const Filter = ({
}
/>
- {filterCount > 0 ? (
-
{pluralize(validFilterCountRef.current, 'filter')}
- ) : (
-
Filter
- )}
+ {filterCount > 0 ?
{pluralize(filterCount, 'filter')} :
Filter
}
{/* {areFiltersApplied && (
ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')}
diff --git a/frontend/src/TooljetDatabase/Sidebar/index.jsx b/frontend/src/TooljetDatabase/Sidebar/index.jsx
index bd143f11ac..437f7c52a0 100644
--- a/frontend/src/TooljetDatabase/Sidebar/index.jsx
+++ b/frontend/src/TooljetDatabase/Sidebar/index.jsx
@@ -3,18 +3,40 @@ import List from '../TableList';
import CreateTableDrawer from '../Drawers/CreateTableDrawer';
import { OrganizationList } from '@/modules/dashboard/components';
import cx from 'classnames';
+import LicenseBanner from '@/modules/common/components/LicenseBanner';
+import { authenticationService } from '@/_services';
export default function Sidebar({ collapseSidebar }) {
const [bannerVisible, setBannerVisible] = useState(false);
+ const [tablesLimit, setTablesLimit] = useState({});
+ const isAdmin = authenticationService.currentSessionValue?.admin === true;
+ const isResourceLimitReached = tablesLimit?.percentage === 100;
return (
-
+
diff --git a/frontend/src/_components/OverflowTooltip.jsx b/frontend/src/_components/OverflowTooltip.jsx
index 297f17d236..1523ae449a 100644
--- a/frontend/src/_components/OverflowTooltip.jsx
+++ b/frontend/src/_components/OverflowTooltip.jsx
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { ToolTip } from '@/_components';
-export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', ...rest }) {
+export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', placement = 'bottom', ...rest }) {
const [isOverflowed, setIsOverflow] = useState(false);
const textElementRef = useRef();
@@ -17,7 +17,7 @@ export default function OverflowTooltip({ children, className, whiteSpace = 'now
className={className}
delay={{ show: '0', hide: '0' }}
tooltipClassName="overflow-tooltip"
- placement="bottom"
+ placement={placement}
message={children}
show={isOverflowed}
width={rest?.width}
diff --git a/frontend/src/_helpers/authorizeWorkspace.js b/frontend/src/_helpers/authorizeWorkspace.js
index 540d4f9b73..a03d7b986d 100644
--- a/frontend/src/_helpers/authorizeWorkspace.js
+++ b/frontend/src/_helpers/authorizeWorkspace.js
@@ -226,7 +226,7 @@ export const authorizeUserAndHandleErrors = (workspace_id, workspace_slug, callb
const unauthorized_organization_slug = workspace_slug;
/* get current session's workspace id */
- authenticationService
+ sessionService
.validateSession()
.then(({ current_organization_id, ...restSessionData }) => {
/* change current organization id to valid one [current logged in organization] */
diff --git a/frontend/src/_services/custom_styles.service.js b/frontend/src/_services/custom_styles.service.js
index d3c98d6382..73de321466 100644
--- a/frontend/src/_services/custom_styles.service.js
+++ b/frontend/src/_services/custom_styles.service.js
@@ -3,13 +3,13 @@ import { authHeader, handleResponse, handleResponseWithoutValidation } from '@/_
function save(body) {
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
- return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleResponse);
+ return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleResponse);
}
function get(validateResponse = true) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
const handleOutput = validateResponse ? handleResponse : handleResponseWithoutValidation;
- return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleOutput);
+ return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleOutput);
}
function getForAppViewerEditor(validateResponse = true) {
diff --git a/frontend/src/_services/organization_constants.service.js b/frontend/src/_services/organization_constants.service.js
index 0ab57eb776..cd85a6fe15 100644
--- a/frontend/src/_services/organization_constants.service.js
+++ b/frontend/src/_services/organization_constants.service.js
@@ -15,7 +15,7 @@ export const orgEnvironmentConstantService = {
function getAll(type = null) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
const queryParams = type ? `?type=${type}` : '';
- return fetch(`${config.apiUrl}/organization-constants${queryParams}`, requestOptions).then(handleResponse);
+ return fetch(`${config.apiUrl}/organization-constants/decrypted${queryParams}`, requestOptions).then(handleResponse);
}
function create(name, value, type, environments) {
diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss
index 5f742b2153..e84756dca7 100644
--- a/frontend/src/_styles/components.scss
+++ b/frontend/src/_styles/components.scss
@@ -493,4 +493,13 @@ $btn-dark-color: #FFFFFF;
}
}
}
+}
+
+//[Container-widget]Show scrollbar only on hover
+.widget-type-container {
+ overflow: hidden auto;
+ scrollbar-width: none;
+ &:hover {
+ scrollbar-width: auto;
+ }
}
\ No newline at end of file
diff --git a/frontend/src/_styles/license.scss b/frontend/src/_styles/license.scss
index eb5d85c0af..14ab98ceab 100644
--- a/frontend/src/_styles/license.scss
+++ b/frontend/src/_styles/license.scss
@@ -198,7 +198,7 @@
.message-wrapper {
display: flex;
flex-direction: column;
- margin-left: 10px;
+ margin-left: 5px;
.heading {
display: unset !important;
@@ -246,9 +246,9 @@
.upgrade-btn-new{
white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- width: auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: auto;
margin-left: auto;
border: 1px solid var(--border-default, #CCD1D5);
background: var(--background-surface-layer-01);
@@ -262,7 +262,9 @@
padding: 8px 16px 8px 16px;
}
-
+ .demo-btn-bg{
+ border: 1px solid var(--border-accent-weak, #97AEFC);
+ }
.start-trial-btn {
min-width: 131px;
}
@@ -318,16 +320,14 @@
max-width: fit-content;
.upgrade-link {
- background: var(--upgrade-default);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
+ margin-left: 2px;
+ color: var(--text-default, #1B1F24);
+ font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
- line-height: 20px;
font-weight: 500;
- width: 67px;
- height: 20px;
+ line-height: 20px;
+ /* 166.667% */
}
}
@@ -345,16 +345,13 @@
max-width: fit-content;
.upgrade-link {
- background: linear-gradient(to right, #ff5f6d, #ffc371);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
+ color: var(--text-default, #1B1F24);
+ font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
- line-height: 20px;
font-weight: 500;
- width: 67px;
- height: 20px;
+ line-height: 20px;
+ /* 166.667% */
}
}
@@ -372,16 +369,13 @@
max-width: fit-content;
.upgrade-link {
- background: linear-gradient(to right, #ff5f6d, #ffc371);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
+ color: var(--text-default, #1B1F24);
+ font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
- line-height: 20px;
font-weight: 500;
- width: 67px;
- height: 20px;
+ line-height: 20px;
+ /* 166.667% */
}
}
@@ -399,16 +393,13 @@
max-width: fit-content;
.upgrade-link {
- background: var(--upgrade-default);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
+ color: var(--text-default, #1B1F24);
+ font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
- line-height: 20px;
font-weight: 500;
- width: 67px;
- height: 20px;
+ line-height: 20px;
+ /* 166.667% */
}
}
diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss
index 8a9eaf972a..7b256c3812 100644
--- a/frontend/src/_styles/queryManager.scss
+++ b/frontend/src/_styles/queryManager.scss
@@ -1250,6 +1250,11 @@ $border-radius: 4px;
color: var(--slate12) !important;
}
}
+ &.data-source-exists {
+ .cm-editor {
+ border-radius: 0 4px 4px 0 !important;
+ }
+ }
}
.rest-api-methods-select-element-container {
diff --git a/frontend/src/_styles/table-component.scss b/frontend/src/_styles/table-component.scss
index 5e0edd5aff..82f6b3de71 100644
--- a/frontend/src/_styles/table-component.scss
+++ b/frontend/src/_styles/table-component.scss
@@ -1467,5 +1467,59 @@
}
.tj-table-tag-col-readonly {
- margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
-}
\ No newline at end of file
+ margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
+}
+
+.jet-data-table {
+ .table-bordered {
+ th,
+ td {
+ border-bottom: 1px solid var(--interactive-overlay-border-pressed) !important;
+ border-right: 1px solid var(--interactive-overlay-border-pressed) !important;
+
+ &:first-child {
+ border-left: none !important;
+ }
+
+ &:last-child {
+ border-right: none !important;
+ }
+ }
+
+ thead th {
+ border-top: none !important;
+
+ &:first-child {
+ border-left: none !important;
+ }
+
+ &:last-child {
+ border-right: none !important;
+ }
+ }
+ }
+
+ .table-striped {
+ tbody {
+ div[data-index]:nth-child(odd) {
+ background-color: transparent !important;
+ }
+
+ div[data-index]:nth-child(even) {
+ background-color: var(--slate2) !important;
+ }
+ }
+ }
+}
+
+@media (hover: none) and (pointer: coarse) {
+ .jet-data-table {
+ overflow: auto;
+ }
+ // hide scrollbar on touch devices
+ .jet-data-table::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ background: transparent;
+ }
+}
diff --git a/frontend/src/_styles/tabler.scss b/frontend/src/_styles/tabler.scss
index 8927cf416e..1533bad5c6 100644
--- a/frontend/src/_styles/tabler.scss
+++ b/frontend/src/_styles/tabler.scss
@@ -5277,7 +5277,8 @@ fieldset:disabled .btn {
width: 100%;
height: 100%;
overflow: hidden;
- outline: 0
+ outline: 0;
+ padding-left: 0 !important;
}
.modal-dialog {
@@ -5311,7 +5312,7 @@ fieldset:disabled .btn {
}
.modal-dialog-scrollable .modal-content {
- max-height: 100%;
+ max-height: 88%;
overflow: hidden
}
@@ -5447,6 +5448,10 @@ fieldset:disabled .btn {
margin: 0
}
+.real-canvas .modal-dialog.modal-fullscreen {
+ width: 100%;
+}
+
.modal-fullscreen .modal-content {
height: 100%;
border: 0;
@@ -5465,6 +5470,12 @@ fieldset:disabled .btn {
border-radius: 0
}
+.modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
+ // Modal header height
+ padding-bottom: 56px;
+ max-height: 100%;
+}
+
@media (max-width:575.98px) {
.modal-fullscreen-sm-down {
width: 100vw;
@@ -19140,4 +19151,4 @@ img {
background: #1f2936;
border-color: #dadcde
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index 3d14fb05aa..239037a852 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -2665,10 +2665,18 @@ hr {
overflow-y: initial !important
}
- .modal-dialog-scrollable .modal-content {
+ .modal-dialog-scrollable:not(.modal-fullscreen) .modal-content {
max-height: 88% !important;
}
+ .modal-dialog-scrollable.modal-fullscreen .modal-content {
+ max-height: 100% !important;
+ }
+
+ .modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
+ // Modal header height
+ padding-bottom: 0;
+ }
}
@@ -7935,7 +7943,7 @@ tbody {
}
.sidebar-container-with-banner {
- height: 140px !important;
+ height: 40px !important;
padding-top: 1px !important;
margin: 0 auto;
display: flex;
@@ -12096,13 +12104,19 @@ tbody {
.sidebar-list-wrap-with-banner {
margin-top: 24px;
padding: 0px 20px 20px 20px;
- height: calc(100vh - 280px);
+ height: calc(100vh - 408px);
overflow: auto;
span {
letter-spacing: -0.02em;
}
}
+.sidebar-list-wrap.sidebar-list-wrap-with-banner.isAdmin {
+ height: calc(100vh - 371px);
+ &.resource-limit-reached {
+ height: calc(100vh - 371px);
+ }
+}
.drawer-footer-btn-wrap,
.variable-form-footer {
@@ -14027,8 +14041,8 @@ tbody {
}
/*
-* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
-* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
+* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
+* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
*/
.tj-dark-mode {
background-color: var(--base) !important;
@@ -18755,4 +18769,52 @@ section.ai-message-prompt-input-wrapper {
#inspector-tabpane-properties .accordion-header {
height:32px;
-}
\ No newline at end of file
+}
+.workspace-constant-value {
+ position: relative;
+
+ .fromEnv {
+ content: '.env';
+ border-radius: 6px;
+ background: var(--Indigo-50, #EEF4FF);
+ padding: 0px 8px;
+ width: 40px;
+ align-items: center;
+ position: absolute;
+ color: var(--Indigo-700, #3538CD);
+ text-align: center;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px;
+ margin-left: 24px;
+ }
+
+ .isDuplicate {
+ padding: 0px 8px;
+ border-radius: 6px;
+ background: var(--Error-50, #FEF3F2);
+ color: var(--Error-700, #B42318);
+ text-align: center;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px;
+ /* 166.667% */
+ margin-left: 24px;
+ }
+
+ .env-secret-hidden-message {
+ border-radius: 16px;
+ background: var(--Warning-50, #FFFAEB);
+ padding: 4px 12px;
+ color: var(--Warning-700, #B54708);
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px;
+ &.dark {
+ background: #FFFAEB !important;
+ }
+ }
+}
diff --git a/frontend/src/_ui/Icon/solidIcons/AICrown.jsx b/frontend/src/_ui/Icon/solidIcons/AICrown.jsx
new file mode 100644
index 0000000000..47776e319a
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/AICrown.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+const AICrown = ({ className = '', fill = '#FCA23F', width = '40', height = '41', ...props }) => {
+ return (
+
+ );
+};
+
+export default AICrown;
diff --git a/frontend/src/_ui/Icon/solidIcons/AppLimitSvg.jsx b/frontend/src/_ui/Icon/solidIcons/AppLimitSvg.jsx
new file mode 100644
index 0000000000..e9490c038c
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/AppLimitSvg.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+const AppLimitSvg = () => (
+
+);
+
+export default AppLimitSvg;
diff --git a/frontend/src/_ui/Icon/solidIcons/BookDemo.jsx b/frontend/src/_ui/Icon/solidIcons/BookDemo.jsx
new file mode 100644
index 0000000000..8716ebdd48
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/BookDemo.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+const BookDemo = ({ className = '', fill = '#4368E3', width = '16', height = '17', ...props }) => {
+ return (
+
+ );
+};
+
+export default BookDemo;
diff --git a/frontend/src/_ui/Icon/solidIcons/CalendarIcon.jsx b/frontend/src/_ui/Icon/solidIcons/CalendarIcon.jsx
new file mode 100644
index 0000000000..cf6b4b288c
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/CalendarIcon.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const CalendarIcon = ({ fill = '#FFAF41', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default CalendarIcon;
diff --git a/frontend/src/_ui/Icon/solidIcons/CalendarSmall.jsx b/frontend/src/_ui/Icon/solidIcons/CalendarSmall.jsx
new file mode 100644
index 0000000000..1dd4793842
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/CalendarSmall.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const CalendarSmall = () => (
+
+);
+
+export default CalendarSmall;
diff --git a/frontend/src/_ui/Icon/solidIcons/Contactv3.jsx b/frontend/src/_ui/Icon/solidIcons/Contactv3.jsx
new file mode 100644
index 0000000000..066c056822
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/Contactv3.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+const Contactv3 = ({ className = '', fill = '#CCD1D5', width = '16', height = '16', ...props }) => {
+ return (
+
+ );
+};
+
+export default Contactv3;
diff --git a/frontend/src/_ui/Icon/solidIcons/NewTabSmall.jsx b/frontend/src/_ui/Icon/solidIcons/NewTabSmall.jsx
new file mode 100644
index 0000000000..8bb3c435fe
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/NewTabSmall.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const NewTabSmall = ({ fill = '#6A727C', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default NewTabSmall;
diff --git a/frontend/src/_ui/Icon/solidIcons/PremiumLogo.jsx b/frontend/src/_ui/Icon/solidIcons/PremiumLogo.jsx
new file mode 100644
index 0000000000..1728165fa6
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/PremiumLogo.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+const PremiumLogo = ({ width = '41', height = '41' }) => (
+
+);
+
+export default PremiumLogo;
diff --git a/frontend/src/_ui/Icon/solidIcons/StudentIcon.jsx b/frontend/src/_ui/Icon/solidIcons/StudentIcon.jsx
new file mode 100644
index 0000000000..ed12fa7d6b
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/StudentIcon.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+const StudentIcon = ({ fill = '#FFAF41', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default StudentIcon;
diff --git a/frontend/src/_ui/Icon/solidIcons/UserGroupsGrey.jsx b/frontend/src/_ui/Icon/solidIcons/UserGroupsGrey.jsx
new file mode 100644
index 0000000000..debccd0039
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/UserGroupsGrey.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+
+const UserGroupsGrey = () => (
+
+);
+
+export default UserGroupsGrey;
diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js
index d036919cd9..34a2410e1d 100644
--- a/frontend/src/_ui/Icon/solidIcons/index.js
+++ b/frontend/src/_ui/Icon/solidIcons/index.js
@@ -219,6 +219,16 @@ import Replace from './Replace.jsx';
import ReplaceAll from './ReplaceAll.jsx';
import Remove02 from './Remove02.jsx';
import TooljetAi from './TooljetAI.jsx';
+import AICrown from './AICrown.jsx';
+import BookDemo from './BookDemo.jsx';
+import Contactv3 from './Contactv3.jsx';
+import PremiumLogo from './PremiumLogo.jsx';
+import StudentIcon from './StudentIcon.jsx';
+import CalendarIcon from './CalendarIcon.jsx';
+import CalendarSmall from './CalendarSmall.jsx';
+import UserGroupsGrey from './UserGroupsGrey.jsx';
+import AppLimitSvg from './AppLimitSvg.jsx';
+import NewTabSmall from './NewTabSmall.jsx';
const Icon = (props) => {
switch (props.name) {
@@ -662,6 +672,26 @@ const Icon = (props) => {
return
;
case 'remove02':
return
;
+ case 'bookdemo':
+ return
;
+ case 'contactv3':
+ return
;
+ case 'premium-logo':
+ return
;
+ case 'calendar-icon':
+ return
;
+ case 'calendar-small':
+ return
;
+ case 'user-groups-grey':
+ return
;
+ case 'app-limit':
+ return
;
+ case 'new-tab-small':
+ return
;
+ case 'student-icon':
+ return
;
+ case 'ai-crown':
+ return
;
default:
return
;
}
diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
index 65a4ece91d..b13e88d767 100644
--- a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
+++ b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
@@ -53,7 +53,7 @@ export const JSONNode = ({ data, ...restProps }) => {
React.useEffect(() => {
if (typeof shouldExpandNode === 'function') {
- set(shouldExpandNode(path, data));
+ set(shouldExpandNode(path, data, currentNode));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathToBeInspected]);
@@ -268,7 +268,15 @@ export const JSONNode = ({ data, ...restProps }) => {
};
return (
-
+
{enableCopyToClipboard && (
{
'group-object-container': shouldDisplayIntendedBlock,
'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
})}
+ id={`inspector-node-${String(currentNode).toLowerCase()}`}
data-cy={`inspector-node-${String(currentNode).toLowerCase()}`}
>
{$NODEIcon && {$NODEIcon}
}
diff --git a/frontend/src/_ui/Label.jsx b/frontend/src/_ui/Label.jsx
index 3cd7f886cb..721ac05203 100644
--- a/frontend/src/_ui/Label.jsx
+++ b/frontend/src/_ui/Label.jsx
@@ -13,6 +13,7 @@ function Label({ label, width, labelRef, color, defaultAlignment, direction, aut
justifyContent: direction == 'right' ? 'flex-end' : 'flex-start',
fontSize: '12px',
height: defaultAlignment === 'top' && '20px',
+ overflow: 'hidden',
}}
>
+
diff --git a/frontend/src/modules/WorkspaceSettings/components/ManageOrgConstantsSettings/ConstantTable.jsx b/frontend/src/modules/WorkspaceSettings/components/ManageOrgConstantsSettings/ConstantTable.jsx
index 704941729c..64e836f976 100644
--- a/frontend/src/modules/WorkspaceSettings/components/ManageOrgConstantsSettings/ConstantTable.jsx
+++ b/frontend/src/modules/WorkspaceSettings/components/ManageOrgConstantsSettings/ConstantTable.jsx
@@ -1,9 +1,17 @@
import React, { useState } from 'react';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
-import { Tooltip } from 'react-tooltip';
+import { ToolTip } from '@/_components';
import EyeHide from '@/../assets/images/onboardingassets/Icons/EyeHide';
import EyeShow from '@/../assets/images/onboardingassets/Icons/EyeShow';
+const WithTooltip = ({ children, message, placement, show = false }) => {
+ return (
+
+ {children}
+
+ );
+};
+
const ConstantTable = ({
constants = [],
canUpdateDeleteConstant = true,
@@ -88,94 +96,134 @@ const ConstantTable = ({
) : (
- {constants.map((constant) => (
-
- |
-
- {String(constant.name).length > 30
- ? String(constant.name).substring(0, 30) + '...'
- : constant.name}
-
- |
-
-
- {!showValues[constant.id] ? '*'.repeat(displayValue(constant).length) : displayValue(constant)}
-
- |
-
- {canUpdateDeleteConstant && (
+ {constants.map((constant) => {
+ return (
+
-
- toggleShowValue(constant.id)}
- data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-constant-visibility`}
- >
- {!showValues[constant.id] ? (
-
- ) : (
-
- )}
-
- onEditBtnClicked(constant)}
- data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-edit-button`}
- >
- Edit
-
-
- onDeleteBtnClicked(constant)}
- data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-delete-button`}
- >
- Delete
-
-
+ {String(constant.name).length > 30
+ ? String(constant.name).substring(0, 30) + '...'
+ : constant.name}
+
|
- )}
-
- ))}
+
+
+ {!showValues[constant.id] ? (
+ '*'.repeat(displayValue(constant).length)
+ ) : constant.type === 'Secret' && constant.fromEnv ? (
+
+ Values fetched at runtime, not stored in ToolJet
+
+ ) : (
+ displayValue(constant)
+ )}
+
+ {constant.fromEnv && .env}
+ {constant.isDuplicate && Duplicate}
+ |
+
+ {canUpdateDeleteConstant && (
+
+
+ toggleShowValue(constant.id)}
+ data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-constant-visibility`}
+ >
+ {!showValues[constant.id] ? (
+
+ ) : (
+
+ )}
+
+ {
+
+ Constants created from environment variables cannot be edited or
+ deleted
+
+ ),
+ placement: 'left',
+ })}
+ >
+
+ onEditBtnClicked(constant)}
+ data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-edit-button`}
+ >
+ Edit
+
+
+ onDeleteBtnClicked(constant)}
+ data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-delete-button`}
+ >
+ Delete
+
+
+
+ }
+
+ |
+ )}
+
+ );
+ })}
)}
diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx
index 637a792b3e..928f45cf4c 100644
--- a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx
+++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx
@@ -549,10 +549,7 @@ class BaseManageGroupPermissions extends React.Component {
fill={'#FDFDFE'}
disabled={!isFeatureEnabled}
>
- {this.props.t(
- 'header.organization.menus.manageGroups.permissions.createNewGroup',
- 'Create new group'
- )}
+ {this.props.t('header.organization.menus.manageGroups.permissions.addNewGroup', 'Add new group')}
)}
@@ -805,6 +802,7 @@ class BaseManageGroupPermissions extends React.Component {
size="xsmall"
type={featureAccess?.licenseStatus?.licenseType}
customMessage={'Custom groups & permissions are available in our paid plans.'}
+ showCustomGroupBanner={true}
/>
)}
diff --git a/frontend/src/modules/WorkspaceSettings/pages/WorkspaceLogin/WorkspaceLoginSettings.jsx b/frontend/src/modules/WorkspaceSettings/pages/WorkspaceLogin/WorkspaceLoginSettings.jsx
index 367de75afc..429490cda4 100644
--- a/frontend/src/modules/WorkspaceSettings/pages/WorkspaceLogin/WorkspaceLoginSettings.jsx
+++ b/frontend/src/modules/WorkspaceSettings/pages/WorkspaceLogin/WorkspaceLoginSettings.jsx
@@ -31,6 +31,7 @@ class OrganizationLogin extends React.Component {
instanceSSO: [],
featureAccess: {},
isBasicPlan: false,
+ isCommunity: false,
canToggleAutomaticSSOLogin: false,
showDisableAutoSSOModal: false,
};
@@ -152,6 +153,7 @@ class OrganizationLogin extends React.Component {
async setLoginConfigs() {
const featureAccess = await licenseService.getFeatureAccess();
const isBasicPlan = !featureAccess?.licenseStatus?.isLicenseValid || featureAccess?.licenseStatus?.isExpired;
+ const isCommunity = !featureAccess?.licenseStatus?.isLicenseValid && featureAccess?.expiry == '';
const settings = await this.fetchSSOSettings();
const instanceSSOResult = await instanceSettingsService.fetchSSOConfigs();
const instanceSSO = !Array.isArray(instanceSSOResult)
@@ -180,7 +182,8 @@ class OrganizationLogin extends React.Component {
const enabledSSOs = combinedSSOConfigs.filter(
(obj) => obj.enabled && obj.sso !== 'form' && (!this.protectedSSO.includes(obj.sso) || featureAccess?.[obj.sso])
);
- let passwordLoginEnabled = isBasicPlan ? true : ssoConfigs?.find((obj) => obj.sso === 'form')?.enabled || false;
+ let passwordLoginEnabled =
+ isBasicPlan && !isCommunity ? true : ssoConfigs?.find((obj) => obj.sso === 'form')?.enabled || false;
if (enabledSSOs.length === 0) {
try {
@@ -211,6 +214,7 @@ class OrganizationLogin extends React.Component {
instanceSSO: [...instanceSSO],
featureAccess: featureAccess,
isBasicPlan: isBasicPlan,
+ isCommunity: isCommunity,
canToggleAutomaticSSOLogin: canToggleAutomaticSSOLogin,
isAnySSOEnabled:
ssoConfigs?.some(
@@ -447,6 +451,7 @@ class OrganizationLogin extends React.Component {
instanceSSO,
featureAccess,
isBasicPlan,
+ isCommunity,
canToggleAutomaticSSOLogin,
} = this.state;
const flexContainerStyle = {
@@ -588,7 +593,7 @@ class OrganizationLogin extends React.Component {
onChange={() => this.handleCheckboxChange('passwordLoginEnabled')}
data-cy="password-enable-toggle"
checked={options?.passwordLoginEnabled === true}
- disabled={isBasicPlan ? true : !isAnySSOEnabled}
+ disabled={isBasicPlan && !isCommunity ? true : !isAnySSOEnabled}
/>