mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Merge conflicts resolved
This commit is contained in:
commit
cb1c730f0e
158 changed files with 4948 additions and 942 deletions
|
|
@ -137,6 +137,7 @@ const WidgetIcon = (props) => {
|
|||
case 'map':
|
||||
return <Map {...props} />;
|
||||
case 'modal':
|
||||
case 'modallegacy':
|
||||
return <Modal {...props} />;
|
||||
case 'multiselect':
|
||||
case 'multiselectv2':
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@
|
|||
"userGroups": "User Groups",
|
||||
"createNewGroup": "Create new group",
|
||||
"updateGroup": "Update group",
|
||||
"addNewGroup": "Create new group",
|
||||
"addNewGroup": "Add new group",
|
||||
"enterName": "Enter group name",
|
||||
"createGroup": "Create Group",
|
||||
"name": "Name"
|
||||
|
|
|
|||
|
|
@ -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?"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const ConfigHandle = ({
|
|||
customClassName = '',
|
||||
showHandle,
|
||||
componentType,
|
||||
visibility,
|
||||
}) => {
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow);
|
||||
|
|
@ -28,16 +29,27 @@ export const ConfigHandle = ({
|
|||
);
|
||||
|
||||
const setComponentToInspect = useStore((state) => state.setComponentToInspect);
|
||||
const 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: position === 'top' ? '-20px' : widgetTop + height - (widgetTop < 10 ? 15 : 10),
|
||||
visibility: _showHandle ? 'visible' : 'hidden',
|
||||
left: '-1px',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
|
|
@ -51,7 +63,7 @@ export const ConfigHandle = ({
|
|||
>
|
||||
<span
|
||||
style={{
|
||||
background: componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
background: isModal && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
}}
|
||||
className="badge handle-content"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -65,9 +65,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-editor-canvas .widget-target:hover > .config-handle {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ export const Container = React.memo(
|
|||
|
||||
const [{ isOverCurrent }, drop] = useDrop({
|
||||
accept: 'box',
|
||||
hover: (item) => {
|
||||
item.canvasRef = realCanvasRef?.current;
|
||||
item.canvasId = id;
|
||||
item.canvasWidth = getContainerCanvasWidth();
|
||||
},
|
||||
drop: async ({ componentType }, monitor) => {
|
||||
const didDrop = monitor.didDrop();
|
||||
if (didDrop) return;
|
||||
|
|
@ -89,14 +94,15 @@ export const Container = React.memo(
|
|||
function getContainerCanvasWidth() {
|
||||
if (canvasWidth !== undefined) {
|
||||
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
|
||||
return canvasWidth;
|
||||
if (id === 'canvas') return canvasWidth;
|
||||
return canvasWidth - 2;
|
||||
}
|
||||
return realCanvasRef?.current?.offsetWidth;
|
||||
}
|
||||
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
|
||||
|
||||
useEffect(() => {
|
||||
useGridStore.getState().actions.setSubContainerWidths(id, (getContainerCanvasWidth() - 2) / NO_OF_GRIDS);
|
||||
useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [canvasWidth, listViewMode, columns]);
|
||||
|
||||
|
|
@ -137,8 +143,7 @@ export const Container = React.memo(
|
|||
}}
|
||||
style={{
|
||||
height: id === 'canvas' ? `${canvasHeight}` : '100%',
|
||||
// backgroundSize: '25.3953px 10px',
|
||||
backgroundSize: `${gridWidth}px 10px`,
|
||||
backgroundSize: `${gridWidth}px ${10}px`,
|
||||
backgroundColor:
|
||||
currentMode === 'view'
|
||||
? computeViewerBackgroundColor(darkMode, canvasBgColor)
|
||||
|
|
@ -169,6 +174,7 @@ export const Container = React.memo(
|
|||
data-parentId={id}
|
||||
canvas-height={canvasHeight}
|
||||
onClick={handleCanvasClick}
|
||||
component-type={componentType}
|
||||
>
|
||||
<div
|
||||
className={cx('container-fluid rm-container p-0', {
|
||||
|
|
|
|||
|
|
@ -186,5 +186,28 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.moveable-guideline {
|
||||
background: #97AEFC !important;
|
||||
opacity: 0.8;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* */
|
||||
.moveable-guideline.moveable-horizontal {
|
||||
height: 1px !important;
|
||||
width: 100% !important;
|
||||
background: #97AEFC !important;
|
||||
left: 0 !important;
|
||||
|
||||
}
|
||||
|
||||
.moveable-guideline.moveable-vertical {
|
||||
width: 1px !important;
|
||||
height: 100% !important;
|
||||
background: #97AEFC !important;
|
||||
top: 0 !important;
|
||||
|
||||
}
|
||||
|
||||
.moveable-guideline-group {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
|
@ -17,9 +17,12 @@ import {
|
|||
hasParentWithClass,
|
||||
getPositionForGroupDrag,
|
||||
adjustWidth,
|
||||
hideGridLines,
|
||||
showGridLines,
|
||||
} 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 +32,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 +55,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 = useGridStore((state) => state.draggingComponentId, shallow);
|
||||
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
|
||||
const [dragParentId, setDragParentId] = useState(null);
|
||||
const [elementGuidelines, setElementGuidelines] = useState([]);
|
||||
const componentsSnappedTo = useRef(null);
|
||||
const prevDragParentId = useRef(null);
|
||||
const newDragParentId = useRef(null);
|
||||
const [isGroupDragging, setIsGroupDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedSet = new Set(selectedComponents);
|
||||
const draggingOrResizingId = draggingComponentId || resizingComponentId;
|
||||
const isGrouped = findHighestLevelofSelection().length > 1;
|
||||
const firstSelectedParent =
|
||||
selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null;
|
||||
const selectedParent = dragParentId || firstSelectedParent;
|
||||
|
||||
const guidelines = boxList
|
||||
.filter((box) => {
|
||||
const isVisible =
|
||||
getResolvedValue(box?.component?.definition?.properties?.visibility?.value) ||
|
||||
getResolvedValue(box?.component?.definition?.styles?.visibility?.value);
|
||||
|
||||
// Early return for non-visible elements
|
||||
if (!isVisible) return false;
|
||||
|
||||
if (isGrouped) {
|
||||
// If component is selected, don't show its guidelines
|
||||
if (selectedSet.has(box.id)) return false;
|
||||
return selectedParent ? box.parent === selectedParent : !box.parent;
|
||||
}
|
||||
|
||||
if (draggingOrResizingId) {
|
||||
if (box.id === draggingOrResizingId) return false;
|
||||
return dragParentId ? box.parent === dragParentId : !box.parent;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((box) => `.ele-${box.id}`);
|
||||
setElementGuidelines(guidelines);
|
||||
}, [boxList, dragParentId, draggingComponentId, resizingComponentId, selectedComponents, getResolvedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setBoxList(
|
||||
|
|
@ -94,7 +141,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
boxList.forEach(({ id, height, width, x, y, gw }) => {
|
||||
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
|
||||
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
|
||||
y = Math.round(y / 10) * 10;
|
||||
y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
gw = gw ? gw : gridWidth;
|
||||
|
||||
const parent = transformedBoxes[id]?.component?.parent;
|
||||
|
|
@ -117,7 +164,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
setComponentLayout({
|
||||
[id]: {
|
||||
height: height ? height : 10,
|
||||
height: height ? height : GRID_HEIGHT,
|
||||
width: newWidth ? newWidth : 1,
|
||||
top: y,
|
||||
left: Math.round(x / gw),
|
||||
|
|
@ -319,7 +366,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
|
||||
// Round y position
|
||||
y = Math.max(0, Math.round(y / 10) * 10);
|
||||
y = Math.max(0, Math.round(y / GRID_HEIGHT) * GRID_HEIGHT);
|
||||
// Adjust height for certain parent components
|
||||
if (parent) {
|
||||
const parentElem = document.getElementById(`canvas-${parent}`);
|
||||
|
|
@ -354,17 +401,16 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
);
|
||||
|
||||
// Add event listeners for config handle visibility when hovering over widget boundary
|
||||
// This is needed even though we have hovered widget state because when hovered on boundary,
|
||||
// the hovered widget state is empty, hence created a separate state for boundary
|
||||
React.useEffect(() => {
|
||||
const moveableBox = document.querySelector(`.moveable-control-box`);
|
||||
const showConfigHandle = (e) => {
|
||||
const targetId = e.target.offsetParent.getAttribute('target-id');
|
||||
const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
|
||||
configHandle.classList.add('config-handle-visible');
|
||||
useStore.getState().setHoveredComponentBoundaryId(targetId);
|
||||
};
|
||||
const hideConfigHandle = (e) => {
|
||||
const targetId = e.target.offsetParent.getAttribute('target-id');
|
||||
const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
|
||||
configHandle.classList.remove('config-handle-visible');
|
||||
const hideConfigHandle = () => {
|
||||
useStore.getState().setHoveredComponentBoundaryId('');
|
||||
};
|
||||
if (moveableBox) {
|
||||
moveableBox.addEventListener('mouseover', showConfigHandle);
|
||||
|
|
@ -376,49 +422,10 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const handleDragGridLinesVisibility = (e, events = []) => {
|
||||
const { clientX, clientY } = e;
|
||||
if (!document.elementFromPoint(clientX, clientY)) return;
|
||||
|
||||
const targetElems = document.elementsFromPoint(clientX, clientY);
|
||||
const draggedOverElements = targetElems.filter(
|
||||
(ele) =>
|
||||
!events.some((event) => event.target.id === ele.id) &&
|
||||
(ele.classList.contains('target') || ele.classList.contains('real-canvas'))
|
||||
);
|
||||
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
|
||||
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
|
||||
const appCanvas = document.getElementById('real-canvas');
|
||||
|
||||
// Show grid line for main canvas
|
||||
draggedOverContainer?.classList.remove('hide-grid');
|
||||
draggedOverContainer?.classList.add('show-grid');
|
||||
|
||||
// Remove 'show-grid' class from all sub-canvases
|
||||
const canvasElms = document.getElementsByClassName('sub-canvas');
|
||||
Array.from(canvasElms).forEach((element) => {
|
||||
element.classList.remove('show-grid');
|
||||
element.classList.add('hide-grid');
|
||||
});
|
||||
|
||||
// Determine the potential new parent
|
||||
const parentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
|
||||
|
||||
// Show grid for the appropriate canvas
|
||||
if (parentId) {
|
||||
const newParentCanvas = document.getElementById('canvas-' + parentId);
|
||||
if (newParentCanvas) {
|
||||
appCanvas?.classList?.remove('show-grid');
|
||||
newParentCanvas?.classList.remove('hide-grid');
|
||||
newParentCanvas?.classList.add('show-grid');
|
||||
}
|
||||
}
|
||||
|
||||
useGridStore.getState().actions.setDragTarget(parentId);
|
||||
};
|
||||
|
||||
const handleDragGroupEnd = (e) => {
|
||||
try {
|
||||
hideGridLines();
|
||||
setIsGroupDragging(false);
|
||||
const { events, clientX, clientY } = e;
|
||||
const initialParent = events[0].target.closest('.real-canvas');
|
||||
// Get potential new parent using same logic as onDragEnd
|
||||
|
|
@ -477,7 +484,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
// Apply transform to return to original position
|
||||
ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${
|
||||
Math.round(_top / 10) * 10
|
||||
Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT
|
||||
}px)`;
|
||||
}
|
||||
});
|
||||
|
|
@ -514,7 +521,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
// Apply grid snapping and bounds
|
||||
const snappedX = Math.round(posX / _gridWidth) * _gridWidth;
|
||||
const snappedY = Math.round(posY / 10) * 10;
|
||||
const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
ev.target.style.transform = `translate(${snappedX}px, ${snappedY}px)`;
|
||||
return {
|
||||
|
|
@ -531,6 +538,18 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const components = Array.from(document.querySelectorAll('.active-target')).filter(
|
||||
(component) => !selectedComponents.includes(component.getAttribute('widgetid'))
|
||||
);
|
||||
const draggingOrResizing = draggingComponentId || resizingComponentId;
|
||||
if (!draggingOrResizing && components.length > 0) {
|
||||
for (const component of components) {
|
||||
component?.classList?.remove('active-target');
|
||||
}
|
||||
}
|
||||
}, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]);
|
||||
|
||||
if (mode !== 'edit') return null;
|
||||
|
||||
return (
|
||||
|
|
@ -557,7 +576,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
||||
if (currentWidget.component?.parent) {
|
||||
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
|
||||
useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent);
|
||||
setDragParentId(currentWidget.component?.parent);
|
||||
} else {
|
||||
document.getElementById('real-canvas').classList.add('show-grid');
|
||||
}
|
||||
|
|
@ -584,9 +603,6 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const maxLeft = containerWidth - e.target.clientWidth;
|
||||
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
|
||||
const maxHeightHit = transformY < 0 || transformY >= maxY;
|
||||
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
|
||||
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
|
||||
|
||||
if (!maxWidthHit || e.width < e.target.clientWidth) {
|
||||
e.target.style.width = `${e.width}px`;
|
||||
}
|
||||
|
|
@ -612,12 +628,12 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
// When clicked on widget boundary/resizer, select the component
|
||||
setSelectedComponents([e.target.id]);
|
||||
}
|
||||
|
||||
showGridLines();
|
||||
if (!isComponentVisible(e.target.id)) {
|
||||
return false;
|
||||
}
|
||||
useGridStore.getState().actions.setResizingComponentId(e.target.id);
|
||||
e.setMin([gridWidth, 10]);
|
||||
e.setMin([gridWidth, GRID_HEIGHT]);
|
||||
}}
|
||||
onResizeEnd={(e) => {
|
||||
try {
|
||||
|
|
@ -629,7 +645,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid');
|
||||
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
||||
let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth;
|
||||
const height = Math.round(e?.lastEvent?.height / 10) * 10;
|
||||
const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
const currentWidth = currentWidget.width * _gridWidth;
|
||||
const diffWidth = e.lastEvent?.width - currentWidth;
|
||||
|
|
@ -654,19 +670,17 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const maxLeft = containerWidth - e.target.clientWidth;
|
||||
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
|
||||
const maxHeightHit = transformY < 0 || transformY >= maxY;
|
||||
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
|
||||
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
|
||||
|
||||
const roundedTransformY = Math.round(transformY / 10) * 10;
|
||||
transformY = transformY % 10 === 5 ? roundedTransformY - 10 : roundedTransformY;
|
||||
const roundedTransformY = Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
transformY = transformY % GRID_HEIGHT === 5 ? roundedTransformY - GRID_HEIGHT : roundedTransformY;
|
||||
e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${
|
||||
Math.round(transformY / 10) * 10
|
||||
Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT
|
||||
}px)`;
|
||||
if (!maxWidthHit || e.width < e.target.clientWidth) {
|
||||
e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`;
|
||||
}
|
||||
if (!maxHeightHit || e.height < e.target.clientHeight) {
|
||||
e.target.style.height = `${Math.round(e.lastEvent.height / 10) * 10}px`;
|
||||
e.target.style.height = `${Math.round(e.lastEvent.height / GRID_HEIGHT) * GRID_HEIGHT}px`;
|
||||
}
|
||||
const resizeData = {
|
||||
id: e.target.id,
|
||||
|
|
@ -682,12 +696,11 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
} catch (error) {
|
||||
console.error('ResizeEnd error ->', error);
|
||||
}
|
||||
useGridStore.getState().actions.setDragTarget();
|
||||
setDragParentId(null);
|
||||
toggleCanvasUpdater();
|
||||
}}
|
||||
onResizeGroupStart={({ events }) => {
|
||||
const parentElm = events[0].target.closest('.real-canvas');
|
||||
parentElm.classList.add('show-grid');
|
||||
showGridLines();
|
||||
}}
|
||||
onResizeGroup={({ events }) => {
|
||||
const parentElm = events[0].target.closest('.real-canvas');
|
||||
|
|
@ -710,8 +723,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const { events } = e;
|
||||
const newBoxs = [];
|
||||
|
||||
const parentElm = events[0].target.closest('.real-canvas');
|
||||
parentElm.classList.remove('show-grid');
|
||||
hideGridLines();
|
||||
|
||||
// TODO: Logic needs to be relooked post go live P2
|
||||
groupResizeDataRef.current.forEach((ev) => {
|
||||
|
|
@ -722,9 +734,9 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
let width = Math.round(ev.width / _gridWidth) * _gridWidth;
|
||||
width = width < _gridWidth ? _gridWidth : width;
|
||||
let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth;
|
||||
let posY = Math.round(ev.drag.translate[1] / 10) * 10;
|
||||
let height = Math.round(ev.height / 10) * 10;
|
||||
height = height < 10 ? 10 : height;
|
||||
let posY = Math.round(ev.drag.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
let height = Math.round(ev.height / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
|
||||
|
||||
ev.target.style.width = `${width}px`;
|
||||
ev.target.style.height = `${height}px`;
|
||||
|
|
@ -752,7 +764,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
let posX = currentWidget?.layouts[currentLayout].left * _gridWidth;
|
||||
let posY = currentWidget?.layouts[currentLayout].top;
|
||||
let height = currentWidget?.layouts[currentLayout].height;
|
||||
height = height < 10 ? 10 : height;
|
||||
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
|
||||
ev.target.style.width = `${width}px`;
|
||||
ev.target.style.height = `${height}px`;
|
||||
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
|
||||
|
|
@ -767,6 +779,11 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}}
|
||||
checkInput
|
||||
onDragStart={(e) => {
|
||||
// This is to prevent parent component from being dragged and the stop the propagation of the event
|
||||
if (getHoveredComponentForGrid() !== e.target.id) {
|
||||
return false;
|
||||
}
|
||||
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
|
||||
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
|
||||
const box = boxList.find((box) => box.id === e.target.id);
|
||||
// Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS
|
||||
|
|
@ -779,7 +796,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
// to handle their own interactions like column resizing or card dragging
|
||||
let isDragOnInnerElement = false;
|
||||
|
||||
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
|
||||
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
|
||||
Also user dont need to drag an calender from using popup */
|
||||
if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) {
|
||||
return false;
|
||||
|
|
@ -809,10 +826,6 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
// This is to prevent parent component from being dragged and the stop the propagation of the event
|
||||
if (getHoveredComponentForGrid() !== e.target.id) {
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
try {
|
||||
|
|
@ -820,141 +833,103 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
useGridStore.getState().actions.setDraggingComponentId(null);
|
||||
isDraggingRef.current = false;
|
||||
}
|
||||
prevDragParentId.current = null;
|
||||
newDragParentId.current = null;
|
||||
setDragParentId(null);
|
||||
|
||||
if (!e.lastEvent) {
|
||||
return;
|
||||
if (!e.lastEvent) return;
|
||||
|
||||
// Build the drag context from the event
|
||||
const dragContext = dragContextBuilder({ event: e, widgets: boxList });
|
||||
const { target, source, dragged } = dragContext;
|
||||
|
||||
const targetSlotId = target?.slotId;
|
||||
const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth;
|
||||
|
||||
// const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || [];
|
||||
// const draggedWidgetType = dragged.widgetType;
|
||||
const isParentChangeAllowed = dragContext.isDroppable;
|
||||
|
||||
// Compute new position
|
||||
let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged);
|
||||
|
||||
const isModalToCanvas = source.isModal && target.slotId === 'real-canvas';
|
||||
|
||||
if (isParentChangeAllowed && !isModalToCanvas) {
|
||||
const parent = target.slotId === 'real-canvas' ? null : target.slotId;
|
||||
// Special case for Modal; If source widget is modal, prevent drops to canvas
|
||||
handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]);
|
||||
} else {
|
||||
const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth;
|
||||
|
||||
left = dragged.left * sourcegridWidth;
|
||||
top = dragged.top;
|
||||
|
||||
!isModalToCanvas ??
|
||||
toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
|
||||
}
|
||||
|
||||
let draggedOverElemId = boxList.find((box) => box.id === e.target.id)?.parent;
|
||||
let draggedOverElemIdType;
|
||||
const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
|
||||
let draggedOverElem;
|
||||
if (document.elementFromPoint(e.clientX, e.clientY) && parentComponent?.component?.component !== 'Modal') {
|
||||
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
|
||||
draggedOverElem = targetElems.find((ele) => {
|
||||
const isOwnChild = e.target.contains(ele); // if the hovered element is a child of actual draged element its not considered
|
||||
if (isOwnChild) return false;
|
||||
// Apply transform for smooth transition
|
||||
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
||||
let isDroppable = ele.id !== e.target.id && ele.classList.contains('drag-container-parent');
|
||||
if (isDroppable) {
|
||||
let widgetId = ele?.getAttribute('component-id') || ele.id;
|
||||
let widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
|
||||
if (!widgetType) {
|
||||
widgetId = widgetId.split('-').slice(0, -1).join('-');
|
||||
widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
|
||||
}
|
||||
if (
|
||||
!['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(
|
||||
widgetType
|
||||
)
|
||||
) {
|
||||
isDroppable = false;
|
||||
}
|
||||
}
|
||||
return isDroppable;
|
||||
});
|
||||
draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id;
|
||||
draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type');
|
||||
}
|
||||
|
||||
const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
|
||||
const currentParentId = boxList.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent;
|
||||
let left = e.lastEvent?.translate[0];
|
||||
let top = e.lastEvent?.translate[1];
|
||||
if (
|
||||
['Listview', 'Kanban', 'Container'].includes(
|
||||
boxList.find((box) => box.id === draggedOverElemId)?.component?.component
|
||||
)
|
||||
) {
|
||||
const elemContainer = e.target.closest('.real-canvas');
|
||||
const containerHeight = elemContainer.clientHeight;
|
||||
const maxY = containerHeight - e.target.clientHeight;
|
||||
top = top > maxY ? maxY : top;
|
||||
}
|
||||
|
||||
const currentWidget = boxList.find(({ id }) => id === e.target.id)?.component?.component;
|
||||
const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId;
|
||||
draggedOverElemIdType = getComponentTypeFromId(parentId);
|
||||
const parentWidget = draggedOverElemIdType === 'Kanban' ? 'Kanban_card' : draggedOverElemIdType;
|
||||
const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
|
||||
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
|
||||
if (draggedOverElemId !== currentParentId) {
|
||||
if (isParentChangeAllowed) {
|
||||
const draggedOverWidget = boxList.find((box) => box.id === draggedOverElemId);
|
||||
|
||||
let parentWidgetType = boxList.find((box) => box.id === draggedOverElemId)?.component?.component;
|
||||
// @TODO - When dropping back to container from canvas, the boxList doesn't have canvas header,
|
||||
// boxList will return null. But we need to tell getMouseDistanceFromParentDiv parentWidgetType is container
|
||||
// As container id is like 'canvas-2375e23765e-123234'
|
||||
if (parentId && !parentWidgetType && draggedOverElemId.includes('-header')) {
|
||||
parentWidgetType = 'Container';
|
||||
}
|
||||
|
||||
let { left: _left, top: _top } = getMouseDistanceFromParentDiv(
|
||||
e,
|
||||
draggedOverWidget?.component?.component === 'Kanban' ? draggedOverElem : draggedOverElemId,
|
||||
parentWidgetType
|
||||
);
|
||||
left = _left;
|
||||
top = _top;
|
||||
} else {
|
||||
const currBox = boxList.find((l) => l.id === e.target.id);
|
||||
left = currBox.left * gridWidth;
|
||||
top = currBox.top;
|
||||
toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`);
|
||||
}
|
||||
}
|
||||
|
||||
e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${
|
||||
Math.round(top / 10) * 10
|
||||
}px)`;
|
||||
if (draggedOverElemId === currentParentId || isParentChangeAllowed) {
|
||||
handleDragEnd([
|
||||
{
|
||||
id: e.target.id,
|
||||
x: left,
|
||||
y: Math.round(top / 10) * 10,
|
||||
parent: isParentChangeAllowed ? draggedOverElemId : undefined,
|
||||
},
|
||||
]);
|
||||
}
|
||||
const box = boxList.find((box) => box.id === e.target.id);
|
||||
//
|
||||
setTimeout(() => setSelectedComponents([box.id]));
|
||||
// Select the dragged component after drop
|
||||
setTimeout(() => setSelectedComponents([dragged.id]));
|
||||
} catch (error) {
|
||||
console.log('draggedOverElemId->error', error);
|
||||
console.error('Error in onDragEnd:', error);
|
||||
}
|
||||
// Hide all sub-canvases
|
||||
var canvasElms = document.getElementsByClassName('sub-canvas');
|
||||
var elementsArray = Array.from(canvasElms);
|
||||
elementsArray.forEach(function (element) {
|
||||
element.classList.remove('show-grid');
|
||||
element.classList.add('hide-grid');
|
||||
});
|
||||
document.getElementById('real-canvas')?.classList.remove('show-grid');
|
||||
setCanvasBounds({ ...CANVAS_BOUNDS });
|
||||
hideGridLines();
|
||||
toggleCanvasUpdater();
|
||||
}}
|
||||
onDrag={(e) => {
|
||||
// Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again
|
||||
if (!isDraggingRef.current) {
|
||||
useGridStore.getState().actions.setDraggingComponentId(e.target.id);
|
||||
showGridLines();
|
||||
isDraggingRef.current = true;
|
||||
}
|
||||
const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
|
||||
const currentWidget = boxList.find((box) => box.id === e.target.id);
|
||||
const currentParentId =
|
||||
currentWidget?.component?.parent === null ? 'canvas' : currentWidget?.component?.parent;
|
||||
const _gridWidth = useGridStore.getState().subContainerWidths[dragParentId] || gridWidth;
|
||||
const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current;
|
||||
|
||||
let top = e.translate[1];
|
||||
let left = e.translate[0];
|
||||
// Snap to grid
|
||||
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
|
||||
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
// This logic is to handle the case when the dragged element is over a new canvas
|
||||
if (_dragParentId !== currentParentId) {
|
||||
left = e.translate[0];
|
||||
top = e.translate[1];
|
||||
}
|
||||
|
||||
// Special case for Modal
|
||||
if (parentComponent?.component?.component === 'Modal') {
|
||||
const elemContainer = e.target.closest('.real-canvas');
|
||||
const containerHeight = elemContainer.clientHeight;
|
||||
const containerWidth = elemContainer.clientWidth;
|
||||
const maxY = containerHeight - e.target.clientHeight;
|
||||
const maxLeft = containerWidth - e.target.clientWidth;
|
||||
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
|
||||
const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
|
||||
const parentComponent = boxList.find((box) => box.id === parentId);
|
||||
const parentWidgetType = parentComponent?.component?.component;
|
||||
const isOnHeaderOrFooter = oldParentId
|
||||
? oldParentId.includes('-header') || oldParentId.includes('-footer')
|
||||
: false;
|
||||
const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter;
|
||||
const isParentNewModal = parentComponent?.component?.component === 'ModalV2';
|
||||
const isParentLegacyModal = parentComponent?.component?.component === 'Modal';
|
||||
const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
|
||||
|
||||
top = top < 0 ? 0 : top > maxY ? maxY : top;
|
||||
left = left < 0 ? 0 : left > maxLeft ? maxLeft : left;
|
||||
if (isParentModal) {
|
||||
const modalContainer = e.target.closest('.tj-modal-widget-content');
|
||||
const mainCanvas = document.getElementById('real-canvas');
|
||||
|
||||
const mainRect = mainCanvas.getBoundingClientRect();
|
||||
const modalRect = modalContainer.getBoundingClientRect();
|
||||
const relativePosition = {
|
||||
top: modalRect.top - mainRect.top,
|
||||
right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
|
||||
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
||||
left: modalRect.left - mainRect.left,
|
||||
};
|
||||
setCanvasBounds({ ...relativePosition });
|
||||
}
|
||||
|
||||
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
|
@ -963,8 +938,32 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
|
||||
);
|
||||
|
||||
handleDragGridLinesVisibility(e, [{ target: e.target }]);
|
||||
// This block is to show grid lines on the canvas when the dragged element is over a new canvas
|
||||
if (document.elementFromPoint(e.clientX, e.clientY)) {
|
||||
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
|
||||
const draggedOverElements = targetElems.filter(
|
||||
(ele) =>
|
||||
(ele.id !== e.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas')
|
||||
);
|
||||
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
|
||||
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
|
||||
|
||||
// Determine potential new parent
|
||||
let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
|
||||
|
||||
if (newParentId === e.target.id) {
|
||||
newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent;
|
||||
} else if (parentComponent?.component?.component === 'Modal') {
|
||||
// Never update parentId for Modal
|
||||
newParentId = parentComponent?.id;
|
||||
}
|
||||
|
||||
if (newParentId !== prevDragParentId.current) {
|
||||
setDragParentId(newParentId === 'canvas' ? null : newParentId);
|
||||
newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
|
||||
prevDragParentId.current = newParentId;
|
||||
}
|
||||
}
|
||||
// Postion ghost element exactly as same at dragged element
|
||||
if (document.getElementById(`moveable-drag-ghost`)) {
|
||||
document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
|
@ -979,31 +978,26 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
parentElm?.classList?.add('show-grid');
|
||||
}
|
||||
|
||||
handleDragGridLinesVisibility(ev, events);
|
||||
|
||||
events.forEach((ev) => {
|
||||
let left = ev.translate[0];
|
||||
let top = ev.translate[1];
|
||||
const currentWidget = boxList.find(({ id }) => id === ev.target.id);
|
||||
const _gridWidth =
|
||||
useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
|
||||
|
||||
let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth;
|
||||
let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
ev.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
});
|
||||
updateNewPosition(events);
|
||||
}}
|
||||
onDragGroupStart={({ events }) => {
|
||||
const parentElm = events[0]?.target?.closest('.real-canvas');
|
||||
parentElm?.classList?.add('show-grid');
|
||||
showGridLines();
|
||||
setIsGroupDragging(true);
|
||||
}}
|
||||
onDragGroupEnd={(e) => {
|
||||
handleDragGroupEnd(e);
|
||||
toggleCanvasUpdater();
|
||||
}}
|
||||
//snap settgins
|
||||
snappable={true}
|
||||
snapThreshold={10}
|
||||
isDisplaySnapDigit={false}
|
||||
bounds={CANVAS_BOUNDS}
|
||||
displayAroundControls={true}
|
||||
controlPadding={20}
|
||||
onClickGroup={(e) => {
|
||||
const targetId =
|
||||
e.inputEvent.target.id || e.inputEvent.target.closest('.moveable-box')?.getAttribute('widgetid');
|
||||
|
|
@ -1019,6 +1013,43 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
}
|
||||
}}
|
||||
//snap settgins
|
||||
snappable={true}
|
||||
snapGap={false}
|
||||
isDisplaySnapDigit={false}
|
||||
snapThreshold={GRID_HEIGHT}
|
||||
bounds={canvasBounds}
|
||||
// Guidelines configuration
|
||||
elementGuidelines={elementGuidelines}
|
||||
snapDirections={{
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
center: false,
|
||||
middle: false,
|
||||
}}
|
||||
elementSnapDirections={{
|
||||
top: true,
|
||||
left: true,
|
||||
bottom: true,
|
||||
right: true,
|
||||
center: false,
|
||||
middle: false,
|
||||
}}
|
||||
onSnap={(e) => {
|
||||
const components = e.elements;
|
||||
if (isArray(componentsSnappedTo.current)) {
|
||||
for (const component of componentsSnappedTo.current) {
|
||||
component?.element?.classList?.remove('active-target');
|
||||
}
|
||||
}
|
||||
componentsSnappedTo.current = components;
|
||||
for (const component of components) {
|
||||
component.element.classList.add('active-target');
|
||||
}
|
||||
}}
|
||||
snapGridAll={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
|
|||
? document.getElementById(id)
|
||||
: id
|
||||
: document.getElementsByClassName('real-canvas')[0];
|
||||
parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id);
|
||||
if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') {
|
||||
parentDiv = document.getElementById('canvas-' + id);
|
||||
}
|
||||
|
|
@ -391,3 +392,25 @@ export function hasParentWithClass(child, className) {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function showGridLines() {
|
||||
var canvasElms = document.getElementsByClassName('sub-canvas');
|
||||
var elementsArray = Array.from(canvasElms);
|
||||
elementsArray.forEach(function (element) {
|
||||
element.classList.remove('hide-grid');
|
||||
element.classList.add('show-grid');
|
||||
});
|
||||
document.getElementById('real-canvas')?.classList.remove('hide-grid');
|
||||
document.getElementById('real-canvas')?.classList.add('show-grid');
|
||||
}
|
||||
|
||||
export function hideGridLines() {
|
||||
var canvasElms = document.getElementsByClassName('sub-canvas');
|
||||
var elementsArray = Array.from(canvasElms);
|
||||
elementsArray.forEach(function (element) {
|
||||
element.classList.remove('show-grid');
|
||||
element.classList.add('hide-grid');
|
||||
});
|
||||
document.getElementById('real-canvas')?.classList.remove('show-grid');
|
||||
document.getElementById('real-canvas')?.classList.add('hide-grid');
|
||||
}
|
||||
|
|
|
|||
266
frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
Normal file
266
frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -89,6 +89,7 @@ const WidgetWrapper = memo(
|
|||
widgetHeight={layoutData.height}
|
||||
showHandle={isWidgetActive}
|
||||
componentType={componentType}
|
||||
visibility={visibility}
|
||||
/>
|
||||
)}
|
||||
<RenderWidget
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const CANVAS_WIDTHS = Object.freeze({
|
|||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,8 @@ export const getAllChildComponents = (allComponents, parentId) => {
|
|||
allComponents[parentId]?.component?.component === 'Tabs' ||
|
||||
allComponents[parentId]?.component?.component === 'Calendar' ||
|
||||
allComponents[parentId]?.component?.component === 'Kanban' ||
|
||||
allComponents[parentId]?.component?.component === 'Container';
|
||||
allComponents[parentId]?.component?.component === 'Container' ||
|
||||
allComponents[parentId]?.component?.component === 'ModalV2';
|
||||
|
||||
if (componentParentId && isParentTabORCalendar) {
|
||||
let childComponent = deepClone(allComponents[componentId]);
|
||||
|
|
@ -249,7 +256,6 @@ export const copyComponents = ({ isCut = false, isCloning = false }) => {
|
|||
const parentComponentId = isChildOfTabsOrCalendar(selectedComponent, allComponents)
|
||||
? selectedComponent.component.parent.split('-').slice(0, -1).join('-')
|
||||
: selectedComponent?.component?.parent;
|
||||
|
||||
if (parentComponentId) {
|
||||
// Check if the parent component is also selected
|
||||
const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId);
|
||||
|
|
@ -320,7 +326,8 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
|
|||
return (
|
||||
parentComponent.component.component === 'Tabs' ||
|
||||
parentComponent.component.component === 'Calendar' ||
|
||||
parentComponent.component.component === 'Container'
|
||||
parentComponent.component.component === 'Container' ||
|
||||
parentComponent.component.component === 'ModalV2'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -483,11 +490,14 @@ export function pasteComponents(targetParentId, copiedComponentObj) {
|
|||
// Prevent pasting if the parent subcontainer was deleted during a cut operation
|
||||
if (
|
||||
targetParentId &&
|
||||
// Check if targetParentId is deleted from the components
|
||||
!Object.keys(components).find(
|
||||
(key) =>
|
||||
targetParentId === key ||
|
||||
(components?.[key]?.component.component === 'Tabs' &&
|
||||
targetParentId?.split('-')?.slice(0, -1)?.join('-') === key)
|
||||
targetParentId?.split('-')?.slice(0, -1)?.join('-') === key) ||
|
||||
(['Container', 'Form', 'Modal'].includes(components?.[key]?.component.component) &&
|
||||
['header', 'footer'].some((section) => targetParentId.includes(section)))
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
|
@ -655,10 +665,23 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
|
|||
return canvasBgColor;
|
||||
};
|
||||
|
||||
export const getParentComponentIdByType = (child, parentComponent, parentId) => {
|
||||
export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => {
|
||||
const { tab } = child;
|
||||
|
||||
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
|
||||
else if (parentComponent === 'Container') return `${parentId}-header`;
|
||||
else if (parentComponent === 'Container' || parentComponent === 'ModalV2') {
|
||||
return `${parentId}-${slotName}`;
|
||||
}
|
||||
return parentId;
|
||||
};
|
||||
|
||||
export const getParentWidgetFromId = (parentType, parentId) => {
|
||||
const isAddingToSlot = parentId?.includes('-header') || parentId?.includes('-footer');
|
||||
|
||||
if (parentType === 'ModalV2' && isAddingToSlot) {
|
||||
return 'ModalSlot';
|
||||
} else if (parentType === 'Kanban') {
|
||||
return 'Kanban_card';
|
||||
}
|
||||
return parentType;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ const TJDBCodeEditor = (props) => {
|
|||
className="cm-codehinter position-relative"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: isOpen ? '350p' : 'auto',
|
||||
height: isOpen ? '350px' : 'auto',
|
||||
}}
|
||||
>
|
||||
<div className={`cm-codehinter ${darkMode && 'cm-codehinter-dark-themed'}`}>
|
||||
|
|
@ -167,14 +167,14 @@ const TJDBCodeEditor = (props) => {
|
|||
componentName={componentName}
|
||||
key={componentName}
|
||||
forceUpdate={forceUpdate}
|
||||
optionalProps={{ styles: { height: 300 }, cls: '' }}
|
||||
optionalProps={{ styles: { height: 300 }, cls: 'tjdb-hinter-portal' }}
|
||||
darkMode={darkMode}
|
||||
selectors={{ className: 'preview-block-portal tjdb-portal-codehinter' }}
|
||||
dragResizePortal={true}
|
||||
callgpt={null}
|
||||
>
|
||||
<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}
|
||||
|
|
|
|||
|
|
@ -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% ;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -214,4 +214,9 @@
|
|||
.input-value-padding {
|
||||
box-sizing: border-box;
|
||||
padding-right: 30px !important;
|
||||
}
|
||||
|
||||
.react-datepicker__navigation{
|
||||
overflow: visible !important;
|
||||
height: inherit !important;
|
||||
}
|
||||
|
|
@ -677,6 +677,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
}}
|
||||
componentName="TooljetDatabase"
|
||||
delayOnChange={false}
|
||||
className="w-100"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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%',
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
|
||||
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
''
|
||||
)
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -624,6 +624,8 @@ class TableComponent extends React.Component {
|
|||
return 'Select';
|
||||
case 'newMultiSelect':
|
||||
return 'Multiselect';
|
||||
case 'json':
|
||||
return 'JSON';
|
||||
default:
|
||||
capitalize(text ?? '');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -299,7 +299,6 @@ export const dropdownV2Config = {
|
|||
],
|
||||
},
|
||||
label: { value: 'Select' },
|
||||
value: { value: '{{"2"}}' },
|
||||
optionsLoadingState: { value: '{{false}}' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
visibility: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const modalConfig = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
name: 'ModalLegacy',
|
||||
displayName: 'Modal (Legacy)',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'Modal',
|
||||
defaultSize: {
|
||||
|
|
|
|||
277
frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
Normal file
277
frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
Normal 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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -45,7 +45,7 @@ export const Container = ({
|
|||
border: `1px solid ${borderColor}`,
|
||||
height,
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
overflow: 'hidden auto',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
boxShadow,
|
||||
};
|
||||
|
|
@ -66,9 +66,7 @@ export const Container = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`jet-container tw-flex tw-flex-col ${isLoading && 'jet-container-loading'} ${
|
||||
properties.showHeader && 'jet-container--with-header'
|
||||
}`}
|
||||
className={`jet-container widget-type-container ${properties.loadingState && 'jet-container-loading'}`}
|
||||
id={id}
|
||||
data-disabled={isDisabled}
|
||||
style={computedStyles}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 ****/
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
134
frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
Normal file
134
frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
243
frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx
Normal file
243
frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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';
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
44
frontend/src/AppBuilder/Widgets/ModalV2/helpers/utils.js
Normal file
44
frontend/src/AppBuilder/Widgets/ModalV2/helpers/utils.js
Normal 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`;
|
||||
};
|
||||
84
frontend/src/AppBuilder/Widgets/ModalV2/hooks/useModalCSA.js
Normal file
84
frontend/src/AppBuilder/Widgets/ModalV2/hooks/useModalCSA.js
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 };
|
||||
}
|
||||
66
frontend/src/AppBuilder/Widgets/ModalV2/style.scss
Normal file
66
frontend/src/AppBuilder/Widgets/ModalV2/style.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
191
frontend/src/AppBuilder/Widgets/Table/Json.jsx
Normal file
191
frontend/src/AppBuilder/Widgets/Table/Json.jsx
Normal 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;
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 || '';
|
||||
},
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ 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';
|
||||
|
||||
|
|
@ -106,6 +107,7 @@ export const AllComponents = {
|
|||
Multiselect,
|
||||
MultiselectV2,
|
||||
Modal,
|
||||
ModalV2,
|
||||
Chart,
|
||||
Map: MapComponent,
|
||||
QrScanner,
|
||||
|
|
|
|||
|
|
@ -336,6 +336,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);
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export const createAppSlice = (set, get) => ({
|
|||
setResolvedPageConstants,
|
||||
setPageSwitchInProgress,
|
||||
currentMode,
|
||||
isLicenseValid,
|
||||
license,
|
||||
modules: {
|
||||
canvas: { pages },
|
||||
},
|
||||
|
|
@ -129,7 +129,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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { debounce } from 'lodash';
|
|||
|
||||
const initialState = {
|
||||
hoveredComponentForGrid: '',
|
||||
hoveredComponentBoundaryId: '',
|
||||
triggerCanvasUpdater: false,
|
||||
lastCanvasIdClick: '',
|
||||
lastCanvasClickPosition: null,
|
||||
|
|
@ -13,6 +14,8 @@ export const createGridSlice = (set, get) => ({
|
|||
setHoveredComponentForGrid: (id) =>
|
||||
set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }),
|
||||
getHoveredComponentForGrid: () => get().hoveredComponentForGrid,
|
||||
setHoveredComponentBoundaryId: (id) =>
|
||||
set(() => ({ hoveredComponentBoundaryId: id }), false, { type: 'setHoveredComponentBoundaryId', id }),
|
||||
toggleCanvasUpdater: () =>
|
||||
set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }),
|
||||
debouncedToggleCanvasUpdater: debounce(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -299,7 +299,6 @@ export const dropdownV2Config = {
|
|||
],
|
||||
},
|
||||
label: { value: 'Select' },
|
||||
value: { value: '{{"2"}}' },
|
||||
optionsLoadingState: { value: '{{false}}' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
visibility: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const modalConfig = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
name: 'ModalLegacy',
|
||||
displayName: 'Modal (Legacy)',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'Modal',
|
||||
defaultSize: {
|
||||
|
|
|
|||
277
frontend/src/Editor/WidgetManager/configs/modalV2.js
Normal file
277
frontend/src/Editor/WidgetManager/configs/modalV2.js
Normal 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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1 +1,7 @@
|
|||
export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy', 'RadioButtonLegacy'];
|
||||
export const LEGACY_ITEMS = [
|
||||
'ToggleSwitchLegacy',
|
||||
'DropdownLegacy',
|
||||
'MultiselectLegacy',
|
||||
'RadioButtonLegacy',
|
||||
'ModalLegacy',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import { useLicenseStore } from '@/_stores/licenseStore';
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
|
||||
import HeaderSkeleton from '@/_ui/FolderSkeleton/HeaderSkeleton';
|
||||
import { UserGroupMigrationModal } from './MigrationModal/UserGroupMigrationModal';
|
||||
import {
|
||||
ImportAppMenu,
|
||||
AppActionModal,
|
||||
|
|
@ -136,13 +135,7 @@ class HomePageComponent extends React.Component {
|
|||
this.fetchWorkflowsWorkspaceLimit();
|
||||
this.fetchOrgGit();
|
||||
this.setQueryParameter();
|
||||
const hasShownModal = localStorage.getItem('hasShownUserGroupMigrationModal');
|
||||
const hasClosedBanner = localStorage.getItem('hasClosedGroupMigrationBanner');
|
||||
//Only show the modal once
|
||||
if (!hasShownModal) {
|
||||
this.setState({ showUserGroupMigrationModal: true });
|
||||
localStorage.setItem('hasShownUserGroupMigrationModal', 'true');
|
||||
}
|
||||
|
||||
//Only show the banner once
|
||||
if (hasClosedBanner) {
|
||||
|
|
@ -790,6 +783,16 @@ class HomePageComponent extends React.Component {
|
|||
handleCommitChange = (commitEnabled) => {
|
||||
this.setState({ commitEnabled: commitEnabled });
|
||||
};
|
||||
shouldShowMigrationBanner = () => {
|
||||
const { currentSessionValue } = authenticationService;
|
||||
const { appType } = this.props;
|
||||
return (
|
||||
currentSessionValue?.admin &&
|
||||
this.state.showGroupMigrationBanner &&
|
||||
new Date(currentSessionValue?.current_user?.created_at) < new Date('2025-02-01') &&
|
||||
appType !== 'workflow'
|
||||
);
|
||||
};
|
||||
render() {
|
||||
const {
|
||||
apps,
|
||||
|
|
@ -885,15 +888,6 @@ class HomePageComponent extends React.Component {
|
|||
return (
|
||||
<Layout switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode}>
|
||||
<div className="wrapper home-page">
|
||||
{authenticationService.currentSessionValue?.admin &&
|
||||
showUserGroupMigrationModal &&
|
||||
this.props.appType !== 'workflow' && (
|
||||
<UserGroupMigrationModal
|
||||
show={showUserGroupMigrationModal}
|
||||
onHide={() => this.setShowUserGroupMigrationModal()}
|
||||
darkMode={this.props.darkMode}
|
||||
/>
|
||||
)}
|
||||
<AppActionModal
|
||||
modalStates={{
|
||||
showCreateAppModal,
|
||||
|
|
@ -1240,14 +1234,12 @@ class HomePageComponent extends React.Component {
|
|||
classes={`${this.props.darkMode ? 'theme-dark dark-theme m-3 trial-banner' : 'm-3 trial-banner'}`}
|
||||
/>
|
||||
)}
|
||||
{authenticationService.currentSessionValue?.admin &&
|
||||
showGroupMigrationBanner &&
|
||||
this.props.appType !== 'workflow' && (
|
||||
<UserGroupMigrationBanner
|
||||
classes={`${this.props.darkMode ? 'theme-dark dark-theme m-3 trial-banner' : 'm-3 trial-banner'}`}
|
||||
closeBanner={this.setShowGroupMigrationBanner}
|
||||
/>
|
||||
)}
|
||||
{this.shouldShowMigrationBanner() && (
|
||||
<UserGroupMigrationBanner
|
||||
classes={`${this.props.darkMode ? 'theme-dark dark-theme m-3 trial-banner' : 'm-3 trial-banner'}`}
|
||||
closeBanner={this.setShowGroupMigrationBanner}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OrganizationList />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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'] },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
|||
import { BreadCrumbContext } from '@/App/App';
|
||||
import LicenseBanner from '@/modules/common/components/LicenseBanner';
|
||||
|
||||
export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) {
|
||||
export default function CreateTableDrawer({ bannerVisible, setBannerVisible, tablesLimit, setTablesLimit }) {
|
||||
const { organizationId, setSelectedTable, setTables, tables } = useContext(TooljetDatabaseContext);
|
||||
const [isCreateTableDrawerOpen, setIsCreateTableDrawerOpen] = useState(false);
|
||||
const { updateSidebarNAV } = useContext(BreadCrumbContext);
|
||||
const [tablesLimit, setTablesLimit] = useState({});
|
||||
setBannerVisible(tablesLimit?.current >= tablesLimit?.total - 1 || false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -42,15 +41,6 @@ export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) {
|
|||
Create new table
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
<LicenseBanner
|
||||
classes="mb-3 small"
|
||||
limits={tablesLimit}
|
||||
type="tables"
|
||||
size="small"
|
||||
style={{ marginTop: '20px' }}
|
||||
z-index="10000"
|
||||
/>
|
||||
|
||||
<Drawer
|
||||
isOpen={isCreateTableDrawerOpen}
|
||||
onClose={() => setIsCreateTableDrawerOpen(false)}
|
||||
|
|
|
|||
|
|
@ -251,11 +251,7 @@ const Filter = ({
|
|||
}
|
||||
/>
|
||||
<div className="tw-flex items-center tw-ml-[3px]">
|
||||
{filterCount > 0 ? (
|
||||
<span>{pluralize(validFilterCountRef.current, 'filter')}</span>
|
||||
) : (
|
||||
<div> Filter</div>
|
||||
)}
|
||||
{filterCount > 0 ? <span>{pluralize(filterCount, 'filter')}</span> : <div> Filter</div>}
|
||||
</div>
|
||||
{/* {areFiltersApplied && (
|
||||
<span>ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')}</span>
|
||||
|
|
|
|||
|
|
@ -3,18 +3,40 @@ import List from '../TableList';
|
|||
import CreateTableDrawer from '../Drawers/CreateTableDrawer';
|
||||
import { OrganizationList } from '@/modules/dashboard/components';
|
||||
import cx from 'classnames';
|
||||
import LicenseBanner from '@/modules/common/components/LicenseBanner';
|
||||
import { authenticationService } from '@/_services';
|
||||
|
||||
export default function Sidebar({ collapseSidebar }) {
|
||||
const [bannerVisible, setBannerVisible] = useState(false);
|
||||
const [tablesLimit, setTablesLimit] = useState({});
|
||||
const isAdmin = authenticationService.currentSessionValue?.admin === true;
|
||||
const isResourceLimitReached = tablesLimit?.percentage === 100;
|
||||
return (
|
||||
<div className={cx('tooljet-database-sidebar col d-flex flex-column', { 'visually-hidden': collapseSidebar })}>
|
||||
<div className={`sidebar-container ${!bannerVisible ? '' : 'sidebar-container-with-banner'}`}>
|
||||
<CreateTableDrawer bannerVisible={bannerVisible} setBannerVisible={setBannerVisible} />
|
||||
<CreateTableDrawer
|
||||
bannerVisible={bannerVisible}
|
||||
setBannerVisible={setBannerVisible}
|
||||
tablesLimit={tablesLimit}
|
||||
setTablesLimit={setTablesLimit}
|
||||
/>
|
||||
</div>
|
||||
<div className="col table-left-sidebar" data-cy="all-table-column">
|
||||
<div className={`sidebar-list-wrap ${!bannerVisible ? '' : 'sidebar-list-wrap-with-banner'}`}>
|
||||
<div
|
||||
className={`sidebar-list-wrap ${!bannerVisible ? '' : 'sidebar-list-wrap-with-banner'} ${
|
||||
isAdmin ? 'isAdmin' : ''
|
||||
} ${isResourceLimitReached ? 'resource-limit-reached' : ''}`}
|
||||
>
|
||||
<List />
|
||||
</div>
|
||||
<LicenseBanner
|
||||
classes="mb-3 small"
|
||||
limits={tablesLimit}
|
||||
type="tables"
|
||||
size="small"
|
||||
style={{ marginTop: '20px' }}
|
||||
z-index="10000"
|
||||
/>
|
||||
<OrganizationList />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { ToolTip } from '@/_components';
|
||||
|
||||
export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', ...rest }) {
|
||||
export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', placement = 'bottom', ...rest }) {
|
||||
const [isOverflowed, setIsOverflow] = useState(false);
|
||||
const textElementRef = useRef();
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ export default function OverflowTooltip({ children, className, whiteSpace = 'now
|
|||
className={className}
|
||||
delay={{ show: '0', hide: '0' }}
|
||||
tooltipClassName="overflow-tooltip"
|
||||
placement="bottom"
|
||||
placement={placement}
|
||||
message={children}
|
||||
show={isOverflowed}
|
||||
width={rest?.width}
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ export const authorizeUserAndHandleErrors = (workspace_id, workspace_slug, callb
|
|||
const unauthorized_organization_slug = workspace_slug;
|
||||
|
||||
/* get current session's workspace id */
|
||||
authenticationService
|
||||
sessionService
|
||||
.validateSession()
|
||||
.then(({ current_organization_id, ...restSessionData }) => {
|
||||
/* change current organization id to valid one [current logged in organization] */
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { authHeader, handleResponse, handleResponseWithoutValidation } from '@/_
|
|||
|
||||
function save(body) {
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleResponse);
|
||||
return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function get(validateResponse = true) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
const handleOutput = validateResponse ? handleResponse : handleResponseWithoutValidation;
|
||||
return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleOutput);
|
||||
return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleOutput);
|
||||
}
|
||||
|
||||
function getForAppViewerEditor(validateResponse = true) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const orgEnvironmentConstantService = {
|
|||
function getAll(type = null) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
const queryParams = type ? `?type=${type}` : '';
|
||||
return fetch(`${config.apiUrl}/organization-constants${queryParams}`, requestOptions).then(handleResponse);
|
||||
return fetch(`${config.apiUrl}/organization-constants/decrypted${queryParams}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function create(name, value, type, environments) {
|
||||
|
|
|
|||
|
|
@ -493,4 +493,13 @@ $btn-dark-color: #FFFFFF;
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//[Container-widget]Show scrollbar only on hover
|
||||
.widget-type-container {
|
||||
overflow: hidden auto;
|
||||
scrollbar-width: none;
|
||||
&:hover {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@
|
|||
.message-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
margin-left: 5px;
|
||||
|
||||
.heading {
|
||||
display: unset !important;
|
||||
|
|
@ -246,9 +246,9 @@
|
|||
|
||||
.upgrade-btn-new{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: auto;
|
||||
margin-left: auto;
|
||||
border: 1px solid var(--border-default, #CCD1D5);
|
||||
background: var(--background-surface-layer-01);
|
||||
|
|
@ -262,7 +262,9 @@
|
|||
padding: 8px 16px 8px 16px;
|
||||
|
||||
}
|
||||
|
||||
.demo-btn-bg{
|
||||
border: 1px solid var(--border-accent-weak, #97AEFC);
|
||||
}
|
||||
.start-trial-btn {
|
||||
min-width: 131px;
|
||||
}
|
||||
|
|
@ -318,16 +320,14 @@
|
|||
max-width: fit-content;
|
||||
|
||||
.upgrade-link {
|
||||
background: var(--upgrade-default);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-left: 2px;
|
||||
color: var(--text-default, #1B1F24);
|
||||
font-family: "IBM Plex Sans";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
width: 67px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
/* 166.667% */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -345,16 +345,13 @@
|
|||
max-width: fit-content;
|
||||
|
||||
.upgrade-link {
|
||||
background: linear-gradient(to right, #ff5f6d, #ffc371);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: var(--text-default, #1B1F24);
|
||||
font-family: "IBM Plex Sans";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
width: 67px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
/* 166.667% */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -372,16 +369,13 @@
|
|||
max-width: fit-content;
|
||||
|
||||
.upgrade-link {
|
||||
background: linear-gradient(to right, #ff5f6d, #ffc371);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: var(--text-default, #1B1F24);
|
||||
font-family: "IBM Plex Sans";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
width: 67px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
/* 166.667% */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -399,16 +393,13 @@
|
|||
max-width: fit-content;
|
||||
|
||||
.upgrade-link {
|
||||
background: var(--upgrade-default);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: var(--text-default, #1B1F24);
|
||||
font-family: "IBM Plex Sans";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
width: 67px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
/* 166.667% */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1250,6 +1250,11 @@ $border-radius: 4px;
|
|||
color: var(--slate12) !important;
|
||||
}
|
||||
}
|
||||
&.data-source-exists {
|
||||
.cm-editor {
|
||||
border-radius: 0 4px 4px 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rest-api-methods-select-element-container {
|
||||
|
|
|
|||
|
|
@ -1467,5 +1467,59 @@
|
|||
}
|
||||
|
||||
.tj-table-tag-col-readonly {
|
||||
margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
|
||||
}
|
||||
margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
|
||||
}
|
||||
|
||||
.jet-data-table {
|
||||
.table-bordered {
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid var(--interactive-overlay-border-pressed) !important;
|
||||
border-right: 1px solid var(--interactive-overlay-border-pressed) !important;
|
||||
|
||||
&:first-child {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
thead th {
|
||||
border-top: none !important;
|
||||
|
||||
&:first-child {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-striped {
|
||||
tbody {
|
||||
div[data-index]:nth-child(odd) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
div[data-index]:nth-child(even) {
|
||||
background-color: var(--slate2) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.jet-data-table {
|
||||
overflow: auto;
|
||||
}
|
||||
// hide scrollbar on touch devices
|
||||
.jet-data-table::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5277,7 +5277,8 @@ fieldset:disabled .btn {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
outline: 0
|
||||
outline: 0;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
|
|
@ -5311,7 +5312,7 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.modal-dialog-scrollable .modal-content {
|
||||
max-height: 100%;
|
||||
max-height: 88%;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
|
|
@ -5447,6 +5448,10 @@ fieldset:disabled .btn {
|
|||
margin: 0
|
||||
}
|
||||
|
||||
.real-canvas .modal-dialog.modal-fullscreen {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-fullscreen .modal-content {
|
||||
height: 100%;
|
||||
border: 0;
|
||||
|
|
@ -5465,6 +5470,12 @@ fieldset:disabled .btn {
|
|||
border-radius: 0
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
|
||||
// Modal header height
|
||||
padding-bottom: 56px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width:575.98px) {
|
||||
.modal-fullscreen-sm-down {
|
||||
width: 100vw;
|
||||
|
|
@ -19140,4 +19151,4 @@ img {
|
|||
background: #1f2936;
|
||||
border-color: #dadcde
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2665,10 +2665,18 @@ hr {
|
|||
overflow-y: initial !important
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable .modal-content {
|
||||
.modal-dialog-scrollable:not(.modal-fullscreen) .modal-content {
|
||||
max-height: 88% !important;
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable.modal-fullscreen .modal-content {
|
||||
max-height: 100% !important;
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
|
||||
// Modal header height
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -7935,7 +7943,7 @@ tbody {
|
|||
}
|
||||
|
||||
.sidebar-container-with-banner {
|
||||
height: 140px !important;
|
||||
height: 40px !important;
|
||||
padding-top: 1px !important;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
|
|
@ -12096,13 +12104,19 @@ tbody {
|
|||
.sidebar-list-wrap-with-banner {
|
||||
margin-top: 24px;
|
||||
padding: 0px 20px 20px 20px;
|
||||
height: calc(100vh - 280px);
|
||||
height: calc(100vh - 408px);
|
||||
overflow: auto;
|
||||
|
||||
span {
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
.sidebar-list-wrap.sidebar-list-wrap-with-banner.isAdmin {
|
||||
height: calc(100vh - 371px);
|
||||
&.resource-limit-reached {
|
||||
height: calc(100vh - 371px);
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-footer-btn-wrap,
|
||||
.variable-form-footer {
|
||||
|
|
@ -14027,8 +14041,8 @@ tbody {
|
|||
}
|
||||
|
||||
/*
|
||||
* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
|
||||
* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
|
||||
* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
|
||||
* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
|
||||
*/
|
||||
.tj-dark-mode {
|
||||
background-color: var(--base) !important;
|
||||
|
|
@ -18755,4 +18769,52 @@ section.ai-message-prompt-input-wrapper {
|
|||
|
||||
#inspector-tabpane-properties .accordion-header {
|
||||
height:32px;
|
||||
}
|
||||
}
|
||||
.workspace-constant-value {
|
||||
position: relative;
|
||||
|
||||
.fromEnv {
|
||||
content: '.env';
|
||||
border-radius: 6px;
|
||||
background: var(--Indigo-50, #EEF4FF);
|
||||
padding: 0px 8px;
|
||||
width: 40px;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
color: var(--Indigo-700, #3538CD);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.isDuplicate {
|
||||
padding: 0px 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--Error-50, #FEF3F2);
|
||||
color: var(--Error-700, #B42318);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
/* 166.667% */
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.env-secret-hidden-message {
|
||||
border-radius: 16px;
|
||||
background: var(--Warning-50, #FFFAEB);
|
||||
padding: 4px 12px;
|
||||
color: var(--Warning-700, #B54708);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
&.dark {
|
||||
background: #FFFAEB !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
frontend/src/_ui/Icon/solidIcons/AICrown.jsx
Normal file
22
frontend/src/_ui/Icon/solidIcons/AICrown.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
const AICrown = ({ className = '', fill = '#FCA23F', width = '40', height = '41', ...props }) => {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 40 41"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M36.6169 16.3861L33.2402 31.8226C33.0472 32.9321 32.0342 33.7522 30.9247 33.7522H9.07232C7.91457 33.7522 6.94979 32.9321 6.75683 31.8226L3.38008 16.4343C3.13889 15.1318 4.00719 13.9258 5.30965 13.6847C6.12972 13.5399 6.94979 13.8294 7.52866 14.4565L12.5455 19.8593L17.8519 7.8477C18.3825 6.64172 19.8297 6.11109 20.9874 6.68996C21.518 6.93116 21.904 7.31707 22.1452 7.8477L27.4515 19.811L32.4684 14.4082C33.3367 13.4435 34.8321 13.347 35.8451 14.2153C36.4722 14.7459 36.7617 15.6142 36.6169 16.3861Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default AICrown;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue