diff --git a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx index b9fb85fcae..099cd3763a 100644 --- a/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx +++ b/frontend/src/AppBuilder/CodeBuilder/Elements/TableRowHeightInput.jsx @@ -5,19 +5,13 @@ const MIN_TABLE_ROW_HEIGHT_DEFAULT = 45; const TableRowHeightInput = ({ value, onChange, cyLabel, staticText, styleDefinition }) => { const [inputValue, setInputValue] = useState(value); + const minValue = + styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT; + useEffect(() => { setInputValue(value < minValue ? minValue : value); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, styleDefinition.cellSize?.value]); - useEffect(() => { - onChange( - styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const minValue = - styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT; const handleBlur = () => { const newValue = Math.max(inputValue, minValue); diff --git a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx index 4edd8a0fda..625b11dedd 100644 --- a/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx +++ b/frontend/src/AppBuilder/CodeEditor/TJDBHinter.jsx @@ -150,7 +150,7 @@ const TJDBCodeEditor = (props) => { className="cm-codehinter position-relative" style={{ width: '100%', - height: isOpen ? '350p' : 'auto', + height: isOpen ? '350px' : 'auto', }} >
@@ -167,14 +167,14 @@ const TJDBCodeEditor = (props) => { componentName={componentName} key={componentName} forceUpdate={forceUpdate} - optionalProps={{ styles: { height: 300 }, cls: '' }} + optionalProps={{ styles: { height: 300 }, cls: 'tjdb-hinter-portal' }} darkMode={darkMode} selectors={{ className: 'preview-block-portal tjdb-portal-codehinter' }} dragResizePortal={true} callgpt={null} > -
+
{ // eslint-disable-next-line react-hooks/exhaustive-deps }, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]); - const handleNodeExpansion = (path) => { + const handleNodeExpansion = (path, data, currentNode) => { if (pathToBeInspected && path?.length > 0) { - return pathToBeInspected.includes(path[path.length - 1]); + const shouldExpand = pathToBeInspected.includes(path[path.length - 1]); + + // Scroll to the component in the inspector + if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) { + const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + + return shouldExpand; } else return false; }; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js index 7067cd540d..937fe45b2c 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js @@ -9,6 +9,7 @@ const useCallbackActions = () => { const currentPageComponents = useStore((state) => state?.getCurrentPageComponents(), shallow); const shouldFreeze = useStore((state) => state.getShouldFreeze()); const runQuery = useStore((state) => state.queryPanel.runQuery); + const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll); const handleRemoveComponent = (component) => { deleteComponents([component.id]); @@ -30,30 +31,22 @@ const useCallbackActions = () => { return toast.success('Copied to the clipboard', { position: 'top-center' }); }; - const autoScrollTo = (id) => { - setSelectedComponents([id]); - const target = document.getElementById(id); - target.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }; - const handleAutoScrollToComponent = (data) => { - const currentPageComponents = useStore.getState().getCurrentPageComponents(); - const component = currentPageComponents?.[data.id]; - - let parentId = component?.component?.parent; - if (parentId) { - const regex = /-\d+$/; - if (regex.test(parentId)) { - parentId = parentId.replace(regex, ''); // To get parentId without tab index if parent type is Tab - } - const parentType = currentPageComponents?.[parentId]?.component?.component; - if (parentType && (parentType === 'Modal' || parentType === 'Tabs')) { - autoScrollTo(parentId); // To scroll to parent component if parent type is Modal or Tabs - return; - } + const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(data.id); + if (!isAccessible) { + if (isOnCanvas) { + toast.success( + `This component can't be opened because it's on the main canvas. Close ${computedComponentId} and click "Go to component" to view it there` + ); + } else + toast.success( + `This component can't be opened because it's inside ${computedComponentId}. Open ${computedComponentId} and click "Go to component"to view it.` + ); + return; } - - autoScrollTo(data.id); + setSelectedComponents([computedComponentId]); + const target = document.getElementById(computedComponentId); + target.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }; const callbackActions = [ diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/BaseUrl.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/BaseUrl.jsx index 858f6b3aa6..43a6b672df 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/BaseUrl.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/BaseUrl.jsx @@ -1,21 +1,31 @@ import React from 'react'; +import OverflowTooltip from '@/_components/OverflowTooltip'; -export const BaseUrl = ({ dataSourceURL, theme }) => { +export const BaseUrl = ({ dataSourceURL, theme, className = 'col-auto', style = {} }) => { return ( - {dataSourceURL} + + {dataSourceURL} + ); }; diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx index feaeadcb23..4c89ad849e 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx @@ -28,7 +28,10 @@ class Restapi extends React.Component { this.state = { options, + codeHinterHeight: 32, // Default height }; + this.codeHinterRef = React.createRef(); + this.resizeObserver = null; } componentDidUpdate(prevProps) { @@ -40,21 +43,95 @@ class Restapi extends React.Component { }, }); } + // Setup resize observer if it's not already set up + if (this.codeHinterRef.current && !this.resizeObserver) { + this.setupResizeObserver(); + } } componentDidMount() { try { + if (isEmpty(this.state.options['headers'])) { + this.addNewKeyValuePair('headers'); + } + if (isEmpty(this.state.options['cookies'])) { + this.addNewKeyValuePair('cookies'); + } if (isEmpty(this.state.options['method'])) { changeOption(this, 'method', 'get'); } + setTimeout(() => { + if (isEmpty(this.state.options['url_params'])) { + this.addNewKeyValuePair('url_params'); + } + }, 1000); + setTimeout(() => { + if (isEmpty(this.state.options['body'])) { + this.addNewKeyValuePair('body'); + } + }, 1000); setTimeout(() => { this.initizalizeRetryNetworkErrorsToggle(); }, 1000); + + this.setupResizeObserver(); } catch (error) { console.log(error); } } + componentWillUnmount() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + } + + setupResizeObserver() { + if (!this.codeHinterRef.current) return; + + // Try to find the editor element, checking multiple possible selectors + const findEditorElement = () => { + const element = + this.codeHinterRef.current.querySelector('.cm-editor') || + this.codeHinterRef.current.querySelector('.codehinter-input') || + this.codeHinterRef.current.querySelector('.code-hinter-wrapper'); + return element; + }; + + // Initial attempt to find editor + let editorElement = findEditorElement(); + + // If not found immediately, try again after a short delay + if (!editorElement) { + setTimeout(() => { + editorElement = findEditorElement(); + if (editorElement) { + this.setupObserverForElement(editorElement); + } + }, 100); + return; + } + + this.setupObserverForElement(editorElement); + } + + setupObserverForElement(element) { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + + this.resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + const height = Math.max(32, Math.min(entry.contentRect.height, 220)); + if (height !== this.state.codeHinterHeight) { + this.setState({ codeHinterHeight: height }); + } + } + }); + + this.resizeObserver.observe(element); + } + initizalizeRetryNetworkErrorsToggle = () => { const isRetryNetworkErrorToggleUnused = this.props.options.retry_network_errors === null; if (isRetryNetworkErrorToggleUnused) { @@ -212,13 +289,30 @@ class Restapi extends React.Component { useCustomStyles={true} />
-
+
URL
-
+
{dataSourceURL && ( - + )} -
+
{ > {
{ diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss index 95d2ab56e0..23d1c7f7cf 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss @@ -214,4 +214,9 @@ .input-value-padding { box-sizing: border-box; padding-right: 30px !important; +} + +.react-datepicker__navigation{ + overflow: visible !important; + height: inherit !important; } \ No newline at end of file diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx index e3f31c974d..e873228888 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations.jsx @@ -677,6 +677,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay }} componentName="TooljetDatabase" delayOnChange={false} + className="w-100" />
)} diff --git a/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx b/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx index 34efc57221..2621708532 100644 --- a/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx +++ b/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx @@ -56,6 +56,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { const [containers, setContainers] = useState([]); const [showModal, setShowModal] = useState(false); + const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas); const [activeId, setActiveId] = useState(null); const cardMovementRef = useRef(null); const shouldUpdateData = useRef(false); @@ -117,6 +118,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { } /**** End - Logic to reduce the zIndex of modal control box ****/ } + setModalOpenOnCanvas(`${id}-modal`, showModal); }, [showModal]); useEffect(() => { diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx index 5543ce4ee0..e0f099205f 100644 --- a/frontend/src/AppBuilder/Widgets/Modal.jsx +++ b/frontend/src/AppBuilder/Widgets/Modal.jsx @@ -49,6 +49,7 @@ export const Modal = function Modal({ const size = properties.size ?? 'lg'; const [modalWidth, setModalWidth] = useState(); const mode = useStore((state) => state.currentMode, shallow); + const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas); /**** Start - Logic to reset the zIndex of modal control box ****/ useEffect(() => { @@ -63,6 +64,7 @@ export const Modal = function Modal({ useGridStore.getState().actions.setOpenModalWidgetId(null); } } + setModalOpenOnCanvas(id, showModal); }, [showModal, id, mode]); /**** End - Logic to reset the zIndex of modal control box ****/ diff --git a/frontend/src/AppBuilder/Widgets/Table/Datepicker.jsx b/frontend/src/AppBuilder/Widgets/Table/Datepicker.jsx index 4e5a481dd2..930021c994 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Datepicker.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Datepicker.jsx @@ -263,6 +263,9 @@ export const Datepicker = function Datepicker({ } setIsDateInputFocussed(false); }} + closeOnScroll={(e) => { + return e.target.className === 'table-responsive jet-data-table false false'; + }} />
); diff --git a/frontend/src/AppBuilder/Widgets/Table/Table.jsx b/frontend/src/AppBuilder/Widgets/Table/Table.jsx index 68b1609748..7d378e7d70 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Table.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Table.jsx @@ -1114,10 +1114,9 @@ export const Table = React.memo(
{items.map((virtualRow) => { diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index 14971be9d7..c4448a3bbf 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -44,6 +44,7 @@ const initialState = { currentPageHandle: null, showWidgetDeleteConfirmation: false, focusedParentId: null, + modalsOpenOnCanvas: [], }; export const createComponentsSlice = (set, get) => ({ @@ -1867,4 +1868,17 @@ export const createComponentsSlice = (set, get) => ({ const currentPage = getCurrentPage(moduleId); return currentPage?.autoComputeLayout; }, + setModalOpenOnCanvas: (modalId, isOpen) => { + const { modalsOpenOnCanvas } = get(); + let newModalOpenOnCanvas = []; + + if (isOpen) { + newModalOpenOnCanvas = [...modalsOpenOnCanvas, modalId]; + } else { + newModalOpenOnCanvas = modalsOpenOnCanvas.filter((id) => id !== modalId); + } + set((state) => { + state.modalsOpenOnCanvas = newModalOpenOnCanvas; + }); + }, }); diff --git a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js index 98decac629..367ca4cf0c 100644 --- a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js @@ -37,4 +37,85 @@ export const createLeftSideBarSlice = (set, get) => ({ toggleLeftSidebar(true); } }, + getComponentIdToAutoScroll: (componentId) => { + const { getCurrentPageComponents, getAllExposedValues, modalsOpenOnCanvas } = get(); + const currentPageComponents = getCurrentPageComponents(); + + let targetComponentId = componentId; + let current = componentId; + const visited = new Set(); + let isInsideOpenModal = false; + + // Bubble up to the outermost parent to find the target component + // eslint-disable-next-line no-constant-condition + while (true) { + if (visited.has(current)) break; + visited.add(current); + + const parentId = currentPageComponents?.[current]?.component?.parent; + if (!parentId) break; + + let isComponentVisibleInParent = true; + let nextPossibleCandidate = parentId; + + // If the component exists inside a tab component + const regForTabs = /-(?!\d{12}$)\d+$/; // Parent id for tabs follow the format 'id-index' and index is not UUIDv4 id segment + if (regForTabs.test(parentId)) { + const reg = /-(\d+)$/; + const tabIndex = Number(parentId.match(reg)[1]); // Tab index inside which the component exists + + const tabId = parentId.replace(regForTabs, ''); // Extract tab id from parent id + + const { currentTab } = getAllExposedValues().components?.[tabId] || {}; + const activeTabIndex = Number(currentTab); + + nextPossibleCandidate = tabId; + if (tabIndex !== activeTabIndex) { + isComponentVisibleInParent = false; + } + } + + // If the component exists inside a modal component + if (currentPageComponents?.[parentId]?.component?.component === 'Modal') { + nextPossibleCandidate = parentId; + if (!modalsOpenOnCanvas.includes(parentId)) { + isComponentVisibleInParent = false; + } + } + + // If the component exists inside the kanban component's modal + if (parentId.endsWith('-modal')) { + nextPossibleCandidate = parentId.replace(/-modal$/, ''); // Extract kanban id from parent id + if (!modalsOpenOnCanvas.includes(parentId)) { + isComponentVisibleInParent = false; + } + } + + // If the open modal contains the component + if (modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] === parentId) { + isInsideOpenModal = true; + } + + if (!isComponentVisibleInParent) { + targetComponentId = nextPossibleCandidate; + } + current = nextPossibleCandidate; + } + + if (modalsOpenOnCanvas.length > 0 && !isInsideOpenModal) { + const targetId = visited.size === 1 ? modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] : current; + const componentName = currentPageComponents?.[targetId]?.component?.name; + + return { + isAccessible: false, + computedComponentId: componentName, + isOnCanvas: visited.size === 1, + }; + } + + return { + isAccessible: true, + computedComponentId: targetComponentId, + }; + }, }); diff --git a/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx b/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx index 5bfe9818e5..4d4b7a2e48 100644 --- a/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx +++ b/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx @@ -42,7 +42,7 @@ export const CustomComponent = (props) => { setCustomProps({ ...customPropRef.current, ...e.data.updatedObj }); } else if (e.data.message === 'RUN_QUERY') { const options = { - parameters: e.data.parameters, + parameters: JSON.parse(e.data.parameters), queryName: e.data.queryName, }; onEvent('onTrigger', [], options); diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index 72e2b39664..a01ed895e0 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -436,6 +436,7 @@ export const DropdownV2 = ({ onChange={(selectedOption, actionProps) => { if (actionProps.action === 'clear') { setInputValue(null); + fireEvent('onSelect'); } if (actionProps.action === 'select-option') { setInputValue(selectedOption.value); diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx index 66548cbef8..aba2ca6af1 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx @@ -5,6 +5,7 @@ import JSONTreeViewer from '@/_ui/JSONTreeViewer'; import cx from 'classnames'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import useStore from '@/AppBuilder/_stores/store'; +import { toast } from 'react-hot-toast'; function Logs({ logProps, idx }) { const [open, setOpen] = React.useState(false); @@ -52,10 +53,19 @@ function Logs({ logProps, idx }) { } }; + const copyToClipboard = (data) => { + const stringified = JSON.stringify(data, null, 2).replace(/\\/g, ''); + navigator.clipboard.writeText(stringified); + return toast.success('Value copied to clipboard', { position: 'top-center' }); + }; + const callbackActions = [ { for: 'all', - actions: [{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }], + actions: [ + { name: 'Copy value', dispatchAction: copyToClipboard, icon: false }, + { name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }, + ], enableForAllChildren: true, enableFor1stLevelChildren: true, }, diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx index 40818a3bb9..7a4a0b0bce 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx @@ -163,7 +163,7 @@ export const DateTimePicker = ({
Save Changes
-
+
Esc
Discard Changes
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss index 95d2ab56e0..55d0e7f3ed 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss @@ -214,4 +214,24 @@ .input-value-padding { box-sizing: border-box; padding-right: 30px !important; +} + +.datepicker-widget.theme-tjdb{ + .react-datepicker__navigation{ + overflow: visible !important; + height: inherit !important; + } +} + +.esc-btn-datepicker{ + height: 18px ; + align-items: center; +} + +.tjdb-td-wrapper{ + .react-datepicker-time__input{ + input{ + line-height: normal !important; + } + } } \ No newline at end of file diff --git a/frontend/src/TooljetDatabase/Filter/index.jsx b/frontend/src/TooljetDatabase/Filter/index.jsx index 84b0110fe4..6c8f7c811d 100644 --- a/frontend/src/TooljetDatabase/Filter/index.jsx +++ b/frontend/src/TooljetDatabase/Filter/index.jsx @@ -251,11 +251,7 @@ const Filter = ({ } />
- {filterCount > 0 ? ( - {pluralize(validFilterCountRef.current, 'filter')} - ) : ( -
  Filter
- )} + {filterCount > 0 ? {pluralize(filterCount, 'filter')} :
  Filter
}
{/* {areFiltersApplied && ( ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')} diff --git a/frontend/src/_components/OverflowTooltip.jsx b/frontend/src/_components/OverflowTooltip.jsx index 297f17d236..1523ae449a 100644 --- a/frontend/src/_components/OverflowTooltip.jsx +++ b/frontend/src/_components/OverflowTooltip.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { ToolTip } from '@/_components'; -export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', ...rest }) { +export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', placement = 'bottom', ...rest }) { const [isOverflowed, setIsOverflow] = useState(false); const textElementRef = useRef(); @@ -17,7 +17,7 @@ export default function OverflowTooltip({ children, className, whiteSpace = 'now className={className} delay={{ show: '0', hide: '0' }} tooltipClassName="overflow-tooltip" - placement="bottom" + placement={placement} message={children} show={isOverflowed} width={rest?.width} diff --git a/frontend/src/_services/custom_styles.service.js b/frontend/src/_services/custom_styles.service.js index d3c98d6382..73de321466 100644 --- a/frontend/src/_services/custom_styles.service.js +++ b/frontend/src/_services/custom_styles.service.js @@ -3,13 +3,13 @@ import { authHeader, handleResponse, handleResponseWithoutValidation } from '@/_ function save(body) { const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) }; - return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleResponse); + return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleResponse); } function get(validateResponse = true) { const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' }; const handleOutput = validateResponse ? handleResponse : handleResponseWithoutValidation; - return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleOutput); + return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleOutput); } function getForAppViewerEditor(validateResponse = true) { diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss index 8a9eaf972a..7b256c3812 100644 --- a/frontend/src/_styles/queryManager.scss +++ b/frontend/src/_styles/queryManager.scss @@ -1250,6 +1250,11 @@ $border-radius: 4px; color: var(--slate12) !important; } } + &.data-source-exists { + .cm-editor { + border-radius: 0 4px 4px 0 !important; + } + } } .rest-api-methods-select-element-container { diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx index 65a4ece91d..b13e88d767 100644 --- a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx +++ b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx @@ -53,7 +53,7 @@ export const JSONNode = ({ data, ...restProps }) => { React.useEffect(() => { if (typeof shouldExpandNode === 'function') { - set(shouldExpandNode(path, data)); + set(shouldExpandNode(path, data, currentNode)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathToBeInspected]); @@ -268,7 +268,15 @@ export const JSONNode = ({ data, ...restProps }) => { }; return ( -
+
{enableCopyToClipboard && ( { 'group-object-container': shouldDisplayIntendedBlock, 'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array', })} + id={`inspector-node-${String(currentNode).toLowerCase()}`} data-cy={`inspector-node-${String(currentNode).toLowerCase()}`} > {$NODEIcon &&
{$NODEIcon}
} diff --git a/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx b/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx index f157f4744b..e2a4360943 100644 --- a/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx +++ b/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx @@ -9,6 +9,7 @@ import { utils } from '@/modules/common/helpers'; import { getSubpath } from '@/_helpers/routes'; import { TJLoader } from '@/_ui/TJLoader/TJLoader'; import useOnboardingStore from '@/modules/common/helpers/onboardingStoreHelper'; +import useInvitationsStore from '@/modules/common/helpers/invitationStoreHelper'; const PostOnboardingComponent = () => ; export const InvitationPage = (darkMode = false) => { @@ -24,7 +25,7 @@ export const InvitationPage = (darkMode = false) => { const source = searchParams.get('source'); const redirectTo = searchParams.get('redirectTo'); - const { initiateInvitedUserOnboarding } = invitationsStore(); + const { initiateInvitedUserOnboarding } = useInvitationsStore(); const { resumeSignupOnboarding, isOnboardingStepsCompleted } = useOnboardingStore(); useEffect(() => { // getUserDetails(); diff --git a/server/src/modules/app/module.ts b/server/src/modules/app/module.ts index 1f3939cd32..c0ca97e4be 100644 --- a/server/src/modules/app/module.ts +++ b/server/src/modules/app/module.ts @@ -40,6 +40,7 @@ import { ImportExportResourcesModule } from '@modules/import-export-resources/mo import { TooljetDbModule } from '@modules/tooljet-db/module'; import { WorkflowsModule } from '@modules/workflows/module'; import { AiModule } from '@modules/ai/module'; +import { CustomStylesModule } from '@modules/custom-styles/module'; export class AppModule implements OnModuleInit { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -92,6 +93,7 @@ export class AppModule implements OnModuleInit { await TooljetDbModule.register(configs), await WorkflowsModule.register(configs), await AiModule.register(configs), + await CustomStylesModule.register(configs), ]; return { diff --git a/server/src/modules/apps/ability/index.ts b/server/src/modules/apps/ability/index.ts index 4200a1c463..d53309202f 100644 --- a/server/src/modules/apps/ability/index.ts +++ b/server/src/modules/apps/ability/index.ts @@ -15,7 +15,13 @@ export class FeatureAbilityFactory extends AbilityFactory return App; } - protected defineAbilityFor(can: AbilityBuilder['can'], UserAllPermissions: UserAllPermissions): void { + protected defineAbilityFor( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + extractedMetadata: { moduleName: string; features: string[] }, + request?: any + ): void { + const appId = request?.tj_resource_id; const { superAdmin, isAdmin, userPermission } = UserAllPermissions; const userAppPermissions = userPermission?.[MODULES.APP]; @@ -51,7 +57,10 @@ export class FeatureAbilityFactory extends AbilityFactory can(FEATURE_KEY.CREATE, App); } - if (isAllAppsEditable) { + if ( + isAllAppsEditable || + (userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId)) + ) { can( [ FEATURE_KEY.UPDATE, @@ -70,35 +79,14 @@ export class FeatureAbilityFactory extends AbilityFactory can(FEATURE_KEY.DELETE, App); } return; - } else if (userAppPermissions?.editableAppsId?.length) { - can( - [ - FEATURE_KEY.DELETE, - FEATURE_KEY.UPDATE_ICON, - FEATURE_KEY.GET_ONE, - FEATURE_KEY.GET_BY_SLUG, - FEATURE_KEY.RELEASE, - FEATURE_KEY.VALIDATE_PRIVATE_APP_ACCESS, - FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS, - FEATURE_KEY.UPDATE, - FEATURE_KEY.GET_ASSOCIATED_TABLES, - ], - App, - { id: { $in: userAppPermissions.editableAppsId } } - ); - if (isAllAppsDeletable) { - // Gives delete permission only for editable apps - can(FEATURE_KEY.DELETE, App, { id: { $in: userAppPermissions.editableAppsId } }); - } } - if (isAllAppsViewable) { - // add view permissions for all apps + if ( + isAllAppsViewable || + (userAppPermissions?.viewableAppsId?.length && appId && userAppPermissions.viewableAppsId.includes(appId)) + ) { + // add view permissions for all apps or specific app can([FEATURE_KEY.GET_ONE, FEATURE_KEY.GET_BY_SLUG, FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS], App); - } else if (userAppPermissions?.viewableAppsId?.length) { - can([FEATURE_KEY.GET_ONE, FEATURE_KEY.GET_BY_SLUG, FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS], App, { - id: { $in: userAppPermissions.viewableAppsId }, - }); } } } diff --git a/server/src/modules/custom-styles/module.ts b/server/src/modules/custom-styles/module.ts index 9ff9dc290b..20608341f2 100644 --- a/server/src/modules/custom-styles/module.ts +++ b/server/src/modules/custom-styles/module.ts @@ -1,11 +1,21 @@ -import { Module } from '@nestjs/common'; -import { CustomStylesController } from '@modules/custom-styles/controller'; -import { CustomStylesService } from '@modules/custom-styles/service'; +import { DynamicModule } from '@nestjs/common'; +import { getImportPath } from '@modules/app/constants'; +import { OrganizationsModule } from '@modules/organizations/module'; +import { FeatureAbilityFactory } from './ability'; +import { OrganizationRepository } from '@modules/organizations/repository'; +import { AppsRepository } from '@modules/apps/repository'; -@Module({ - imports: [], - providers: [CustomStylesService], - controllers: [CustomStylesController], - exports: [], -}) -export class CustomStylesModule {} +export class CustomStylesModule { + static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { + const importPath = await getImportPath(configs?.IS_GET_CONTEXT); + const { CustomStylesController } = await import(`${importPath}/custom-styles/controller`); + const { CustomStylesService } = await import(`${importPath}/custom-styles/service`); + return { + module: CustomStylesModule, + imports: [await OrganizationsModule.register(configs)], + providers: [CustomStylesService, FeatureAbilityFactory, OrganizationRepository, AppsRepository], + controllers: [CustomStylesController], + exports: [], + }; + } +} diff --git a/server/src/modules/organization-themes/constants/feature.ts b/server/src/modules/organization-themes/constants/feature.ts index 0fa5e3dfd3..25b8274fa4 100644 --- a/server/src/modules/organization-themes/constants/feature.ts +++ b/server/src/modules/organization-themes/constants/feature.ts @@ -7,7 +7,7 @@ export const FEATURES: FeaturesConfig = { [MODULES.ORGANIZATION_THEMES]: { [FEATURE_KEY.THEMES_CREATE]: { license: LICENSE_FIELD.CUSTOM_THEMES }, [FEATURE_KEY.THEMES_DELETE]: { license: LICENSE_FIELD.CUSTOM_THEMES }, - [FEATURE_KEY.THEMES_GET_ALL]: { license: LICENSE_FIELD.CUSTOM_THEMES }, + [FEATURE_KEY.THEMES_GET_ALL]: {}, [FEATURE_KEY.THEMES_UPDATE_DEFAULT]: { license: LICENSE_FIELD.CUSTOM_THEMES }, [FEATURE_KEY.THEMES_UPDATE_DEFINITION]: { license: LICENSE_FIELD.CUSTOM_THEMES }, [FEATURE_KEY.THEMES_UPDATE_NAME]: { license: LICENSE_FIELD.CUSTOM_THEMES }, diff --git a/server/src/modules/users/repository.ts b/server/src/modules/users/repository.ts index 2771eb5d9b..0a4f77902a 100644 --- a/server/src/modules/users/repository.ts +++ b/server/src/modules/users/repository.ts @@ -65,6 +65,7 @@ export class UserRepository extends Repository { organizationId: true, organization: { name: true, + status: true, }, }, },