diff --git a/.github/workflows/render-preview-deploy.yml b/.github/workflows/render-preview-deploy.yml index a025290d39..ead9ba50bf 100644 --- a/.github/workflows/render-preview-deploy.yml +++ b/.github/workflows/render-preview-deploy.yml @@ -215,7 +215,7 @@ jobs: - name: Delete service run: | export SERVICE_ID=$(curl --request GET \ - --url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ + --url 'https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ --header 'accept: application/json' \ --header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \ jq -r '.[0].service.id') @@ -583,7 +583,7 @@ jobs: - name: Delete service run: | export SERVICE_ID=$(curl --request GET \ - --url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ + --url 'https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ --header 'accept: application/json' \ --header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \ jq -r '.[0].service.id') diff --git a/frontend/ee b/frontend/ee index 7e6cf7d2e9..d93ee7e131 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 7e6cf7d2e9c2dfaa4ca3d1308406cfed3c95715b +Subproject commit d93ee7e1318f044ef2327671b8b257648071453d diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index 84d31bc6ac..aaedf9deb9 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -3,9 +3,13 @@ import { shallow } from 'zustand/shallow'; import './configHandle.scss'; import useStore from '@/AppBuilder/_stores/store'; import { findHighestLevelofSelection } from '../Grid/gridUtils'; +import SolidIcon from '@/_ui/Icon/solidIcons/index'; + +const CONFIG_HANDLE_HEIGHT = 20; +const BUFFER_HEIGHT = 1; + export const ConfigHandle = ({ id, - position, widgetTop, widgetHeight, setSelectedComponentAsModal = () => null, //! Only Modal widget passes this uses props down. All other widgets use selecto lib @@ -28,6 +32,7 @@ export const ConfigHandle = ({ (state) => componentType === 'Tabs' && state.getExposedValueOfComponent(id)?.currentTab, shallow ); + const position = widgetTop < 15 ? 'bottom' : 'top'; const setComponentToInspect = useStore((state) => state.setComponentToInspect); const isModal = componentType === 'Modal' || componentType === 'ModalV2'; @@ -49,7 +54,12 @@ export const ConfigHandle = ({ className={`config-handle ${customClassName}`} widget-id={id} style={{ - top: position === 'top' ? '-20px' : widgetTop + height - (widgetTop < 10 ? 15 : 10), + top: + componentType === 'Modal' && isModalOpen + ? '0px' + : position === 'top' + ? '-20px' + : `${height - (CONFIG_HANDLE_HEIGHT + BUFFER_HEIGHT)}px`, visibility: _showHandle ? 'visible' : 'hidden', left: '-1px', }} @@ -64,7 +74,10 @@ export const ConfigHandle = ({ > @@ -78,17 +91,30 @@ export const ConfigHandle = ({ data-cy={`${componentName?.toLowerCase()}-config-handle`} className="text-truncate" > - + {/* Settings Icon */} + + + {componentName} + {/* Divider */} +
+ {/* Delete Button */} {!isMultipleComponentsSelected && !shouldFreeze && ( -
+
- { deleteComponents([id]); }} data-cy={`${componentName.toLowerCase()}-delete-button`} - className="delete-icon" - /> + > + +
)} diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss index e7322959e5..5cb1b94268 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss @@ -31,22 +31,7 @@ .badge { font-size: 9px; border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - - .delete-part { - margin-left: 10px; - float: right; - } - - .delete-part::before { - height: 12px; - display: inline-block; - width: 2px; - background-color: rgba(255, 255, 255, 0.8); - opacity: 0.5; - content: ""; - vertical-align: middle; - } + border-bottom-right-radius: 0 } } diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index fc163939f6..e622e1a2cd 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -6,7 +6,15 @@ import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { useDrop } from 'react-dnd'; import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils'; -import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants'; +import { + CANVAS_WIDTHS, + NO_OF_GRIDS, + WIDGETS_WITH_DEFAULT_CHILDREN, + GRID_HEIGHT, + CONTAINER_FORM_CANVAS_PADDING, + SUBCONTAINER_CANVAS_BORDER_WIDTH, + BOX_PADDING, +} from './appCanvasConstants'; import { useGridStore } from '@/_stores/gridStore'; import NoComponentCanvasContainer from './NoComponentCanvasContainer'; import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants'; @@ -35,10 +43,10 @@ export const Container = React.memo( canvasMaxWidth, isViewerSidebarPinned, pageSidebarStyle, + componentType, }) => { const realCanvasRef = useRef(null); const components = useStore((state) => state.getContainerChildrenMapping(id), shallow); - const componentType = useStore((state) => state.getComponentTypeFromId(id), shallow); const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow); const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow); const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow); @@ -95,12 +103,16 @@ export const Container = React.memo( if (canvasWidth !== undefined) { if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2; if (id === 'canvas') return canvasWidth; - return canvasWidth - 2; + if (componentType === 'Container' || componentType === 'Form') { + return ( + canvasWidth - (2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING) + ); + } + return canvasWidth - 2; // Need to update this 2 to correct value for other subcontainers } return realCanvasRef?.current?.offsetWidth; } const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS; - useEffect(() => { useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -143,7 +155,7 @@ export const Container = React.memo( }} style={{ height: id === 'canvas' ? `${canvasHeight}` : '100%', - backgroundSize: `${gridWidth}px ${10}px`, + backgroundSize: `${gridWidth}px ${GRID_HEIGHT}px`, backgroundColor: currentMode === 'view' ? computeViewerBackgroundColor(darkMode, canvasBgColor) diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.css b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.css index 5abcc430d4..e1c6bb3baa 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.css +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.css @@ -1,17 +1,6 @@ .target, .nested-target { position: absolute; - /* width: 100px; - height: 100px; */ - /* top: 150px; - left: 100px; */ - /* line-height: 100px; */ - /* text-align: center; */ - /* background: #ee8; */ - /* color: #333; */ - /* font-weight: bold; */ box-sizing: border-box; - /* transition: transform 0.1s; */ - /* z-index: 3001; */ } .target.hovered{ @@ -76,43 +65,6 @@ background: #8DA4EF !important; } - - -/* Hides all the control lines*/ -/* .moveable-line { - color: transparent !important; - --moveable-color: transparent !important; -} - -.moveable-control { - visibility: hidden; -} - -.target { - outline: 1px solid #4af; -} */ - - -.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover { - outline: 1px solid #4af; - z-index: 4 !important; -} - -.main-editor-canvas .widget-target:has(.nested-target:hover):hover { - outline: 0px solid #4af; -} - - -.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { - outline: 1px solid #4af; - z-index: 4 !important; -} - -.active-target, .resizing-target { - outline: 1px solid #4af !important; - /* z-index: 1000000 !important; */ -} - .moveable-control-box:not([data-able-groupable]) .moveable-control-box:not(:hover) { opacity: 0; } @@ -141,10 +93,6 @@ height: 0px !important; } -.resizing-target * { - opacity: 0; -} - .moveable-control { width: 8px !important; @@ -210,4 +158,19 @@ .moveable-guideline-group { z-index: 9999; -} \ No newline at end of file +} + +.dragging-component-canvas { + outline: 1px solid var(--border-accent-strong) !important; + outline-offset: 0px; /* Creates space between element and outline */ + z-index: 999 !important; +} + +.non-dragging-component { + outline: 1px dotted var(--border-accent-weak) !important; + outline-offset: 0px; /* Creates space between element and outline */ + z-index: 999 !important; +} + + + diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index 6821f43ce2..44f148a000 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -19,6 +19,9 @@ import { adjustWidth, hideGridLines, showGridLines, + handleActivateTargets, + handleDeactivateTargets, + handleActivateNonDraggingComponents, } from './gridUtils'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { resolveWidgetFieldValue } from '@/_helpers/utils'; @@ -56,7 +59,7 @@ export default function Grid({ gridWidth, currentLayout }) { 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 draggingComponentId = useStore((state) => state.draggingComponentId, shallow); const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); const [dragParentId, setDragParentId] = useState(null); const [elementGuidelines, setElementGuidelines] = useState([]); @@ -580,7 +583,7 @@ export default function Grid({ gridWidth, currentLayout }) { } else { document.getElementById('real-canvas').classList.add('show-grid'); } - + handleActivateTargets(currentWidget.component?.parent); const currentWidth = currentWidget.width * _gridWidth; const diffWidth = e.width - currentWidth; const diffHeight = e.height - currentWidget.height; @@ -632,6 +635,7 @@ export default function Grid({ gridWidth, currentLayout }) { if (!isComponentVisible(e.target.id)) { return false; } + handleActivateNonDraggingComponents(); useGridStore.getState().actions.setResizingComponentId(e.target.id); e.setMin([gridWidth, GRID_HEIGHT]); }} @@ -641,8 +645,7 @@ export default function Grid({ gridWidth, currentLayout }) { const currentWidget = boxList.find(({ id }) => { return id === e.target.id; }); - document.getElementById('real-canvas')?.classList.remove('show-grid'); - document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid'); + hideGridLines(); let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth; const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT; @@ -696,17 +699,19 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('ResizeEnd error ->', error); } + handleDeactivateTargets(); setDragParentId(null); toggleCanvasUpdater(); }} onResizeGroupStart={({ events }) => { showGridLines(); + handleActivateNonDraggingComponents(); }} onResizeGroup={({ events }) => { const parentElm = events[0].target.closest('.real-canvas'); const parentWidth = parentElm?.clientWidth; const parentHeight = parentElm?.clientHeight; - + handleActivateTargets(parentElm?.id?.replace('canvas-', '')); const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight); events.forEach((ev) => { ev.target.style.width = `${ev.width}px`; @@ -775,6 +780,7 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('Error resizing group', error); } + handleDeactivateTargets(); toggleCanvasUpdater(); }} checkInput @@ -785,6 +791,7 @@ export default function Grid({ gridWidth, currentLayout }) { } newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent; e?.moveable?.controlBox?.removeAttribute('data-off-screen'); + const box = boxList.find((box) => box.id === e.target.id); // Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS if (SUBCONTAINER_WIDGETS.includes(box?.component?.component) && e.inputEvent.shiftKey) { @@ -818,7 +825,6 @@ export default function Grid({ gridWidth, currentLayout }) { container.contains(e.inputEvent.target) ); } - if (['RangeSlider', 'BoundedBox'].includes(box?.component?.component) || isDragOnInnerElement) { const targetElems = document.elementsFromPoint(e.clientX, e.clientY); const isHandle = targetElems.find((ele) => ele.classList.contains('handle-content')); @@ -826,11 +832,13 @@ export default function Grid({ gridWidth, currentLayout }) { return false; } } + handleActivateNonDraggingComponents(); }} onDragEnd={(e) => { + handleDeactivateTargets(); try { if (isDraggingRef.current) { - useGridStore.getState().actions.setDraggingComponentId(null); + useStore.getState().setDraggingComponentId(null); isDraggingRef.current = false; } prevDragParentId.current = null; @@ -884,7 +892,7 @@ export default function Grid({ gridWidth, currentLayout }) { onDrag={(e) => { // Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again if (!isDraggingRef.current) { - useGridStore.getState().actions.setDraggingComponentId(e.target.id); + useStore.getState().setDraggingComponentId(e.target.id); showGridLines(); isDraggingRef.current = true; } @@ -962,6 +970,7 @@ export default function Grid({ gridWidth, currentLayout }) { setDragParentId(newParentId === 'canvas' ? null : newParentId); newDragParentId.current = newParentId === 'canvas' ? null : newParentId; prevDragParentId.current = newParentId; + handleActivateTargets(newParentId); } } // Postion ghost element exactly as same at dragged element @@ -988,14 +997,17 @@ export default function Grid({ gridWidth, currentLayout }) { ev.target.style.transform = `translate(${left}px, ${top}px)`; }); + handleActivateTargets(parentElm?.id?.replace('canvas-', '')); updateNewPosition(events); }} onDragGroupStart={({ events }) => { showGridLines(); setIsGroupDragging(true); + handleActivateNonDraggingComponents(); }} onDragGroupEnd={(e) => { handleDragGroupEnd(e); + handleDeactivateTargets(); toggleCanvasUpdater(); }} onClickGroup={(e) => { diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index 2889fc06db..da179bc11d 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -1,7 +1,7 @@ import { useGridStore } from '@/_stores/gridStore'; import { isEmpty } from 'lodash'; import useStore from '@/AppBuilder/_stores/store'; - +import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils'; export function correctBounds(layout, bounds) { layout = scaleLayouts(layout); const collidesWith = []; @@ -414,3 +414,77 @@ export function hideGridLines() { document.getElementById('real-canvas')?.classList.remove('show-grid'); document.getElementById('real-canvas')?.classList.add('hide-grid'); } + +// Track previously active elements for efficient cleanup +let previousActiveWidgets = null; +let previousActiveCanvas = null; + +export const handleActivateNonDraggingComponents = () => { + // Only add non-dragging class to visible components in viewport + document.querySelectorAll('.moveable-box:not(.active-target)').forEach((component) => { + // Check if element is visible in viewport + const rect = component.getBoundingClientRect(); + const isVisible = + rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; + + if (isVisible) { + component.classList.add('non-dragging-component'); + } + }); +}; + +export const handleActivateTargets = (parentId) => { + const WIDGETS_WITH_CANVAS_OUTLINE = ['Container', 'Modal', 'Form', 'Listview', 'Kanban']; + + const newParentType = document.getElementById('canvas-' + parentId)?.getAttribute('component-type'); + let _parentId = parentId; + if (newParentType === 'Tabs') { + _parentId = getTabId(parentId); + } else if (WIDGETS_WITH_CANVAS_OUTLINE.includes(newParentType)) { + _parentId = getSubContainerIdWithSlots(parentId); + } + + // Clean up previous active elements + if (previousActiveWidgets) { + previousActiveWidgets.classList.remove('dragging-component-canvas'); + previousActiveWidgets = null; + } + + if (previousActiveCanvas) { + previousActiveCanvas.classList.remove('dragging-component-canvas'); + previousActiveCanvas = null; + } + + const parentComponent = document.getElementById(_parentId); + if (!parentComponent) return; + + if (WIDGETS_WITH_CANVAS_OUTLINE?.includes(newParentType)) { + // If it's multiple canvas in single widget, highlight the specific canvas + const canvasElm = document.getElementById('canvas-' + parentId); + if (canvasElm) { + canvasElm.classList.add('dragging-component-canvas'); + previousActiveCanvas = canvasElm; + } + } else { + // Otherwise highlight the component box + parentComponent.classList.remove('non-dragging-component'); + parentComponent.classList.add('dragging-component-canvas'); + previousActiveWidgets = parentComponent; + } +}; + +export const handleDeactivateTargets = () => { + if (previousActiveWidgets) { + previousActiveWidgets.classList.remove('dragging-component-canvas'); + previousActiveWidgets = null; + } + + if (previousActiveCanvas) { + previousActiveCanvas.classList.remove('dragging-component-canvas'); + previousActiveCanvas = null; + } + + document.querySelectorAll('.non-dragging-component').forEach((component) => { + component.classList.remove('non-dragging-component'); + }); +}; diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 427cced97b..b26586dc08 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -6,7 +6,7 @@ import { OverlayTrigger } from 'react-bootstrap'; import { renderTooltip } from '@/_helpers/appUtils'; import { useTranslation } from 'react-i18next'; import ErrorBoundary from '@/_ui/ErrorBoundary'; - +import { BOX_PADDING } from './appCanvasConstants'; const shouldAddBoxShadowAndVisibility = [ 'Table', 'TextInput', @@ -164,7 +164,7 @@ const RenderWidget = ({
state.getComponentDefinition(id)?.layouts?.[currentLayout], shallow); const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow); - const isDragging = useGridStore((state) => state.draggingComponentId === id); + const isDragging = useStore((state) => state.draggingComponentId === id); const isResizing = useGridStore((state) => state.resizingComponentId === id); const componentType = useStore((state) => state.getComponentDefinition(id)?.component?.component, shallow); const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow); @@ -52,7 +52,9 @@ const WidgetWrapper = memo( height: visibility === false ? '10px' : `${height}px`, transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`, WebkitFontSmoothing: 'antialiased', + border: visibility === false ? `1px solid var(--border-default)` : 'none', }; + if (!componentType) return null; return ( <> @@ -67,8 +69,8 @@ const WidgetWrapper = memo( data-id={`${id}`} id={id} widgetid={id} + component-type={componentType} style={{ - // transform: `translate(332px, -134px)`, // zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null, ...styles, }} @@ -84,7 +86,6 @@ const WidgetWrapper = memo( {mode == 'edit' && ( { allComponents[parentId]?.component?.component === 'Calendar' || allComponents[parentId]?.component?.component === 'Kanban' || allComponents[parentId]?.component?.component === 'Container' || + allComponents[parentId]?.component?.component === 'Form' || allComponents[parentId]?.component?.component === 'ModalV2'; if (componentParentId && isParentTabORCalendar) { @@ -327,6 +328,7 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar' || parentComponent.component.component === 'Container' || + parentComponent.component.component === 'Form' || parentComponent.component.component === 'ModalV2' ); } @@ -665,11 +667,14 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => { return canvasBgColor; }; -export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => { +export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName }) => { const { tab } = child; if (parentComponent === 'Tabs') return `${parentId}-${tab}`; - else if (parentComponent === 'Container' || parentComponent === 'ModalV2') { + else if ( + slotName && + (parentComponent === 'Form' || parentComponent === 'Container' || parentComponent === 'ModalV2') + ) { return `${parentId}-${slotName}`; } return parentId; @@ -685,3 +690,19 @@ export const getParentWidgetFromId = (parentType, parentId) => { } return parentType; }; + +export const getTabId = (parentId) => { + return parentId.split('-').slice(0, -1).join('-'); +}; + +export const getSubContainerIdWithSlots = (parentId) => { + let cleanParentId = parentId; + if (parentId) { + if (parentId.includes('header')) { + cleanParentId = parentId.replace('-header', ''); + } else if (parentId.includes('footer')) { + cleanParentId = parentId.replace('-footer', ''); + } + } + return cleanParentId; +}; diff --git a/frontend/src/AppBuilder/AppCanvas/selecto.scss b/frontend/src/AppBuilder/AppCanvas/selecto.scss index 9ca8a37f41..5602b35d5a 100644 --- a/frontend/src/AppBuilder/AppCanvas/selecto.scss +++ b/frontend/src/AppBuilder/AppCanvas/selecto.scss @@ -3,15 +3,18 @@ } .main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover { - z-index: 4 !important; -} - -.main-editor-canvas .widget-target:has(.nested-target:hover):hover { - outline: 0px solid #4af; -} - -.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { outline: 1px solid #4af; z-index: 4 !important; } +.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { + // outline: 1px solid #4af; + z-index: 4 !important; +} + +// .main-editor-canvas .widget-target:hover { +// outline: 1px solid #4af; +// } + + + diff --git a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx index b9fb85fcae..099cd3763a 100644 --- a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx +++ b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx @@ -5,19 +5,13 @@ const MIN_TABLE_ROW_HEIGHT_DEFAULT = 45; const TableRowHeightInput = ({ value, onChange, cyLabel, staticText, styleDefinition }) => { const [inputValue, setInputValue] = useState(value); + const minValue = + styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT; + useEffect(() => { setInputValue(value < minValue ? minValue : value); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, styleDefinition.cellSize?.value]); - useEffect(() => { - onChange( - styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const minValue = - styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT; const handleBlur = () => { const newValue = Math.max(inputValue, minValue); diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx index 033d266e03..f95baaa328 100644 --- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx @@ -300,10 +300,10 @@ const MultiLineCodeEditor = (props) => { editable={editable} //for transformations in query manager onCreateEditor={(view) => setEditorView(view)} onUpdate={(view) => { - const icon = document.querySelector('.codehinter-search-btn-wrapper'); + const icon = document.querySelector('.codehinter-search-btn'); if (searchPanelOpen(view.state)) { - icon.style.top = '44px'; - } else icon.style.top = '0px'; + icon.style.display = 'none'; + } else icon.style.display = 'block'; }} />
diff --git a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx index 2b807f718b..28f7451b95 100644 --- a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx @@ -1,3 +1,4 @@ +/* eslint-disable import/no-unresolved */ import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { @@ -9,12 +10,13 @@ import { replaceNext, replaceAll, openSearchPanel, - // eslint-disable-next-line import/no-unresolved } from '@codemirror/search'; import './SearchBox.scss'; import InputComponent from '@/components/ui/Input/Index.jsx'; import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx'; import { ToolTip } from '@/_components/ToolTip'; +import { SelectionRange } from '@codemirror/state'; +import { useHotkeys } from 'react-hotkeys-hook'; export const handleSearchPanel = (view) => { const dom = document.createElement('div'); @@ -35,6 +37,11 @@ function SearchPanel({ view }) { replace: replaceTerm, }); view.dispatch({ effects: setSearchQuery.of(query) }); + + const currentPos = view.state.selection.main.head; + view.dispatch({ + selection: SelectionRange.create(currentPos, currentPos), + }); }; useEffect(() => { @@ -44,12 +51,28 @@ function SearchPanel({ view }) { return () => clearTimeout(handler); }, [searchText, replaceText]); + const [shortcutEnabled, setShortcutEnabled] = useState(false); + + // Shortcuts for search input field + useHotkeys( + ['shift+enter', 'enter'], + (event, handler) => { + if (handler.shift && handler.keys[0] === 'enter') findPrevious(view); + else if (handler.keys[0] === 'enter') findNext(view); + }, + { + enabled: shortcutEnabled, + enableOnFormTags: true, + } + ); + const displaySearchField = () => (
setSearchText(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && findNext(view)} + onFocus={() => setShortcutEnabled(true)} + onBlur={() => setShortcutEnabled(false)} placeholder="Find" size="small" value={searchText} diff --git a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx index 625b11dedd..dab57d0f20 100644 --- a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx +++ b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx @@ -56,8 +56,8 @@ const TJDBCodeEditor = (props) => { const handleOnChange = (value) => { if (value === '') { - setErrorState(true); - setError('JSON cannot be empty'); + setErrorState(false); + setError(null); setCurrentValue(value); return; } @@ -150,7 +150,7 @@ const TJDBCodeEditor = (props) => { className="cm-codehinter position-relative" style={{ width: '100%', - height: isOpen ? '350px' : 'auto', + height: isOpen ? '350p' : 'auto', }} >
@@ -178,7 +178,7 @@ const TJDBCodeEditor = (props) => { { // eslint-disable-next-line react-hooks/exhaustive-deps }, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]); - const handleNodeExpansion = (path) => { + const handleNodeExpansion = (path, data, currentNode) => { if (pathToBeInspected && path?.length > 0) { - return pathToBeInspected.includes(path[path.length - 1]); + const shouldExpand = pathToBeInspected.includes(path[path.length - 1]); + + // Scroll to the component in the inspector + if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) { + const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + + return shouldExpand; } else return false; }; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js index 7067cd540d..937fe45b2c 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js @@ -9,6 +9,7 @@ const useCallbackActions = () => { const currentPageComponents = useStore((state) => state?.getCurrentPageComponents(), shallow); const shouldFreeze = useStore((state) => state.getShouldFreeze()); const runQuery = useStore((state) => state.queryPanel.runQuery); + const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll); const handleRemoveComponent = (component) => { deleteComponents([component.id]); @@ -30,30 +31,22 @@ const useCallbackActions = () => { return toast.success('Copied to the clipboard', { position: 'top-center' }); }; - const autoScrollTo = (id) => { - setSelectedComponents([id]); - const target = document.getElementById(id); - target.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }; - const handleAutoScrollToComponent = (data) => { - const currentPageComponents = useStore.getState().getCurrentPageComponents(); - const component = currentPageComponents?.[data.id]; - - let parentId = component?.component?.parent; - if (parentId) { - const regex = /-\d+$/; - if (regex.test(parentId)) { - parentId = parentId.replace(regex, ''); // To get parentId without tab index if parent type is Tab - } - const parentType = currentPageComponents?.[parentId]?.component?.component; - if (parentType && (parentType === 'Modal' || parentType === 'Tabs')) { - autoScrollTo(parentId); // To scroll to parent component if parent type is Modal or Tabs - return; - } + const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(data.id); + if (!isAccessible) { + if (isOnCanvas) { + toast.success( + `This component can't be opened because it's on the main canvas. Close ${computedComponentId} and click "Go to component" to view it there` + ); + } else + toast.success( + `This component can't be opened because it's inside ${computedComponentId}. Open ${computedComponentId} and click "Go to component"to view it.` + ); + return; } - - autoScrollTo(data.id); + setSelectedComponents([computedComponentId]); + const target = document.getElementById(computedComponentId); + target.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }; const callbackActions = [ diff --git a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx index 0cdcdb844b..f0a3bbe811 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx @@ -7,7 +7,7 @@ import { getWorkspaceId, decodeEntities } from '@/_helpers/utils'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore'; import { useDataQueriesActions } from '@/_stores/dataQueriesStore'; -import { staticDataSources as staticDatasources } from '../constants'; +import { defaultSources, staticDataSources as staticDatasources } from '../constants'; import { useQueryPanelActions } from '@/_stores/queryPanelStore'; import Search from '@/_ui/Icon/solidIcons/Search'; import { Tooltip } from 'react-tooltip'; @@ -135,7 +135,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
{' '} - {source.name} + {defaultSources[cleanWord(source.name)].name}
), @@ -178,6 +178,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc } }; + function cleanWord(word) { + return word.replace(/default/g, ''); + } + return (