From 1497c227206cd7627986b901e4f1edeb73231707 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Fri, 21 Mar 2025 04:22:45 +0530 Subject: [PATCH 01/29] feature: add alignment property to components --- .../WidgetManager/widgets/buttonGroup.js | 9 +++ .../AppBuilder/WidgetManager/widgets/link.js | 9 +++ .../WidgetManager/widgets/pagination.js | 9 +++ .../WidgetManager/widgets/svgImage.js | 9 +++ .../AppBuilder/WidgetManager/widgets/tags.js | 9 +++ .../src/Editor/Components/ButtonGroup.jsx | 74 +++++++++++-------- .../src/Editor/Components/Image/Image.jsx | 13 +++- frontend/src/Editor/Components/Link.jsx | 3 +- frontend/src/Editor/Components/Pagination.jsx | 9 ++- frontend/src/Editor/Components/SvgImage.jsx | 7 +- frontend/src/Editor/Components/Tags.jsx | 5 +- .../src/Editor/WidgetManager/configs/link.js | 1 + .../services/widget-config/buttonGroup.js | 9 +++ .../apps/services/widget-config/link.js | 9 +++ .../apps/services/widget-config/pagination.js | 9 +++ .../apps/services/widget-config/svgImage.js | 9 +++ .../apps/services/widget-config/tags.js | 9 +++ 17 files changed, 164 insertions(+), 38 deletions(-) diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js index c0fa889dd5..4ecdd5feec 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js @@ -123,6 +123,14 @@ export const buttonGroupConfig = { defaultValue: '#007bff', }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: { selected: [1], @@ -148,6 +156,7 @@ export const buttonGroupConfig = { disabledState: { value: '{{false}}' }, selectedTextColor: { value: '#FFFFFF' }, selectedBackgroundColor: { value: '#4368E3' }, + alignment: { value: 'left' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/link.js b/frontend/src/AppBuilder/WidgetManager/widgets/link.js index bf419e16c0..fcbf648a61 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/link.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/link.js @@ -82,6 +82,14 @@ export const linkConfig = { defaultValue: true, }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: {}, actions: [ @@ -106,6 +114,7 @@ export const linkConfig = { textSize: { value: '{{14}}' }, underline: { value: 'on-hover' }, visibility: { value: '{{true}}' }, + alignment: { value: 'left' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/pagination.js b/frontend/src/AppBuilder/WidgetManager/widgets/pagination.js index 6fabfa889a..116d0e9849 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/pagination.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/pagination.js @@ -50,6 +50,14 @@ export const paginationConfig = { defaultValue: false, }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: { totalPages: null, @@ -73,6 +81,7 @@ export const paginationConfig = { styles: { visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + alignment: { value: 'left' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/svgImage.js b/frontend/src/AppBuilder/WidgetManager/widgets/svgImage.js index 315a6e2c28..9780f0cdd6 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/svgImage.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/svgImage.js @@ -32,6 +32,14 @@ export const svgImageConfig = { defaultValue: true, }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: { value: {}, @@ -50,6 +58,7 @@ export const svgImageConfig = { events: [], styles: { visibility: { value: '{{true}}' }, + alignment: { value: 'left' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/tags.js b/frontend/src/AppBuilder/WidgetManager/widgets/tags.js index 8af289b23a..73cd44b550 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/tags.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/tags.js @@ -38,6 +38,14 @@ export const tagsConfig = { defaultValue: true, }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: {}, definition: { @@ -54,6 +62,7 @@ export const tagsConfig = { events: [], styles: { visibility: { value: '{{true}}' }, + alignment: { value: 'left' }, }, }, }; diff --git a/frontend/src/Editor/Components/ButtonGroup.jsx b/frontend/src/Editor/Components/ButtonGroup.jsx index 7364348271..af02824367 100644 --- a/frontend/src/Editor/Components/ButtonGroup.jsx +++ b/frontend/src/Editor/Components/ButtonGroup.jsx @@ -25,6 +25,7 @@ export const ButtonGroup = function Button({ selectedBackgroundColor, selectedTextColor, boxShadow, + alignment, } = styles; const computedStyles = { @@ -87,38 +88,53 @@ export const ButtonGroup = function Button({ fireEvent('onClick'); } }; + + const mapAlignment = (alignment) => { + switch (alignment) { + case 'left': + return 'flex-start'; + case 'right': + return 'flex-end'; + case 'center': + return 'center'; + default: + return 'flex-start'; // Default to left alignment if the value is unknown + } + }; return ( -
- {label && ( -

- {label} -

- )} +
- {data?.map((item, index) => ( - - ))} + {label} +

+ )} +
+ {data?.map((item, index) => ( + + ))} +
); diff --git a/frontend/src/Editor/Components/Image/Image.jsx b/frontend/src/Editor/Components/Image/Image.jsx index 0f3ea0e2b0..03b4daefda 100644 --- a/frontend/src/Editor/Components/Image/Image.jsx +++ b/frontend/src/Editor/Components/Image/Image.jsx @@ -23,8 +23,17 @@ export const Image = function Image({ }) { const { imageFormat, source, jsSchema, alternativeText, zoomButtons, rotateButton, loadingState, disabledState } = properties; - const { imageFit, imageShape, backgroundColor, padding, customPadding, boxShadow, borderRadius, borderColor } = - styles; + const { + imageFit, + imageShape, + backgroundColor, + padding, + customPadding, + boxShadow, + borderRadius, + borderColor, + alignment, + } = styles; const isInitialRender = useRef(true); diff --git a/frontend/src/Editor/Components/Link.jsx b/frontend/src/Editor/Components/Link.jsx index ff993acc17..871d092e5e 100644 --- a/frontend/src/Editor/Components/Link.jsx +++ b/frontend/src/Editor/Components/Link.jsx @@ -3,13 +3,14 @@ import cx from 'classnames'; export const Link = ({ height, properties, styles, fireEvent, setExposedVariable, dataCy }) => { const { linkTarget, linkText, targetType } = properties; - const { textColor, textSize, underline, visibility, boxShadow } = styles; + const { textColor, textSize, underline, visibility, boxShadow, alignment } = styles; const clickRef = useRef(); const computedStyles = { fontSize: textSize, height, boxShadow, + justifyContent: alignment, }; useEffect(() => { diff --git a/frontend/src/Editor/Components/Pagination.jsx b/frontend/src/Editor/Components/Pagination.jsx index c7fe5f2993..b2b052def9 100644 --- a/frontend/src/Editor/Components/Pagination.jsx +++ b/frontend/src/Editor/Components/Pagination.jsx @@ -12,7 +12,7 @@ export const Pagination = ({ width, }) => { const isInitialRender = useRef(true); - const { visibility, disabledState, boxShadow } = styles; + const { visibility, disabledState, boxShadow, alignment } = styles; const [currentPage, setCurrentPage] = useState(() => properties?.defaultPageIndex ?? 1); const pageChanged = (number) => { @@ -65,7 +65,12 @@ export const Pagination = ({ }; return ( -
+
    -
    +
); }; diff --git a/frontend/src/Editor/Components/Tags.jsx b/frontend/src/Editor/Components/Tags.jsx index a4d282ca14..c11966106e 100644 --- a/frontend/src/Editor/Components/Tags.jsx +++ b/frontend/src/Editor/Components/Tags.jsx @@ -2,14 +2,15 @@ import React from 'react'; export const Tags = function Tags({ width, height, properties, styles, dataCy }) { const { data } = properties; - const { visibility, boxShadow } = styles; + const { visibility, boxShadow, alignment } = styles; const computedStyles = { width, height, - display: visibility ? '' : 'none', + display: visibility ? 'flex' : 'none', overflowY: 'auto', boxShadow, + justifyContent: alignment, }; function renderTag(item, index) { diff --git a/frontend/src/Editor/WidgetManager/configs/link.js b/frontend/src/Editor/WidgetManager/configs/link.js index bf419e16c0..7252ab7b07 100644 --- a/frontend/src/Editor/WidgetManager/configs/link.js +++ b/frontend/src/Editor/WidgetManager/configs/link.js @@ -106,6 +106,7 @@ export const linkConfig = { textSize: { value: '{{14}}' }, underline: { value: 'on-hover' }, visibility: { value: '{{true}}' }, + alignment: { value: 'left' }, }, }, }; diff --git a/server/src/modules/apps/services/widget-config/buttonGroup.js b/server/src/modules/apps/services/widget-config/buttonGroup.js index c0fa889dd5..4ecdd5feec 100644 --- a/server/src/modules/apps/services/widget-config/buttonGroup.js +++ b/server/src/modules/apps/services/widget-config/buttonGroup.js @@ -123,6 +123,14 @@ export const buttonGroupConfig = { defaultValue: '#007bff', }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: { selected: [1], @@ -148,6 +156,7 @@ export const buttonGroupConfig = { disabledState: { value: '{{false}}' }, selectedTextColor: { value: '#FFFFFF' }, selectedBackgroundColor: { value: '#4368E3' }, + alignment: { value: 'left' }, }, }, }; diff --git a/server/src/modules/apps/services/widget-config/link.js b/server/src/modules/apps/services/widget-config/link.js index bf419e16c0..fcbf648a61 100644 --- a/server/src/modules/apps/services/widget-config/link.js +++ b/server/src/modules/apps/services/widget-config/link.js @@ -82,6 +82,14 @@ export const linkConfig = { defaultValue: true, }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: {}, actions: [ @@ -106,6 +114,7 @@ export const linkConfig = { textSize: { value: '{{14}}' }, underline: { value: 'on-hover' }, visibility: { value: '{{true}}' }, + alignment: { value: 'left' }, }, }, }; diff --git a/server/src/modules/apps/services/widget-config/pagination.js b/server/src/modules/apps/services/widget-config/pagination.js index 6fabfa889a..116d0e9849 100644 --- a/server/src/modules/apps/services/widget-config/pagination.js +++ b/server/src/modules/apps/services/widget-config/pagination.js @@ -50,6 +50,14 @@ export const paginationConfig = { defaultValue: false, }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: { totalPages: null, @@ -73,6 +81,7 @@ export const paginationConfig = { styles: { visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + alignment: { value: 'left' }, }, }, }; diff --git a/server/src/modules/apps/services/widget-config/svgImage.js b/server/src/modules/apps/services/widget-config/svgImage.js index 315a6e2c28..9780f0cdd6 100644 --- a/server/src/modules/apps/services/widget-config/svgImage.js +++ b/server/src/modules/apps/services/widget-config/svgImage.js @@ -32,6 +32,14 @@ export const svgImageConfig = { defaultValue: true, }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: { value: {}, @@ -50,6 +58,7 @@ export const svgImageConfig = { events: [], styles: { visibility: { value: '{{true}}' }, + alignment: { value: 'left' }, }, }, }; diff --git a/server/src/modules/apps/services/widget-config/tags.js b/server/src/modules/apps/services/widget-config/tags.js index 8af289b23a..73cd44b550 100644 --- a/server/src/modules/apps/services/widget-config/tags.js +++ b/server/src/modules/apps/services/widget-config/tags.js @@ -38,6 +38,14 @@ export const tagsConfig = { defaultValue: true, }, }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'left', + }, + }, }, exposedVariables: {}, definition: { @@ -54,6 +62,7 @@ export const tagsConfig = { events: [], styles: { visibility: { value: '{{true}}' }, + alignment: { value: 'left' }, }, }, }; From ed8b50d7305d3099d84930e236918d57676b8e3c Mon Sep 17 00:00:00 2001 From: Nakul Nagargade Date: Tue, 25 Mar 2025 15:56:04 +0530 Subject: [PATCH 02/29] Add padding for Tab and Modal --- .../src/AppBuilder/AppCanvas/Container.jsx | 14 ++++---- .../AppCanvas/appCanvasConstants.js | 4 +++ .../AppBuilder/AppCanvas/appCanvasUtils.js | 33 ++++++++++++++++++- .../Widgets/ModalV2/Components/Footer.jsx | 1 + .../Widgets/ModalV2/Components/Header.jsx | 1 + .../Widgets/ModalV2/Components/Modal.jsx | 3 +- .../Widgets/ModalV2/helpers/stylesFactory.js | 7 ++-- frontend/src/AppBuilder/Widgets/Tabs.jsx | 10 ++++-- 8 files changed, 60 insertions(+), 13 deletions(-) diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index e622e1a2cd..55fd0abaac 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -5,7 +5,12 @@ import WidgetWrapper from './WidgetWrapper'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { useDrop } from 'react-dnd'; -import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils'; +import { + addChildrenWidgetsToParent, + addNewWidgetToTheEditor, + computeViewerBackgroundColor, + getSubContainerWidthAfterPadding, +} from './appCanvasUtils'; import { CANVAS_WIDTHS, NO_OF_GRIDS, @@ -103,12 +108,7 @@ export const Container = React.memo( if (canvasWidth !== undefined) { if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2; if (id === 'canvas') return canvasWidth; - if (componentType === 'Container' || componentType === 'Form') { - return ( - canvasWidth - (2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING) - ); - } - return canvasWidth - 2; // Need to update this 2 to correct value for other subcontainers + return getSubContainerWidthAfterPadding(canvasWidth, componentType, id); } return realCanvasRef?.current?.offsetWidth; } diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js index e6a789fba0..5725baf7ed 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js @@ -23,3 +23,7 @@ export const CONTAINER_FORM_CANVAS_PADDING = 7; export const SUBCONTAINER_CANVAS_BORDER_WIDTH = 1; export const BOX_PADDING = 2; + +export const TAB_CANVAS_PADDING = 7.5; + +export const MODAL_CANVAS_PADDING = 5; diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js index 8dae9e001c..a0253499ff 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js @@ -6,7 +6,16 @@ import { toast } from 'react-hot-toast'; import _, { debounce } from 'lodash'; import { useGridStore } from '@/_stores/gridStore'; import { findHighestLevelofSelection } from './Grid/gridUtils'; -import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants'; +import { + CANVAS_WIDTHS, + NO_OF_GRIDS, + WIDGETS_WITH_DEFAULT_CHILDREN, + CONTAINER_FORM_CANVAS_PADDING, + SUBCONTAINER_CANVAS_BORDER_WIDTH, + BOX_PADDING, + TAB_CANVAS_PADDING, + MODAL_CANVAS_PADDING, +} from './appCanvasConstants'; export function snapToGrid(canvasWidth, x, y) { const gridX = canvasWidth / 43; @@ -706,3 +715,25 @@ export const getSubContainerIdWithSlots = (parentId) => { } return cleanParentId; }; + +export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, componentId) => { + let padding = 2; //Need to update this 2 to correct value for other subcontainers + if (componentType === 'Container' || componentType === 'Form') { + padding = 2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING; + } + if (componentType === 'Tabs') { + padding = 2 * TAB_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING; + } + if (componentType === 'ModalV2') { + const isModalHeader = componentId?.includes('header'); + if (isModalHeader) { + const isModalHeaderCloseBtnEnabled = !useStore.getState().getResolvedComponent(componentId)?.properties + ?.hideCloseButton; + console.log('isModalHeaderCloseBtnEnabled', isModalHeaderCloseBtnEnabled); + padding = 2 * (MODAL_CANVAS_PADDING + (isModalHeaderCloseBtnEnabled ? 56 : 0)); + } else { + padding = 2 * MODAL_CANVAS_PADDING; + } + } + return canvasWidth - padding; +}; diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx index e25027ce33..8ff4c0cbb1 100644 --- a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx +++ b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx @@ -19,6 +19,7 @@ export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode, overflowX: 'hidden', overflowY: isDisabled ? 'hidden' : 'auto', }} + componentType="ModalV2" /> {isDisabled && (
{isDisabled && ( diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx index 25796a9951..2615ca3d5c 100644 --- a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx +++ b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx @@ -105,9 +105,10 @@ export const ModalWidget = ({ ...restProps }) => { ) : ( diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/helpers/stylesFactory.js b/frontend/src/AppBuilder/Widgets/ModalV2/helpers/stylesFactory.js index ee536dcfe1..9c6d4eb0c8 100644 --- a/frontend/src/AppBuilder/Widgets/ModalV2/helpers/stylesFactory.js +++ b/frontend/src/AppBuilder/Widgets/ModalV2/helpers/stylesFactory.js @@ -1,4 +1,5 @@ -var tinycolor = require('tinycolor2'); +const tinycolor = require('tinycolor2'); +import { MODAL_CANVAS_PADDING } from '@/AppBuilder/AppCanvas/appCanvasConstants'; export function createModalStyles({ height, @@ -17,25 +18,27 @@ export function createModalStyles({ 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', + padding: `${MODAL_CANVAS_PADDING}px`, }, modalHeader: { backgroundColor: ['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor, height: headerHeightPx, overflowY: isDisabledModal ? 'hidden' : 'auto', + padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`, }, modalFooter: { backgroundColor: ['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor, height: footerHeightPx, overflowY: isDisabledModal ? 'hidden' : 'auto', + padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`, }, buttonStyles: { backgroundColor: triggerButtonBackgroundColor, diff --git a/frontend/src/AppBuilder/Widgets/Tabs.jsx b/frontend/src/AppBuilder/Widgets/Tabs.jsx index 7f4fb527e4..dc3a55dcd2 100644 --- a/frontend/src/AppBuilder/Widgets/Tabs.jsx +++ b/frontend/src/AppBuilder/Widgets/Tabs.jsx @@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect } from 'react'; import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; import { resolveWidgetFieldValue, isExpectedDataType } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; - +import { TAB_CANVAS_PADDING } from '@/AppBuilder/AppCanvas/appCanvasConstants'; export const Tabs = function Tabs({ id, component, @@ -117,6 +117,7 @@ export const Tabs = function Tabs({ position: 'absolute', top: parsedHideTabs ? '0px' : '41px', width: '100%', + padding: TAB_CANVAS_PADDING, }} >
    Date: Wed, 26 Mar 2025 04:48:27 +0530 Subject: [PATCH 03/29] Fixed table negative value sort --- frontend/src/AppBuilder/Widgets/Table/columns/index.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/AppBuilder/Widgets/Table/columns/index.jsx b/frontend/src/AppBuilder/Widgets/Table/columns/index.jsx index 257de7c4ae..b7ebe93721 100644 --- a/frontend/src/AppBuilder/Widgets/Table/columns/index.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/columns/index.jsx @@ -130,6 +130,8 @@ export default function generateColumnsData({ return 1; } }; + } else if (columnType === 'number') { + sortType = 'basic'; } const width = columnSize || defaultColumn.width; return { From 8c6373f4b1830ed690d0674c8e9fc840d2fdc17a Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Wed, 26 Mar 2025 11:08:52 +0530 Subject: [PATCH 04/29] feat: add open webpage target on event --- .../RightSideBar/Inspector/EventManager.jsx | 13 +++++++++++++ .../src/AppBuilder/_stores/slices/eventsSlice.js | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx index c3add3f1cc..b6b6f0c4f9 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx @@ -30,6 +30,8 @@ import { appService } from '@/_services'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; import useStore from '@/AppBuilder/_stores/store'; import { useEventActions, useEvents } from '@/AppBuilder/_stores/slices/eventsSlice'; +import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup'; +import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem'; export const EventManager = ({ sourceId, @@ -512,6 +514,17 @@ export const EventManager = ({ usePortalEditor={false} component={component} /> +
    + + handlerChanged(index, 'windowTarget', _value)} + defaultValue={event?.windowTarget || 'newTab'} + style={{ width: '58%' }} + > + New tab + Current tab + +
)} diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 948ac39b39..c9f62df177 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -584,7 +584,7 @@ export const createEventsSlice = (set, get) => ({ //! if resolvecode default value should be the value itself not empty string ... Ask KAVIN const resolvedValue = getResolvedValue(event.url, customVariables); // const url = resolveReferences(event.url, undefined, customVariables); - window.open(resolvedValue, '_blank'); + window.open(resolvedValue, event?.windowTarget === 'newTab' ? '_blank' : '_self'); return Promise.resolve(); } case 'go-to-app': { From 6497e1a78928910b11f7e5d70eb9329ec122164f Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 27 Mar 2025 12:22:24 +0530 Subject: [PATCH 05/29] fix: window target styling --- .../src/AppBuilder/RightSideBar/Inspector/EventManager.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx index b6b6f0c4f9..6c8398724d 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx @@ -505,7 +505,7 @@ export const EventManager = ({ )} {event.actionId === 'open-webpage' && ( -
+
-
+
handlerChanged(index, 'windowTarget', _value)} defaultValue={event?.windowTarget || 'newTab'} - style={{ width: '58%' }} + style={{ width: '74%' }} > New tab Current tab From 7c7f20b4ba0df68534fde42f6207fcefc638d10a Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 27 Mar 2025 14:06:16 +0530 Subject: [PATCH 06/29] Added support for tab navigation in editor and viewer. --- .../src/AppBuilder/AppCanvas/Container.jsx | 40 ++++++++++++++++++- .../src/AppBuilder/AppCanvas/Grid/Grid.jsx | 15 +++++++ frontend/src/_stores/gridStore.js | 9 +++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index e622e1a2cd..9de350b810 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -58,9 +58,13 @@ export const Container = React.memo( const currentMode = useStore((state) => state.currentMode, shallow); const currentLayout = useStore((state) => state.currentLayout, shallow); const setFocusedParentId = useStore((state) => state.setFocusedParentId, shallow); + const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents, shallow); const isContainerReadOnly = useMemo(() => { return (index !== 0 && (componentType === 'Listview' || componentType === 'Kanban')) || currentMode === 'view'; }, [componentType, index, currentMode]); + const reorderContainerChildren = useGridStore((state) => state.reorderContainerChildren); + const prevForceUpdateRef = useRef(0); + const prevComponentsOrder = useRef(components); const [{ isOverCurrent }, drop] = useDrop({ accept: 'box', @@ -146,6 +150,40 @@ export const Container = React.memo( [setLastCanvasClickPosition] ); + // Function to sort the components based on position in container for tab navigation + const sortedComponents = useMemo(() => { + const { triggerUpdate, containerId } = reorderContainerChildren; + + // If a forced update occurred for a different container, return the previous order + const isForcedUpdate = prevForceUpdateRef.current !== triggerUpdate; + if (isForcedUpdate) { + prevForceUpdateRef.current = triggerUpdate; + if (containerId !== id) { + return prevComponentsOrder.current; + } + } + + const currentPageComponents = getCurrentPageComponents() + + const newComponentsOrder = [...components].sort((a, b) => { + const aTop = currentPageComponents?.[a]?.layouts?.[currentLayout]?.top; + const bTop = currentPageComponents?.[b]?.layouts?.[currentLayout]?.top; + if (aTop !== bTop) { + return aTop - bTop; + } else { + const aLeft = currentPageComponents?.[a]?.layouts?.[currentLayout]?.left; + const bLeft = currentPageComponents?.[b]?.layouts?.[currentLayout]?.left; + if (aLeft !== bLeft) { + return aLeft - bLeft; + } + } + }); + + prevComponentsOrder.current = newComponentsOrder; + return newComponentsOrder; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [components, currentLayout, reorderContainerChildren.triggerUpdate]); + return (
- {components.map((id) => ( + {sortedComponents.map((id) => ( ', error); } @@ -775,6 +777,11 @@ export default function Grid({ gridWidth, currentLayout }) { ev.target.style.transform = `translate(${posX}px, ${posY}px)`; }); } + + const groupParentId = + boxList.find(({ id }) => id === groupResizeDataRef.current[0].target.id)?.parent ?? 'canvas'; + useGridStore.getState().actions.setReorderContainerChildren(groupParentId); + groupResizeDataRef.current = []; reloadGrid(); } catch (error) { @@ -841,6 +848,8 @@ export default function Grid({ gridWidth, currentLayout }) { useStore.getState().setDraggingComponentId(null); isDraggingRef.current = false; } + + const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent ?? 'canvas'; prevDragParentId.current = null; newDragParentId.current = null; setDragParentId(null); @@ -880,6 +889,12 @@ export default function Grid({ gridWidth, currentLayout }) { // Apply transform for smooth transition e.target.style.transform = `translate(${left}px, ${top}px)`; + // Force reordering of conatiner if the parent has not changed + const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId; + if (oldParentId === newParentId) { + useGridStore.getState().actions.setReorderContainerChildren(newParentId); + } + // Select the dragged component after drop setTimeout(() => setSelectedComponents([dragged.id])); } catch (error) { diff --git a/frontend/src/_stores/gridStore.js b/frontend/src/_stores/gridStore.js index 213f07ac16..d7112dd9ef 100644 --- a/frontend/src/_stores/gridStore.js +++ b/frontend/src/_stores/gridStore.js @@ -12,6 +12,10 @@ const initialState = { idGroupDragged: false, openModalWidgetId: null, subContainerWidths: {}, + reorderContainerChildren: { + containerId: null, + triggerUpdate: 0, + }, }; export const useGridStore = create( @@ -26,6 +30,11 @@ export const useGridStore = create( setOpenModalWidgetId: (openModalWidgetId) => set({ openModalWidgetId }), setSubContainerWidths: (id, width) => set((state) => ({ subContainerWidths: { ...state.subContainerWidths, [id]: width } })), + setReorderContainerChildren: (containerId) => + // Function to trigger reordering of specific container for tab navigation + set((state) => ({ + reorderContainerChildren: { containerId, triggerUpdate: state.reorderContainerChildren.triggerUpdate + 1 }, + })), }, }), { name: 'Grid Store' } From 7b844f51512e47b856095ca265a40543027259b2 Mon Sep 17 00:00:00 2001 From: Adish M <44204658+adishM98@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:33:13 +0530 Subject: [PATCH 07/29] Fixed forked branch checkout issue in Render deployment (#12407) Issue: - Forked branch was not being pulled by Render create service. Fix: - Added fix in preview code to correctly fetch and checkout to forked branch. --- .github/workflows/render-preview-deploy.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/render-preview-deploy.yml b/.github/workflows/render-preview-deploy.yml index ead9ba50bf..d4700d06e7 100644 --- a/.github/workflows/render-preview-deploy.yml +++ b/.github/workflows/render-preview-deploy.yml @@ -13,12 +13,31 @@ permissions: jobs: # Community Edition - create-ce-review-app: if: ${{ github.event.action == 'labeled' && (github.event.label.name == 'create-ce-review-app' || github.event.label.name == 'review-app') }} runs-on: ubuntu-latest steps: + - name: Sync repo + uses: actions/checkout@v3 + + - name: Check if PR is from the same repo + id: check_repo + run: echo "::set-output name=is_fork::$(if [[ '${{ github.event.pull_request.head.repo.full_name }}' != '${{ github.event.pull_request.base.repo.full_name }}' ]]; then echo true; else echo false; fi)" + + - name: Fetch the remote branch if it's a forked PR + if: steps.check_repo.outputs.is_fork == 'true' + run: | + git fetch origin pull/${{ github.event.number }}/head:${{ env.BRANCH_NAME }} + git checkout ${{ env.BRANCH_NAME }} + + - name: Checkout + if: steps.check_repo.outputs.is_fork == 'false' + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Creating deployment for CE id: create-ce-deployment run: | From 40ec66344194da1fbdd25d4975d99b7adfbc2992 Mon Sep 17 00:00:00 2001 From: Adish M Date: Fri, 28 Mar 2025 15:52:18 +0530 Subject: [PATCH 08/29] =?UTF-8?q?Fix=20forked=20branch=20handling=20by=20d?= =?UTF-8?q?ynamically=20setting=20the=20repo=20URL.=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detect if the branch is from a fork - Dynamically set the repo URL to use the fork's owner - Ensure correct repository reference during workflow execution --- .github/workflows/render-preview-deploy.yml | 31 ++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/workflows/render-preview-deploy.yml b/.github/workflows/render-preview-deploy.yml index d4700d06e7..203ee88150 100644 --- a/.github/workflows/render-preview-deploy.yml +++ b/.github/workflows/render-preview-deploy.yml @@ -21,22 +21,33 @@ jobs: - name: Sync repo uses: actions/checkout@v3 - - name: Check if PR is from the same repo + - name: Check if Forked Repository id: check_repo - run: echo "::set-output name=is_fork::$(if [[ '${{ github.event.pull_request.head.repo.full_name }}' != '${{ github.event.pull_request.base.repo.full_name }}' ]]; then echo true; else echo false; fi)" + run: | + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + echo "is_fork=true" >> $GITHUB_ENV + echo "FORKED_OWNER=${{ github.event.pull_request.head.repo.owner.login }}" >> $GITHUB_ENV + else + echo "is_fork=false" >> $GITHUB_ENV + fi - - name: Fetch the remote branch if it's a forked PR - if: steps.check_repo.outputs.is_fork == 'true' + - name: Set Repository URL + run: | + if [[ "$is_fork" == "true" ]]; then + echo "REPO_URL=https://github.com/${FORKED_OWNER}/ToolJet" >> $GITHUB_ENV + else + echo "REPO_URL=https://github.com/ToolJet/ToolJet" >> $GITHUB_ENV + fi + + - name: Fetch and Checkout Forked Branch + if: env.is_fork == 'true' run: | git fetch origin pull/${{ github.event.number }}/head:${{ env.BRANCH_NAME }} git checkout ${{ env.BRANCH_NAME }} - - name: Checkout - if: steps.check_repo.outputs.is_fork == 'false' + - name: Checkout Default Branch + if: env.is_fork == 'false' uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.ref }} - - name: Creating deployment for CE id: create-ce-deployment @@ -53,7 +64,7 @@ jobs: "name": "ToolJet CE PR #${{ env.PR_NUMBER }}", "notifyOnFail": "default", "ownerId": "tea-caeo4bj19n072h3dddc0", - "repo": "https://github.com/ToolJet/ToolJet", + "repo": "'"$REPO_URL"'", "slug": "tooljet-ce-pr-${{ env.PR_NUMBER }}", "suspended": "not_suspended", "suspenders": [], From 27c32cc12b5603084c709095a314cb7184de52b8 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 13 Mar 2025 04:42:49 +0530 Subject: [PATCH 09/29] fix: custom debug and transformation entities bugs --- .../AppBuilder/_stores/slices/eventsSlice.js | 2 +- .../_stores/slices/queryPanelSlice.js | 97 ++++++++++++------- .../LeftSidebar/SidebarDebugger/Logs.jsx | 4 +- frontend/src/_styles/left-sidebar.scss | 1 + 4 files changed, 67 insertions(+), 37 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 948ac39b39..5c449e1f04 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -470,7 +470,7 @@ export const createEventsSlice = (set, get) => ({ error: { message: error.message, description: JSON.stringify(error.message, null, 2), - ...(event.component && componentId && { componentId: componentId }), + ...(event.component === 'component' && componentId && { componentId: componentId }), }, errorTarget: constructErrorTarget(), options: options, diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index fd49d2a5e4..b2a4e4340d 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -425,7 +425,7 @@ export const createQueryPanelSlice = (set, get) => ({ query, 'edit' ); - if (finalData.status === 'failed') { + if (finalData?.status === 'failed') { setResolvedQuery(queryId, { isLoading: false, }); @@ -749,18 +749,30 @@ export const createQueryPanelSlice = (set, get) => ({ runTransformation: async (rawData, transformation, transformationLanguage = 'javascript', query, mode = 'edit') => { const data = rawData; const { - queryPanel: { runPythonTransformation }, + queryPanel: { runPythonTransformation, createProxy }, getResolvedState, } = get(); - let result = []; + let result = {}; const currentState = getResolvedState(); if (transformationLanguage === 'python') { result = await runPythonTransformation(currentState, data, transformation, query, mode); } else if (transformationLanguage === 'javascript') { try { + const { eventsSlice } = get(); + const { generateAppActions } = eventsSlice; + const queriesInResolvedState = deepClone(currentState.queries); + const actions = generateAppActions('', mode); + + const proxiedComponents = createProxy(currentState?.components, 'components'); + const proxiedGlobals = createProxy(currentState?.globals, 'globals'); + const proxiedConstants = createProxy(currentState?.constants, 'constants'); + const proxiedVariables = createProxy(currentState?.variables, 'variables'); + const proxiedPage = createProxy(deepClone(currentState?.page, 'page')); + const proxiedQueriesInResolvedState = createProxy(queriesInResolvedState, 'queries'); + const evalFunction = Function( - ['data', 'moment', '_', 'components', 'queries', 'globals', 'variables', 'page', 'constants'], + ['data', 'moment', '_', 'components', 'queries', 'globals', 'variables', 'page', 'constants', 'actions'], transformation ); @@ -768,32 +780,45 @@ export const createQueryPanelSlice = (set, get) => ({ data, moment, _, - currentState.components, - currentState.queries, - currentState.globals, - currentState.variables, - currentState.page, - currentState.constants + proxiedComponents, + proxiedQueriesInResolvedState, + proxiedGlobals, + proxiedVariables, + proxiedPage, + proxiedConstants, + { + logError: actions.logError, + logInfo: actions.logInfo, + log: actions.log, + } ); } catch (err) { - result = { - message: err.stack.split('\n')[0], - status: 'failed', - data: data, - }; + const stackLines = err.stack.split('\n'); + const errorLocation = + stackLines[2]?.match(/:(\d+):(\d+)/) ?? stackLines[1]?.match(/:(\d+):(\d+)/); + + let lineNumber = null; + + if (errorLocation) { + lineNumber = errorLocation[1] - 2; + } + + console.log('JS execution failed: ', err); + let error = err.message || err.stack.split('\n')[0] || 'JS execution failed'; + result = { status: 'failed', data: { message: error, description: error, lineNumber } }; + get().debugger.log({ + logLevel: result?.status === 'failed' ? 'error' : 'success', + type: 'transformation', + kind: query.kind, + key: query.name, + message: result?.message, + error: result?.data, + isTransformation: true, + isQuerySuccessLog: result?.status === 'failed' ? false : true, + errorTarget: 'Queries', + }); } } - get().debugger.log({ - logLevel: result?.status === 'failed' ? 'error' : 'success', - type: 'transformation', - kind: query.kind, - key: query.name, - message: result?.message, - error: result, - isTransformation: true, - isQuerySuccessLog: result?.status === 'failed' ? false : true, - errorTarget: 'Queries', - }); return result; }, @@ -861,12 +886,13 @@ export const createQueryPanelSlice = (set, get) => ({ createProxy: (obj, path = '') => { const { queryPanel } = get(); const { createProxy } = queryPanel; + return new Proxy(obj, { get(target, prop) { const fullPath = path ? `${path}.${prop}` : prop; if (!(prop in target)) { - throw new Error(`Property "${fullPath}" is not defined`); + throw new Error(`ReferenceError: ${fullPath} is not defined`); } const value = target[prop]; @@ -955,13 +981,16 @@ export const createQueryPanelSlice = (set, get) => ({ //Proxy Func required to get current execution line number from stack to log in debugger - const proxiedComponents = createProxy(resolvedState?.components); - const proxiedGlobals = createProxy(resolvedState?.globals); - const proxiedConstants = createProxy(resolvedState?.constants); - const proxiedVariables = createProxy(resolvedState?.variables); - const proxiedPage = createProxy(deepClone(resolvedState?.page)); - const proxiedQueriesInResolvedState = createProxy(queriesInResolvedState); - const proxiedFormattedParams = createProxy(!_.isEmpty(proxiedFormattedParams) ? [proxiedFormattedParams] : []); + const proxiedComponents = createProxy(resolvedState?.components, 'components'); + const proxiedGlobals = createProxy(resolvedState?.globals, 'globals'); + const proxiedConstants = createProxy(resolvedState?.constants, 'constants'); + const proxiedVariables = createProxy(resolvedState?.variables, 'variables'); + const proxiedPage = createProxy(deepClone(resolvedState?.page, 'page')); + const proxiedQueriesInResolvedState = createProxy(queriesInResolvedState, 'queries'); + const proxiedFormattedParams = createProxy( + !_.isEmpty(proxiedFormattedParams) ? [proxiedFormattedParams] : [], + 'params' + ); const fnParams = [ 'moment', diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx index aba2ca6af1..20855a1f0b 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx @@ -27,7 +27,7 @@ function Logs({ logProps, idx }) { logProps?.description || (isString(logProps?.message) && logProps?.message) || (isString(logProps?.error?.description) && logProps?.error?.description) || //added string check since description can be an object. eg: runpy - logProps?.error?.message.trim() + logProps?.error?.message }`; const defaultStyles = { @@ -113,7 +113,7 @@ function Logs({ logProps, idx }) {
Date: Thu, 13 Mar 2025 14:52:16 +0530 Subject: [PATCH 10/29] fix: custom logs format --- .../AppBuilder/_stores/slices/eventsSlice.js | 26 ++++++++++++++++--- .../_stores/slices/queryPanelSlice.js | 2 +- .../LeftSidebar/SidebarDebugger/Logs.jsx | 9 ++++--- frontend/src/_styles/left-sidebar.scss | 9 ++++++- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 5c449e1f04..6b2c7e8ae7 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -457,7 +457,7 @@ export const createEventsSlice = (set, get) => ({ page: 'Event Errors with page', component: 'Component Event', query: 'Event Errors with query', - customLog: 'Custom Log', + customLog: 'Queries', }; return errorTargetMap[source]; @@ -1103,29 +1103,47 @@ export const createEventsSlice = (set, get) => ({ }; const logInfo = (log) => { + const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId); const error = new Error(); const stackLine = error.stack.split('\n')[2]; const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/); const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; - const event = { actionId: 'log-info', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' }; + const event = { + actionId: 'log-info', + description: `${query.name}, ${log}, Line ${lineNumber - 2}`, + eventType: 'customLog', + query, + }; return executeAction(event, mode, {}); }; const logError = (log) => { + const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId); const error = new Error(); const stackLine = error.stack.split('\n')[2]; const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/); const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; - const event = { actionId: 'log-error', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' }; + const event = { + actionId: 'log-error', + description: `${query.name}, ${log}, Line ${lineNumber - 2}`, + eventType: 'customLog', + query, + }; return executeAction(event, mode, {}); }; const log = (log) => { + const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId); const error = new Error(); const stackLine = error.stack.split('\n')[2]; const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/); const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; - const event = { actionId: 'log', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' }; + const event = { + actionId: 'log', + description: `${query.name}, ${log}, Line ${lineNumber - 2}`, + eventType: 'customLog', + query, + }; return executeAction(event, mode, {}); }; diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index b2a4e4340d..b9509a45f5 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -762,7 +762,7 @@ export const createQueryPanelSlice = (set, get) => ({ const { eventsSlice } = get(); const { generateAppActions } = eventsSlice; const queriesInResolvedState = deepClone(currentState.queries); - const actions = generateAppActions('', mode); + const actions = generateAppActions(query?.id, mode); const proxiedComponents = createProxy(currentState?.components, 'components'); const proxiedGlobals = createProxy(currentState?.globals, 'globals'); diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx index 20855a1f0b..083221d47f 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx @@ -103,17 +103,20 @@ function Logs({ logProps, idx }) {
{logProps?.errorTarget}
{moment(logProps?.timestamp).fromNow()}
+ {logProps?.type === 'Custom Log' && ( +
Custom Log
+ )}
Date: Wed, 26 Mar 2025 03:55:10 +0530 Subject: [PATCH 11/29] fix: styling --- .../AppBuilder/_stores/slices/eventsSlice.js | 12 +++++++---- .../_stores/slices/queryPanelSlice.js | 12 +++++------ .../LeftSidebar/SidebarDebugger/Logs.jsx | 12 ++++++----- frontend/src/_styles/left-sidebar.scss | 6 ++++++ frontend/src/_ui/Icon/solidIcons/Code.jsx | 21 +++++++++++++++++++ frontend/src/_ui/Icon/solidIcons/index.js | 3 +++ 6 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 frontend/src/_ui/Icon/solidIcons/Code.jsx diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 6b2c7e8ae7..bb1374af2e 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -444,7 +444,7 @@ export const createEventsSlice = (set, get) => ({ component: `[Page ${pageName}] [Component ${componentName}] [Event ${event?.eventId}] [Action ${event.actionId}]`, page: `[Page ${pageName}] [Event ${event.eventId}] [Action ${event.actionId}]`, query: `[Query ${getQueryName()}] [Event ${event.eventId}] [Action ${event.actionId}]`, - customLog: `${event.description}`, + customLog: `${event.key}`, }; return headerMap[source] || ''; @@ -472,6 +472,7 @@ export const createEventsSlice = (set, get) => ({ description: JSON.stringify(error.message, null, 2), ...(event.component === 'component' && componentId && { componentId: componentId }), }, + description: event?.description, errorTarget: constructErrorTarget(), options: options, strace: 'app_level', @@ -1110,7 +1111,8 @@ export const createEventsSlice = (set, get) => ({ const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log-info', - description: `${query.name}, ${log}, Line ${lineNumber - 2}`, + key: `${query.name}, Line ${lineNumber - 2}`, + description: log, eventType: 'customLog', query, }; @@ -1125,7 +1127,8 @@ export const createEventsSlice = (set, get) => ({ const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log-error', - description: `${query.name}, ${log}, Line ${lineNumber - 2}`, + key: `${query.name}, Line ${lineNumber - 2}`, + description: log, eventType: 'customLog', query, }; @@ -1140,7 +1143,8 @@ export const createEventsSlice = (set, get) => ({ const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log', - description: `${query.name}, ${log}, Line ${lineNumber - 2}`, + key: `${query.name}, Line ${lineNumber - 2}`, + description: log, eventType: 'customLog', query, }; diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index b9509a45f5..ab29ed5b39 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -810,7 +810,7 @@ export const createQueryPanelSlice = (set, get) => ({ logLevel: result?.status === 'failed' ? 'error' : 'success', type: 'transformation', kind: query.kind, - key: query.name, + key: `${query.name}, transformation, line ${result?.data?.lineNumber}`, message: result?.message, error: result?.data, isTransformation: true, @@ -981,12 +981,12 @@ export const createQueryPanelSlice = (set, get) => ({ //Proxy Func required to get current execution line number from stack to log in debugger - const proxiedComponents = createProxy(resolvedState?.components, 'components'); - const proxiedGlobals = createProxy(resolvedState?.globals, 'globals'); - const proxiedConstants = createProxy(resolvedState?.constants, 'constants'); - const proxiedVariables = createProxy(resolvedState?.variables, 'variables'); + const proxiedComponents = createProxy(deepClone(resolvedState?.components), 'components'); + const proxiedGlobals = createProxy(deepClone(resolvedState?.globals), 'globals'); + const proxiedConstants = createProxy(deepClone(resolvedState?.constants), 'constants'); + const proxiedVariables = createProxy(deepClone(resolvedState?.variables), 'variables'); const proxiedPage = createProxy(deepClone(resolvedState?.page, 'page')); - const proxiedQueriesInResolvedState = createProxy(queriesInResolvedState, 'queries'); + const proxiedQueriesInResolvedState = createProxy(deepClone(queriesInResolvedState), 'queries'); const proxiedFormattedParams = createProxy( !_.isEmpty(proxiedFormattedParams) ? [proxiedFormattedParams] : [], 'params' diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx index 083221d47f..5303534ad9 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx @@ -87,12 +87,12 @@ function Logs({ logProps, idx }) {

{ - setOpen((prev) => !prev); + logProps?.type !== 'Custom Log' && setOpen((prev) => !prev); }} style={{ pointerEvents: logProps?.isQuerySuccessLog ? 'none' : 'default', position: 'relative' }} > - + {logProps?.type !== 'Custom Log' && } {logProps.type === 'navToDisablePage' ? ( @@ -104,7 +104,9 @@ function Logs({ logProps, idx }) { {moment(logProps?.timestamp).fromNow()}

{logProps?.type === 'Custom Log' && ( -
Custom Log
+
+ Custom Log +
)}
+ {logProps?.type == 'Custom Log' &&
{message}
} - {message} - {logProps?.error?.lineNumber ? `, Line ${logProps.error.lineNumber}` : ''} + {logProps?.type !== 'Custom Log' && message} )} diff --git a/frontend/src/_styles/left-sidebar.scss b/frontend/src/_styles/left-sidebar.scss index 217ea20ccb..8efdcabdd3 100644 --- a/frontend/src/_styles/left-sidebar.scss +++ b/frontend/src/_styles/left-sidebar.scss @@ -261,10 +261,16 @@ } .error-target-custom-log { + display: flex; + align-items: center; margin-top: 5px; background: var(--purple5) !important; color: var(--purple11); width: fit-content; + + svg { + margin-right: 1px; + } } } diff --git a/frontend/src/_ui/Icon/solidIcons/Code.jsx b/frontend/src/_ui/Icon/solidIcons/Code.jsx new file mode 100644 index 0000000000..d4022f94d0 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/Code.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const Code = ({ fill = 'var(--purple11)', width = '25', className = '', viewBox = '0 0 25 25' }) => ( + + + +); + +export default Code; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index 34a2410e1d..02ef072f4a 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -229,6 +229,7 @@ import CalendarSmall from './CalendarSmall.jsx'; import UserGroupsGrey from './UserGroupsGrey.jsx'; import AppLimitSvg from './AppLimitSvg.jsx'; import NewTabSmall from './NewTabSmall.jsx'; +import Code from './Code.jsx'; const Icon = (props) => { switch (props.name) { @@ -308,6 +309,8 @@ const Icon = (props) => { return ; case 'clearrectangle': return ; + case 'code': + return ; case 'clock': return ; case 'column': From 573537159e3ea24371b3571c0a853ed53e6fe98a Mon Sep 17 00:00:00 2001 From: Akshay Sasidharan Date: Fri, 28 Mar 2025 18:46:34 +0530 Subject: [PATCH 12/29] add azurerepos in plugins assets --- server/src/assets/marketplace/plugins.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/assets/marketplace/plugins.json b/server/src/assets/marketplace/plugins.json index a312406724..7024fad778 100644 --- a/server/src/assets/marketplace/plugins.json +++ b/server/src/assets/marketplace/plugins.json @@ -193,5 +193,13 @@ "author": "Tooljet", "timestamp": "Tue, 21 Jan 2025 16:55:28 GMT", "tags": ["AI"] + }, + { + "name": "azurerepos", + "description": "api plugin from azurerepos", + "version": "1.0.0", + "id": "azurerepos", + "author": "Tooljet", + "timestamp": "Mon, 23 Dec 2024 11:57:30 GMT" } ] From 6a095deecbaf9667761bf265bd30c245aad66302 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Tue, 1 Apr 2025 17:19:29 +0530 Subject: [PATCH 13/29] Fix dropdown going out of scope in query manager. --- .../TooljetDatabase/DropDownSelect.jsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx index 3b32706f67..b3f614730f 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx @@ -83,6 +83,7 @@ const DropDownSelect = ({ //following two states are to determine whether the value is truncated or not to show tooltip const valueRef = useRef(null); const [isTruncated, setIsTruncated] = useState(false); + const prevShowMenu = useRef(false); useEffect(() => { if (shouldCloseFkMenu) { @@ -130,6 +131,39 @@ const DropDownSelect = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [selected]); + useEffect(() => { + const container = document.getElementsByClassName('query-details')[0]; + const popoverBtn = document.getElementById(popoverBtnId.current); + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + if (prevShowMenu.current) { + setShowMenu(true); + prevShowMenu.current = false; + } + } else if (showMenu) { + setShowMenu(false); + prevShowMenu.current = true; + } + if (!entry.isIntersecting && showMenu) { + setShowMenu(false); + } + }, + { root: container, threshold: [0.5] } + ); + + if (popoverBtn) { + observer.observe(popoverBtn); + } + + return () => { + if (popoverBtn) { + observer.unobserve(popoverBtn); + } + }; + }, [showMenu]); + function checkElementPosition() { if (isForeignKeyInEditCell) { return 'bottom-start'; From b6b89c0b901f75126da1ffc607272370f16d3db8 Mon Sep 17 00:00:00 2001 From: Parth <108089718+parthy007@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:53:40 +0530 Subject: [PATCH 14/29] Change azurerepo icon (#12444) --- marketplace/plugins/azurerepos/lib/icon.svg | 83 ++++----------------- 1 file changed, 13 insertions(+), 70 deletions(-) diff --git a/marketplace/plugins/azurerepos/lib/icon.svg b/marketplace/plugins/azurerepos/lib/icon.svg index 2bddce89fd..44aa77adc7 100644 --- a/marketplace/plugins/azurerepos/lib/icon.svg +++ b/marketplace/plugins/azurerepos/lib/icon.svg @@ -1,72 +1,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - From 6f87d3890aa26801dcff7116d6765c166f9a3247 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 3 Apr 2025 11:54:35 +0530 Subject: [PATCH 15/29] fix: transformation lines on restapi --- .../AppBuilder/_stores/slices/eventsSlice.js | 18 +++++++++--------- .../_stores/slices/queryPanelSlice.js | 12 +++++++++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index bb1374af2e..8ab0909d0a 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -1103,15 +1103,15 @@ export const createEventsSlice = (set, get) => ({ return executeAction(event, mode, {}); }; - const logInfo = (log) => { + const logInfo = (log, isFromTransformation) => { const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId); const error = new Error(); - const stackLine = error.stack.split('\n')[2]; + const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2]; const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/); const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log-info', - key: `${query.name}, Line ${lineNumber - 2}`, + key: `${query.name}${isFromTransformation && ', transformation'}, line ${lineNumber - 2}`, description: log, eventType: 'customLog', query, @@ -1119,15 +1119,15 @@ export const createEventsSlice = (set, get) => ({ return executeAction(event, mode, {}); }; - const logError = (log) => { + const logError = (log, isFromTransformation = false) => { const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId); const error = new Error(); - const stackLine = error.stack.split('\n')[2]; + const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2]; const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/); const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log-error', - key: `${query.name}, Line ${lineNumber - 2}`, + key: `${query.name}${isFromTransformation && ', transformation'}, line ${lineNumber - 2}`, description: log, eventType: 'customLog', query, @@ -1135,15 +1135,15 @@ export const createEventsSlice = (set, get) => ({ return executeAction(event, mode, {}); }; - const log = (log) => { + const log = (log, isFromTransformation = false) => { const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId); const error = new Error(); - const stackLine = error.stack.split('\n')[2]; + const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2]; const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/); const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log', - key: `${query.name}, Line ${lineNumber - 2}`, + key: `${query.name}${isFromTransformation && ', transformation'}, line ${lineNumber - 2}`, description: log, eventType: 'customLog', query, diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index ab29ed5b39..d2f3bc4dff 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -787,9 +787,15 @@ export const createQueryPanelSlice = (set, get) => ({ proxiedPage, proxiedConstants, { - logError: actions.logError, - logInfo: actions.logInfo, - log: actions.log, + logError: function (log) { + return actions.logError.call(actions, log, true); + }, + logInfo: function (log) { + return actions.logInfo.call(actions, log, true); + }, + log: function (log) { + return actions.log.call(actions, log, true); + }, } ); } catch (err) { From 246dafb64dad0624beb47e171777352e6454953b Mon Sep 17 00:00:00 2001 From: Manish Kushare <37823141+manishkushare@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:59:50 +0530 Subject: [PATCH 16/29] Fix: In TJDB the error message text not having proper column name while uploading bulk data (#12346) * Fix: In TJDB the error message text not having proper column name while uploading bulk data * Change the variable name and removed capitalize loadsh method --- server/src/modules/tooljet-db/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/modules/tooljet-db/types.ts b/server/src/modules/tooljet-db/types.ts index 73b72013aa..86f0f4ec8c 100644 --- a/server/src/modules/tooljet-db/types.ts +++ b/server/src/modules/tooljet-db/types.ts @@ -1,6 +1,5 @@ import { QueryFailedError } from 'typeorm'; import { InternalTable } from 'src/entities/internal_table.entity'; -import { capitalize } from 'lodash'; export const TJDB = { character_varying: 'character varying' as const, @@ -150,10 +149,11 @@ export class TooljetDatabaseError extends QueryFailedError { } toString(): string { + const capitalizeSentence = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); const errorMessage = errorCodeMapping[this.code]?.[this.context.origin] || errorCodeMapping[this.code]?.['default'] || - capitalize(this.message); + capitalizeSentence(this.message); return this.replaceErrorPlaceholders(errorMessage); } From 11471421748d1b45599f8d399a15dbdaa5b1675d Mon Sep 17 00:00:00 2001 From: Kushagra Srivastava Date: Thu, 3 Apr 2025 12:00:16 +0530 Subject: [PATCH 17/29] Added detailed error descriptions in BigQuery Plugin (#12384) * Added detailed error descriptions in BigQuery Plugin Signed-off-by: thesynthax * Update index.ts --------- Signed-off-by: thesynthax --- plugins/packages/bigquery/lib/index.ts | 32 +++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/plugins/packages/bigquery/lib/index.ts b/plugins/packages/bigquery/lib/index.ts index b154736ed6..37f09db6c3 100644 --- a/plugins/packages/bigquery/lib/index.ts +++ b/plugins/packages/bigquery/lib/index.ts @@ -104,7 +104,37 @@ export default class Bigquery implements QueryService { } } catch (error) { console.log(error); - throw new QueryError('Query could not be completed', error.message, {}); + const errorMessage = error.message || "An unknown error occurred."; + let errorDetails: any = {}; + + const errorSuggestions = { + "notFound": "Check if the table or dataset exists in the specified location.", + "accessDenied": "Verify that the service account has the necessary permissions.", + "invalidQuery": "Check the SQL syntax and ensure that all referenced columns exist.", + "rateLimitExceeded": "You are making too many requests. Try again after some time.", + "backendError": "BigQuery encountered an internal error. Retry the request after some time.", + "quotaExceeded": "You have exceeded your quota limits. Consider upgrading your plan or reducing query size.", + "duplicate": "A resource with this name already exists. Try using a different name.", + "badRequest": "Check the request parameters and ensure they are correctly formatted.", + }; + + if (error && error instanceof Error) { + const bigqueryError = error as any; + errorDetails.error = bigqueryError; + + const reason = bigqueryError.response?.status?.errorResult?.reason || "unknownError"; + errorDetails.reason = reason; + errorDetails.message = errorMessage; + errorDetails.jobId = bigqueryError.response?.jobReference?.jobId; + errorDetails.location = bigqueryError.response?.jobReference?.location; + errorDetails.query = bigqueryError.response?.configuration?.query?.query; + + + const suggestion = errorSuggestions[reason]; + errorDetails.suggestion = suggestion; + } + + throw new QueryError('Query could not be completed', errorMessage, errorDetails); } return { From 04bc740d954217c884eb7bf1f73b7eb3e8411843 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com> Date: Thu, 3 Apr 2025 12:02:39 +0530 Subject: [PATCH 18/29] Feat: Auto install plugin based on queries on app import (#12350) * dependent plugins will be auto imported on App import * added error handling * added error handling for edge cases * API permissions are updated --- frontend/src/HomePage/HomePage.jsx | 77 ++++++++++++++---- frontend/src/_components/AppModal.jsx | 6 +- .../_components/PluginsListForAppModal.jsx | 6 +- frontend/src/_services/library-app.service.js | 4 +- frontend/src/_services/plugins.service.js | 39 +++++++++ server/src/modules/plugins/ability/index.ts | 18 ++++- .../src/modules/plugins/constants/features.ts | 3 + server/src/modules/plugins/constants/index.ts | 3 + server/src/modules/plugins/controller.ts | 18 ++++- server/src/modules/plugins/service.ts | 80 +++++++++++++------ server/src/modules/plugins/types/index.ts | 3 + server/src/modules/templates/controller.ts | 4 +- server/src/modules/templates/service.ts | 6 +- 13 files changed, 208 insertions(+), 59 deletions(-) diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index ab50cbb05d..8ee5ab3cc8 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -8,6 +8,7 @@ import { libraryAppService, gitSyncService, licenseService, + pluginsService, } from '@/_services'; import { ConfirmDialog, AppModal } from '@/_components'; import Select from '@/_ui/Select'; @@ -113,7 +114,7 @@ class HomePageComponent extends React.Component { showUserGroupMigrationModal: false, showGroupMigrationBanner: true, shouldAutoImportPlugin: false, - dependentPluginsForTemplate: [], + dependentPlugins: [], dependentPluginsDetail: {}, }; } @@ -310,7 +311,7 @@ class HomePageComponent extends React.Component { const fileReader = new FileReader(); const fileName = file.name.replace('.json', '').substring(0, 50); fileReader.readAsText(file, 'UTF-8'); - fileReader.onload = (event) => { + fileReader.onload = async (event) => { const result = event.target.result; let fileContent; try { @@ -319,8 +320,26 @@ class HomePageComponent extends React.Component { toast.error(`Could not import: ${parseError}`); return; } - this.setState({ fileContent, fileName, showImportAppModal: true }); + + const importedAppDef = fileContent.app || fileContent.appV2; + const dataSourcesUsedInApps = []; + importedAppDef.forEach((appDefinition) => { + appDefinition?.definition?.appV2?.dataSources.forEach((dataSource) => { + dataSourcesUsedInApps.push(dataSource); + }); + }); + + const dependentPluginsResponse = await pluginsService.findDependentPlugins(dataSourcesUsedInApps); + const { pluginsToBeInstalled = [], pluginsListIdToDetailsMap = {} } = dependentPluginsResponse.data; + this.setState({ + fileContent, + fileName, + showImportAppModal: true, + dependentPlugins: pluginsToBeInstalled, + dependentPluginsDetail: { ...pluginsListIdToDetailsMap }, + }); }; + fileReader.onerror = (error) => { toast.error(`Could not import the app: ${error}`); return; @@ -348,12 +367,19 @@ class HomePageComponent extends React.Component { importJSON.app[0].appName = appName; } const requestBody = { organization_id, ...importJSON }; + let installedPluginsInfo = []; try { + if (this.state.dependentPlugins.length) { + ({ installedPluginsInfo = [] } = await pluginsService.installDependentPlugins( + this.state.dependentPlugins, + true + )); + } + const data = await appsService.importResource(requestBody); toast.success('App imported successfully.'); - this.setState({ - isImportingApp: false, - }); + this.setState({ isImportingApp: false }); + if (!isEmpty(data.imports.app)) { this.props.navigate(`/${getWorkspaceId()}/apps/${data.imports.app[0].id}`, { state: { commitEnabled: this.state.commitEnabled }, @@ -362,12 +388,13 @@ class HomePageComponent extends React.Component { this.props.navigate(`/${getWorkspaceId()}/database`); } } catch (error) { - this.setState({ - isImportingApp: false, - }); - if (error.statusCode === 409) { - return false; + if (installedPluginsInfo.length) { + const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id); + await pluginsService.uninstallPlugins(pluginsId); } + + this.setState({ isImportingApp: false }); + if (error.statusCode === 409) return false; toast.error(error?.error || error?.message || 'App import failed'); } }; @@ -380,7 +407,7 @@ class HomePageComponent extends React.Component { const data = await libraryAppService.deploy( id, appName, - this.state.dependentPluginsForTemplate, + this.state.dependentPlugins, this.state.shouldAutoImportPlugin ); this.setState({ deploying: false }); @@ -732,7 +759,7 @@ class HomePageComponent extends React.Component { selectedTemplate: template, ...(plugins_to_be_installed.length && { shouldAutoImportPlugin: true, - dependentPluginsForTemplate: plugins_to_be_installed, + dependentPlugins: plugins_to_be_installed, dependentPluginsDetail: { ...plugins_detail_by_id }, }), }); @@ -750,7 +777,7 @@ class HomePageComponent extends React.Component { this.setState({ showCreateAppFromTemplateModal: false, selectedTemplate: null, - dependentPluginsForTemplate: [], + dependentPlugins: [], dependentPluginsDetail: {}, shouldAutoImportPlugin: false, }); @@ -763,6 +790,20 @@ class HomePageComponent extends React.Component { closeCreateAppModal = () => { this.setState({ showCreateAppModal: false, showCreateModuleModal: false }); }; + + openImportAppModal = async () => { + this.setState({ showImportAppModal: true }); + }; + + closeImportAppModal = () => { + this.setState({ + showImportAppModal: false, + dependentPlugins: [], + dependentPluginsDetail: {}, + shouldAutoImportPlugin: false, + }); + }; + isWithinSevenDaysOfSignUp = (date) => { const currentDate = new Date().toISOString(); const differenceInTime = new Date(currentDate).getTime() - new Date(date).getTime(); @@ -836,7 +877,7 @@ class HomePageComponent extends React.Component { workflowInstanceLevelLimit, showUserGroupMigrationModal, showGroupMigrationBanner, - dependentPluginsForTemplate, + dependentPlugins, dependentPluginsDetail, } = this.state; const modalConfigs = { @@ -865,12 +906,14 @@ class HomePageComponent extends React.Component { modalType: 'import', closeModal: () => this.setState({ showImportAppModal: false }), processApp: this.importFile, - show: () => this.setState({ showImportAppModal: true }), + show: this.openImportAppModal, title: 'Import app', actionButton: 'Import app', actionLoadingButton: 'Importing', fileContent: fileContent, selectedAppName: fileName, + dependentPluginsDetail: dependentPluginsDetail, + dependentPlugins: dependentPlugins, }, template: { modalType: 'template', @@ -882,7 +925,7 @@ class HomePageComponent extends React.Component { actionLoadingButton: 'Creating', templateDetails: this.state.selectedTemplate, dependentPluginsDetail: dependentPluginsDetail, - dependentPluginsForTemplate: dependentPluginsForTemplate, + dependentPlugins: dependentPlugins, }, }; return ( diff --git a/frontend/src/_components/AppModal.jsx b/frontend/src/_components/AppModal.jsx index 7d45e4c36e..54c623dbed 100644 --- a/frontend/src/_components/AppModal.jsx +++ b/frontend/src/_components/AppModal.jsx @@ -29,7 +29,7 @@ export function AppModal({ handleCommitEnableChange, appType, dependentPluginsDetail = [], - dependentPluginsForTemplate = [], + dependentPlugins = [], }) { if (!selectedAppName && templateDetails) { selectedAppName = templateDetails?.name || ''; @@ -238,10 +238,10 @@ export function AppModal({
)}
- {dependentPluginsForTemplate && dependentPluginsForTemplate.length >= 1 && ( + {dependentPlugins && dependentPlugins.length >= 1 && (
e.stopPropagation()}>
diff --git a/frontend/src/_components/PluginsListForAppModal.jsx b/frontend/src/_components/PluginsListForAppModal.jsx index 56d7e19504..074e3b9025 100644 --- a/frontend/src/_components/PluginsListForAppModal.jsx +++ b/frontend/src/_components/PluginsListForAppModal.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import config from 'config'; -export const PluginsListForAppModal = ({ dependentPluginsForTemplate, dependentPluginsDetail }) => { +export const PluginsListForAppModal = ({ dependentPlugins, dependentPluginsDetail }) => { const [isExpanded, setIsExpanded] = useState(false); const toggleExpanded = () => { @@ -29,7 +29,7 @@ export const PluginsListForAppModal = ({ dependentPluginsForTemplate, dependentP )} - {isExpanded && dependentPluginsForTemplate && dependentPluginsForTemplate.length > 0 && ( + {isExpanded && dependentPlugins && dependentPlugins.length > 0 && (
- {dependentPluginsForTemplate.map((plugin, index) => { + {dependentPlugins.map((plugin, index) => { const pluginsName = dependentPluginsDetail[plugin].name || plugin; const iconSrc = `${config.TOOLJET_MARKETPLACE_URL}/marketplace-assets/${plugin}/lib/icon.svg`; return ( diff --git a/frontend/src/_services/library-app.service.js b/frontend/src/_services/library-app.service.js index ed8b464c71..815fa560c0 100644 --- a/frontend/src/_services/library-app.service.js +++ b/frontend/src/_services/library-app.service.js @@ -8,11 +8,11 @@ export const libraryAppService = { findDependentPluginsInTemplate, }; -function deploy(identifier, appName, dependentPluginsForTemplate = [], shouldAutoImportPlugin = false) { +function deploy(identifier, appName, dependentPlugins = [], shouldAutoImportPlugin = false) { const body = { identifier, appName, - dependentPluginsForTemplate, + dependentPlugins, shouldAutoImportPlugin, }; diff --git a/frontend/src/_services/plugins.service.js b/frontend/src/_services/plugins.service.js index 14437b1c97..e66cdd759c 100644 --- a/frontend/src/_services/plugins.service.js +++ b/frontend/src/_services/plugins.service.js @@ -1,4 +1,6 @@ import HttpClient from '@/_helpers/http-client'; +import config from 'config'; +import { authHeader, handleResponse } from '@/_helpers'; const adapter = new HttpClient(); @@ -22,10 +24,47 @@ function reloadPlugin(id) { return adapter.post(`/plugins/${id}/reload`); } +function findDependentPlugins(dataSources) { + return adapter.post(`/plugins/findDependentPlugins`, dataSources); +} + +function installDependentPlugins(dependentPlugins, shouldAutoImportPlugin) { + const body = { + dependentPlugins, + shouldAutoImportPlugin, + }; + + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/plugins/installDependentPlugins`, requestOptions).then(handleResponse); +} + +function uninstallPlugins(pluginsId) { + const body = { + pluginsId: pluginsId, + }; + + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + + return fetch(`${config.apiUrl}/plugins/uninstallPlugins`, requestOptions).then(handleResponse); +} + export const pluginsService = { findAll, installPlugin, updatePlugin, deletePlugin, reloadPlugin, + findDependentPlugins, + installDependentPlugins, + uninstallPlugins, }; diff --git a/server/src/modules/plugins/ability/index.ts b/server/src/modules/plugins/ability/index.ts index ee0d6dcaf9..1bdc0c1a96 100644 --- a/server/src/modules/plugins/ability/index.ts +++ b/server/src/modules/plugins/ability/index.ts @@ -15,10 +15,20 @@ export class FeatureAbilityFactory extends AbilityFactory } protected defineAbilityFor(can: AbilityBuilder['can'], UserAllPermissions: UserAllPermissions): void { - const { superAdmin, isAdmin } = UserAllPermissions; - if (superAdmin || isAdmin) { - // Admin or super admin and do all operations - can([FEATURE_KEY.INSTALL, FEATURE_KEY.UPDATE, FEATURE_KEY.DELETE], Plugin); + const { superAdmin, isAdmin, isBuilder } = UserAllPermissions; + if (superAdmin || isAdmin || isBuilder) { + // Admin, super admin and Builder can do all operations + can( + [ + FEATURE_KEY.INSTALL, + FEATURE_KEY.UPDATE, + FEATURE_KEY.DELETE, + FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS, + FEATURE_KEY.UNINSTALL_PLUGINS, + FEATURE_KEY.DEPENDENT_PLUGINS, + ], + Plugin + ); } // These two operations are available to all can([FEATURE_KEY.GET_ONE, FEATURE_KEY.RELOAD, FEATURE_KEY.GET], Plugin); diff --git a/server/src/modules/plugins/constants/features.ts b/server/src/modules/plugins/constants/features.ts index 230ffc7f43..43e1412455 100644 --- a/server/src/modules/plugins/constants/features.ts +++ b/server/src/modules/plugins/constants/features.ts @@ -10,5 +10,8 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.INSTALL]: {}, [FEATURE_KEY.RELOAD]: {}, [FEATURE_KEY.UPDATE]: {}, + [FEATURE_KEY.DEPENDENT_PLUGINS]: {}, + [FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS]: {}, + [FEATURE_KEY.UNINSTALL_PLUGINS]: {}, }, }; diff --git a/server/src/modules/plugins/constants/index.ts b/server/src/modules/plugins/constants/index.ts index dcf733519e..2fb9051cb0 100644 --- a/server/src/modules/plugins/constants/index.ts +++ b/server/src/modules/plugins/constants/index.ts @@ -5,4 +5,7 @@ export enum FEATURE_KEY { GET = 'get', GET_ONE = 'get_one', RELOAD = 'reload', + DEPENDENT_PLUGINS = 'dependent_plugins', + INSTALL_DEPENDENT_PLUGINS = 'install_dependent_plugins', + UNINSTALL_PLUGINS = 'uninstall_plugins', } diff --git a/server/src/modules/plugins/controller.ts b/server/src/modules/plugins/controller.ts index 7a80771bee..cb3b4816e7 100644 --- a/server/src/modules/plugins/controller.ts +++ b/server/src/modules/plugins/controller.ts @@ -70,8 +70,24 @@ export class PluginsController implements IPluginsController { return this.pluginsService.reload(id); } - @Post('/findDepedentPlugins') + @Post('findDependentPlugins') + @InitFeature(FEATURE_KEY.DEPENDENT_PLUGINS) async findDependentPluginsToBeInstalledFromDataSources(@Body() dataSources) { return this.pluginsService.checkIfPluginsToBeInstalled(dataSources); } + + @Post('installDependentPlugins') + @InitFeature(FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS) + async installDependentPlugins( + @Body('dependentPlugins') dependentPlugins, + @Body('shouldAutoImportPlugin') shouldAutoImportPlugin + ) { + return this.pluginsService.autoInstallPluginsForTemplates(dependentPlugins, shouldAutoImportPlugin); + } + + @Post('uninstallPlugins') + @InitFeature(FEATURE_KEY.UNINSTALL_PLUGINS) + async uninstallPlugins(@Body('pluginsId') pluginsId) { + return this.pluginsService.uninstallPlugins(pluginsId); + } } diff --git a/server/src/modules/plugins/service.ts b/server/src/modules/plugins/service.ts index 7b6fd0782b..48398d5ebe 100644 --- a/server/src/modules/plugins/service.ts +++ b/server/src/modules/plugins/service.ts @@ -136,7 +136,7 @@ export class PluginsService implements IPluginsService { return Array.from(marketplacePluginsUsed); } - private async arePluginsInstalled(pluginsId: Array): Promise<{ pluginsToBeInstalled: Array }> { + private async findPluginsToBeInstalled(pluginsId: Array): Promise<{ pluginsToBeInstalled: Array }> { const pluginsToBeInstalled = []; if (!pluginsId.length) return { pluginsToBeInstalled }; @@ -154,30 +154,62 @@ export class PluginsService implements IPluginsService { async checkIfPluginsToBeInstalled( dataSources ): Promise<{ pluginsToBeInstalled: Array; pluginsListIdToDetailsMap: any }> { - const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins(); - const marketplacePluginsUsed = this.filterMarketplacePluginsFromDatasources(dataSources, pluginsListIdToDetailsMap); - const { pluginsToBeInstalled } = await this.arePluginsInstalled(marketplacePluginsUsed); - return { pluginsToBeInstalled, pluginsListIdToDetailsMap }; - } - - async autoInstallPluginsForTemplates(pluginsToBeInstalled: Array, shouldAutoInstall: boolean) { - const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins(); - if (shouldAutoInstall && pluginsToBeInstalled.length) { - const installedPluginsName = []; - for (const pluginId of pluginsToBeInstalled) { - const pluginDetails = pluginsListIdToDetailsMap[pluginId]; - const installedPlugin = await this.install(pluginDetails); - installedPluginsName.push(installedPlugin.name); - } - return installedPluginsName; - } - - if (!shouldAutoInstall && pluginsToBeInstalled.length) { - throw new NotFoundException( - `Plugins ( ${pluginsToBeInstalled - .map((pluginToBeInstalled) => pluginsListIdToDetailsMap[pluginToBeInstalled].name || pluginToBeInstalled) - .join(', ')} ) is not installed yet!` + try { + const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins(); + const marketplacePluginsUsed = this.filterMarketplacePluginsFromDatasources( + dataSources, + pluginsListIdToDetailsMap + ); + const { pluginsToBeInstalled } = await this.findPluginsToBeInstalled(marketplacePluginsUsed); + return { pluginsToBeInstalled, pluginsListIdToDetailsMap }; + } catch (error) { + throw new InternalServerErrorException( + error, + 'An error occurred while checking whether plugins need to be installed.' ); } } + + async autoInstallPluginsForTemplates(pluginsToBeInstalled: Array, shouldAutoInstall: boolean) { + const installedPluginsList = []; + const installedPluginsInfo = []; + try { + const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins(); + if (shouldAutoInstall && pluginsToBeInstalled.length) { + for (const pluginId of pluginsToBeInstalled) { + const pluginDetails = pluginsListIdToDetailsMap[pluginId]; + const installedPluginInfo = await this.install(pluginDetails); + installedPluginsList.push(installedPluginInfo.name); + installedPluginsInfo.push(installedPluginInfo); + } + return { installedPluginsList, installedPluginsInfo }; + } + + if (!shouldAutoInstall && pluginsToBeInstalled.length) { + throw new NotFoundException( + `Plugins ( ${pluginsToBeInstalled + .map((pluginToBeInstalled) => pluginsListIdToDetailsMap[pluginToBeInstalled].name || pluginToBeInstalled) + .join(', ')} ) is not installed yet!` + ); + } + } catch (error) { + if (installedPluginsInfo.length) { + const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id); + await this.uninstallPlugins(pluginsId); + } + throw new InternalServerErrorException(error, 'Error while installing marketplace plugins'); + } + } + + async uninstallPlugins(pluginsId: Array) { + try { + if (!pluginsId.length) return; + for (const pluginId of pluginsId) { + await this.remove(pluginId); + } + return; + } catch (error) { + throw new InternalServerErrorException(error, 'Error while uninstalling marketplace plugins'); + } + } } diff --git a/server/src/modules/plugins/types/index.ts b/server/src/modules/plugins/types/index.ts index 0abca9f13a..e073b0f1df 100644 --- a/server/src/modules/plugins/types/index.ts +++ b/server/src/modules/plugins/types/index.ts @@ -9,6 +9,9 @@ interface Features { [FEATURE_KEY.INSTALL]: FeatureConfig; [FEATURE_KEY.RELOAD]: FeatureConfig; [FEATURE_KEY.UPDATE]: FeatureConfig; + [FEATURE_KEY.DEPENDENT_PLUGINS]: FeatureConfig; + [FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS]: FeatureConfig; + [FEATURE_KEY.UNINSTALL_PLUGINS]: FeatureConfig; } export interface FeaturesConfig { diff --git a/server/src/modules/templates/controller.ts b/server/src/modules/templates/controller.ts index 70479a63d7..98a85c71ce 100644 --- a/server/src/modules/templates/controller.ts +++ b/server/src/modules/templates/controller.ts @@ -22,14 +22,14 @@ export class TemplateAppsController { @User() user, @Body('identifier') identifier, @Body('appName') appName, - @Body('dependentPluginsForTemplate') dependentPluginsForTemplate, + @Body('dependentPlugins') dependentPlugins, @Body('shouldAutoImportPlugin') shouldAutoImportPlugin ) { const newApp = await this.templatesService.perform( user, identifier, appName, - dependentPluginsForTemplate, + dependentPlugins, shouldAutoImportPlugin ); diff --git a/server/src/modules/templates/service.ts b/server/src/modules/templates/service.ts index 42abdbf251..81483e1514 100644 --- a/server/src/modules/templates/service.ts +++ b/server/src/modules/templates/service.ts @@ -27,12 +27,12 @@ export class TemplatesService { currentUser: User, identifier: string, appName: string, - dependentPluginsForTemplate: Array, + dependentPlugins: Array, shouldAutoImportPlugin: boolean ) { const templateDefinition = this.findTemplateDefinition(identifier); - if (dependentPluginsForTemplate.length) - await this.pluginsService.autoInstallPluginsForTemplates(dependentPluginsForTemplate, shouldAutoImportPlugin); + if (dependentPlugins.length) + await this.pluginsService.autoInstallPluginsForTemplates(dependentPlugins, shouldAutoImportPlugin); return this.importTemplate(currentUser, templateDefinition, appName, identifier); } From 739f8a2eb30bf5afe48361ca28775493acc11547 Mon Sep 17 00:00:00 2001 From: Manish Kushare <37823141+manishkushare@users.noreply.github.com> Date: Thu, 3 Apr 2025 12:03:41 +0530 Subject: [PATCH 19/29] [Fix] : The data in the time field of the calendar is not visible and when scrolling vertically, blank space appears (#12352) * Bug fixed * Bug fixed , line height issue in column form for table edit and create table * Popover position issue fixed * Enhance DateTimePicker popper styling and adjust class usage for better positioning --- .../TooljetDatabase/DateTimePicker/styles.scss | 7 +++++++ .../DateTimePicker/DateTimePicker.jsx | 4 ++-- .../TooljetDatabase/DateTimePicker/styles.scss | 12 +++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss index 23d1c7f7cf..3ddf2da932 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss @@ -219,4 +219,11 @@ .react-datepicker__navigation{ overflow: visible !important; height: inherit !important; +} +.tjdb-td-wrapper{ + .react-datepicker-time__input{ + input{ + line-height: normal !important; + } + } } \ No newline at end of file diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx index 7a4a0b0bce..874de20b33 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx @@ -300,7 +300,7 @@ export const DateTimePicker = ({ return (
{ diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss index 55d0e7f3ed..44eecf6ac5 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss @@ -117,6 +117,9 @@ margin-top: 4px; box-shadow: 0px 8px 16px 0px #3032331A; } +.react-datepicker-popper { + z-index: 10001 !important; +} .react-datepicker-time__caption{ margin-left:20px @@ -234,4 +237,11 @@ line-height: normal !important; } } -} \ No newline at end of file +} +.table-schema-row{ + .react-datepicker-time__input-container{ + input{ + line-height: normal !important; + } + } +} From 86590f58c40d4377345544ba3137f5b870699432 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com> Date: Thu, 3 Apr 2025 12:04:08 +0530 Subject: [PATCH 20/29] Fix: DTO validation for data types in ToolJet database (#12368) * datasource configuration page mounted when no particular datasource is selected * dto validation for TJDB table datatype and app export by specific versioin exports tjdb tables --- frontend/src/HomePage/ExportAppModal.jsx | 2 +- .../dataSources/components/GlobalDataSources/index.jsx | 2 +- server/src/modules/tooljet-db/dto/index.ts | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/HomePage/ExportAppModal.jsx b/frontend/src/HomePage/ExportAppModal.jsx index 463687421a..1ccb7734fc 100644 --- a/frontend/src/HomePage/ExportAppModal.jsx +++ b/frontend/src/HomePage/ExportAppModal.jsx @@ -70,7 +70,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam }); } - if (item.kind === 'tooljetdb' && item.options.table_id) extractedIdData.push(item.options.table_id); + if (item.kind === 'tooljetdb' && item.options.tableId) extractedIdData.push(item.options.tableId); }); const uniqueSet = new Set(extractedIdData); const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item })); diff --git a/frontend/src/modules/dataSources/components/GlobalDataSources/index.jsx b/frontend/src/modules/dataSources/components/GlobalDataSources/index.jsx index 1ab03f6e72..9975bf42d4 100644 --- a/frontend/src/modules/dataSources/components/GlobalDataSources/index.jsx +++ b/frontend/src/modules/dataSources/components/GlobalDataSources/index.jsx @@ -474,7 +474,7 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
- {containerRef && containerRef?.current && ( + {containerRef && containerRef?.current && selectedDataSource && ( { @@ -189,11 +190,11 @@ export class PostgrestTableColumnDto { @Validate(SQLInjectionValidator, { message: 'Column name does not support special characters' }) column_name: string; - @IsString() + @IsIn(Object.values(TJDB), { message: 'Incorrect datatype.' }) @IsNotEmpty() @Transform(({ value }) => sanitizeInput(value)) @Validate(SQLInjectionValidator) - data_type: string; + data_type: TooljetDatabaseDataTypes; @IsOptional() @Transform(({ value, obj }) => { @@ -290,11 +291,11 @@ export class EditColumnTableDto { @Validate(SQLInjectionValidator, { message: 'Column name does not support special characters' }) column_name: string; - @IsString() + @IsIn(Object.values(TJDB), { message: 'Incorrect datatype.' }) @IsNotEmpty() @Transform(({ value }) => sanitizeInput(value)) @Validate(SQLInjectionValidator) - data_type: string; + data_type: TooljetDatabaseDataTypes; @IsOptional() @Transform(({ value, obj }) => { From 6d5665177fffcb73bb0b10231529e57f15bb609f Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 3 Apr 2025 13:13:56 +0530 Subject: [PATCH 21/29] Fix: Move state from gridStore to gridSlice. --- frontend/src/AppBuilder/AppCanvas/Container.jsx | 2 +- frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx | 9 +++++---- frontend/src/AppBuilder/_stores/slices/gridSlice.js | 10 ++++++++++ frontend/src/_stores/gridStore.js | 9 --------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index 9de350b810..19d97f0061 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -62,7 +62,7 @@ export const Container = React.memo( const isContainerReadOnly = useMemo(() => { return (index !== 0 && (componentType === 'Listview' || componentType === 'Kanban')) || currentMode === 'view'; }, [componentType, index, currentMode]); - const reorderContainerChildren = useGridStore((state) => state.reorderContainerChildren); + const reorderContainerChildren = useStore((state) => state.reorderContainerChildren, shallow); const prevForceUpdateRef = useRef(0); const prevComponentsOrder = useRef(components); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index b7c9ebd6b6..174c68b475 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -67,6 +67,7 @@ export default function Grid({ gridWidth, currentLayout }) { const prevDragParentId = useRef(null); const newDragParentId = useRef(null); const [isGroupDragging, setIsGroupDragging] = useState(false); + const setReorderContainerChildren = useStore((state) => state.setReorderContainerChildren, shallow); useEffect(() => { const selectedSet = new Set(selectedComponents); @@ -536,7 +537,7 @@ export default function Grid({ gridWidth, currentLayout }) { }) ); } - useGridStore.getState().actions.setReorderContainerChildren(draggedOverElemId ?? 'canvas'); + setReorderContainerChildren(draggedOverElemId ?? 'canvas'); } catch (error) { console.error('Error dragging group', error); } @@ -697,7 +698,7 @@ export default function Grid({ gridWidth, currentLayout }) { resizeData.gw = _gridWidth; } handleResizeStop([resizeData]); - useGridStore.getState().actions.setReorderContainerChildren(currentWidget?.parent ?? 'canvas'); + setReorderContainerChildren(currentWidget?.parent ?? 'canvas'); } catch (error) { console.error('ResizeEnd error ->', error); } @@ -780,7 +781,7 @@ export default function Grid({ gridWidth, currentLayout }) { const groupParentId = boxList.find(({ id }) => id === groupResizeDataRef.current[0].target.id)?.parent ?? 'canvas'; - useGridStore.getState().actions.setReorderContainerChildren(groupParentId); + setReorderContainerChildren(groupParentId); groupResizeDataRef.current = []; reloadGrid(); @@ -892,7 +893,7 @@ export default function Grid({ gridWidth, currentLayout }) { // Force reordering of conatiner if the parent has not changed const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId; if (oldParentId === newParentId) { - useGridStore.getState().actions.setReorderContainerChildren(newParentId); + setReorderContainerChildren(newParentId); } // Select the dragged component after drop diff --git a/frontend/src/AppBuilder/_stores/slices/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js index 642266a32b..37de5cf81a 100644 --- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js @@ -8,6 +8,10 @@ const initialState = { lastCanvasIdClick: '', lastCanvasClickPosition: null, draggingComponentId: null, + reorderContainerChildren: { + containerId: null, + triggerUpdate: 0, + }, }; export const createGridSlice = (set, get) => ({ @@ -73,4 +77,10 @@ export const createGridSlice = (set, get) => ({ setLastCanvasClickPosition: (position) => { set({ lastCanvasClickPosition: position }); }, + setReorderContainerChildren: (containerId) => { + // Function to trigger reordering of specific container for tab navigation + set((state) => ({ + reorderContainerChildren: { containerId, triggerUpdate: state.reorderContainerChildren.triggerUpdate + 1 }, + })); + }, }); diff --git a/frontend/src/_stores/gridStore.js b/frontend/src/_stores/gridStore.js index d7112dd9ef..213f07ac16 100644 --- a/frontend/src/_stores/gridStore.js +++ b/frontend/src/_stores/gridStore.js @@ -12,10 +12,6 @@ const initialState = { idGroupDragged: false, openModalWidgetId: null, subContainerWidths: {}, - reorderContainerChildren: { - containerId: null, - triggerUpdate: 0, - }, }; export const useGridStore = create( @@ -30,11 +26,6 @@ export const useGridStore = create( setOpenModalWidgetId: (openModalWidgetId) => set({ openModalWidgetId }), setSubContainerWidths: (id, width) => set((state) => ({ subContainerWidths: { ...state.subContainerWidths, [id]: width } })), - setReorderContainerChildren: (containerId) => - // Function to trigger reordering of specific container for tab navigation - set((state) => ({ - reorderContainerChildren: { containerId, triggerUpdate: state.reorderContainerChildren.triggerUpdate + 1 }, - })), }, }), { name: 'Grid Store' } From 4ea5f33265699ae58319f0f9f39fac557308a08f Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 3 Apr 2025 13:36:41 +0530 Subject: [PATCH 22/29] Fix: Extracted the sorting logic to a custom hook. --- .../src/AppBuilder/AppCanvas/Container.jsx | 39 +-------------- .../AppBuilder/_hooks/useSortedComponents.js | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 frontend/src/AppBuilder/_hooks/useSortedComponents.js diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index 19d97f0061..fcc0a95d4e 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -20,6 +20,7 @@ import NoComponentCanvasContainer from './NoComponentCanvasContainer'; import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants'; import { isPDFSupported } from '@/_helpers/appUtils'; import toast from 'react-hot-toast'; +import useSortedComponents from '../_hooks/useSortedComponents'; //TODO: Revisit the logic of height (dropRef) @@ -58,13 +59,9 @@ export const Container = React.memo( const currentMode = useStore((state) => state.currentMode, shallow); const currentLayout = useStore((state) => state.currentLayout, shallow); const setFocusedParentId = useStore((state) => state.setFocusedParentId, shallow); - const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents, shallow); const isContainerReadOnly = useMemo(() => { return (index !== 0 && (componentType === 'Listview' || componentType === 'Kanban')) || currentMode === 'view'; }, [componentType, index, currentMode]); - const reorderContainerChildren = useStore((state) => state.reorderContainerChildren, shallow); - const prevForceUpdateRef = useRef(0); - const prevComponentsOrder = useRef(components); const [{ isOverCurrent }, drop] = useDrop({ accept: 'box', @@ -150,39 +147,7 @@ export const Container = React.memo( [setLastCanvasClickPosition] ); - // Function to sort the components based on position in container for tab navigation - const sortedComponents = useMemo(() => { - const { triggerUpdate, containerId } = reorderContainerChildren; - - // If a forced update occurred for a different container, return the previous order - const isForcedUpdate = prevForceUpdateRef.current !== triggerUpdate; - if (isForcedUpdate) { - prevForceUpdateRef.current = triggerUpdate; - if (containerId !== id) { - return prevComponentsOrder.current; - } - } - - const currentPageComponents = getCurrentPageComponents() - - const newComponentsOrder = [...components].sort((a, b) => { - const aTop = currentPageComponents?.[a]?.layouts?.[currentLayout]?.top; - const bTop = currentPageComponents?.[b]?.layouts?.[currentLayout]?.top; - if (aTop !== bTop) { - return aTop - bTop; - } else { - const aLeft = currentPageComponents?.[a]?.layouts?.[currentLayout]?.left; - const bLeft = currentPageComponents?.[b]?.layouts?.[currentLayout]?.left; - if (aLeft !== bLeft) { - return aLeft - bLeft; - } - } - }); - - prevComponentsOrder.current = newComponentsOrder; - return newComponentsOrder; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [components, currentLayout, reorderContainerChildren.triggerUpdate]); + const sortedComponents = useSortedComponents(components, currentLayout, id); return (
{ + const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents, shallow); + const reorderContainerChildren = useStore((state) => state.reorderContainerChildren, shallow); + const prevForceUpdateRef = useRef(0); + const prevComponentsOrder = useRef(components); + + // Function to sort the components based on position in container for tab navigation + const sortedComponents = useMemo(() => { + const { triggerUpdate, containerId } = reorderContainerChildren; + + // If a forced update occurred for a different container, return the previous order + const isForcedUpdate = prevForceUpdateRef.current !== triggerUpdate; + if (isForcedUpdate) { + prevForceUpdateRef.current = triggerUpdate; + if (containerId !== id) { + return prevComponentsOrder.current; + } + } + + const currentPageComponents = getCurrentPageComponents(); + + const newComponentsOrder = [...components].sort((a, b) => { + const aTop = currentPageComponents?.[a]?.layouts?.[currentLayout]?.top; + const bTop = currentPageComponents?.[b]?.layouts?.[currentLayout]?.top; + if (aTop !== bTop) { + return aTop - bTop; + } else { + const aLeft = currentPageComponents?.[a]?.layouts?.[currentLayout]?.left; + const bLeft = currentPageComponents?.[b]?.layouts?.[currentLayout]?.left; + if (aLeft !== bLeft) { + return aLeft - bLeft; + } + return 0; + } + }); + + prevComponentsOrder.current = newComponentsOrder; + return newComponentsOrder; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [components, currentLayout, reorderContainerChildren.triggerUpdate, id]); + + return sortedComponents; +}; + +export default useSortedComponents; From cb5a03ac692219e9f6d275b33972c1521efa72e3 Mon Sep 17 00:00:00 2001 From: Akshay Sasidharan Date: Thu, 3 Apr 2025 13:44:41 +0530 Subject: [PATCH 23/29] update git submodules --- frontend/ee | 2 +- server/ee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/ee b/frontend/ee index 715a830c7a..4b950ed3d0 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 715a830c7a8d75efc7f77106292d9e4499005b69 +Subproject commit 4b950ed3d0ba15edddf217936e9c9ae1ca3cf11a diff --git a/server/ee b/server/ee index 0eefbb71a1..683647f83d 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 0eefbb71a1d5288f49641af5efaaab25970f27d1 +Subproject commit 683647f83d3efeeadbe69c40b8e8dd5ba4e8ea06 From 4b6e6ee5cdefc1ee65297b7c8ce653563ca2794b Mon Sep 17 00:00:00 2001 From: Akshay Date: Thu, 3 Apr 2025 13:47:49 +0530 Subject: [PATCH 24/29] Feature: Dynamic form validations (#12292) * fixed datasource page crash as function definition was referenced wrongly (#11562) * Add new dynamicform * Refactor postgres manifest file * Add new input-v3 component * Conditionally render DynamicformV2 * Make change to design system component * Remove key-value label over header input and increase width * Add validation function for individual inputs * Add validations on datasource creation * Update custom input wrapper * Update manifest file * Add validation setup for dynamic form with JSON schema * Fix input labels * Add more validation checks * Update manifest * Remove console logs * Add props for header component * Skip validation for encrypted fields * Add validations while saving datasource * Remove validations for connection-options * Add fetch manifest function * Centralise validation errors * Add property name in datapath * Initialize and map validation errors to property * Reuse validationErrors while saving datasource * Bypass design system validation by implementing custom validation prop * Skip initial render validation Skip validation message for unchanged elements * Remove fetchManifest * Add text input for connection string * Add workflow schema * Fix double border on error or success * Remove redundant default populating logic * Fix the error helper text color to red * Validate all fields post initial render * Show label name in helper-text for failed validation * Correctly switch between the password eye svg * Incorporate edit button on encrypted inputs * Resolve lint issue --------- Co-authored-by: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com> Co-authored-by: Parth Adhikari Co-authored-by: Parth Adhikari Co-authored-by: Parth Adhikari Co-authored-by: parthy007 --- frontend/src/_components/DynamicFormV2.jsx | 510 ++++++++++++++++++ .../src/_helpers/dataSourceSchemaManager.js | 119 ++++ frontend/src/_ui/HttpHeaders/SourceEditor.jsx | 2 +- frontend/src/_ui/Input-V3/index.js | 85 +++ .../components/ui/Input/CommonInput/Index.jsx | 69 ++- .../ui/Input/CommonInput/NumberInput.jsx | 6 +- .../ui/Input/CommonInput/TextInput.jsx | 6 +- frontend/src/components/ui/Input/Index.jsx | 2 +- frontend/src/components/ui/Input/Input.jsx | 40 +- .../ui/Input/InputUtils/InputUtils.jsx | 2 +- .../components/DataSourceComponents/index.js | 76 ++- .../DataSourceManager/DataSourceManager.jsx | 66 ++- plugins/packages/postgresql/lib/manifest.json | 351 +++++++----- 13 files changed, 1162 insertions(+), 172 deletions(-) create mode 100644 frontend/src/_components/DynamicFormV2.jsx create mode 100644 frontend/src/_helpers/dataSourceSchemaManager.js create mode 100644 frontend/src/_ui/Input-V3/index.js diff --git a/frontend/src/_components/DynamicFormV2.jsx b/frontend/src/_components/DynamicFormV2.jsx new file mode 100644 index 0000000000..1a6269976f --- /dev/null +++ b/frontend/src/_components/DynamicFormV2.jsx @@ -0,0 +1,510 @@ +import React from 'react'; +import cx from 'classnames'; +import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager'; +import Textarea from '@/_ui/Textarea'; +import Input from '@/_ui/Input'; +import Select from '@/_ui/Select'; +import Headers from '@/_ui/HttpHeaders'; +import Toggle from '@/_ui/Toggle'; +import InputV3 from '@/_ui/Input-V3'; +import { filter, find, isEmpty } from 'lodash'; +import { ButtonSolid } from './AppButton'; +import { useGlobalDataSourcesStatus } from '@/_stores/dataSourcesStore'; +import { canDeleteDataSource, canUpdateDataSource } from '@/_helpers'; +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; +import { orgEnvironmentVariableService, orgEnvironmentConstantService } from '../_services'; +import { Constants } from '@/_helpers/utils'; + +const DynamicFormV2 = ({ + schema, + options, + optionchanged, + optionsChanged, + selectedDataSource, + isEditMode, + layout = 'vertical', + onBlur, + setDefaultOptions, + currentAppEnvironmentId, + isGDS, + validationMessages, + setValidationMessages, + clearValidationMessages, +}) => { + const uiProperties = schema['tj:ui:properties'] || {}; + const dsm = React.useMemo(() => new DataSourceSchemaManager(schema), [schema]); + const encryptedProperties = React.useMemo(() => dsm.getEncryptedProperties(), [dsm]); + const [conditionallyRequiredProperties, setConditionallyRequiredProperties] = React.useState([]); + const [workspaceVariables, setWorkspaceVariables] = React.useState([]); + const [currentOrgEnvironmentConstants, setCurrentOrgEnvironmentConstants] = React.useState([]); + const [computedProps, setComputedProps] = React.useState({}); + const [hasUserInteracted, setHasUserInteracted] = React.useState(false); + const [interactedFields, setInteractedFields] = React.useState(new Set()); + + const isHorizontalLayout = layout === 'horizontal'; + const prevDataSourceIdRef = React.useRef(selectedDataSource?.id); + + const globalDataSourcesStatus = useGlobalDataSourcesStatus(); + const { isEditing: isDataSourceEditing } = globalDataSourcesStatus; + + React.useEffect(() => { + if (isGDS) { + orgEnvironmentConstantService.getConstantsFromEnvironment(currentAppEnvironmentId).then((data) => { + const constants = { + globals: {}, + secrets: {}, + }; + data.constants.forEach((constant) => { + if (constant.type === Constants.Secret) { + constants.secrets[constant.name] = constant.value; + } else { + constants.globals[constant.name] = constant.value; + } + }); + + setCurrentOrgEnvironmentConstants(constants); + }); + + orgEnvironmentVariableService.getVariables().then((data) => { + const client_variables = {}; + const server_variables = {}; + data.variables.map((variable) => { + if (variable.variable_type === 'server') { + server_variables[variable.variable_name] = 'HiddenEnvironmentVariable'; + } else { + client_variables[variable.variable_name] = variable.value; + } + }); + + setWorkspaceVariables({ client: client_variables, server: server_variables }); + }); + } + + return () => { + setWorkspaceVariables([]); + setCurrentOrgEnvironmentConstants([]); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentAppEnvironmentId]); + + React.useEffect(() => { + if (!hasUserInteracted) return; + const { valid, errors } = dsm.validateData(options); + + if (valid) { + clearValidationMessages(); + } else { + setValidationMessages(errors, schema); + const requiredFields = errors + .filter((error) => error.keyword === 'required') + .map((error) => error.params.missingProperty); + setConditionallyRequiredProperties(requiredFields); + } + }, [options]); + + React.useEffect(() => { + const prevDataSourceId = prevDataSourceIdRef.current; + prevDataSourceIdRef.current = selectedDataSource?.id; + const uiProperties = schema['tj:ui:properties']; + if (!isEmpty(uiProperties)) { + let fields = {}; + let encryptedFieldsProps = {}; + const flipComponentDropdown = find(uiProperties, ['widget', 'dropdown-component-flip']); + + if (flipComponentDropdown) { + const selector = options?.[flipComponentDropdown?.key]?.value; + const commonFieldsFromSslCertificate = uiProperties[selector]?.ssl_certificate?.commonFields; + fields = { + ...commonFieldsFromSslCertificate, + ...flipComponentDropdown?.commonFields, + ...uiProperties[selector], + }; + } else { + fields = { ...uiProperties }; + } + + const processFields = (fieldsObject) => { + Object.keys(fieldsObject).forEach((key) => { + const field = fieldsObject[key]; + const { widget, encrypted, key: propertyKey } = field; + + if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) { + encryptedFieldsProps[propertyKey] = { + disabled: !!selectedDataSource?.id, + }; + } else if (!isDataSourceEditing) { + if (widget === 'password' || encrypted) { + encryptedFieldsProps[propertyKey] = { + disabled: true, + }; + } + } else { + if ((widget === 'password' || encrypted) && !(propertyKey in computedProps)) { + encryptedFieldsProps[propertyKey] = { + disabled: !!selectedDataSource?.id, + }; + } + } + + // To check for nested dropdown-component-flip + if (widget === 'dropdown-component-flip') { + const selectedOption = options?.[field.key]?.value; + + if (field.commonFields) { + processFields(field.commonFields); + } + + if (selectedOption && fieldsObject[selectedOption]) { + processFields(fieldsObject[selectedOption]); + } + } + }); + }; + + processFields(fields); + + if (uiProperties.renderForm) { + Object.keys(uiProperties.renderForm).forEach((sectionKey) => { + const section = uiProperties.renderForm[sectionKey]; + const { inputs } = section; + if (inputs) { + processFields(inputs); + } + }); + } + + if (prevDataSourceId !== selectedDataSource?.id) { + setComputedProps({ ...encryptedFieldsProps }); + } else { + setComputedProps({ ...computedProps, ...encryptedFieldsProps }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedDataSource?.id, options, isDataSourceEditing]); + + const getElement = (type) => { + switch (type) { + case 'password': + case 'text': + return Input; + case 'password-v3': + case 'text-v3': + return InputV3; + case 'textarea': + return Textarea; + case 'toggle': + return Toggle; + case 'react-component-headers': + return Headers; + // TODO: Move dropdown component flip logic to be handled here + // case 'dropdown-component-flip': + // return Select; + default: + return
Type is invalid
; + } + }; + + const getElementProps = (uiProperties) => { + const { label, description, widget, required, width, key, help_text: helpText, list, buttonText } = uiProperties; + + const isRequired = required || conditionallyRequiredProperties.includes(key); + const isEncrypted = widget === 'password-v3' || encryptedProperties.includes(key); + const currentValue = options?.[key]?.value; + + const handleOptionChange = (key, value, flag) => { + if (!hasUserInteracted) { + setHasUserInteracted(true); + } + setInteractedFields((prev) => new Set(prev).add(key)); + optionchanged(key, value, flag); + }; + + switch (widget) { + case 'password': + case 'text': + case 'textarea': { + return { + key, + widget, + label, + placeholder: isEncrypted ? '**************' : description, + className: cx('form-control', { + 'dynamic-form-encrypted-field': isEncrypted, + }), + style: { marginBottom: '0px !important' }, + helpText: helpText, + value: currentValue || '', + onChange: (e) => optionchanged(key, e.target.value, true), + isGDS: true, + workspaceVariables: [], + workspaceConstants: [], + encrypted: isEncrypted, + onBlur, + }; + } + case 'password-v3': + case 'text-v3': { + return { + key, + widget, + label, + placeholder: isEncrypted ? '**************' : description, + className: cx('form-control', { + 'dynamic-form-encrypted-field': isEncrypted, + }), + style: { marginBottom: '0px !important' }, + helpText: helpText, + value: currentValue || '', + onChange: (e) => handleOptionChange(key, e.target.value, true), + isGDS: true, + workspaceVariables: [], + workspaceConstants: [], + encrypted: isEncrypted, + onBlur, + isRequired: isRequired, + isValidatedMessages: + !hasUserInteracted || !interactedFields.has(key) + ? { valid: null, message: '' } // skip validation for initial render and untouched elements + : validationMessages[key] + ? { valid: false, message: validationMessages[key] } + : isRequired && !isEncrypted + ? { valid: true, message: '' } + : { valid: null, message: '' }, // handle optional && encrypted fields + isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(), + }; + } + case 'react-component-headers': { + let isRenderedAsQueryEditor; + if (isGDS) { + isRenderedAsQueryEditor = false; + } else { + isRenderedAsQueryEditor = !isGDS; + } + return { + getter: key, + options: isRenderedAsQueryEditor + ? options?.[key] ?? schema?.defaults?.[key] + : options?.[key]?.value ?? schema?.defaults?.[key]?.value, + optionchanged, + isRenderedAsQueryEditor, + workspaceConstants: currentOrgEnvironmentConstants, + isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(), + encrypted: isEncrypted, + buttonText, + width: width, + }; + } + case 'toggle': + return { + defaultChecked: currentValue, + checked: currentValue, + onChange: (e) => optionchanged(key, e.target.checked), + }; + case 'dropdown': + case 'dropdown-component-flip': + return { + options: list, + value: options?.[key]?.value || options?.[key], + onChange: (value) => optionchanged(key, value), + width: width || '100%', + encrypted: options?.[key]?.encrypted, + }; + default: + return {}; + } + }; + + const getLayout = (uiProperties) => { + if (isEmpty(uiProperties)) return null; + const flipComponentDropdown = isFlipComponentDropdown(uiProperties); + + if (flipComponentDropdown) { + return flipComponentDropdown; + } + + const handleEncryptedFieldsToggle = (event, field) => { + if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) { + return; + } + const isEditing = computedProps[field]['disabled']; + if (isEditing) { + optionchanged(field, ''); + } else { + //Send old field value if editing mode disabled for encrypted fields + const newOptions = { ...options }; + const oldFieldValue = selectedDataSource?.['options']?.[field]; + if (oldFieldValue) { + optionsChanged({ ...newOptions, [field]: oldFieldValue }); + } else { + delete newOptions[field]; + optionsChanged({ ...newOptions }); + } + } + setComputedProps({ + ...computedProps, + [field]: { + ...computedProps[field], + disabled: !isEditing, + }, + }); + }; + + const renderLabel = (label, tooltip) => { + const labelElement = ( + + ); + + if (tooltip) { + return ( + {tooltip}} + > + {labelElement} + + ); + } + + return labelElement; + }; + + return ( +
+ {Object.keys(uiProperties).map((key) => { + const { label, widget, encrypted, className, key: propertyKey } = uiProperties[key]; + const Element = getElement(widget); + const isSpecificComponent = ['tooljetdb-operations', 'react-component-api-endpoint'].includes(widget); + + return ( +
+ {!isSpecificComponent && ( +
+ {label && + widget !== 'text-v3' && + widget !== 'password-v3' && + renderLabel(label, uiProperties[key].tooltip)} +
+ )} +
+ +
+
+ ); + })} +
+ ); + }; + + const FlipComponentDropdown = (uiProperties) => { + const flipComponentDropdowns = filter(uiProperties, ['widget', 'dropdown-component-flip']); + + const dropdownComponents = flipComponentDropdowns.map((flipComponentDropdown) => { + const selector = options?.[flipComponentDropdown?.key]?.value || options?.[flipComponentDropdown?.key]; + + return ( +
+
+ {flipComponentDropdown.commonFields && getLayout(flipComponentDropdown.commonFields)} + +
+ {(flipComponentDropdown.label || isHorizontalLayout) && ( + + )} + +
+ + + {isPasswordField && ( +
+ {isPasswordVisible ? ( + + ) : ( + + )} +
)} - ref={ref} - {...props} - /> + ); }); Input.displayName = 'Input'; diff --git a/frontend/src/components/ui/Input/InputUtils/InputUtils.jsx b/frontend/src/components/ui/Input/InputUtils/InputUtils.jsx index 5e09cec4fa..57128adf73 100644 --- a/frontend/src/components/ui/Input/InputUtils/InputUtils.jsx +++ b/frontend/src/components/ui/Input/InputUtils/InputUtils.jsx @@ -13,7 +13,7 @@ export const ValidationMessage = ({ response, validationMessage, className }) => htmlFor="validation" type="helper" size="default" - className={`tw-font-normal ${response === true ? 'tw-text-text-success' : 'tw-text-text-warning'}`} + className={`tw-font-normal ${response === true ? 'tw-text-text-success' : '!tw-text-text-warning'}`} data-cy="validation-label" > {validationMessage} diff --git a/frontend/src/modules/common/components/DataSourceComponents/index.js b/frontend/src/modules/common/components/DataSourceComponents/index.js index e783da7021..386db12b00 100644 --- a/frontend/src/modules/common/components/DataSourceComponents/index.js +++ b/frontend/src/modules/common/components/DataSourceComponents/index.js @@ -1,5 +1,6 @@ import React from 'react'; import DynamicForm from '@/_components/DynamicForm'; +import DynamicFormV2 from '@/_components/DynamicFormV2'; import RunjsSchema from './Runjs.schema.json'; import TooljetDbSchema from '@/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/manifest.json'; import RunpySchema from './Runpy.schema.json'; @@ -7,12 +8,37 @@ import WorkflowsSchema from './Workflows.schema.json'; // eslint-disable-next-line import/no-unresolved import { allManifests } from '@tooljet/plugins/client'; +import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager'; + +const getSchemaDetailsForRender = (schema) => { + if (schema['tj:version']) { + const dsm = new DataSourceSchemaManager(schema); + const initialSourceValues = dsm.getDefaults(); + return { + name: schema['tj:source'].name, + kind: schema['tj:source'].kind, + type: schema['tj:source'].type, + options: initialSourceValues, + }; + } + + const _source = schema.source; + const def = schema.defaults ?? {}; + + return { ..._source, defaults: def }; +}; + +const getSchemaMetadata = (schema, key) => { + if (schema['tj:version']) return schema['tj:source'][key]; + // Need to depreciate old schema format + if (key === 'type') return schema.type; + return schema.source[key]; +}; //Commonly Used DS - export const CommonlyUsedDataSources = Object.keys(allManifests) .reduce((accumulator, currentValue) => { - const sourceName = allManifests[currentValue]?.source?.name; + const sourceName = getSchemaMetadata(allManifests[currentValue], 'name'); if ( sourceName === 'REST API' || sourceName === 'MongoDB' || @@ -20,9 +46,7 @@ export const CommonlyUsedDataSources = Object.keys(allManifests) sourceName === 'Google Sheets' || sourceName === 'PostgreSQL' ) { - const _source = allManifests[currentValue].source; - const def = allManifests[currentValue]?.defaults ?? {}; - accumulator.push({ ..._source, defaults: def }); + accumulator.push(getSchemaDetailsForRender(allManifests[currentValue])); } return accumulator; @@ -33,31 +57,23 @@ export const CommonlyUsedDataSources = Object.keys(allManifests) }); export const DataBaseSources = Object.keys(allManifests).reduce((accumulator, currentValue) => { - if (allManifests[currentValue].type === 'database') { - const _source = allManifests[currentValue].source; - const def = allManifests[currentValue]?.defaults ?? {}; - - accumulator.push({ ..._source, defaults: def }); + if (getSchemaMetadata(allManifests[currentValue], 'type') === 'database') { + accumulator.push(getSchemaDetailsForRender(allManifests[currentValue])); } return accumulator; }, []); -export const ApiSources = Object.keys(allManifests).reduce((accumulator, currentValue) => { - if (allManifests[currentValue].type === 'api') { - const _source = allManifests[currentValue].source; - const def = allManifests[currentValue]?.defaults ?? {}; - accumulator.push({ ..._source, defaults: def }); +export const ApiSources = Object.keys(allManifests).reduce((accumulator, currentValue) => { + if (getSchemaMetadata(allManifests[currentValue], 'type') === 'api') { + accumulator.push(getSchemaDetailsForRender(allManifests[currentValue])); } return accumulator; }, []); export const CloudStorageSources = Object.keys(allManifests).reduce((accumulator, currentValue) => { - if (allManifests[currentValue].type === 'cloud-storage') { - const _source = allManifests[currentValue].source; - const def = allManifests[currentValue]?.defaults ?? {}; - - accumulator.push({ ..._source, defaults: def }); + if (getSchemaMetadata(allManifests[currentValue], 'type') === 'cloud-storage') { + accumulator.push(getSchemaDetailsForRender(allManifests[currentValue])); } return accumulator; @@ -73,8 +89,24 @@ export const DataSourceTypes = [ ]; export const SourceComponents = Object.keys(allManifests).reduce((accumulator, currentValue) => { - accumulator[currentValue] = (props) => ; + accumulator[currentValue] = (props) => { + const schema = allManifests[currentValue]; + + if (schema['tj:version']) { + return ; + } + + return ; + }; return accumulator; }, {}); -export const SourceComponent = (props) => ; +export const SourceComponent = (props) => { + const schema = props.dataSourceSchema; + + if (schema['tj:version']) { + return ; + } + + return ; +}; diff --git a/frontend/src/modules/dataSources/components/DataSourceManager/DataSourceManager.jsx b/frontend/src/modules/dataSources/components/DataSourceManager/DataSourceManager.jsx index 4447ef19d2..dd19801dfd 100644 --- a/frontend/src/modules/dataSources/components/DataSourceManager/DataSourceManager.jsx +++ b/frontend/src/modules/dataSources/components/DataSourceManager/DataSourceManager.jsx @@ -34,6 +34,7 @@ import { LicenseTooltip } from '@/LicenseTooltip'; import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; import './dataSourceManager.theme.scss'; import { canUpdateDataSource } from '@/_helpers'; +import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager'; import MultiEnvTabs from './MultiEnvTabs'; class DataSourceManagerComponent extends React.Component { @@ -81,6 +82,8 @@ class DataSourceManagerComponent extends React.Component { unsavedChangesModal: false, datasourceName, creatingApp: false, + validationError: [], + validationMessages: {}, }; } @@ -208,8 +211,31 @@ class DataSourceManagerComponent extends React.Component { }; createDataSource = () => { - const { appId, options, selectedDataSource, selectedDataSourcePluginId, dataSourceMeta, dataSourceSchema } = - this.state; + const { + appId, + options, + selectedDataSource, + selectedDataSourcePluginId, + dataSourceMeta, + dataSourceSchema, + validationMessages, + } = this.state; + + if (!isEmpty(validationMessages)) { + const validationMessageArray = Object.values(validationMessages); + this.setState({ validationError: validationMessageArray }); + toast.error( + this.props.t( + 'editor.queryManager.dataSourceManager.toast.error.validationFailed', + 'Validation failed. Please check your inputs.' + ), + { position: 'top-center' } + ); + if (validationMessageArray.length > 0) { + return false; + } + } + const OAuthDs = ['slack', 'zendesk', 'googlesheets', 'salesforce']; const name = selectedDataSource.name; const kind = selectedDataSource?.kind; @@ -231,6 +257,7 @@ class DataSourceManagerComponent extends React.Component { const value = localStorage.getItem('OAuthCode'); parsedOptions.push({ key: 'code', value, encrypted: false }); } + if (name.trim() !== '') { let service = scope === 'global' ? globalDatasourceService : datasourceService; if (selectedDataSource.id) { @@ -335,6 +362,25 @@ class DataSourceManagerComponent extends React.Component { this.setState({ suggestingDatasources: true, activeDatasourceList: '#' }); }; + setValidationMessages = (errors, schema) => { + const errorMap = errors.reduce((acc, error) => { + // Get property name from either required error or dataPath + const property = + error.keyword === 'required' + ? error.params.missingProperty + : error.dataPath?.replace(/^[./]/, '') || error.instancePath?.replace(/^[./]/, ''); + + if (property) { + const propertySchema = schema.properties?.[property]; + const propertyTitle = propertySchema?.title; + acc[property] = + error.keyword === 'required' ? `${propertyTitle} is required` : `${propertyTitle} ${error.message}`; + } + return acc; + }, {}); + this.setState({ validationMessages: errorMap }); + }; + renderSourceComponent = (kind, isPlugin = false) => { const { options, isSaving } = this.state; @@ -352,6 +398,9 @@ class DataSourceManagerComponent extends React.Component { selectedDataSource={this.state.selectedDataSource} isEditMode={!isEmpty(this.state.selectedDataSource)} currentAppEnvironmentId={this.props.currentEnvironment?.id} + validationMessages={this.state.validationMessages} + setValidationMessages={this.setValidationMessages} + clearValidationMessages={() => this.setState({ validationMessages: {} })} setDefaultOptions={this.setDefaultOptions} /> ); @@ -851,6 +900,7 @@ class DataSourceManagerComponent extends React.Component { dataSourceConfirmModalProps, addingDataSource, datasourceName, + validationError, } = this.state; const isPlugin = dataSourceSchema ? true : false; const createSelectedDataSource = (dataSource) => { @@ -1056,6 +1106,18 @@ class DataSourceManagerComponent extends React.Component {
)} + {validationError && validationError.length > 0 && ( +
+
+ {validationError.map((error, index) => ( +
+ {error} +
+ ))} +
+
+ )} +
Date: Thu, 3 Apr 2025 14:09:20 +0530 Subject: [PATCH 25/29] fix: styling --- .../src/AppBuilder/_stores/slices/eventsSlice.js | 6 +++--- .../src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 8ab0909d0a..9df1f6bbdf 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -1111,7 +1111,7 @@ export const createEventsSlice = (set, get) => ({ const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log-info', - key: `${query.name}${isFromTransformation && ', transformation'}, line ${lineNumber - 2}`, + key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`, description: log, eventType: 'customLog', query, @@ -1127,7 +1127,7 @@ export const createEventsSlice = (set, get) => ({ const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log-error', - key: `${query.name}${isFromTransformation && ', transformation'}, line ${lineNumber - 2}`, + key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`, description: log, eventType: 'customLog', query, @@ -1143,7 +1143,7 @@ export const createEventsSlice = (set, get) => ({ const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown'; const event = { actionId: 'log', - key: `${query.name}${isFromTransformation && ', transformation'}, line ${lineNumber - 2}`, + key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`, description: log, eventType: 'customLog', query, diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx index 5303534ad9..a30585bf42 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx @@ -108,11 +108,15 @@ function Logs({ logProps, idx }) { Custom Log
)} -
+
From 0e09fd7aa9266294d724100890f4b13e30810496 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 3 Apr 2025 14:27:21 +0530 Subject: [PATCH 26/29] fix: error toast on preview --- frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index d2f3bc4dff..10a3b62c22 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -621,7 +621,7 @@ export const createQueryPanelSlice = (set, get) => ({ query, 'edit' ); - if (finalData.status === 'failed') { + if (finalData?.status === 'failed') { onEvent('onDataQueryFailure', queryEvents); setPreviewLoading(false); setIsPreviewQueryLoading(false); From ec6e22613069102fb912ca9d52d3186768cf7acb Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 3 Apr 2025 14:36:07 +0530 Subject: [PATCH 27/29] fix: image alignment --- .../src/AppBuilder/WidgetManager/widgets/image.js | 13 +++++++++++-- frontend/src/Editor/Components/Image/Image.jsx | 1 + frontend/src/Editor/WidgetManager/configs/image.js | 10 ++++++++++ .../modules/apps/services/widget-config/image.js | 13 +++++++++++-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/image.js b/frontend/src/AppBuilder/WidgetManager/widgets/image.js index c4bd7b6147..b962c270a5 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/image.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/image.js @@ -143,6 +143,15 @@ export const imageConfig = { }, accordian: 'Image', }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'center', + }, + accordian: 'Image', + }, backgroundColor: { type: 'color', displayName: 'Background', @@ -179,11 +188,11 @@ export const imageConfig = { padding: { type: 'switch', displayName: 'Padding', - validation: { schema: { type: 'string' }, defaultValue: 'default' }, options: [ { displayName: 'Default', value: 'default' }, { displayName: 'Custom', value: 'custom' }, ], + validation: { schema: { type: 'string' }, defaultValue: 'default' }, accordian: 'Container', isFxNotRequired: true, }, @@ -244,7 +253,6 @@ export const imageConfig = { loadingState: { value: '{{false}}' }, disabledState: { value: '{{false}}' }, visibility: { value: '{{true}}' }, - visible: { value: '{{true}}' }, }, events: [], styles: { @@ -256,6 +264,7 @@ export const imageConfig = { boxShadow: { value: '0px 0px 0px 0px #00000090' }, padding: { value: 'default' }, customPadding: { value: '{{0}}' }, + alignment: { value: 'center' }, }, }, }; diff --git a/frontend/src/Editor/Components/Image/Image.jsx b/frontend/src/Editor/Components/Image/Image.jsx index 03b4daefda..6df6bd047e 100644 --- a/frontend/src/Editor/Components/Image/Image.jsx +++ b/frontend/src/Editor/Components/Image/Image.jsx @@ -177,6 +177,7 @@ export const Image = function Image({ border: '1px solid', borderRadius: imageShape === 'circle' ? '50%' : `${borderRadius}px`, borderColor: borderColor ? borderColor : 'transparent', + objectPosition: alignment, }} height={height} onClick={() => fireEvent('onClick')} diff --git a/frontend/src/Editor/WidgetManager/configs/image.js b/frontend/src/Editor/WidgetManager/configs/image.js index 453d9b4ed6..b962c270a5 100644 --- a/frontend/src/Editor/WidgetManager/configs/image.js +++ b/frontend/src/Editor/WidgetManager/configs/image.js @@ -143,6 +143,15 @@ export const imageConfig = { }, accordian: 'Image', }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'center', + }, + accordian: 'Image', + }, backgroundColor: { type: 'color', displayName: 'Background', @@ -255,6 +264,7 @@ export const imageConfig = { boxShadow: { value: '0px 0px 0px 0px #00000090' }, padding: { value: 'default' }, customPadding: { value: '{{0}}' }, + alignment: { value: 'center' }, }, }, }; diff --git a/server/src/modules/apps/services/widget-config/image.js b/server/src/modules/apps/services/widget-config/image.js index c4bd7b6147..b962c270a5 100644 --- a/server/src/modules/apps/services/widget-config/image.js +++ b/server/src/modules/apps/services/widget-config/image.js @@ -143,6 +143,15 @@ export const imageConfig = { }, accordian: 'Image', }, + alignment: { + type: 'alignButtons', + displayName: 'Alignment', + validation: { + schema: { type: 'string' }, + defaultValue: 'center', + }, + accordian: 'Image', + }, backgroundColor: { type: 'color', displayName: 'Background', @@ -179,11 +188,11 @@ export const imageConfig = { padding: { type: 'switch', displayName: 'Padding', - validation: { schema: { type: 'string' }, defaultValue: 'default' }, options: [ { displayName: 'Default', value: 'default' }, { displayName: 'Custom', value: 'custom' }, ], + validation: { schema: { type: 'string' }, defaultValue: 'default' }, accordian: 'Container', isFxNotRequired: true, }, @@ -244,7 +253,6 @@ export const imageConfig = { loadingState: { value: '{{false}}' }, disabledState: { value: '{{false}}' }, visibility: { value: '{{true}}' }, - visible: { value: '{{true}}' }, }, events: [], styles: { @@ -256,6 +264,7 @@ export const imageConfig = { boxShadow: { value: '0px 0px 0px 0px #00000090' }, padding: { value: 'default' }, customPadding: { value: '{{0}}' }, + alignment: { value: 'center' }, }, }, }; From d71880542d3817dd590313f4f65b660e5b82de24 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 4 Apr 2025 10:30:48 +0530 Subject: [PATCH 28/29] Fix: all modals open together when in view and add parameter modal overflowing. --- .../Components/ParameterDetails.jsx | 12 +++++ .../TooljetDatabase/DropDownSelect.jsx | 42 ++++------------- .../AppBuilder/_hooks/usePopoverObserver.js | 46 +++++++++++++++++++ 3 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 frontend/src/AppBuilder/_hooks/usePopoverObserver.js diff --git a/frontend/src/AppBuilder/QueryManager/Components/ParameterDetails.jsx b/frontend/src/AppBuilder/QueryManager/Components/ParameterDetails.jsx index 6ff93ee9b6..2b9d03dc28 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/ParameterDetails.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/ParameterDetails.jsx @@ -4,6 +4,7 @@ import cx from 'classnames'; import PlusRectangle from '@/_ui/Icon/solidIcons/PlusRectangle'; import Remove from '@/_ui/Icon/bulkIcons/Remove'; import ParameterForm from './ParameterForm'; +import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver'; const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRemove, otherParams }) => { const [showModal, setShowModal] = useState(false); @@ -47,6 +48,17 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe } }; + usePopoverObserver( + document.getElementsByClassName('query-details')[0], + isEdit + ? document.getElementById(`query-param-${String(name).toLowerCase()}`) + : document.getElementById('runjs-param-add-btn'), + document.getElementById('parameter-form-popover'), + showModal, + () => setShowModal(true), + closeMenu + ); + return ( { if (shouldCloseFkMenu) { @@ -131,38 +131,14 @@ const DropDownSelect = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [selected]); - useEffect(() => { - const container = document.getElementsByClassName('query-details')[0]; - const popoverBtn = document.getElementById(popoverBtnId.current); - - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - if (prevShowMenu.current) { - setShowMenu(true); - prevShowMenu.current = false; - } - } else if (showMenu) { - setShowMenu(false); - prevShowMenu.current = true; - } - if (!entry.isIntersecting && showMenu) { - setShowMenu(false); - } - }, - { root: container, threshold: [0.5] } - ); - - if (popoverBtn) { - observer.observe(popoverBtn); - } - - return () => { - if (popoverBtn) { - observer.unobserve(popoverBtn); - } - }; - }, [showMenu]); + usePopoverObserver( + document.getElementsByClassName('query-details')[0], + document.getElementById(popoverBtnId.current), + document.getElementById(popoverId.current), + showMenu, + () => setShowMenu(true), + () => setShowMenu(false) + ); function checkElementPosition() { if (isForeignKeyInEditCell) { diff --git a/frontend/src/AppBuilder/_hooks/usePopoverObserver.js b/frontend/src/AppBuilder/_hooks/usePopoverObserver.js new file mode 100644 index 0000000000..a5c0cda1c4 --- /dev/null +++ b/frontend/src/AppBuilder/_hooks/usePopoverObserver.js @@ -0,0 +1,46 @@ +import { useEffect, useRef } from 'react'; + +function usePopoverObserver(containerRef, triggerRef, popoverRef, show, onShow, onHide, threshold = 0.5) { + const prevShow = useRef(false); + + // Check if it is a ref or a DOM element + const container = containerRef?.current ? containerRef.current : containerRef; + const trigger = triggerRef?.current ? triggerRef.current : triggerRef; + const popover = popoverRef?.current ? popoverRef.current : popoverRef; + + useEffect(() => { + if (!container || !trigger) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + if (prevShow.current) { + onShow(); + prevShow.current = false; + } + } else if (show) { + onHide(); + prevShow.current = true; + } + }, + { root: container, threshold: [threshold] } + ); + + observer.observe(trigger); + + const handleOutsideClick = (event) => { + if (popover && !popover.contains(event.target) && prevShow.current) { + prevShow.current = false; + } + }; + + document.addEventListener('mousedown', handleOutsideClick); + + return () => { + observer.unobserve(trigger); + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, [containerRef, triggerRef, popoverRef, show, onShow, onHide, threshold]); +} + +export default usePopoverObserver; From 94158e4cb8a311278b73a72b119aed1eb00b311c Mon Sep 17 00:00:00 2001 From: Akshay Sasidharan Date: Fri, 4 Apr 2025 17:33:16 +0530 Subject: [PATCH 29/29] bump to v3.8.0 --- .version | 2 +- frontend/.version | 2 +- server/.version | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.version b/.version index 7c69a55dbb..19811903a7 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.7.0 +3.8.0 diff --git a/frontend/.version b/frontend/.version index 7c69a55dbb..19811903a7 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.7.0 +3.8.0 diff --git a/server/.version b/server/.version index 7c69a55dbb..19811903a7 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -3.7.0 +3.8.0