Merge branch 'modularisation/v3' of https://github.com/ToolJet/ToolJet into feat/new-table-mod

This commit is contained in:
Kavin Venkatachalam 2025-03-11 17:06:03 +05:30
commit b79b073b38
167 changed files with 4969 additions and 1270 deletions

View file

@ -215,7 +215,7 @@ jobs:
- name: Delete service
run: |
export SERVICE_ID=$(curl --request GET \
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \
--url 'https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
jq -r '.[0].service.id')
@ -583,7 +583,7 @@ jobs:
- name: Delete service
run: |
export SERVICE_ID=$(curl --request GET \
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \
--url 'https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
jq -r '.[0].service.id')

View file

@ -137,6 +137,7 @@ const WidgetIcon = (props) => {
case 'map':
return <Map {...props} />;
case 'modal':
case 'modallegacy':
return <Modal {...props} />;
case 'multiselect':
case 'multiselectv2':

@ -1 +1 @@
Subproject commit bef751a47e40ad8ea052f278edcf0993363c1b93
Subproject commit 8a7ed384a53f3354735cfa525e6824201844af52

View file

@ -131,9 +131,13 @@ class AppComponent extends React.Component {
}
return '';
};
render() {
const { updateAvailable, darkMode, isEditorOrViewer } = this.state;
const mergedProps = {
...this.props,
switchDarkMode: this.switchDarkMode,
darkMode: darkMode,
};
let toastOptions = {
style: {
wordBreak: 'break-all',
@ -256,7 +260,7 @@ class AppComponent extends React.Component {
}
/>
)}
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...this.props} />}></Route>
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...mergedProps} />}></Route>
<Route path="settings/*" element={<InstanceSettings {...this.props} />}></Route>
<Route path="/:workspaceId/settings/*" element={<Settings {...this.props} />}></Route>
@ -270,7 +274,7 @@ class AppComponent extends React.Component {
</PrivateRoute>
}
/>
{getDataSourcesRoutes(this.props)}
{getDataSourcesRoutes(mergedProps)}
<Route
exact
path="/applications/:id/versions/:versionId/:pageHandle?"

View file

@ -3,9 +3,13 @@ import { shallow } from 'zustand/shallow';
import './configHandle.scss';
import useStore from '@/AppBuilder/_stores/store';
import { findHighestLevelofSelection } from '../Grid/gridUtils';
import SolidIcon from '@/_ui/Icon/solidIcons/index';
const CONFIG_HANDLE_HEIGHT = 20;
const BUFFER_HEIGHT = 1;
export const ConfigHandle = ({
id,
position,
widgetTop,
widgetHeight,
setSelectedComponentAsModal = () => null, //! Only Modal widget passes this uses props down. All other widgets use selecto lib
@ -13,6 +17,7 @@ export const ConfigHandle = ({
customClassName = '',
showHandle,
componentType,
visibility,
}) => {
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow);
@ -26,18 +31,33 @@ export const ConfigHandle = ({
(state) => componentType === 'Tabs' && state.getExposedValueOfComponent(id)?.currentTab,
shallow
);
const position = widgetTop < 15 ? 'bottom' : 'top';
const setComponentToInspect = useStore((state) => state.setComponentToInspect);
const isModal = componentType === 'Modal' || componentType === 'ModalV2';
const _showHandle = useStore((state) => {
const isWidgetHovered = state.getHoveredComponentForGrid() === id || state.hoveredComponentBoundaryId === id;
const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== '';
// If one component is hovered and one is selected, show the handle for the hovered component
return (
isWidgetHovered ||
(showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered)
);
}, shallow);
let height = visibility === false ? 10 : widgetHeight;
return (
<div
className={`config-handle ${customClassName}`}
widget-id={id}
style={{
top: position === 'top' ? '-20px' : widgetTop + widgetHeight - (widgetTop < 10 ? 15 : 10),
visibility:
showHandle && (!isMultipleComponentsSelected || (componentType === 'Modal' && isModalOpen))
? 'visible'
: 'hidden',
top:
componentType === 'Modal' && isModalOpen
? '0px'
: position === 'top'
? '-20px'
: `${height - (CONFIG_HANDLE_HEIGHT + BUFFER_HEIGHT)}px`,
visibility: _showHandle ? 'visible' : 'hidden',
left: '-1px',
}}
onClick={(e) => {
@ -51,7 +71,10 @@ export const ConfigHandle = ({
>
<span
style={{
background: componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
background:
visibility === false ? '#c6cad0' : componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
border: position === 'bottom' ? '1px solid white' : 'none',
color: visibility === false && 'var(--text-placeholder)',
}}
className="badge handle-content"
>
@ -65,17 +88,30 @@ export const ConfigHandle = ({
data-cy={`${componentName?.toLowerCase()}-config-handle`}
className="text-truncate"
>
<img
style={{ cursor: 'pointer', marginRight: '5px', verticalAlign: 'middle' }}
src="assets/images/icons/settings.svg"
width="12"
height="12"
draggable="false"
/>
{/* Settings Icon */}
<span style={{ cursor: 'pointer', marginRight: '5px' }}>
<SolidIcon
name="settings"
width="12"
height="12"
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
/>
</span>
<span>{componentName}</span>
{/* Divider */}
<hr
style={{
marginLeft: '10px',
height: '12px',
width: '2px',
backgroundColor: visibility === false ? 'var(--text-placeholder)' : '#fff',
opacity: 0.5,
}}
/>
</div>
{/* Delete Button */}
{!isMultipleComponentsSelected && !shouldFreeze && (
<div className="delete-part">
<div>
<img
style={{ cursor: 'pointer', marginLeft: '5px' }}
src="assets/images/icons/inspect.svg"
@ -87,19 +123,20 @@ export const ConfigHandle = ({
data-cy={`${componentName.toLowerCase()}-inspect-button`}
className="config-handle-inspect"
/>
<img
<span
style={{ cursor: 'pointer', marginLeft: '5px' }}
src="assets/images/icons/trash-light.svg"
width="12"
role="button"
height="12"
draggable="false"
onClick={() => {
deleteComponents([id]);
}}
data-cy={`${componentName.toLowerCase()}-delete-button`}
className="delete-icon"
/>
>
<SolidIcon
name="trash"
width="12"
height="12"
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
/>
</span>
</div>
)}
</span>

View file

@ -31,22 +31,7 @@
.badge {
font-size: 9px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
.delete-part {
margin-left: 10px;
float: right;
}
.delete-part::before {
height: 12px;
display: inline-block;
width: 2px;
background-color: rgba(255, 255, 255, 0.8);
opacity: 0.5;
content: "";
vertical-align: middle;
}
border-bottom-right-radius: 0
}
}
@ -65,9 +50,3 @@
}
}
}
.main-editor-canvas .widget-target:hover > .config-handle {
visibility: visible !important;
}

View file

@ -6,7 +6,15 @@ import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { useDrop } from 'react-dnd';
import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils';
import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants';
import {
CANVAS_WIDTHS,
NO_OF_GRIDS,
WIDGETS_WITH_DEFAULT_CHILDREN,
GRID_HEIGHT,
CONTAINER_FORM_CANVAS_PADDING,
SUBCONTAINER_CANVAS_BORDER_WIDTH,
BOX_PADDING,
} from './appCanvasConstants';
import { useGridStore } from '@/_stores/gridStore';
import NoComponentCanvasContainer from './NoComponentCanvasContainer';
import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants';
@ -35,10 +43,10 @@ export const Container = React.memo(
canvasMaxWidth,
isViewerSidebarPinned,
pageSidebarStyle,
componentType,
}) => {
const realCanvasRef = useRef(null);
const components = useStore((state) => state.getContainerChildrenMapping(id), shallow);
const componentType = useStore((state) => state.getComponentTypeFromId(id), shallow);
const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow);
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow);
const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow);
@ -56,6 +64,11 @@ export const Container = React.memo(
const [{ isOverCurrent }, drop] = useDrop({
accept: 'box',
hover: (item) => {
item.canvasRef = realCanvasRef?.current;
item.canvasId = id;
item.canvasWidth = getContainerCanvasWidth();
},
drop: async ({ componentType }, monitor) => {
const didDrop = monitor.didDrop();
if (didDrop) return;
@ -89,14 +102,19 @@ export const Container = React.memo(
function getContainerCanvasWidth() {
if (canvasWidth !== undefined) {
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
return canvasWidth;
if (id === 'canvas') return canvasWidth;
if (componentType === 'Container' || componentType === 'Form') {
return (
canvasWidth - (2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING)
);
}
return canvasWidth - 2; // Need to update this 2 to correct value for other subcontainers
}
return realCanvasRef?.current?.offsetWidth;
}
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
useEffect(() => {
useGridStore.getState().actions.setSubContainerWidths(id, (getContainerCanvasWidth() - 2) / NO_OF_GRIDS);
useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canvasWidth, listViewMode, columns]);
@ -137,8 +155,7 @@ export const Container = React.memo(
}}
style={{
height: id === 'canvas' ? `${canvasHeight}` : '100%',
// backgroundSize: '25.3953px 10px',
backgroundSize: `${gridWidth}px 10px`,
backgroundSize: `${gridWidth}px ${GRID_HEIGHT}px`,
backgroundColor:
currentMode === 'view'
? computeViewerBackgroundColor(darkMode, canvasBgColor)
@ -169,6 +186,7 @@ export const Container = React.memo(
data-parentId={id}
canvas-height={canvasHeight}
onClick={handleCanvasClick}
component-type={componentType}
>
<div
className={cx('container-fluid rm-container p-0', {

View file

@ -1,17 +1,6 @@
.target, .nested-target {
position: absolute;
/* width: 100px;
height: 100px; */
/* top: 150px;
left: 100px; */
/* line-height: 100px; */
/* text-align: center; */
/* background: #ee8; */
/* color: #333; */
/* font-weight: bold; */
box-sizing: border-box;
/* transition: transform 0.1s; */
/* z-index: 3001; */
}
.target.hovered{
@ -76,43 +65,6 @@
background: #8DA4EF !important;
}
/* Hides all the control lines*/
/* .moveable-line {
color: transparent !important;
--moveable-color: transparent !important;
}
.moveable-control {
visibility: hidden;
}
.target {
outline: 1px solid #4af;
} */
.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover {
outline: 1px solid #4af;
z-index: 4 !important;
}
.main-editor-canvas .widget-target:has(.nested-target:hover):hover {
outline: 0px solid #4af;
}
.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
outline: 1px solid #4af;
z-index: 4 !important;
}
.active-target, .resizing-target {
outline: 1px solid #4af !important;
/* z-index: 1000000 !important; */
}
.moveable-control-box:not([data-able-groupable]) .moveable-control-box:not(:hover) {
opacity: 0;
}
@ -141,10 +93,6 @@
height: 0px !important;
}
.resizing-target * {
opacity: 0;
}
.moveable-control {
width: 8px !important;
@ -186,5 +134,43 @@
display: none;
}
.moveable-guideline {
background: #97AEFC !important;
opacity: 0.8;
z-index: 9999;
}
.moveable-guideline.moveable-horizontal {
height: 1px !important;
width: 100% !important;
background: #97AEFC !important;
left: 0 !important;
}
.moveable-guideline.moveable-vertical {
width: 1px !important;
height: 100% !important;
background: #97AEFC !important;
top: 0 !important;
}
.moveable-guideline-group {
z-index: 9999;
}
.dragging-component-canvas {
outline: 1px solid var(--border-accent-strong) !important;
outline-offset: 0px; /* Creates space between element and outline */
z-index: 999 !important;
}
.non-dragging-component {
outline: 1px dotted var(--border-accent-weak) !important;
outline-offset: 0px; /* Creates space between element and outline */
z-index: 999 !important;
}
/* */

View file

@ -17,9 +17,15 @@ import {
hasParentWithClass,
getPositionForGroupDrag,
adjustWidth,
hideGridLines,
showGridLines,
handleActivateTargets,
handleDeactivateTargets,
handleActivateNonDraggingComponents,
} from './gridUtils';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
import useStore from '@/AppBuilder/_stores/store';
import './Grid.css';
import { NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
@ -29,6 +35,7 @@ const RESIZABLE_CONFIG = {
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
};
export const GRID_HEIGHT = 10;
export default function Grid({ gridWidth, currentLayout }) {
const lastDraggedEventsRef = useRef(null);
@ -51,6 +58,49 @@ export default function Grid({ gridWidth, currentLayout }) {
const canvasWidth = NO_OF_GRIDS * gridWidth;
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
const draggingComponentId = useStore((state) => state.draggingComponentId, shallow);
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
const [dragParentId, setDragParentId] = useState(null);
const [elementGuidelines, setElementGuidelines] = useState([]);
const componentsSnappedTo = useRef(null);
const prevDragParentId = useRef(null);
const newDragParentId = useRef(null);
const [isGroupDragging, setIsGroupDragging] = useState(false);
useEffect(() => {
const selectedSet = new Set(selectedComponents);
const draggingOrResizingId = draggingComponentId || resizingComponentId;
const isGrouped = findHighestLevelofSelection().length > 1;
const firstSelectedParent =
selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null;
const selectedParent = dragParentId || firstSelectedParent;
const guidelines = boxList
.filter((box) => {
const isVisible =
getResolvedValue(box?.component?.definition?.properties?.visibility?.value) ||
getResolvedValue(box?.component?.definition?.styles?.visibility?.value);
// Early return for non-visible elements
if (!isVisible) return false;
if (isGrouped) {
// If component is selected, don't show its guidelines
if (selectedSet.has(box.id)) return false;
return selectedParent ? box.parent === selectedParent : !box.parent;
}
if (draggingOrResizingId) {
if (box.id === draggingOrResizingId) return false;
return dragParentId ? box.parent === dragParentId : !box.parent;
}
return true;
})
.map((box) => `.ele-${box.id}`);
setElementGuidelines(guidelines);
}, [boxList, dragParentId, draggingComponentId, resizingComponentId, selectedComponents, getResolvedValue]);
useEffect(() => {
setBoxList(
@ -94,7 +144,7 @@ export default function Grid({ gridWidth, currentLayout }) {
boxList.forEach(({ id, height, width, x, y, gw }) => {
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
y = Math.round(y / 10) * 10;
y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
gw = gw ? gw : gridWidth;
const parent = transformedBoxes[id]?.component?.parent;
@ -117,7 +167,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
setComponentLayout({
[id]: {
height: height ? height : 10,
height: height ? height : GRID_HEIGHT,
width: newWidth ? newWidth : 1,
top: y,
left: Math.round(x / gw),
@ -319,7 +369,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
// Round y position
y = Math.max(0, Math.round(y / 10) * 10);
y = Math.max(0, Math.round(y / GRID_HEIGHT) * GRID_HEIGHT);
// Adjust height for certain parent components
if (parent) {
const parentElem = document.getElementById(`canvas-${parent}`);
@ -354,17 +404,16 @@ export default function Grid({ gridWidth, currentLayout }) {
);
// Add event listeners for config handle visibility when hovering over widget boundary
// This is needed even though we have hovered widget state because when hovered on boundary,
// the hovered widget state is empty, hence created a separate state for boundary
React.useEffect(() => {
const moveableBox = document.querySelector(`.moveable-control-box`);
const showConfigHandle = (e) => {
const targetId = e.target.offsetParent.getAttribute('target-id');
const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
configHandle.classList.add('config-handle-visible');
useStore.getState().setHoveredComponentBoundaryId(targetId);
};
const hideConfigHandle = (e) => {
const targetId = e.target.offsetParent.getAttribute('target-id');
const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
configHandle.classList.remove('config-handle-visible');
const hideConfigHandle = () => {
useStore.getState().setHoveredComponentBoundaryId('');
};
if (moveableBox) {
moveableBox.addEventListener('mouseover', showConfigHandle);
@ -376,49 +425,10 @@ export default function Grid({ gridWidth, currentLayout }) {
};
}, []);
const handleDragGridLinesVisibility = (e, events = []) => {
const { clientX, clientY } = e;
if (!document.elementFromPoint(clientX, clientY)) return;
const targetElems = document.elementsFromPoint(clientX, clientY);
const draggedOverElements = targetElems.filter(
(ele) =>
!events.some((event) => event.target.id === ele.id) &&
(ele.classList.contains('target') || ele.classList.contains('real-canvas'))
);
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
const appCanvas = document.getElementById('real-canvas');
// Show grid line for main canvas
draggedOverContainer?.classList.remove('hide-grid');
draggedOverContainer?.classList.add('show-grid');
// Remove 'show-grid' class from all sub-canvases
const canvasElms = document.getElementsByClassName('sub-canvas');
Array.from(canvasElms).forEach((element) => {
element.classList.remove('show-grid');
element.classList.add('hide-grid');
});
// Determine the potential new parent
const parentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
// Show grid for the appropriate canvas
if (parentId) {
const newParentCanvas = document.getElementById('canvas-' + parentId);
if (newParentCanvas) {
appCanvas?.classList?.remove('show-grid');
newParentCanvas?.classList.remove('hide-grid');
newParentCanvas?.classList.add('show-grid');
}
}
useGridStore.getState().actions.setDragTarget(parentId);
};
const handleDragGroupEnd = (e) => {
try {
hideGridLines();
setIsGroupDragging(false);
const { events, clientX, clientY } = e;
const initialParent = events[0].target.closest('.real-canvas');
// Get potential new parent using same logic as onDragEnd
@ -477,7 +487,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// Apply transform to return to original position
ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${
Math.round(_top / 10) * 10
Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT
}px)`;
}
});
@ -514,7 +524,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// Apply grid snapping and bounds
const snappedX = Math.round(posX / _gridWidth) * _gridWidth;
const snappedY = Math.round(posY / 10) * 10;
const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${snappedX}px, ${snappedY}px)`;
return {
@ -531,6 +541,18 @@ export default function Grid({ gridWidth, currentLayout }) {
}
};
React.useEffect(() => {
const components = Array.from(document.querySelectorAll('.active-target')).filter(
(component) => !selectedComponents.includes(component.getAttribute('widgetid'))
);
const draggingOrResizing = draggingComponentId || resizingComponentId;
if (!draggingOrResizing && components.length > 0) {
for (const component of components) {
component?.classList?.remove('active-target');
}
}
}, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]);
if (mode !== 'edit') return null;
return (
@ -557,11 +579,11 @@ export default function Grid({ gridWidth, currentLayout }) {
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
if (currentWidget.component?.parent) {
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent);
setDragParentId(currentWidget.component?.parent);
} else {
document.getElementById('real-canvas').classList.add('show-grid');
}
handleActivateTargets(currentWidget.component?.parent);
const currentWidth = currentWidget.width * _gridWidth;
const diffWidth = e.width - currentWidth;
const diffHeight = e.height - currentWidget.height;
@ -584,9 +606,6 @@ export default function Grid({ gridWidth, currentLayout }) {
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${e.width}px`;
}
@ -612,12 +631,13 @@ export default function Grid({ gridWidth, currentLayout }) {
// When clicked on widget boundary/resizer, select the component
setSelectedComponents([e.target.id]);
}
showGridLines();
if (!isComponentVisible(e.target.id)) {
return false;
}
handleActivateNonDraggingComponents();
useGridStore.getState().actions.setResizingComponentId(e.target.id);
e.setMin([gridWidth, 10]);
e.setMin([gridWidth, GRID_HEIGHT]);
}}
onResizeEnd={(e) => {
try {
@ -625,11 +645,10 @@ export default function Grid({ gridWidth, currentLayout }) {
const currentWidget = boxList.find(({ id }) => {
return id === e.target.id;
});
document.getElementById('real-canvas')?.classList.remove('show-grid');
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid');
hideGridLines();
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth;
const height = Math.round(e?.lastEvent?.height / 10) * 10;
const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT;
const currentWidth = currentWidget.width * _gridWidth;
const diffWidth = e.lastEvent?.width - currentWidth;
@ -654,19 +673,17 @@ export default function Grid({ gridWidth, currentLayout }) {
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
const roundedTransformY = Math.round(transformY / 10) * 10;
transformY = transformY % 10 === 5 ? roundedTransformY - 10 : roundedTransformY;
const roundedTransformY = Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT;
transformY = transformY % GRID_HEIGHT === 5 ? roundedTransformY - GRID_HEIGHT : roundedTransformY;
e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${
Math.round(transformY / 10) * 10
Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT
}px)`;
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`;
}
if (!maxHeightHit || e.height < e.target.clientHeight) {
e.target.style.height = `${Math.round(e.lastEvent.height / 10) * 10}px`;
e.target.style.height = `${Math.round(e.lastEvent.height / GRID_HEIGHT) * GRID_HEIGHT}px`;
}
const resizeData = {
id: e.target.id,
@ -682,18 +699,19 @@ export default function Grid({ gridWidth, currentLayout }) {
} catch (error) {
console.error('ResizeEnd error ->', error);
}
useGridStore.getState().actions.setDragTarget();
handleDeactivateTargets();
setDragParentId(null);
toggleCanvasUpdater();
}}
onResizeGroupStart={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
parentElm.classList.add('show-grid');
showGridLines();
handleActivateNonDraggingComponents();
}}
onResizeGroup={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
const parentWidth = parentElm?.clientWidth;
const parentHeight = parentElm?.clientHeight;
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight);
events.forEach((ev) => {
ev.target.style.width = `${ev.width}px`;
@ -710,8 +728,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const { events } = e;
const newBoxs = [];
const parentElm = events[0].target.closest('.real-canvas');
parentElm.classList.remove('show-grid');
hideGridLines();
// TODO: Logic needs to be relooked post go live P2
groupResizeDataRef.current.forEach((ev) => {
@ -722,9 +739,9 @@ export default function Grid({ gridWidth, currentLayout }) {
let width = Math.round(ev.width / _gridWidth) * _gridWidth;
width = width < _gridWidth ? _gridWidth : width;
let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth;
let posY = Math.round(ev.drag.translate[1] / 10) * 10;
let height = Math.round(ev.height / 10) * 10;
height = height < 10 ? 10 : height;
let posY = Math.round(ev.drag.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
let height = Math.round(ev.height / GRID_HEIGHT) * GRID_HEIGHT;
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
@ -752,7 +769,7 @@ export default function Grid({ gridWidth, currentLayout }) {
let posX = currentWidget?.layouts[currentLayout].left * _gridWidth;
let posY = currentWidget?.layouts[currentLayout].top;
let height = currentWidget?.layouts[currentLayout].height;
height = height < 10 ? 10 : height;
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
@ -763,11 +780,18 @@ export default function Grid({ gridWidth, currentLayout }) {
} catch (error) {
console.error('Error resizing group', error);
}
handleDeactivateTargets();
toggleCanvasUpdater();
}}
checkInput
onDragStart={(e) => {
// This is to prevent parent component from being dragged and the stop the propagation of the event
if (getHoveredComponentForGrid() !== e.target.id) {
return false;
}
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
const box = boxList.find((box) => box.id === e.target.id);
// Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS
if (SUBCONTAINER_WIDGETS.includes(box?.component?.component) && e.inputEvent.shiftKey) {
@ -779,7 +803,7 @@ export default function Grid({ gridWidth, currentLayout }) {
// to handle their own interactions like column resizing or card dragging
let isDragOnInnerElement = false;
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
Also user dont need to drag an calender from using popup */
if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) {
return false;
@ -801,7 +825,6 @@ export default function Grid({ gridWidth, currentLayout }) {
container.contains(e.inputEvent.target)
);
}
if (['RangeSlider', 'BoundedBox'].includes(box?.component?.component) || isDragOnInnerElement) {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
const isHandle = targetElems.find((ele) => ele.classList.contains('handle-content'));
@ -809,152 +832,112 @@ export default function Grid({ gridWidth, currentLayout }) {
return false;
}
}
// This is to prevent parent component from being dragged and the stop the propagation of the event
if (getHoveredComponentForGrid() !== e.target.id) {
return false;
}
handleActivateNonDraggingComponents();
}}
onDragEnd={(e) => {
handleDeactivateTargets();
try {
if (isDraggingRef.current) {
useGridStore.getState().actions.setDraggingComponentId(null);
useStore.getState().setDraggingComponentId(null);
isDraggingRef.current = false;
}
prevDragParentId.current = null;
newDragParentId.current = null;
setDragParentId(null);
if (!e.lastEvent) {
return;
if (!e.lastEvent) return;
// Build the drag context from the event
const dragContext = dragContextBuilder({ event: e, widgets: boxList });
const { target, source, dragged } = dragContext;
const targetSlotId = target?.slotId;
const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth;
// const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || [];
// const draggedWidgetType = dragged.widgetType;
const isParentChangeAllowed = dragContext.isDroppable;
// Compute new position
let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged);
const isModalToCanvas = source.isModal && target.slotId === 'real-canvas';
if (isParentChangeAllowed && !isModalToCanvas) {
const parent = target.slotId === 'real-canvas' ? null : target.slotId;
// Special case for Modal; If source widget is modal, prevent drops to canvas
handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]);
} else {
const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth;
left = dragged.left * sourcegridWidth;
top = dragged.top;
!isModalToCanvas ??
toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
}
let draggedOverElemId = boxList.find((box) => box.id === e.target.id)?.parent;
let draggedOverElemIdType;
const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
let draggedOverElem;
if (document.elementFromPoint(e.clientX, e.clientY) && parentComponent?.component?.component !== 'Modal') {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
draggedOverElem = targetElems.find((ele) => {
const isOwnChild = e.target.contains(ele); // if the hovered element is a child of actual draged element its not considered
if (isOwnChild) return false;
// Apply transform for smooth transition
e.target.style.transform = `translate(${left}px, ${top}px)`;
let isDroppable = ele.id !== e.target.id && ele.classList.contains('drag-container-parent');
if (isDroppable) {
let widgetId = ele?.getAttribute('component-id') || ele.id;
let widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
if (!widgetType) {
widgetId = widgetId.split('-').slice(0, -1).join('-');
widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
}
if (
!['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(
widgetType
)
) {
isDroppable = false;
}
}
return isDroppable;
});
draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id;
draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type');
}
const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
const currentParentId = boxList.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent;
let left = e.lastEvent?.translate[0];
let top = e.lastEvent?.translate[1];
if (
['Listview', 'Kanban', 'Container'].includes(
boxList.find((box) => box.id === draggedOverElemId)?.component?.component
)
) {
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const maxY = containerHeight - e.target.clientHeight;
top = top > maxY ? maxY : top;
}
const currentWidget = boxList.find(({ id }) => id === e.target.id)?.component?.component;
const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId;
draggedOverElemIdType = getComponentTypeFromId(parentId);
const parentWidget = draggedOverElemIdType === 'Kanban' ? 'Kanban_card' : draggedOverElemIdType;
const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
if (draggedOverElemId !== currentParentId) {
if (isParentChangeAllowed) {
const draggedOverWidget = boxList.find((box) => box.id === draggedOverElemId);
let parentWidgetType = boxList.find((box) => box.id === draggedOverElemId)?.component?.component;
// @TODO - When dropping back to container from canvas, the boxList doesn't have canvas header,
// boxList will return null. But we need to tell getMouseDistanceFromParentDiv parentWidgetType is container
// As container id is like 'canvas-2375e23765e-123234'
if (parentId && !parentWidgetType && draggedOverElemId.includes('-header')) {
parentWidgetType = 'Container';
}
let { left: _left, top: _top } = getMouseDistanceFromParentDiv(
e,
draggedOverWidget?.component?.component === 'Kanban' ? draggedOverElem : draggedOverElemId,
parentWidgetType
);
left = _left;
top = _top;
} else {
const currBox = boxList.find((l) => l.id === e.target.id);
left = currBox.left * gridWidth;
top = currBox.top;
toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`);
}
}
e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${
Math.round(top / 10) * 10
}px)`;
if (draggedOverElemId === currentParentId || isParentChangeAllowed) {
handleDragEnd([
{
id: e.target.id,
x: left,
y: Math.round(top / 10) * 10,
parent: isParentChangeAllowed ? draggedOverElemId : undefined,
},
]);
}
const box = boxList.find((box) => box.id === e.target.id);
//
setTimeout(() => setSelectedComponents([box.id]));
// Select the dragged component after drop
setTimeout(() => setSelectedComponents([dragged.id]));
} catch (error) {
console.log('draggedOverElemId->error', error);
console.error('Error in onDragEnd:', error);
}
// Hide all sub-canvases
var canvasElms = document.getElementsByClassName('sub-canvas');
var elementsArray = Array.from(canvasElms);
elementsArray.forEach(function (element) {
element.classList.remove('show-grid');
element.classList.add('hide-grid');
});
document.getElementById('real-canvas')?.classList.remove('show-grid');
setCanvasBounds({ ...CANVAS_BOUNDS });
hideGridLines();
toggleCanvasUpdater();
}}
onDrag={(e) => {
// Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again
if (!isDraggingRef.current) {
useGridStore.getState().actions.setDraggingComponentId(e.target.id);
useStore.getState().setDraggingComponentId(e.target.id);
showGridLines();
isDraggingRef.current = true;
}
const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
const currentWidget = boxList.find((box) => box.id === e.target.id);
const currentParentId =
currentWidget?.component?.parent === null ? 'canvas' : currentWidget?.component?.parent;
const _gridWidth = useGridStore.getState().subContainerWidths[dragParentId] || gridWidth;
const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current;
let top = e.translate[1];
let left = e.translate[0];
// Snap to grid
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
// This logic is to handle the case when the dragged element is over a new canvas
if (_dragParentId !== currentParentId) {
left = e.translate[0];
top = e.translate[1];
}
// Special case for Modal
if (parentComponent?.component?.component === 'Modal') {
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const containerWidth = elemContainer.clientWidth;
const maxY = containerHeight - e.target.clientHeight;
const maxLeft = containerWidth - e.target.clientWidth;
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
const parentComponent = boxList.find((box) => box.id === parentId);
const parentWidgetType = parentComponent?.component?.component;
const isOnHeaderOrFooter = oldParentId
? oldParentId.includes('-header') || oldParentId.includes('-footer')
: false;
const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter;
const isParentNewModal = parentComponent?.component?.component === 'ModalV2';
const isParentLegacyModal = parentComponent?.component?.component === 'Modal';
const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
top = top < 0 ? 0 : top > maxY ? maxY : top;
left = left < 0 ? 0 : left > maxLeft ? maxLeft : left;
if (isParentModal) {
const modalContainer = e.target.closest('.tj-modal-widget-content');
const mainCanvas = document.getElementById('real-canvas');
const mainRect = mainCanvas.getBoundingClientRect();
const modalRect = modalContainer.getBoundingClientRect();
const relativePosition = {
top: modalRect.top - mainRect.top,
right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
bottom: modalRect.height + (modalRect.top - mainRect.top),
left: modalRect.left - mainRect.left,
};
setCanvasBounds({ ...relativePosition });
}
e.target.style.transform = `translate(${left}px, ${top}px)`;
@ -963,8 +946,33 @@ export default function Grid({ gridWidth, currentLayout }) {
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
);
handleDragGridLinesVisibility(e, [{ target: e.target }]);
// This block is to show grid lines on the canvas when the dragged element is over a new canvas
if (document.elementFromPoint(e.clientX, e.clientY)) {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
const draggedOverElements = targetElems.filter(
(ele) =>
(ele.id !== e.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas')
);
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
// Determine potential new parent
let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
if (newParentId === e.target.id) {
newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent;
} else if (parentComponent?.component?.component === 'Modal') {
// Never update parentId for Modal
newParentId = parentComponent?.id;
}
if (newParentId !== prevDragParentId.current) {
setDragParentId(newParentId === 'canvas' ? null : newParentId);
newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
prevDragParentId.current = newParentId;
handleActivateTargets(newParentId);
}
}
// Postion ghost element exactly as same at dragged element
if (document.getElementById(`moveable-drag-ghost`)) {
document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`;
@ -979,31 +987,29 @@ export default function Grid({ gridWidth, currentLayout }) {
parentElm?.classList?.add('show-grid');
}
handleDragGridLinesVisibility(ev, events);
events.forEach((ev) => {
let left = ev.translate[0];
let top = ev.translate[1];
const currentWidget = boxList.find(({ id }) => id === ev.target.id);
const _gridWidth =
useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth;
let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${left}px, ${top}px)`;
});
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
updateNewPosition(events);
}}
onDragGroupStart={({ events }) => {
const parentElm = events[0]?.target?.closest('.real-canvas');
parentElm?.classList?.add('show-grid');
showGridLines();
setIsGroupDragging(true);
handleActivateNonDraggingComponents();
}}
onDragGroupEnd={(e) => {
handleDragGroupEnd(e);
handleDeactivateTargets();
toggleCanvasUpdater();
}}
//snap settgins
snappable={true}
snapThreshold={10}
isDisplaySnapDigit={false}
bounds={CANVAS_BOUNDS}
displayAroundControls={true}
controlPadding={20}
onClickGroup={(e) => {
const targetId =
e.inputEvent.target.id || e.inputEvent.target.closest('.moveable-box')?.getAttribute('widgetid');
@ -1019,6 +1025,43 @@ export default function Grid({ gridWidth, currentLayout }) {
}
}
}}
//snap settgins
snappable={true}
snapGap={false}
isDisplaySnapDigit={false}
snapThreshold={GRID_HEIGHT}
bounds={canvasBounds}
// Guidelines configuration
elementGuidelines={elementGuidelines}
snapDirections={{
top: true,
right: true,
bottom: true,
left: true,
center: false,
middle: false,
}}
elementSnapDirections={{
top: true,
left: true,
bottom: true,
right: true,
center: false,
middle: false,
}}
onSnap={(e) => {
const components = e.elements;
if (isArray(componentsSnappedTo.current)) {
for (const component of componentsSnappedTo.current) {
component?.element?.classList?.remove('active-target');
}
}
componentsSnappedTo.current = components;
for (const component of components) {
component.element.classList.add('active-target');
}
}}
snapGridAll={true}
/>
</>
);

View file

@ -1,7 +1,7 @@
import { useGridStore } from '@/_stores/gridStore';
import { isEmpty } from 'lodash';
import useStore from '@/AppBuilder/_stores/store';
import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils';
export function correctBounds(layout, bounds) {
layout = scaleLayouts(layout);
const collidesWith = [];
@ -291,6 +291,7 @@ export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
? document.getElementById(id)
: id
: document.getElementsByClassName('real-canvas')[0];
parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id);
if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') {
parentDiv = document.getElementById('canvas-' + id);
}
@ -391,3 +392,99 @@ export function hasParentWithClass(child, className) {
return false;
}
export function showGridLines() {
var canvasElms = document.getElementsByClassName('sub-canvas');
var elementsArray = Array.from(canvasElms);
elementsArray.forEach(function (element) {
element.classList.remove('hide-grid');
element.classList.add('show-grid');
});
document.getElementById('real-canvas')?.classList.remove('hide-grid');
document.getElementById('real-canvas')?.classList.add('show-grid');
}
export function hideGridLines() {
var canvasElms = document.getElementsByClassName('sub-canvas');
var elementsArray = Array.from(canvasElms);
elementsArray.forEach(function (element) {
element.classList.remove('show-grid');
element.classList.add('hide-grid');
});
document.getElementById('real-canvas')?.classList.remove('show-grid');
document.getElementById('real-canvas')?.classList.add('hide-grid');
}
// Track previously active elements for efficient cleanup
let previousActiveWidgets = null;
let previousActiveCanvas = null;
export const handleActivateNonDraggingComponents = () => {
// Only add non-dragging class to visible components in viewport
document.querySelectorAll('.moveable-box:not(.active-target)').forEach((component) => {
// Check if element is visible in viewport
const rect = component.getBoundingClientRect();
const isVisible =
rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
if (isVisible) {
component.classList.add('non-dragging-component');
}
});
};
export const handleActivateTargets = (parentId) => {
const WIDGETS_WITH_CANVAS_OUTLINE = ['Container', 'Modal', 'Form', 'Listview', 'Kanban'];
const newParentType = document.getElementById('canvas-' + parentId)?.getAttribute('component-type');
let _parentId = parentId;
if (newParentType === 'Tabs') {
_parentId = getTabId(parentId);
} else if (WIDGETS_WITH_CANVAS_OUTLINE.includes(newParentType)) {
_parentId = getSubContainerIdWithSlots(parentId);
}
// Clean up previous active elements
if (previousActiveWidgets) {
previousActiveWidgets.classList.remove('dragging-component-canvas');
previousActiveWidgets = null;
}
if (previousActiveCanvas) {
previousActiveCanvas.classList.remove('dragging-component-canvas');
previousActiveCanvas = null;
}
const parentComponent = document.getElementById(_parentId);
if (!parentComponent) return;
if (WIDGETS_WITH_CANVAS_OUTLINE?.includes(newParentType)) {
// If it's multiple canvas in single widget, highlight the specific canvas
const canvasElm = document.getElementById('canvas-' + parentId);
if (canvasElm) {
canvasElm.classList.add('dragging-component-canvas');
previousActiveCanvas = canvasElm;
}
} else {
// Otherwise highlight the component box
parentComponent.classList.remove('non-dragging-component');
parentComponent.classList.add('dragging-component-canvas');
previousActiveWidgets = parentComponent;
}
};
export const handleDeactivateTargets = () => {
if (previousActiveWidgets) {
previousActiveWidgets.classList.remove('dragging-component-canvas');
previousActiveWidgets = null;
}
if (previousActiveCanvas) {
previousActiveCanvas.classList.remove('dragging-component-canvas');
previousActiveCanvas = null;
}
document.querySelectorAll('.non-dragging-component').forEach((component) => {
component.classList.remove('non-dragging-component');
});
};

View file

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

View file

@ -6,7 +6,7 @@ import { OverlayTrigger } from 'react-bootstrap';
import { renderTooltip } from '@/_helpers/appUtils';
import { useTranslation } from 'react-i18next';
import ErrorBoundary from '@/_ui/ErrorBoundary';
import { BOX_PADDING } from './appCanvasConstants';
const shouldAddBoxShadowAndVisibility = [
'Table',
'TextInput',
@ -164,7 +164,7 @@ const RenderWidget = ({
<div
style={{
height: '100%',
padding: resolvedStyles?.padding == 'none' ? '0px' : '2px', //chart and image has a padding property other than container padding
padding: resolvedStyles?.padding == 'none' ? '0px' : `${BOX_PADDING}px`, //chart and image has a padding property other than container padding
}}
role={'Box'}
className={inCanvas ? `_tooljet-${component?.component} _tooljet-${component?.name}` : ''} //required for custom CSS

View file

@ -27,7 +27,7 @@ const WidgetWrapper = memo(
);
const layoutData = useStore((state) => state.getComponentDefinition(id)?.layouts?.[currentLayout], shallow);
const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow);
const isDragging = useGridStore((state) => state.draggingComponentId === id);
const isDragging = useStore((state) => state.draggingComponentId === id);
const isResizing = useGridStore((state) => state.resizingComponentId === id);
const componentType = useStore((state) => state.getComponentDefinition(id)?.component?.component, shallow);
const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow);
@ -52,7 +52,9 @@ const WidgetWrapper = memo(
height: visibility === false ? '10px' : `${height}px`,
transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`,
WebkitFontSmoothing: 'antialiased',
border: visibility === false ? `1px solid var(--border-default)` : 'none',
};
if (!componentType) return null;
return (
<>
@ -67,8 +69,8 @@ const WidgetWrapper = memo(
data-id={`${id}`}
id={id}
widgetid={id}
component-type={componentType}
style={{
// transform: `translate(332px, -134px)`,
// zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null,
...styles,
}}
@ -84,11 +86,11 @@ const WidgetWrapper = memo(
{mode == 'edit' && (
<ConfigHandle
id={id}
position={layoutData.top < 15 ? 'bottom' : 'top'}
widgetTop={layoutData.top}
widgetHeight={layoutData.height}
showHandle={isWidgetActive}
componentType={componentType}
visibility={visibility}
/>
)}
<RenderWidget

View file

@ -1,12 +1,14 @@
export const NO_OF_GRIDS = 43;
export const GRID_HEIGHT = 10;
export const CANVAS_WIDTHS = Object.freeze({
deviceWindowWidth: 450,
leftSideBarWidth: 48,
rightSideBarWidth: 300,
});
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container'];
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container', 'ModalV2'];
export const DEFAULT_CANVAS_WIDTH = 1292;
@ -15,3 +17,9 @@ export const APP_HEADER_HEIGHT = 47;
export const LEFT_SIDEBAR_WIDTH = 348; // exclusive of border
export const SUBCONTAINER_WIDGETS = ['Container', 'Tabs', 'Listview', 'Kanban', 'Form'];
export const CONTAINER_FORM_CANVAS_PADDING = 7;
export const SUBCONTAINER_CANVAS_BORDER_WIDTH = 1;
export const BOX_PADDING = 2;

View file

@ -89,7 +89,8 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
const defaultChildren = deepClone(parentMeta)['defaultChildren'];
defaultChildren.forEach((child) => {
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child;
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles, slotName } =
child;
const componentMeta = deepClone(componentTypes.find((component) => component.component === componentName));
const componentData = JSON.parse(JSON.stringify(componentMeta));
@ -139,7 +140,12 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
}
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
const _parent = getParentComponentIdByType(child, parentMeta.component, parentId);
const _parent = getParentComponentIdByType({
child,
parentComponent: parentMeta.component,
parentId,
slotName,
});
const newChildComponent = {
id: uuidv4(),
@ -199,7 +205,9 @@ export const getAllChildComponents = (allComponents, parentId) => {
allComponents[parentId]?.component?.component === 'Tabs' ||
allComponents[parentId]?.component?.component === 'Calendar' ||
allComponents[parentId]?.component?.component === 'Kanban' ||
allComponents[parentId]?.component?.component === 'Container';
allComponents[parentId]?.component?.component === 'Container' ||
allComponents[parentId]?.component?.component === 'Form' ||
allComponents[parentId]?.component?.component === 'ModalV2';
if (componentParentId && isParentTabORCalendar) {
let childComponent = deepClone(allComponents[componentId]);
@ -249,7 +257,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 +327,9 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
return (
parentComponent.component.component === 'Tabs' ||
parentComponent.component.component === 'Calendar' ||
parentComponent.component.component === 'Container'
parentComponent.component.component === 'Container' ||
parentComponent.component.component === 'Form' ||
parentComponent.component.component === 'ModalV2'
);
}
@ -483,11 +492,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 +667,42 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
return canvasBgColor;
};
export const getParentComponentIdByType = (child, parentComponent, parentId) => {
export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName }) => {
const { tab } = child;
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
else if (parentComponent === 'Container') return `${parentId}-header`;
else if (
slotName &&
(parentComponent === 'Form' || parentComponent === 'Container' || parentComponent === 'ModalV2')
) {
return `${parentId}-${slotName}`;
}
return parentId;
};
export const getParentWidgetFromId = (parentType, parentId) => {
const isAddingToSlot = parentId?.includes('-header') || parentId?.includes('-footer');
if (parentType === 'ModalV2' && isAddingToSlot) {
return 'ModalSlot';
} else if (parentType === 'Kanban') {
return 'Kanban_card';
}
return parentType;
};
export const getTabId = (parentId) => {
return parentId.split('-').slice(0, -1).join('-');
};
export const getSubContainerIdWithSlots = (parentId) => {
let cleanParentId = parentId;
if (parentId) {
if (parentId.includes('header')) {
cleanParentId = parentId.replace('-header', '');
} else if (parentId.includes('footer')) {
cleanParentId = parentId.replace('-footer', '');
}
}
return cleanParentId;
};

View file

@ -3,15 +3,18 @@
}
.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover {
z-index: 4 !important;
}
.main-editor-canvas .widget-target:has(.nested-target:hover):hover {
outline: 0px solid #4af;
}
.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
outline: 1px solid #4af;
z-index: 4 !important;
}
.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
// outline: 1px solid #4af;
z-index: 4 !important;
}
// .main-editor-canvas .widget-target:hover {
// outline: 1px solid #4af;
// }

View file

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

View file

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

View file

@ -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 = () => (
<div className="search-replace-inputs">
<InputComponent
leadingIcon="search01"
onChange={(e) => setSearchText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && findNext(view)}
onFocus={() => setShortcutEnabled(true)}
onBlur={() => setShortcutEnabled(false)}
placeholder="Find"
size="small"
value={searchText}

View file

@ -56,8 +56,8 @@ const TJDBCodeEditor = (props) => {
const handleOnChange = (value) => {
if (value === '') {
setErrorState(true);
setError('JSON cannot be empty');
setErrorState(false);
setError(null);
setCurrentValue(value);
return;
}
@ -167,18 +167,18 @@ const TJDBCodeEditor = (props) => {
componentName={componentName}
key={componentName}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: '' }}
optionalProps={{ styles: { height: 300 }, cls: 'tjdb-hinter-portal' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal tjdb-portal-codehinter' }}
dragResizePortal={true}
callgpt={null}
>
<ErrorBoundary>
<div className={`${errorState && 'tjdb-hinter-error'}`} data-cy={`${cyLabel}-input-field`}>
<div className={`${errorState && 'tjdb-hinter-error'} h-100`} data-cy={`${cyLabel}-input-field`}>
<CodeMirror
value={currentValue}
placeholder={placeholder}
height={isOpen ? '350px' : '32px'}
height={isOpen ? '32px' : '32px'}
maxHeight={'350px'}
width="100%"
theme={theme}

View file

@ -644,11 +644,19 @@
}
.cm-searchMatch {
background-color: #F9E71A !important;
.cm-selectionMatch {
background-color: #F9E71A !important;
}
}
.cm-searchMatch.cm-searchMatch-selected {
background-color: #F28F2D;
background-color: #F28F2D !important;
}
.tjdb-hinter-portal{
.cm-theme{
height: 100% ;
}
}

View file

@ -88,9 +88,19 @@ const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]);
const handleNodeExpansion = (path) => {
const handleNodeExpansion = (path, data, currentNode) => {
if (pathToBeInspected && path?.length > 0) {
return pathToBeInspected.includes(path[path.length - 1]);
const shouldExpand = pathToBeInspected.includes(path[path.length - 1]);
// Scroll to the component in the inspector
if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) {
const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
return shouldExpand;
} else return false;
};

View file

@ -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 = [

View file

@ -45,15 +45,15 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
const queryName = selectedQuery?.name ?? '';
const sourcecomponentName = selectedDataSource?.kind?.charAt(0).toUpperCase() + selectedDataSource?.kind?.slice(1);
const ElementToRender = selectedDataSource?.pluginId ? source : allSources[sourcecomponentName];
const ElementToRender = selectedDataSource?.plugin_id ? source : allSources[sourcecomponentName];
const defaultOptions = useRef({});
const isFreezed = useStore((state) => state.getShouldFreeze());
useEffect(() => {
setDataSourceMeta(
selectedQuery?.pluginId
? selectedQuery?.manifestFile?.data?.source
selectedQuery?.plugin_id
? selectedQuery?.manifest_file?.data?.source
: DataSourceTypes.find((source) => source.kind === selectedQuery?.kind)
);
setSelectedQueryId(selectedQuery?.id);
@ -188,7 +188,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
<ElementToRender
renderCopilot={renderCopilot}
key={selectedQuery?.id}
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
pluginSchema={selectedDataSource?.plugin?.operations_file?.data}
selectedDataSource={selectedDataSource}
options={selectedQuery?.options}
optionsChanged={optionsChanged}
@ -281,7 +281,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE;
const docLink = isSampleDb
? 'https://docs.tooljet.com/docs/data-sources/sample-data-sources'
: selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
: selectedDataSource?.plugin_id && selectedDataSource.plugin_id.trim() !== ''
? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
: `https://docs.tooljet.com/docs/data-sources/${selectedDataSource?.kind}`;
return (

View file

@ -1,21 +1,31 @@
import React from 'react';
import OverflowTooltip from '@/_components/OverflowTooltip';
export const BaseUrl = ({ dataSourceURL, theme }) => {
export const BaseUrl = ({ dataSourceURL, theme, className = 'col-auto', style = {} }) => {
return (
<span
className="col-auto"
htmlFor=""
className={`${className} base-url-container`}
style={{
padding: '5px',
border: theme === 'default' ? '1px solid rgb(217 220 222)' : '1px solid white',
borderRightWidth: 0,
background: theme === 'default' ? 'rgb(246 247 251)' : '#20211e',
color: theme === 'default' ? '#9ca1a6' : '#9e9e9e',
height: '32px',
borderRadius: '6px 0 0 6px',
display: 'flex',
transition: 'height 0.2s ease',
...style,
}}
>
{dataSourceURL}
<OverflowTooltip
text={dataSourceURL}
width="559px"
whiteSpace="normal"
placement="auto"
style={{ height: '100%' }}
>
{dataSourceURL}
</OverflowTooltip>
</span>
);
};

View file

@ -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}
/>
</div>
<div className={`field w-100 rest-methods-url`}>
<div
className={`field rest-methods-url ${dataSourceURL && 'data-source-exists'}`}
style={{ width: 'calc(100% - 214px)' }}
>
<div className="font-weight-medium color-slate12">URL</div>
<div className="d-flex">
<div className="d-flex h-100 w-100">
{dataSourceURL && (
<BaseUrl theme={this.props.darkMode ? 'monokai' : 'default'} dataSourceURL={dataSourceURL} />
<BaseUrl
theme={this.props.darkMode ? 'monokai' : 'default'}
dataSourceURL={dataSourceURL}
style={{
overflowWrap: 'anywhere',
maxWidth: '40%',
width: 'fit-content',
height: `${this.state.codeHinterHeight}px`,
minHeight: '32px',
maxHeight: '220px',
}}
/>
)}
<div className={`flex-grow-1 rest-api-url-codehinter ${dataSourceURL ? 'url-input-group' : ''}`}>
<div
ref={this.codeHinterRef}
className={` flex-grow-1 rest-api-url-codehinter ${dataSourceURL ? 'url-input-group' : ''}`}
>
<CodeHinter
type="basic"
initialValue={options.url}

View file

@ -33,7 +33,7 @@ export const BulkUploadPrimaryKey = () => {
>
<input
type="text"
value={bulkUpdatePrimaryKey?.primary_key?.join() || ''}
value={bulkUpdatePrimaryKey?.primary_key?.join(', ') || ''}
style={{
width: '100%',
height: '100%',
@ -53,7 +53,7 @@ export const BulkUploadPrimaryKey = () => {
<div className="field flex-grow-1 minw-400-w-400">
<CodeHinter
type="basic"
initialValue={bulkUpdatePrimaryKey?.rows_update ?? {}}
initialValue={`{{${JSON.stringify(bulkUpdatePrimaryKey?.rows_update ?? [])}}}`}
className="codehinter-plugins"
placeholder="{{ [ { 'column1': 'value', ... } ] }}"
onChange={(newValue) => {

View file

@ -214,4 +214,9 @@
.input-value-padding {
box-sizing: border-box;
padding-right: 30px !important;
}
.react-datepicker__navigation{
overflow: visible !important;
height: inherit !important;
}

View file

@ -677,6 +677,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
}}
componentName="TooljetDatabase"
delayOnChange={false}
className="w-100"
/>
</div>
)}

View file

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

View file

@ -2,12 +2,14 @@ import React, { useEffect } from 'react';
import { WidgetBox } from '../WidgetBox';
import { useDrag, useDragLayer } from 'react-dnd';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils';
import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants';
export const DragLayer = ({ index, component }) => {
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: 'box',
item: { componentType: component.component },
item: { componentType: component.component, component },
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
}),
[component.component]
@ -18,7 +20,6 @@ export const DragLayer = ({ index, component }) => {
}, []);
const size = component.defaultSize || { width: 30, height: 40 };
return (
<>
{isDragging && <CustomDragLayer size={size} />}
@ -30,32 +31,39 @@ export const DragLayer = ({ index, component }) => {
};
const CustomDragLayer = ({ size }) => {
const { currentOffset } = useDragLayer((monitor) => ({
const { currentOffset, item } = useDragLayer((monitor) => ({
currentOffset: monitor.getSourceClientOffset(),
item: monitor.getItem(),
}));
if (!currentOffset) return null;
const canvasWidth = document.getElementsByClassName('real-canvas')[0]?.getBoundingClientRect()?.width;
const canvasWidth = item?.canvasWidth;
const canvasBounds = item?.canvasRef?.getBoundingClientRect();
const height = size.height;
const width = (canvasWidth * size.width) / 43;
const width = (canvasWidth * size.width) / NO_OF_GRIDS;
// Calculate position relative to the current canvas (parent or child)
const left = currentOffset.x - (canvasBounds?.left || 0);
const top = currentOffset.y - (canvasBounds?.top || 0);
const [x, y] = snapToGrid(canvasWidth, left, top);
return (
<div
style={{
position: 'fixed',
pointerEvents: 'none',
zIndex: -1,
left: 0,
top: 0,
zIndex: 1000,
left: canvasBounds?.left || 0,
top: canvasBounds?.top || 0,
height: `${height}px`,
width: `${width}px`,
}}
>
<div
style={{
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
transform: `translate(${x}px, ${y}px)`,
background: '#D9E2FC',
opacity: '0.7',
height: '100%',

View file

@ -1 +1 @@
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];

View file

@ -23,6 +23,7 @@ const SHOW_ADDITIONAL_ACTIONS = [
'Button',
'RichTextEditor',
'Image',
'ModalV2',
];
const PROPERTIES_VS_ACCORDION_TITLE = {
Text: 'Data',
@ -34,6 +35,7 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
Button: 'Data',
Image: 'Data',
Container: 'Data',
ModalV2: 'Data',
};
export const DefaultComponent = ({ componentMeta, darkMode, ...restProps }) => {
@ -151,7 +153,8 @@ export const baseComponentProperties = (
'properties',
currentState,
allComponents,
darkMode
darkMode,
''
)
),
});

View file

@ -19,16 +19,34 @@ export const Form = ({
allComponents,
pages,
}) => {
const properties = Object.keys(componentMeta.properties);
const tempComponentMeta = deepClone(componentMeta);
let properties = [];
let additionalActions = [];
let dataProperties = [];
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
const tempComponentMeta = deepClone(componentMeta);
for (const [key] of Object.entries(componentMeta?.properties)) {
if (componentMeta?.properties[key]?.section === 'additionalActions') {
additionalActions.push(key);
} else if (componentMeta?.properties[key]?.accordian === 'Data') {
dataProperties.push(key);
} else {
properties.push(key);
}
}
const { id } = component;
const newOptions = [{ name: 'None', value: 'none' }];
Object.entries(allComponents).forEach(([componentId, component]) => {
if (component.component.parent === id && component?.component?.component === 'Button') {
newOptions.push({ name: component.component.name, value: componentId });
Object.entries(allComponents).forEach(([componentId, _component]) => {
const validParent =
_component.component.parent === id ||
_component.component.parent === `${id}-footer` ||
_component.component.parent === `${id}-header`;
if (validParent && _component?.component?.component === 'Button') {
newOptions.push({ name: _component.component.name, value: componentId });
}
});
@ -48,7 +66,8 @@ export const Form = ({
allComponents,
validations,
darkMode,
pages
pages,
additionalActions
);
return <Accordion items={accordionItems} />;
@ -68,7 +87,8 @@ export const baseComponentProperties = (
allComponents,
validations,
darkMode,
pages
pages,
additionalActions
) => {
let items = [];
if (properties.length > 0) {
@ -90,6 +110,24 @@ export const baseComponentProperties = (
});
}
items.push({
title: 'Additional actions',
isOpen: true,
children: additionalActions?.map((property) =>
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode
)
),
});
if (events.length > 0) {
items.push({
title: `${i18next.t('widget.common.events', 'Events')}`,

View file

@ -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 <Accordion items={accordionItems} />;
};

View file

@ -37,7 +37,6 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
if (!Array.isArray(optionsValue)) {
optionsValue = Object.values(optionsValue);
}
const valuesToResolve = ['label', 'value'];
let options = [];
if (isDynamicOptionsEnabled || typeof optionsValue === 'string') {
@ -202,9 +201,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
});
setOptions(_options);
updateAllOptionsParams(_options);
setMarkedAsDefault(_value);
paramUpdated({ name: 'value' }, 'value', _value, 'properties');
updateAllOptionsParams(_options);
}
};

View file

@ -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 = ({
)}
</div>
)}
{column.columnType === 'json' && (
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
<ProgramaticallyHandleProperties
label="Indent"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="jsonIndentation"
props={column}
component={component}
paramMeta={{ type: 'toggle', displayName: 'Indent' }}
paramType="properties"
/>
</div>
</div>
)}
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
<ProgramaticallyHandleProperties

View file

@ -122,9 +122,18 @@ export const StylesTabElements = ({
</div>
)}
{['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' && (
<div data-cy={`input-and-label-text-color`} className="field px-3">

View file

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

View file

@ -624,6 +624,8 @@ class TableComponent extends React.Component {
return 'Select';
case 'newMultiSelect':
return 'Multiselect';
case 'json':
return 'JSON';
default:
capitalize(text ?? '');
}

View file

@ -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 (
<div className={`field ${options.className}`} style={{ marginBottom: '8px' }}>
<CodeEditor

View file

@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { DefaultComponent } from './Components/DefaultComponent';
import { FilePicker } from './Components/FilePicker';
import { Modal } from './Components/Modal';
import { ModalV2 } from './Components/ModalV2';
import { CustomComponent } from './Components/CustomComponent';
import { Icon } from './Components/Icon';
import useFocus from '@/_hooks/use-focus';
@ -78,6 +79,7 @@ const NEW_REVAMPED_COMPONENTS = [
'Icon',
'Image',
'Container',
'ModalV2',
];
export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => {
@ -703,6 +705,9 @@ const GetAccordion = React.memo(
case 'FilePicker':
return <FilePicker {...restProps} />;
case 'ModalV2':
return <ModalV2 {...restProps} />;
case 'Modal':
return <Modal {...restProps} />;

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
height: 480,
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 10,
left: 1,
height: 40,
},
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 12,
left: 32,
height: 36,
},
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'color',
displayName: 'Background color',
@ -274,26 +358,13 @@ export const formConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
exposedVariables: {
data: {},
isValid: true,
isVisible: true,
isDisabled: false,
isLoading: false,
},
actions: [
{
@ -304,6 +375,21 @@ export const formConfig = {
handle: 'resetForm',
displayName: 'Reset Form',
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set Disable',
params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set Loading',
params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
@ -317,15 +403,18 @@ export const formConfig = {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
buttonToSubmit: { value: '{{"none"}}' },
showHeader: { value: '{{false}}' },
showFooter: { value: '{{false}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -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,

View file

@ -48,8 +48,12 @@ export const listviewConfig = {
data: {
type: 'code',
displayName: 'List data',
validation: {
schema: { type: 'array', element: { type: 'object' } },
schema: {
type: 'union',
schemas: [
{ type: 'array', element: { type: 'object' } },
{ type: 'array', element: { type: 'string' } },
],
defaultValue: "[{text: 'Sample text 1'}]",
},
},

View file

@ -1,6 +1,6 @@
export const modalConfig = {
name: 'Modal',
displayName: 'Modal',
name: 'ModalLegacy',
displayName: 'Modal (Legacy)',
description: 'Show pop-up windows',
component: 'Modal',
defaultSize: {

View file

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

View file

@ -1,101 +0,0 @@
import React, { useMemo } from 'react';
import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container';
import Spinner from '@/_ui/Spinner';
import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
export const Container = ({
id,
properties,
styles,
darkMode,
height,
width,
setExposedVariables,
setExposedVariable,
}) => {
const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles;
const { isDisabled, isVisible, isLoading } = useExposeState(
properties.loadingState,
properties.visibility,
properties.disabledState,
setExposedVariables,
setExposedVariable
);
const contentBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor,
};
}, [styles.backgroundColor, darkMode]);
const headerBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode
? '#232E3C'
: styles.headerBackgroundColor,
};
}, [styles.headerBackgroundColor, darkMode]);
const computedStyles = {
backgroundColor: contentBgColor.backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
border: `1px solid ${borderColor}`,
height,
display: isVisible ? 'flex' : 'none',
overflow: 'hidden auto',
position: 'relative',
boxShadow,
};
const computedHeaderStyles = {
...headerBgColor,
height: `${headerHeight}px`,
flexShrink: 0,
flexGrow: 0,
borderBottom: `1px solid var(--border-weak)`,
};
const computedContentStyles = {
...contentBgColor,
flex: 1,
overflow: 'auto',
};
return (
<div
className={`jet-container tw-flex tw-flex-col ${isLoading && 'jet-container-loading'} ${
properties.showHeader && 'jet-container--with-header'
}`}
id={id}
data-disabled={isDisabled}
style={computedStyles}
>
{properties.showHeader && (
<ContainerComponent
id={`${id}-header`}
styles={computedHeaderStyles}
canvasHeight={headerHeight / 10}
canvasWidth={width}
allowContainerSelect={true}
darkMode={darkMode}
/>
)}
{isLoading ? (
<div className="h-100 d-flex align-items-center">
<Spinner />
</div>
) : (
<ContainerComponent
id={id}
styles={computedContentStyles}
canvasHeight={height}
canvasWidth={width}
darkMode={darkMode}
/>
)}
</div>
);
};

View file

@ -0,0 +1,119 @@
import React, { useMemo } from 'react';
import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container';
import Spinner from '@/_ui/Spinner';
import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
import { shallow } from 'zustand/shallow';
import {
CONTAINER_FORM_CANVAS_PADDING,
SUBCONTAINER_CANVAS_BORDER_WIDTH,
} from '@/AppBuilder/AppCanvas/appCanvasConstants';
import useStore from '@/AppBuilder/_stores/store';
import './container.scss';
export const Container = ({
id,
properties,
styles,
darkMode,
height,
width,
setExposedVariables,
setExposedVariable,
}) => {
const { isDisabled, isVisible, isLoading } = useExposeState(
properties.loadingState,
properties.visibility,
properties.disabledState,
setExposedVariables,
setExposedVariable
);
const isWidgetInContainerDragging = useStore(
(state) => state.containerChildrenMapping?.[id]?.includes(state?.draggingComponentId),
shallow
);
const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles;
const contentBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor,
};
}, [styles.backgroundColor, darkMode]);
const headerBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode
? '#232E3C'
: styles.headerBackgroundColor,
};
}, [styles.headerBackgroundColor, darkMode]);
const computedStyles = {
backgroundColor: contentBgColor.backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`,
height,
display: isVisible ? 'flex' : 'none',
flexDirection: 'column',
position: 'relative',
boxShadow,
};
const containerHeaderStyles = {
flexShrink: 0,
padding: `${CONTAINER_FORM_CANVAS_PADDING}px ${CONTAINER_FORM_CANVAS_PADDING}px 3px ${CONTAINER_FORM_CANVAS_PADDING}px`,
...headerBgColor,
};
const containerContentStyles = {
overflow: 'hidden auto',
display: 'flex',
height: '100%',
padding: `${CONTAINER_FORM_CANVAS_PADDING}px`,
};
return (
<div
className={`jet-container ${isLoading ? 'jet-container-loading' : ''}`}
id={id}
data-disabled={isDisabled}
style={computedStyles}
>
{isLoading ? (
<Spinner />
) : (
<>
{properties.showHeader && (
<div style={containerHeaderStyles} className="wj-container-header">
<ContainerComponent
id={`${id}-header`}
styles={{ ...headerBgColor, height: `${headerHeight}px` }}
canvasHeight={headerHeight / 10}
canvasWidth={width}
allowContainerSelect={true}
darkMode={darkMode}
componentType="Container"
/>
</div>
)}
<div style={containerContentStyles}>
<ContainerComponent
id={id}
styles={{
...contentBgColor,
// Prevent the scroll when dragging a widget inside the container or moving out of the container
overflow: isWidgetInContainerDragging ? 'hidden' : 'hidden auto',
}}
canvasHeight={height}
canvasWidth={width}
darkMode={darkMode}
componentType="Container"
/>
</div>
</>
)}
</div>
);
};

View file

@ -0,0 +1,13 @@
.wj-container-header {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: -7px;
right: -7px;
height: 1px;
background-color: var(--border-weak);
}
}

View file

@ -1,17 +1,25 @@
import React, { useRef, useState, useEffect, useMemo } from 'react';
import React, { useRef, useState, useEffect } from 'react';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import _, { debounce, omit } from 'lodash';
import { Box } from '@/Editor/Box';
import { generateUIComponents } from './FormUtils';
import { useMounted } from '@/_hooks/use-mount';
import { onComponentClick, removeFunctionObjects } from '@/_helpers/appUtils';
import { useAppInfo } from '@/_stores/appDataStore';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import RenderSchema from './RenderSchema';
import useStore from '@/AppBuilder/_stores/store';
import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
import { shallow } from 'zustand/shallow';
import {
CONTAINER_FORM_CANVAS_PADDING,
SUBCONTAINER_CANVAS_BORDER_WIDTH,
} from '@/AppBuilder/AppCanvas/appCanvasConstants';
import './form.scss';
const getCanvasHeight = (height) => {
const parsedHeight = height.includes('px') ? parseInt(height, 10) : height;
return Math.ceil(parsedHeight);
};
export const Form = function Form(props) {
const {
@ -29,27 +37,71 @@ export const Form = function Form(props) {
dataCy,
} = props;
const childComponents = useStore((state) => state.getChildComponents(id), shallow);
const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles;
const { buttonToSubmit, loadingState, advanced, JSONSchema } = properties;
const {
borderRadius,
borderColor,
boxShadow,
headerHeight,
footerHeight,
footerBackgroundColor,
headerBackgroundColor,
} = styles;
const {
buttonToSubmit,
loadingState,
advanced,
JSONSchema,
showHeader = false,
showFooter = false,
visibility,
disabledState,
} = properties;
const { isDisabled, isVisible, isLoading } = useExposeState(
properties.loadingState,
properties.visibility,
properties.disabledState,
setExposedVariables,
setExposedVariable
);
const backgroundColor =
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor;
const computedStyles = {
backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
border: `1px solid ${borderColor}`,
border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`,
height,
display: visibility ? 'flex' : 'none',
display: isVisible ? 'flex' : 'none',
position: 'relative',
overflow: 'hidden auto',
boxShadow,
flexDirection: 'column',
};
const childIdNameMap = useMemo(() => {
return Object.keys(childComponents).reduce((acc, id) => {
const component = childComponents[id]?.component?.component;
return { ...acc, [id]: component?.name };
}, {});
}, [childComponents]);
const formHeader = {
flexShrink: 0,
paddingBottom: '3px',
paddingTop: '7px',
paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
backgroundColor:
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
};
const formContent = {
overflow: 'hidden auto',
display: 'flex',
height: '100%',
paddingTop: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingBottom: showFooter ? '3px' : '7px',
paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
};
const formFooter = {
flexShrink: 0,
padding: `${CONTAINER_FORM_CANVAS_PADDING}px`,
backgroundColor:
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
};
const parentRef = useRef(null);
const childDataRef = useRef({});
@ -58,6 +110,8 @@ export const Form = function Form(props) {
const [isValid, setValidation] = useState(true);
const [uiComponents, setUIComponents] = useState([]);
const mounted = useMounted();
const canvasHeaderHeight = getCanvasHeight(headerHeight) / 10;
const canvasFooterHeight = getCanvasHeight(footerHeight) / 10;
useEffect(() => {
const exposedVariables = {
@ -155,7 +209,7 @@ export const Form = function Form(props) {
};
setExposedVariables(exposedVariables);
setValidation(childValidation);
}, [childrenData, advanced, JSON.stringify(childIdNameMap)]);
}, [childrenData, advanced]);
useEffect(() => {
document.addEventListener('submitForm', handleFormSubmission);
@ -245,105 +299,113 @@ export const Form = function Form(props) {
if (e.target.className === 'real-canvas') onComponentClick(id, component);
}} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked
>
{loadingState ? (
<div className="p-2" style={{ margin: '0px auto' }}>
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
</div>
) : (
<fieldset disabled={disabledState} style={{ width: '100%' }}>
{!advanced && (
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: '100%' }}>
<SubContainer
id={id}
canvasHeight={computedStyles.height}
canvasWidth={width}
onOptionChange={onOptionChange}
onOptionsChange={onOptionsChange}
styles={{ backgroundColor: computedStyles.backgroundColor }}
darkMode={darkMode}
/>
{/* <SubContainer
parentComponent={component}
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
removeComponent={removeComponent}
onOptionChange={function ({ component, optionName, value, componentId }) {
if (componentId) {
onOptionChange({ component, optionName, value, componentId });
}
}}
currentPageId={props.currentPageId}
{...props}
{...containerProps}
height={'100%'} // This height is required since Subcontainer has a issue if height is provided, it stores it in the ref and never updates that ref
/>
<SubCustomDragLayer
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
currentLayout={currentLayout}
/> */}
</div>
{showHeader && (
<div style={formHeader} className="wj-form-header">
<SubContainer
id={`${id}-header`}
canvasHeight={canvasHeaderHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
backgroundColor: 'transparent',
height: headerHeight,
}}
componentType="Form"
/>
{isDisabled && (
<div
id={`${id}-header-disabled`}
className="tj-form-disabled-overlay"
style={{ height: headerHeight || '100%' }}
onClick={() => {}}
onDrop={(e) => e.stopPropagation()}
/>
)}
{advanced &&
uiComponents?.map((item, index) => {
return (
<div
//check to avoid labels for these widgets as label is already present for them
className={
![
'Checkbox',
'StarRating',
'Multiselect',
'DropDown',
'RadioButton',
'ToggleSwitch',
'ToggleSwitchV2',
].includes(uiComponents?.[index + 1]?.component)
? `json-form-wrapper json-form-wrapper-disabled`
: `json-form-wrapper json-form-wrapper-disabled form-label-restricted`
}
key={index}
>
<div style={{ position: 'relative' }} className={`form-ele form-${id}-${index}`}>
<RenderSchema
component={item}
parent={id}
id={index}
darkMode={darkMode}
onOptionChange={onComponentOptionChangedForSubcontainer}
onOptionsChange={onComponentOptionsChangedForSubcontainer}
/>
</div>
)}
<div className="jet-form-body" style={formContent}>
{isLoading ? (
<div className="p-2 tw-flex tw-items-center tw-justify-center" style={{ margin: '0px auto' }}>
<div className="spinner-border" role="status"></div>
</div>
) : (
<fieldset disabled={isDisabled} style={{ width: '100%' }}>
{!advanced && (
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: '100%' }}>
<SubContainer
id={id}
canvasHeight={computedStyles.height}
canvasWidth={width}
onOptionChange={onOptionChange}
onOptionsChange={onOptionsChange}
styles={{ backgroundColor: computedStyles.backgroundColor }}
darkMode={darkMode}
componentType="Form"
/>
</div>
)}
{advanced &&
uiComponents?.map((item, index) => {
return (
<div
//check to avoid labels for these widgets as label is already present for them
className={
![
'Checkbox',
'StarRating',
'Multiselect',
'DropDown',
'RadioButton',
'ToggleSwitch',
'ToggleSwitchV2',
].includes(uiComponents?.[index + 1]?.component)
? `json-form-wrapper json-form-wrapper-disabled`
: `json-form-wrapper json-form-wrapper-disabled form-label-restricted`
}
key={index}
>
<div style={{ position: 'relative' }} className={`form-ele form-${id}-${index}`}>
<RenderSchema
component={item}
parent={id}
id={index}
darkMode={darkMode}
onOptionChange={onComponentOptionChangedForSubcontainer}
onOptionsChange={onComponentOptionsChangedForSubcontainer}
/>
</div>
</div>
{/* <Box
{...props}
component={item}
id={index}
width={width}
height={item.defaultSize.height}
mode={mode}
inCanvas={true}
paramUpdated={paramUpdated}
onEvent={onEvent}
onComponentClick={onComponentClick}
darkMode={darkMode}
removeComponent={removeComponent}
// canvasWidth={width}
// readOnly={readOnly}
// customResolvables={customResolvables}
parentId={id}
getContainerProps={getContainerProps}
onOptionChanged={onComponentOptionChangedForSubcontainer}
onOptionsChanged={onComponentOptionsChanged}
isFromSubContainer={true}
/> */}
</div>
);
})}
</fieldset>
);
})}
</fieldset>
)}
</div>
{showFooter && (
<div className="jet-form-footer wj-form-footer" style={formFooter}>
<SubContainer
id={`${id}-footer`}
canvasHeight={canvasFooterHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
margin: 0,
backgroundColor: 'transparent',
height: footerHeight,
}}
componentType="Form"
/>
{isDisabled && (
<div
id={`${id}-footer-disabled`}
className="tj-form-disabled-overlay"
style={{ height: footerHeight || '100%' }}
onClick={() => {}}
onDrop={(e) => e.stopPropagation()}
/>
)}
</div>
)}
</form>
);

View file

@ -0,0 +1,40 @@
.wj-form-header {
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: -7px;
right: -7px;
height: 1px;
background-color: var(--border-weak);
}
}
.wj-form-footer {
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: -7px;
right: -7px;
height: 1px;
background-color: var(--border-weak);
}
}
.tj-form-disabled-overlay {
/* TODO: Make slot overlays common */
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
margin: 0;
box-sizing: content-box;
padding: 4px 0;
}

View file

@ -56,6 +56,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) {
const [containers, setContainers] = useState([]);
const [showModal, setShowModal] = useState(false);
const setModalOpenOnCanvas = useStore((state) => 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(() => {
@ -410,6 +412,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) {
width: `${(Number(cardWidth) || 300) + 48}px`,
}}
kanbanProps={kanbanProps}
componentType="Kanban"
>
{items[columnId] && (
<SortableContext items={items[columnId]} strategy={verticalListSortingStrategy}>

View file

@ -12,11 +12,8 @@ import { shallow } from 'zustand/shallow';
export const Listview = function Listview({
id,
component,
width,
height,
containerProps,
removeComponent,
properties,
styles,
fireEvent,
@ -270,38 +267,8 @@ export const Listview = function Listview({
columns={positiveColumns}
listViewMode={mode}
darkMode={darkMode}
componentType="Listview"
/>
{/* <SubContainer
columns={positiveColumns}
listmode={mode}
parentComponent={component}
containerCanvasWidth={width}
parent={`${id}`}
parentName={component.name}
{...containerProps}
readOnly={index !== 0}
customResolvables={{ listItem }}
parentRef={parentRef}
removeComponent={removeComponent}
listViewItemOptions={{ index }}
exposedVariables={childrenData[index]}
onOptionChange={function ({ component, optionName, value, componentId }) {
setChildrenData((prevData) => {
const changedData = { [component.name]: { [optionName]: value } };
const existingDataAtIndex = prevData[index] ?? {};
const newDataAtIndex = {
...prevData[index],
[component.name]: {
...existingDataAtIndex[component.name],
...changedData[component.name],
id: componentId,
},
};
const newChildrenData = { ...prevData, [index]: newDataAtIndex };
return { ...prevData, ...newChildrenData };
});
}}
/> */}
</div>
))}
</div>

View file

@ -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 ****/

View file

@ -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 (
<BootstrapModal.Footer style={{ ...customStyles.modalFooter }} data-cy={`modal-footer`} onClick={onClick}>
<SubContainer
id={`${id}-footer`}
canvasHeight={canvasFooterHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
margin: 0,
backgroundColor: 'transparent',
overflowX: 'hidden',
overflowY: isDisabled ? 'hidden' : 'auto',
}}
/>
{isDisabled && (
<div
id={`${id}-footer-disabled`}
className="tj-modal-disabled-overlay"
style={{ height: footerHeight || '100%' }}
onClick={onClick}
onDrop={(e) => e.stopPropagation()}
/>
)}
</BootstrapModal.Footer>
);
});

View file

@ -0,0 +1,69 @@
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 ModalHeader = React.memo(
({ id, isDisabled, customStyles, hideCloseButton, darkMode, width, onHideModal, headerHeight, onClick }) => {
const canvasHeaderHeight = getCanvasHeight(headerHeight);
return (
<BootstrapModal.Header style={{ ...customStyles.modalHeader }} data-cy={`modal-header`} onClick={onClick}>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<SubContainer
id={`${id}-header`}
canvasHeight={canvasHeaderHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
backgroundColor: 'transparent',
overflowX: 'hidden',
overflowY: isDisabled ? 'hidden' : 'auto',
}}
/>
</div>
{isDisabled && (
<div
id={`${id}-header-disabled`}
className="tj-modal-disabled-overlay"
style={{ height: headerHeight || '100%' }}
onClick={onClick}
onDrop={(e) => e.stopPropagation()}
/>
)}
{!hideCloseButton && (
<div className="tw-w-14 tw-h-14 tw-flex tw-items-center tw-justify-center tw-pr-4 tw-relative">
<span
className={`tj-modal-close-button ${isDisabled ? 'is-disabled' : ''}`}
data-cy={`modal-close-button`}
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onHideModal();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icon-tabler-x"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</span>
</div>
)}
</BootstrapModal.Header>
);
}
);

View file

@ -0,0 +1,134 @@
import React, { useEffect } from 'react';
import { default as BootstrapModal } from 'react-bootstrap/Modal';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
import { ConfigHandle } from '@/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle';
import { ModalHeader } from '@/AppBuilder/Widgets/ModalV2/Components/Header';
import { ModalFooter } from '@/AppBuilder/Widgets/ModalV2/Components/Footer';
export const ModalWidget = ({ ...restProps }) => {
const {
customStyles,
parentRef,
id,
showConfigHandler,
isDisabled,
isLoading,
modalBodyHeight,
onHideModal,
hideCloseButton,
darkMode,
modalWidth,
showHeader,
hideOnEsc,
showFooter,
headerHeight,
footerHeight,
onSelectModal,
} = restProps['modalProps'];
// When the modal body is clicked capture it and use the callback to set the selected component as modal
const handleModalSlotClick = (event) => {
const clickedComponentId = event.target.getAttribute('component-id');
const clickedId = event.target.getAttribute('id');
// Check if the clicked element is part of the modal canvas & same widget with id
if (clickedComponentId?.includes(id)) {
onSelectModal(id);
} else if (clickedId?.includes(id)) {
onSelectModal(id);
}
};
useEffect(() => {
// When modal is active, prevent drop event on backdrop (else widgets droppped will get added to canvas)
const preventBackdropDrop = (e) => {
if (e.target.className === 'fade modal show') {
e.preventDefault();
e.stopPropagation();
}
};
document.addEventListener('drop', preventBackdropDrop);
return () => {
document.removeEventListener('drop', preventBackdropDrop);
};
}, []);
return (
<BootstrapModal
{...restProps}
contentClassName="modal-component tj-modal-widget-content"
animation={true}
onEscapeKeyDown={(e) => {
e.preventDefault();
if (hideOnEsc) {
onHideModal();
}
}}
onClick={handleModalSlotClick}
>
{showConfigHandler && (
<ConfigHandle
id={id}
customClassName={showHeader ? '' : 'modalWidget-config-handle tw-h-0'}
showHandle={showConfigHandler}
setSelectedComponentAsModal={onSelectModal}
componentType="Modal"
isModalOpen={true}
/>
)}
{showHeader && (
<ModalHeader
id={id}
isDisabled={isDisabled}
customStyles={customStyles}
hideCloseButton={hideCloseButton}
darkMode={darkMode}
width={modalWidth}
onHideModal={onHideModal}
headerHeight={headerHeight}
onClick={handleModalSlotClick}
/>
)}
<BootstrapModal.Body style={{ ...customStyles.modalBody }} ref={parentRef} id={id} data-cy={`modal-body`}>
{isDisabled && (
<div
id={`${id}-body-disabled`}
className="tj-modal-disabled-overlay"
style={{
height: modalBodyHeight || '100%',
}}
onDrop={(e) => e.stopPropagation()}
/>
)}
{!isLoading ? (
<>
<SubContainer
id={`${id}`}
canvasHeight={modalBodyHeight}
styles={{ backgroundColor: customStyles.modalBody.backgroundColor, height: 'inherit' }}
canvasWidth={modalWidth}
darkMode={darkMode}
/>
</>
) : (
<div className="p-2">
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
</div>
)}
</BootstrapModal.Body>
{showFooter && (
<ModalFooter
id={id}
isDisabled={isDisabled}
darkMode={darkMode}
customStyles={customStyles}
width={modalWidth}
footerHeight={footerHeight}
onClick={handleModalSlotClick}
/>
)}
</BootstrapModal>
);
};

View file

@ -0,0 +1,243 @@
import React, { useRef, useState, useEffect } from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { useExposeState } from '@/AppBuilder/Widgets/ModalV2/hooks/useModalCSA';
import { useResetZIndex } from '@/AppBuilder/Widgets/ModalV2/hooks/useModalZIndex';
import { useModalEventSideEffects } from '@/AppBuilder/Widgets/ModalV2/hooks/useResizeSideEffects';
import { useEventListener } from '@/_hooks/use-event-listener';
import { ModalWidget } from '@/AppBuilder/Widgets/ModalV2/Components/Modal';
import {
getModalBodyHeight,
getModalHeaderHeight,
getModalFooterHeight,
} from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
import { createModalStyles } from '@/AppBuilder/Widgets/ModalV2/helpers/stylesFactory';
import { onShowSideEffects, onHideSideEffects } from '@/AppBuilder/Widgets/ModalV2/helpers/sideEffects';
import '@/AppBuilder/Widgets/ModalV2/style.scss';
export const ModalV2 = function Modal({
id,
component,
darkMode,
properties,
styles,
setExposedVariable,
setExposedVariables,
fireEvent,
dataCy,
height,
}) {
const [showModal, setShowModal] = useState(false);
const {
closeOnClickingOutside = false,
hideOnEsc,
hideCloseButton,
hideTitleBar,
useDefaultButton,
triggerButtonLabel,
modalHeight,
showHeader,
showFooter,
headerHeight,
footerHeight,
} = properties;
const {
headerBackgroundColor,
footerBackgroundColor,
bodyBackgroundColor,
triggerButtonBackgroundColor,
triggerButtonTextColor,
boxShadow,
} = styles;
const isInitialRender = useRef(true);
const title = properties.title ?? '';
const titleAlignment = properties.titleAlignment ?? 'left';
const size = properties.size ?? 'lg';
const setSelectedComponentAsModal = useStore((state) => state.setSelectedComponentAsModal, shallow);
const mode = useStore((state) => state.currentMode, shallow);
const computedModalBodyHeight = getModalBodyHeight(modalHeight, showHeader, showFooter, headerHeight, footerHeight);
const headerHeightPx = getModalHeaderHeight(showHeader, headerHeight);
const footerHeightPx = getModalFooterHeight(showFooter, footerHeight);
const isFullScreen = properties.size === 'fullscreen';
const computedCanvasHeight = isFullScreen
? `calc(100vh - 48px - 40px - ${headerHeightPx} - ${footerHeightPx})`
: computedModalBodyHeight;
useEffect(() => {
const exposedVariables = {
open: async function () {
setExposedVariable('show', true);
setShowModal(true);
},
close: async function () {
setExposedVariable('show', false);
setShowModal(false);
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function hideModal() {
setExposedVariable('show', false);
setShowModal(false);
}
function openModal() {
setExposedVariable('show', true);
setShowModal(true);
}
useEventListener('resize', onShowSideEffects, window);
const onShowModal = () => {
openModal();
onShowSideEffects();
fireEvent('onOpen');
setSelectedComponentAsModal(id);
};
const onHideModal = () => {
onHideSideEffects(() => fireEvent('onOpen'));
hideModal();
setSelectedComponentAsModal(null);
};
useEffect(() => {
if (isInitialRender.current) {
isInitialRender.current = false;
return;
}
const inputRef = document?.getElementsByClassName('tj-text-input-widget')?.[0];
inputRef?.blur();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showModal]);
useEffect(() => {
// When modal is active, prevent drop event on backdrop (else widgets droppped will get added to canvas)
const preventBackdropDrop = (e) => {
if (e.target.className === 'fade modal show') {
e.preventDefault();
e.stopPropagation();
}
};
document.addEventListener('drop', preventBackdropDrop);
return () => {
document.removeEventListener('drop', preventBackdropDrop);
};
}, []);
const { controlBoxRef } = useResetZIndex({ showModal, id, mode });
const { isDisabledTrigger, isDisabledModal, isVisible, isLoading } = useExposeState({
loadingState: properties.loadingState,
visibleState: properties.visibility,
disabledModalState: properties.disabledModal,
disabledTriggerState: properties.disabledTrigger,
setExposedVariables,
setExposedVariable,
onHideModal,
onShowModal,
});
const customStyles = createModalStyles({
height,
modalHeight,
computedCanvasHeight,
bodyBackgroundColor,
darkMode,
isDisabledModal,
headerBackgroundColor,
headerHeightPx,
footerBackgroundColor,
footerHeightPx,
triggerButtonBackgroundColor,
triggerButtonTextColor,
isVisible,
boxShadow,
});
const { modalWidth, parentRef } = useModalEventSideEffects({
showModal,
size,
id,
onShowSideEffects,
closeOnClickingOutside,
onHideModal,
});
return (
<div
className="container d-flex align-items-center"
data-disabled={isDisabledTrigger}
data-cy={dataCy}
style={{ height }}
>
{useDefaultButton && isVisible && (
<button
disabled={isDisabledTrigger}
className="jet-button btn btn-primary p-1 overflow-hidden"
style={customStyles.buttonStyles}
onClick={(event) => {
/**** Start - Logic to reduce the zIndex of modal control box ****/
controlBoxRef.current = document.querySelector(`.selected-component.sc-${id}`)?.parentElement;
if (mode === 'edit' && controlBoxRef.current) {
controlBoxRef.current.classList.add('modal-moveable');
}
/**** End - Logic to reduce the zIndex of modal control box ****/
event.stopPropagation();
setShowModal(true);
}}
data-cy={`${dataCy}-launch-button`}
>
{triggerButtonLabel ?? 'Show Modal'}
</button>
)}
<ModalWidget
show={showModal}
contentClassName="modal-component"
container={document.getElementsByClassName('real-canvas')[0]}
size={size}
keyboard={true}
enforceFocus={false}
animation={false}
onShow={() => onShowModal()}
onHide={() => onHideModal()}
onEscapeKeyDown={() => hideOnEsc && onHideModal()}
id="modal-container"
component-id={id}
backdrop={'static'}
scrollable={true}
modalProps={{
customStyles,
parentRef,
id,
title,
titleAlignment,
hideTitleBar,
hideCloseButton,
onHideModal,
component,
hideOnEsc,
modalHeight,
isLoading,
isDisabled: isDisabledModal,
showConfigHandler: mode === 'edit',
fullscreen: isFullScreen,
showHeader,
showFooter,
headerHeight: headerHeightPx,
footerHeight: footerHeightPx,
modalBodyHeight: computedCanvasHeight,
modalWidth,
onSelectModal: setSelectedComponentAsModal,
}}
/>
</div>
);
};

View file

@ -0,0 +1,35 @@
// Side effects for modal, which include dom manipulation to hide overflow when opening
// And cleaning up dom when modal is closed
export const onShowSideEffects = () => {
const canvasElement = document.querySelector('.page-container.canvas-container');
const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
const allModalContainers = realCanvasEl.querySelectorAll('.modal');
const modalContainer = allModalContainers[allModalContainers.length - 1];
if (canvasElement && realCanvasEl && modalContainer) {
const currentScroll = canvasElement.scrollTop;
canvasElement.style.overflowY = 'hidden';
modalContainer.style.height = `${canvasElement.offsetHeight}px`;
modalContainer.style.top = `${currentScroll}px`;
}
};
export const onHideSideEffects = (callback = () => {}) => {
const canvasElement = document.querySelector('.page-container.canvas-container');
const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
const allModalContainers = realCanvasEl.querySelectorAll('.modal');
const modalContainer = allModalContainers[allModalContainers.length - 1];
const hasManyModalsOpen = allModalContainers.length > 1;
if (canvasElement && realCanvasEl && modalContainer) {
modalContainer.style.height = ``;
modalContainer.style.top = ``;
callback();
// fireEvent('onClose');
}
if (canvasElement && !hasManyModalsOpen) {
canvasElement.style.overflow = 'auto';
}
};

View file

@ -0,0 +1,49 @@
var tinycolor = require('tinycolor2');
export function createModalStyles({
height,
modalHeight,
computedCanvasHeight,
bodyBackgroundColor,
darkMode,
isDisabledModal,
headerBackgroundColor,
headerHeightPx,
footerBackgroundColor,
footerHeightPx,
triggerButtonBackgroundColor,
triggerButtonTextColor,
isVisible,
boxShadow,
}) {
const backwardCompatibilityCheck = height == '34' || modalHeight != undefined ? true : false;
return {
modalBody: {
height: backwardCompatibilityCheck ? computedCanvasHeight : height,
backgroundColor:
['#fff', '#ffffffff'].includes(bodyBackgroundColor) && darkMode ? '#1F2837' : bodyBackgroundColor,
overflowY: isDisabledModal ? 'hidden' : 'auto',
},
modalHeader: {
backgroundColor:
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
height: headerHeightPx,
overflowY: isDisabledModal ? 'hidden' : 'auto',
},
modalFooter: {
backgroundColor:
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
height: footerHeightPx,
overflowY: isDisabledModal ? 'hidden' : 'auto',
},
buttonStyles: {
backgroundColor: triggerButtonBackgroundColor,
color: triggerButtonTextColor,
width: '100%',
display: isVisible ? '' : 'none',
'--tblr-btn-color-darker': tinycolor(triggerButtonBackgroundColor).darken(8).toString(),
boxShadow,
},
};
}

View file

@ -0,0 +1,44 @@
const MODAL_HEADER = {
HEIGHT: 80,
};
const MODAL_FOOTER = {
HEIGHT: 80,
};
export const getCanvasHeight = (height) => {
const parsedHeight = height.includes('px') ? parseInt(height, 10) : height;
return Math.ceil(parsedHeight);
};
export const getModalBodyHeight = (
height,
showHeader,
showFooter,
headerHeight = MODAL_HEADER.HEIGHT,
footerHeight = MODAL_FOOTER.HEIGHT
) => {
let modalHeight = height ? parseInt(height, 10) : 0;
let parsedHeaderHeight = showHeader ? parseInt(headerHeight, 10) : 0;
let parsedFooterHeight = showFooter ? parseInt(footerHeight, 10) : 0;
if (showHeader) {
modalHeight = modalHeight - parsedHeaderHeight;
}
if (showFooter) {
modalHeight = modalHeight - parsedFooterHeight;
}
return `${Math.max(modalHeight, 40)}px`;
};
export const getModalHeaderHeight = (showHeader, headerHeight = MODAL_FOOTER.HEIGHT) => {
let parsedHeight = showHeader ? parseInt(headerHeight, 10) : 0;
return `${parsedHeight}px`;
};
export const getModalFooterHeight = (showFooter, footerHeight = MODAL_FOOTER.HEIGHT) => {
let parsedHeight = showFooter ? parseInt(footerHeight, 10) : 0;
return `${parsedHeight}px`;
};

View file

@ -0,0 +1,84 @@
import { useEffect, useState, useRef } from 'react';
export const useExposeState = ({
loadingState,
visibleState,
disabledModalState,
disabledTriggerState,
setExposedVariables,
setExposedVariable,
onHideModal,
onShowModal,
}) => {
const [isVisible, setVisibility] = useState(visibleState ?? true);
const [isLoading, setLoading] = useState(loadingState ?? false);
const [isDisabledModal, setDisabledModal] = useState(disabledModalState ?? false);
const [isDisabledTrigger, setDisabledTrigger] = useState(disabledTriggerState ?? false);
// Track previous values to prevent redundant updates
const prevValues = useRef({});
// Effect to sync state with props (only when props change)
useEffect(() => {
setDisabledModal(disabledModalState);
}, [disabledModalState]);
useEffect(() => {
setDisabledTrigger(disabledTriggerState);
}, [disabledTriggerState]);
useEffect(() => {
setVisibility(visibleState);
}, [visibleState]);
useEffect(() => {
setLoading(loadingState);
}, [loadingState]);
// Expose state-modifying functions only once
useEffect(() => {
setExposedVariables({
setDisableTrigger: async (value) => setDisabledTrigger(value),
setDisableModal: async (value) => setDisabledModal(value),
setVisibility: async (value) => setVisibility(value),
setLoading: async (value) => setLoading(value),
open: async () => onShowModal(),
close: async () => onHideModal(),
});
}, []);
// Prevent redundant updates to `setExposedVariable`
const updateExposedVariable = (key, value) => {
if (prevValues.current[key] !== value) {
prevValues.current[key] = value;
setExposedVariable(key, value);
}
};
useEffect(() => {
updateExposedVariable('isDisabledModal', isDisabledModal);
}, [isDisabledModal]);
useEffect(() => {
updateExposedVariable('isDisabledTrigger', isDisabledTrigger);
}, [isDisabledTrigger]);
useEffect(() => {
updateExposedVariable('isVisible', isVisible);
}, [isVisible]);
useEffect(() => {
updateExposedVariable('isLoading', isLoading);
}, [isLoading]);
return {
isDisabledTrigger,
setDisabledTrigger,
isDisabledModal,
setDisabledModal,
isVisible,
setVisibility,
isLoading,
setLoading,
};
};

View file

@ -0,0 +1,26 @@
import { useEffect, useRef } from 'react';
import { useGridStore } from '@/_stores/gridStore';
/**** Start - Logic to reset the zIndex of modal control box ****/
export const useResetZIndex = ({ showModal, id, mode }) => {
const controlBoxRef = useRef(null);
useEffect(() => {
if (!showModal && mode === 'edit') {
controlBoxRef.current?.classList?.remove('modal-moveable');
controlBoxRef.current = null;
}
if (showModal) {
useGridStore.getState().actions.setOpenModalWidgetId(id);
} else {
if (useGridStore.getState().openModalWidgetId === id) {
useGridStore.getState().actions.setOpenModalWidgetId(null);
}
}
}, [showModal, id, mode]);
/**** End - Logic to reset the zIndex of modal control box ****/
return {
controlBoxRef,
};
};

View file

@ -0,0 +1,80 @@
import { useState, useEffect, useRef } from 'react';
export function useModalEventSideEffects({
showModal,
size,
id,
onShowSideEffects,
closeOnClickingOutside,
onHideModal,
}) {
const [modalWidth, setModalWidth] = useState();
const parentRef = useRef(null);
// When query panel opens or closes, the modal container height should change to
// accomodate the new height of the canvas
useEffect(() => {
// Select the DOM element
const canvasElement = document.querySelector('.page-container.canvas-container');
if (!canvasElement) return; // Ensure the element exists
// Create a ResizeObserver
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
// Update the height state when the element's height changes
onShowSideEffects();
// When modal is in fullscreen and width of browser changes, update the modal width
if (size === 'fullscreen') {
const width = entry.target.offsetWidth;
setModalWidth(width);
}
}
});
// Observe the canvas element
resizeObserver.observe(canvasElement);
return () => {
// Cleanup observer on component unmount
resizeObserver.disconnect();
};
}, [size, onShowSideEffects]);
useEffect(() => {
if (showModal && parentRef.current) {
if (size === 'fullscreen') {
// First time modal is opened, the whole modal is part of the full body, later put into the canvas
// This delay is to get the correct width of the modal
const canvasElement = document.querySelector('.page-container.canvas-container');
const width = canvasElement.offsetWidth;
setModalWidth(width);
} else {
setModalWidth(parentRef.current.offsetWidth);
}
}
}, [showModal, size, id]);
useEffect(() => {
if (closeOnClickingOutside) {
const handleClickOutside = (event) => {
const modalRef = parentRef?.current?.parentElement?.parentElement?.parentElement;
if (modalRef && modalRef === event.target) {
onHideModal();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [closeOnClickingOutside, parentRef]);
return { modalWidth, parentRef };
}

View file

@ -0,0 +1,66 @@
.modal-open .modal.show {
/* To fix the padding issue */
scrollbar-gutter: auto;
// padding-right: 0 !important;
}
.modal-content.modal-component.tj-modal-widget-content {
border: 0;
.tj-modal-close-button {
padding: 8px;
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 4px;
transition: background-color 0.2s ease;
cursor: pointer;
&:hover {
background-color: var(--interactive-hover);
}
&.is-disabled {
position: absolute;
top: 8px;
bottom: 0;
display: inline-flex;
align-items: center;
justify-content: center;
left: 0;
z-index: 1;
}
}
.tj-modal-disabled-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
margin: 0;
}
.modal-header {
padding: 0;
position: relative;
}
.modal-body {
overflow-x: hidden;
position: relative;
}
.modal-footer {
position: relative;
padding: 0;
border-top: 1px solid var(--border-weak);
overflow-x: hidden;
width: 100%;
}
}

View file

@ -263,6 +263,9 @@ export const Datepicker = function Datepicker({
}
setIsDateInputFocussed(false);
}}
closeOnScroll={(e) => {
return e.target.className === 'table-responsive jet-data-table false false';
}}
/>
</div>
);

View file

@ -0,0 +1,191 @@
/* eslint-disable no-undef */
import React, { useState, useEffect } from 'react';
import { determineJustifyContentValue } from '@/_helpers/utils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
const Json = ({
isEditable,
jsonIndentation,
darkMode,
handleCellValueChange,
cellTextColor,
cellValue,
column,
containerWidth,
cell,
horizontalAlignment,
isMaxRowHeightAuto,
cellSize,
maxRowHeightValue,
}) => {
const ref = React.useRef(null);
const [hovered, setHovered] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const cellStyles = {
color: cellTextColor ?? 'inherit',
};
useEffect(() => {
if (!isEditable && isEditing) {
setIsEditing(false);
}
}, [isEditable]);
function format(obj) {
if (typeof obj !== 'object' || obj === null) {
return typeof obj === 'string' ? `"${obj}"` : obj;
}
if (Array.isArray(obj)) {
return `[ ${obj.map(format).join(', ')} ]`;
}
return `{ ${Object.entries(obj)
.map(([key, value]) => `"${key}": ${format(value)}`)
.join(', ')} }`;
}
const formatCellValue = (value, overlay = false) => {
try {
if (typeof value === 'object') {
if (jsonIndentation === true && !overlay) {
return JSON.stringify(value, null, 4).replace(/":/g, '": ');
}
const formattedJSON = format(value);
return formattedJSON;
} else {
if (jsonIndentation === true && !overlay) {
return JSON.stringify(JSON.parse(value), null, 4).replace(/":/g, '": ');
}
const formattedJSON = format(JSON.parse(value));
return formattedJSON;
}
} catch (error) {
return value;
}
};
const _renderString = () => (
<div
ref={ref}
contentEditable={'true'}
className={`h-100 text-container long-text-input d-flex align-items-center ${
darkMode ? ' textarea-dark-theme' : ''
} justify-content-${determineJustifyContentValue(horizontalAlignment)}`}
tabIndex={-1}
style={{
color: cellTextColor ? cellTextColor : 'inherit',
outline: 'none',
border: 'none',
background: 'inherit',
position: 'relative',
height: '100%',
}}
readOnly={!isEditable}
onBlur={(e) => {
setIsEditing(false);
if (cellValue !== e.target.textContent) {
const value = JSON.stringify(JSON.parse(e.target.textContent.replace(/\n/g, '')));
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
ref.current.blur();
if (cellValue !== e.target.textContent) {
const value = JSON.stringify(JSON.parse(e.target.textContent.replace(/\n/g, '')));
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}
}
}}
onFocus={(e) => {
setIsEditing(true);
e.stopPropagation();
}}
>
{String(formatCellValue(cellValue))}
</div>
);
const getOverlay = () => {
return (
<div
className={`overlay-cell-table ${darkMode && 'dark-theme'}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ color: 'var(--text-primary)' }}
>
<span
style={{
width: `${containerWidth}px`,
whiteSpace: 'pre-wrap',
}}
>
{String(formatCellValue(cellValue, true))}
</span>
</div>
);
};
const _showOverlay =
ref?.current &&
(ref?.current?.clientWidth < ref?.current?.children[0]?.offsetWidth ||
ref?.current?.clientHeight < ref?.current?.children[0]?.offsetHeight);
return (
<>
<OverlayTrigger
placement="bottom"
overlay={_showOverlay ? getOverlay() : <div></div>}
trigger={_showOverlay && ['hover', 'focus']}
rootClose={true}
show={_showOverlay && hovered && !isEditing}
>
{!isEditable ? (
<div
className={`d-flex align-items-center h-100 w-100 justify-content-${determineJustifyContentValue(
horizontalAlignment
)}`}
style={cellStyles}
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseLeave={() => {
setHovered(false);
}}
ref={ref}
>
<span
style={{
maxHeight: isMaxRowHeightAuto
? 'fit-content'
: maxRowHeightValue
? `${maxRowHeightValue}px`
: cellSize === 'condensed'
? '39px'
: '45px',
whiteSpace: 'pre-wrap',
}}
>
{String(formatCellValue(cellValue))}
</span>
</div>
) : (
<div className="h-100 d-flex flex-column justify-content-center position-relative">
<div
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
className={`${isEditing ? 'h-100 content-editing' : ''} h-100`}
>
{_renderString()}
</div>
</div>
)}
</OverlayTrigger>
</>
);
};
export default Json;

View file

@ -1114,10 +1114,9 @@ export const Table = React.memo(
<div
style={{
position: 'absolute',
top: 0,
top: `${items[0]?.start ?? 0}px`,
left: 0,
width: '100%',
transform: `translateY(${items[0]?.start ?? 0}px)`,
}}
>
{items.map((virtualRow) => {

View file

@ -14,6 +14,7 @@ import { CustomSelect } from '../CustomSelect';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import Text from '../Text';
import StringColumn from '../String';
import Json from '../Json';
export default function generateColumnsData({
columnProperties,
@ -714,6 +715,27 @@ export default function generateColumnsData({
</div>
);
}
case 'json': {
const jsonIndentation = getResolvedValue(column?.jsonIndentation) ?? true;
return (
<Json
isEditable={isEditable}
jsonIndentation={jsonIndentation}
darkMode={darkMode}
handleCellValueChange={handleCellValueChange}
cellTextColor={cellTextColor}
horizontalAlignment={horizontalAlignment}
cellValue={cellValue}
column={column}
currentState={currentState}
containerWidth={width}
cell={cell}
isMaxRowHeightAuto={isMaxRowHeightAuto}
cellSize={cellSize}
maxRowHeightValue={maxRowHeightValue}
/>
);
}
}
return cellValue || '';
},

View file

@ -126,6 +126,7 @@ export const Tabs = function Tabs({
allowContainerSelect={true}
styles={{ backgroundColor: bgColor }}
darkMode={darkMode}
componentType="Tabs"
/>
</div>
);

View file

@ -60,12 +60,13 @@ import { BoundedBox } from '@/Editor/Components/BoundedBox/BoundedBox';
import { isPDFSupported } from '@/_helpers/appUtils';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { useEditorStore } from '@/_stores/editorStore';
import { Container } from '@/AppBuilder/Widgets/Container';
import { Container } from '@/AppBuilder/Widgets/Container/Container';
import { Listview } from '@/AppBuilder/Widgets/Listview';
import { Tabs } from '@/AppBuilder/Widgets/Tabs';
import { Kanban } from '@/AppBuilder/Widgets/Kanban/Kanban';
import { Form } from '@/AppBuilder/Widgets/Form/Form';
import { Modal } from '@/AppBuilder/Widgets/Modal';
import { ModalV2 } from '@/AppBuilder/Widgets/ModalV2/ModalV2';
import { Calendar } from '@/AppBuilder/Widgets/Calendar/Calendar';
// import './requestIdleCallbackPolyfill';
@ -107,6 +108,7 @@ export const AllComponents = {
Multiselect,
MultiselectV2,
Modal,
ModalV2,
Chart,
Map: MapComponent,
QrScanner,

View file

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

View file

@ -112,7 +112,7 @@ export const createAppSlice = (set, get) => ({
setResolvedPageConstants,
setPageSwitchInProgress,
currentMode,
isLicenseValid,
license,
modules: {
canvas: { pages },
},
@ -127,7 +127,7 @@ export const createAppSlice = (set, get) => ({
const appId = get().app.appId;
const filteredQueryParams = queryParams.filter(([key, value]) => {
if (!value) return false;
if (key === 'env' && !isLicenseValid()) return false;
if (key === 'env' && !license.isLicenseValid()) return false;
return true;
});

View file

@ -10,7 +10,11 @@ import {
import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { cloneDeep, merge, set as lodashSet } from 'lodash';
import { computeComponentName, getAllChildComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils';
import {
computeComponentName,
getAllChildComponents,
getParentWidgetFromId,
} from '@/AppBuilder/AppCanvas/appCanvasUtils';
import { pageConfig } from '@/AppBuilder/RightSideBar/PageSettingsTab/pageConfig';
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
import { DEFAULT_COMPONENT_STRUCTURE } from './resolvedSlice';
@ -40,6 +44,7 @@ const initialState = {
currentPageHandle: null,
showWidgetDeleteConfirmation: false,
focusedParentId: null,
modalsOpenOnCanvas: [],
};
export const createComponentsSlice = (set, get) => ({
@ -761,7 +766,7 @@ export const createComponentsSlice = (set, get) => ({
const { getComponentTypeFromId } = get();
const transformedParentId = parentId?.length > 36 ? parentId.slice(0, 36) : parentId;
let parentType = getComponentTypeFromId(transformedParentId, moduleId);
const parentWidget = parentType === 'Kanban' ? 'Kanban_card' : parentType;
const parentWidget = getParentWidgetFromId(parentType, parentId);
const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
if (!isParentChangeAllowed)
@ -1742,7 +1747,10 @@ export const createComponentsSlice = (set, get) => ({
getCustomResolvableReference: (value, parentId, moduleId) => {
const { getParentComponentType } = get();
const parentComponentType = getParentComponentType(parentId, moduleId);
if (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) {
if (
(parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) ||
value === '{{listItem}}'
) {
return { entityType: 'components', entityNameOrId: parentId, entityKey: 'listItem' };
} else if (
parentComponentType === 'Kanban' &&
@ -1860,4 +1868,17 @@ export const createComponentsSlice = (set, get) => ({
const currentPage = getCurrentPage(moduleId);
return currentPage?.autoComputeLayout;
},
setModalOpenOnCanvas: (modalId, isOpen) => {
const { modalsOpenOnCanvas } = get();
let newModalOpenOnCanvas = [];
if (isOpen) {
newModalOpenOnCanvas = [...modalsOpenOnCanvas, modalId];
} else {
newModalOpenOnCanvas = modalsOpenOnCanvas.filter((id) => id !== modalId);
}
set((state) => {
state.modalsOpenOnCanvas = newModalOpenOnCanvas;
});
},
});

View file

@ -3,9 +3,11 @@ import { debounce } from 'lodash';
const initialState = {
hoveredComponentForGrid: '',
hoveredComponentBoundaryId: '',
triggerCanvasUpdater: false,
lastCanvasIdClick: '',
lastCanvasClickPosition: null,
draggingComponentId: null,
};
export const createGridSlice = (set, get) => ({
@ -13,11 +15,14 @@ export const createGridSlice = (set, get) => ({
setHoveredComponentForGrid: (id) =>
set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }),
getHoveredComponentForGrid: () => get().hoveredComponentForGrid,
setHoveredComponentBoundaryId: (id) =>
set(() => ({ hoveredComponentBoundaryId: id }), false, { type: 'setHoveredComponentBoundaryId', id }),
toggleCanvasUpdater: () =>
set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }),
debouncedToggleCanvasUpdater: debounce(() => {
get().toggleCanvasUpdater();
}, 200),
setDraggingComponentId: (id) => set(() => ({ draggingComponentId: id })),
moveComponentPosition: (direction) => {
const { setComponentLayout, currentLayout, getSelectedComponentsDefinition, debouncedToggleCanvasUpdater } = get();
let layouts = {};

View file

@ -37,4 +37,85 @@ export const createLeftSideBarSlice = (set, get) => ({
toggleLeftSidebar(true);
}
},
getComponentIdToAutoScroll: (componentId) => {
const { getCurrentPageComponents, getAllExposedValues, modalsOpenOnCanvas } = get();
const currentPageComponents = getCurrentPageComponents();
let targetComponentId = componentId;
let current = componentId;
const visited = new Set();
let isInsideOpenModal = false;
// Bubble up to the outermost parent to find the target component
// eslint-disable-next-line no-constant-condition
while (true) {
if (visited.has(current)) break;
visited.add(current);
const parentId = currentPageComponents?.[current]?.component?.parent;
if (!parentId) break;
let isComponentVisibleInParent = true;
let nextPossibleCandidate = parentId;
// If the component exists inside a tab component
const regForTabs = /-(?!\d{12}$)\d+$/; // Parent id for tabs follow the format 'id-index' and index is not UUIDv4 id segment
if (regForTabs.test(parentId)) {
const reg = /-(\d+)$/;
const tabIndex = Number(parentId.match(reg)[1]); // Tab index inside which the component exists
const tabId = parentId.replace(regForTabs, ''); // Extract tab id from parent id
const { currentTab } = getAllExposedValues().components?.[tabId] || {};
const activeTabIndex = Number(currentTab);
nextPossibleCandidate = tabId;
if (tabIndex !== activeTabIndex) {
isComponentVisibleInParent = false;
}
}
// If the component exists inside a modal component
if (currentPageComponents?.[parentId]?.component?.component === 'Modal') {
nextPossibleCandidate = parentId;
if (!modalsOpenOnCanvas.includes(parentId)) {
isComponentVisibleInParent = false;
}
}
// If the component exists inside the kanban component's modal
if (parentId.endsWith('-modal')) {
nextPossibleCandidate = parentId.replace(/-modal$/, ''); // Extract kanban id from parent id
if (!modalsOpenOnCanvas.includes(parentId)) {
isComponentVisibleInParent = false;
}
}
// If the open modal contains the component
if (modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] === parentId) {
isInsideOpenModal = true;
}
if (!isComponentVisibleInParent) {
targetComponentId = nextPossibleCandidate;
}
current = nextPossibleCandidate;
}
if (modalsOpenOnCanvas.length > 0 && !isInsideOpenModal) {
const targetId = visited.size === 1 ? modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] : current;
const componentName = currentPageComponents?.[targetId]?.component?.name;
return {
isAccessible: false,
computedComponentId: componentName,
isOnCanvas: visited.size === 1,
};
}
return {
isAccessible: true,
computedComponentId: targetComponentId,
};
},
});

View file

@ -42,7 +42,7 @@ export const CustomComponent = (props) => {
setCustomProps({ ...customPropRef.current, ...e.data.updatedObj });
} else if (e.data.message === 'RUN_QUERY') {
const options = {
parameters: e.data.parameters,
parameters: JSON.parse(e.data.parameters),
queryName: e.data.queryName,
};
onEvent('onTrigger', [], options);

View file

@ -61,7 +61,6 @@ export const DropdownV2 = ({
}) => {
const {
label,
value,
advanced,
schema,
placeholder,
@ -89,7 +88,7 @@ export const DropdownV2 = ({
padding,
} = styles;
const isInitialRender = useRef(true);
const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value));
const [currentValue, setCurrentValue] = useState(() => findDefaultItem(schema));
const isMandatory = validation?.mandatory ?? false;
const options = properties?.options;
const [validationStatus, setValidationStatus] = useState(validate(currentValue));
@ -168,18 +167,9 @@ export const DropdownV2 = ({
};
useEffect(() => {
if (advanced) {
setInputValue(findDefaultItem(schema));
}
setInputValue(findDefaultItem(advanced ? schema : options));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, JSON.stringify(schema)]);
useEffect(() => {
if (!advanced) {
setInputValue(value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, value]);
}, [advanced, JSON.stringify(schema), JSON.stringify(options)]);
useEffect(() => {
if (visibility !== properties.visibility) setVisibility(properties.visibility);
@ -446,6 +436,7 @@ export const DropdownV2 = ({
onChange={(selectedOption, actionProps) => {
if (actionProps.action === 'clear') {
setInputValue(null);
fireEvent('onSelect');
}
if (actionProps.action === 'select-option') {
setInputValue(selectedOption.value);

View file

@ -10,7 +10,7 @@ function deepEqualityCheckusingLoDash(obj1, obj2) {
export const shouldUpdate = (prevProps, nextProps) => {
const listToRender = getComponentsToRenders();
// evaluate change in exposedVariables only for Modal component, because open/close in modal relies on exposedVariables
const compareExposedVariables = nextProps.componentName === 'Modal';
const compareExposedVariables = nextProps.componentName === 'Modal' || nextProps.componentName === 'ModalV2';
let needToRender = false;

View file

@ -10,7 +10,7 @@ import { resolveWidgetFieldValue } from '@/_helpers/utils';
import ErrorBoundary from './ErrorBoundary';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
import { useNoOfGrid, useGridStore } from '@/_stores/gridStore';
import { useGridStore } from '@/_stores/gridStore';
import WidgetBox from './WidgetBox';
import * as Sentry from '@sentry/react';
import { findHighestLevelofSelection } from './DragContainer';
@ -61,7 +61,7 @@ const DraggableBox = React.memo(
}) => {
const isResizing = useGridStore((state) => state.resizingComponentId === id);
const [canDrag, setCanDrag] = useState(true);
const noOfGrid = useNoOfGrid();
const noOfGrid = 43;
const {
currentLayout,
setHoveredComponent,

View file

@ -5,6 +5,7 @@ import JSONTreeViewer from '@/_ui/JSONTreeViewer';
import cx from 'classnames';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import useStore from '@/AppBuilder/_stores/store';
import { toast } from 'react-hot-toast';
function Logs({ logProps, idx }) {
const [open, setOpen] = React.useState(false);
@ -52,10 +53,19 @@ function Logs({ logProps, idx }) {
}
};
const copyToClipboard = (data) => {
const stringified = JSON.stringify(data, null, 2).replace(/\\/g, '');
navigator.clipboard.writeText(stringified);
return toast.success('Value copied to clipboard', { position: 'top-center' });
};
const callbackActions = [
{
for: 'all',
actions: [{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }],
actions: [
{ name: 'Copy value', dispatchAction: copyToClipboard, icon: false },
{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true },
],
enableForAllChildren: true,
enableFor1stLevelChildren: true,
},

View file

@ -7,7 +7,6 @@ import { getWorkspaceId, decodeEntities } from '@/_helpers/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { staticDataSources as staticDatasources } from '../constants';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import Search from '@/_ui/Icon/solidIcons/Search';
import { Tooltip } from 'react-tooltip';
@ -16,7 +15,7 @@ import { canCreateDataSource } from '@/_helpers';
import './../queryManager.theme.scss';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, defaultDataSources }) {
function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, staticDataSources }) {
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const sampleDataSource = useSampleDataSource();
@ -33,11 +32,6 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
closePopup();
};
const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
const staticDataSources = workflowsEnabled
? staticDatasources
: staticDatasources.filter((ds) => ds?.kind !== 'workflows');
useEffect(() => {
const shouldAddSampleDataSource = !!sampleDataSource;
const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter(
@ -148,7 +142,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
</div>
),
isDisabled: true,
options: defaultDataSources?.map((source) => ({
options: staticDataSources?.map((source) => ({
label: (
<div>
<DataSourceIcon source={source} height={16} />{' '}

View file

@ -163,7 +163,7 @@ export const DateTimePicker = ({
<div className={`fw-400 tjdbCellMenuShortcutsText`}>Save Changes</div>
</div>
<div className="d-flex align-items-center gap-1">
<div className={`fw-500 tjdbCellMenuShortcutsInfo`} id="escbutton">
<div className={`fw-500 tjdbCellMenuShortcutsInfo esc-btn-datepicker`} id="escbutton">
Esc
</div>
<div className={`fw-400 tjdbCellMenuShortcutsText`}>Discard Changes</div>

View file

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

View file

@ -23,7 +23,7 @@ import { useEditorStore } from '@/_stores/editorStore';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import { useGridStore, useResizingComponentId } from '@/_stores/gridStore';
import { useGridStore } from '@/_stores/gridStore';
import GhostWidget from './GhostWidget';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
@ -68,7 +68,7 @@ export const SubContainer = ({
shallow
);
const resizingComponentId = useResizingComponentId();
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
const noOfGrids = 43;
const { isGridActive } = useGridStore((state) => ({ isGridActive: state.activeGrid === parent }), shallow);

View file

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

View file

@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
height: 480,
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 10,
left: 1,
height: 40,
},
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 12,
left: 32,
height: 36,
},
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'color',
displayName: 'Background color',
@ -274,26 +358,13 @@ export const formConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
exposedVariables: {
data: {},
isValid: true,
isVisible: true,
isDisabled: false,
isLoading: false,
},
actions: [
{
@ -304,6 +375,21 @@ export const formConfig = {
handle: 'resetForm',
displayName: 'Reset Form',
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set Disable',
params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set Loading',
params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
@ -317,14 +403,18 @@ export const formConfig = {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
showHeader: { value: '{{false}}' },
showFooter: { value: '{{false}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -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,

View file

@ -49,7 +49,13 @@ export const listviewConfig = {
type: 'code',
displayName: 'List data',
validation: {
schema: { type: 'array', element: { type: 'object' } },
schema: {
type: 'union',
schemas: [
{ type: 'array', element: { type: 'object' } },
{ type: 'array', element: { type: 'string' } },
],
},
defaultValue: "[{text: 'Sample text 1'}]",
},
},

View file

@ -1,6 +1,6 @@
export const modalConfig = {
name: 'Modal',
displayName: 'Modal',
name: 'ModalLegacy',
displayName: 'Modal (Legacy)',
description: 'Show pop-up windows',
component: 'Modal',
defaultSize: {

View file

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

View file

@ -1 +1,7 @@
export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy', 'RadioButtonLegacy'];
export const LEGACY_ITEMS = [
'ToggleSwitchLegacy',
'DropdownLegacy',
'MultiselectLegacy',
'RadioButtonLegacy',
'ModalLegacy',
];

View file

@ -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,

View file

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

Some files were not shown because too many files have changed in this diff Show more