diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx index 75cbe9cef7..013733494f 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx @@ -1,4 +1,4 @@ -import React, { useState, forwardRef, useRef, useEffect } from 'react'; +import React, { useState, forwardRef, useRef, useEffect, useCallback } from 'react'; import RenameIcon from '../Icons/RenameIcon'; import Eye1 from '@/_ui/Icon/solidIcons/Eye1'; import Play from '@/_ui/Icon/solidIcons/Play'; @@ -13,6 +13,7 @@ import { decodeEntities } from '@/_helpers/utils'; import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers'; import useStore from '@/AppBuilder/_stores/store'; import { useModuleId } from '@/AppBuilder/_contexts/ModuleContext'; +import { debounce } from 'lodash'; export const QueryManagerHeader = forwardRef(({ darkMode, setActiveTab, activeTab }, ref) => { const moduleId = useModuleId(); @@ -166,16 +167,17 @@ const NameInput = ({ onInput, value, darkMode, isDiabled, selectedQuery }) => { } }, [isFocused]); + const debouncedHandleInput = useCallback( + debounce((newName) => { + onInput(newName); + }, 300), + [onInput] + ); + const handleChange = (event) => { const sanitizedValue = event.target.value.replace(/[ \t&]/g, ''); setName(sanitizedValue); - }; - - const handleInput = (newName) => { - const result = onInput(newName); - if (!result) { - setName(value); - } + debouncedHandleInput(sanitizedValue); }; return ( @@ -200,12 +202,12 @@ const NameInput = ({ onInput, value, darkMode, isDiabled, selectedQuery }) => { event.persist(); if (event.keyCode === 13) { setIsFocused(false); - handleInput(event.target.value); + debouncedHandleInput(event.target.value); } }} onBlur={({ target }) => { setIsFocused(false); - handleInput(target.value); + debouncedHandleInput(target.value); }} /> ) : ( diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx index ded481fadb..12d4d68102 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx @@ -295,7 +295,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
page.id === pageId)?.handle, Object.entries(queryParams), true); + switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams)); }; var styles = { bmBurgerButton: { diff --git a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx index d913bffd91..a7111a48a0 100644 --- a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx +++ b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx @@ -95,7 +95,7 @@ export const ViewerSidebarNavigation = ({ version: selectedVersionName, env: selectedEnvironmentName, }; - switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams), true); + switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams)); }; const isLicensed = diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx index db8253d5e4..d367419c31 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx +++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx @@ -41,6 +41,15 @@ export const Filter = memo(({ table, darkMode, setFilters, setShowFilter }) => { [columns] ); + const isFilterComplete = (filter) => { + if (!filter.id) return false; + if (!filter.value?.condition) return false; + // For isEmpty/isNotEmpty operations, we don't need a value + if (['isEmpty', 'isNotEmpty'].includes(filter.value.condition)) return true; + // For other operations, we need a value + return filter.value?.value !== undefined && filter.value?.value !== ''; + }; + const filterOperationChanged = (index, value) => { const newFilters = [...localFilters]; newFilters[index].value = { @@ -52,13 +61,12 @@ export const Filter = memo(({ table, darkMode, setFilters, setShowFilter }) => { newFilters[index].value.value = ''; } setLocalFilters(newFilters); - applyFilters(newFilters.filter((filter) => filter.id !== '')); + debouncedFilterChanged(newFilters); }; const debouncedFilterChanged = useCallback( (newFilters) => { - const validFilters = newFilters.filter((filter) => filter.id !== ''); - + const validFilters = newFilters.filter(isFilterComplete); applyFilters(validFilters); }, [applyFilters] diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/TableContainer.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/TableContainer.jsx index b853999911..11a3fbe18f 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/TableContainer.jsx +++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/TableContainer.jsx @@ -109,6 +109,12 @@ export const TableContainer = ({ })); }, [rowsPerPage, setPagination]); + useEffect(() => { + if (serverSideSearch && globalFilter?.trim() !== '') { + setPagination((prev) => ({ ...prev, pageIndex: 0 })); + } + }, [globalFilter, serverSideSearch, setPagination]); + useEffect(() => { setColumnOrder(columns.map((column) => column.id)); }, [columns, setColumnOrder]); diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx index 36d2d84929..56009fd71b 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx +++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx @@ -27,7 +27,7 @@ export const TableRow = ({ return ( { setExposedVariables({ filters: appliedFilters.map((filter) => filter.value) }); - mounted && fireEvent('onFilterChanged'); + if (appliedFilters.length > 0) { + mounted && fireEvent('onFilterChanged'); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [appliedFilters, setExposedVariables, fireEvent]); // Didn't add mounted as it's not a dependency @@ -248,20 +250,30 @@ export const TableExposedVariables = ({ useEffect(() => { function selectRow(key, value) { - const item = tableData.find((item) => item[key] == value); + const index = data.findIndex((item) => item[key] == value); + const item = index !== -1 ? data[index] : null; if (item) { - setRowSelection({ [item.id - 1]: true }); + setRowSelection({ [index]: true }); } + setExposedVariables({ + selectedRow: item, + selectedRowId: isNaN(item?.id) ? String(item?.id) : item?.id, + }); } function deselectRow(key, value) { - const item = tableData.find((item) => item[key] == value); + const index = data.findIndex((item) => item[key] == value); + const item = index !== -1 ? data[index] : null; if (item) { - setRowSelection({ [item.id - 1]: false }); + setRowSelection({ [index]: false }); } + setExposedVariables({ + selectedRow: {}, + selectedRowId: null, + }); } setExposedVariables({ selectRow, deselectRow }); - }, [tableData, setExposedVariables, setRowSelection]); + }, [data, setExposedVariables, setRowSelection]); // CSA to set & clear filters useEffect(() => { diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTable.js b/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTable.js index 012c3d2d73..45d2b4bd11 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTable.js +++ b/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTable.js @@ -7,7 +7,6 @@ import { getFilteredRowModel, } from '@tanstack/react-table'; import { applyFilters } from '../_components/Header/_components/Filter/filterUtils'; -import { v4 as uuidv4 } from 'uuid'; export function useTable({ data, @@ -33,15 +32,7 @@ export function useTable({ // When the columns change, the data is not getting re-rendered. So, we need to create a new data array // eslint-disable-next-line react-hooks/exhaustive-deps - const newData = useMemo( - () => - data.map((row) => ({ - ...row, - uniqueId: row.uniqueId || uuidv4(), // Use existing ID if available - })), - // eslint-disable-next-line react-hooks/exhaustive-deps - [data, columns] - ); + const newData = useMemo(() => [...data], [data, columns]); const table = useReactTable({ data: newData, @@ -53,7 +44,6 @@ export function useTable({ getFilteredRowModel: getFilteredRowModel(), enableColumnResizing: true, columnResizeMode: 'onChange', - getRowId: (row) => row.uniqueId, enableRowSelection: true, enableMultiRowSelection: showBulkSelector, state: { diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index 8f1c350f99..2064790876 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -326,6 +326,16 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v // navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`); } + + // Add page id and handle to the state on initial load + const currentState = window.history.state || {}; + const pageInfo = { + id: startingPage.id, + handle: startingPage.handle, + }; + const newState = { ...currentState, ...pageInfo }; + window.history.replaceState(newState, '', window.location.href); + setCurrentPageHandle(startingPage.handle); updateFeatureAccess(); setCurrentPageId(startingPage.id, moduleId); diff --git a/frontend/src/AppBuilder/_stores/slices/appSlice.js b/frontend/src/AppBuilder/_stores/slices/appSlice.js index 39cc7a4986..2970657ad8 100644 --- a/frontend/src/AppBuilder/_stores/slices/appSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/appSlice.js @@ -94,7 +94,7 @@ export const createAppSlice = (set, get) => ({ console.error('Error updating page:', error); } }, - switchPage: (pageId, handle, queryParams = []) => { + switchPage: (pageId, handle, queryParams = [], isBackOrForward = false) => { get().debugger.resetUnreadErrorCount(); // reset stores if (get().pageSwitchInProgress) { @@ -139,14 +139,19 @@ export const createAppSlice = (set, get) => ({ const queryParamsString = filteredQueryParams.map(([key, value]) => `${key}=${value}`).join('&'); const slug = get().app.slug; - navigate( - `/${isPreview ? 'applications' : getWorkspaceId() + '/apps'}/${slug ?? appId}/${handle}?${queryParamsString}`, - { - state: { - isSwitchingPage: true, - }, - } - ); + if (!isBackOrForward) { + navigate( + `/${isPreview ? 'applications' : getWorkspaceId() + '/apps'}/${slug ?? appId}/${handle}?${queryParamsString}`, + { + state: { + isSwitchingPage: true, + id: pageId, + handle: handle, + }, + } + ); + } + const newPage = pages.find((p) => p.id === pageId); setResolvedPageConstants({ id: newPage?.id, diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js index 7f7868416d..252e387d70 100644 --- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js +++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js @@ -13,6 +13,7 @@ const initialState = { creatingQueryInProcessId: null, queryConfirmationList: [], queuedActions: {}, + queryUpdates: {}, queries: { modules: { canvas: [], @@ -381,8 +382,11 @@ export const createDataQuerySlice = (set, get) => ({ return; } const versionId = get().currentVersionId; - dataqueryService - .update(newValues?.id, versionId, newValues?.name, newValues?.options) + const updatePromise = dataqueryService.update(newValues?.id, versionId, newValues?.name, newValues?.options); + set((state) => { + state.dataQuery.queryUpdates[newValues?.id] = updatePromise; + }); + updatePromise .then((data) => { localStorage.removeItem('transformation'); set((state) => { @@ -401,7 +405,12 @@ export const createDataQuerySlice = (set, get) => ({ state.dataQuery.isUpdatingQueryInProcess = false; }); }) - .finally(() => setIsAppSaving(false)); + .finally(() => { + setIsAppSaving(false); + set((state) => { + delete state.dataQuery.queryUpdates[newValues?.id]; + }); + }); }, 500), runOnLoadQueries: async () => { const queries = get().dataQuery.queries.modules.canvas; diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index a0d40e7404..71e08d01a1 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -206,7 +206,6 @@ export const createQueryPanelSlice = (set, get) => ({ isOnLoad = false, moduleId = 'canvas' ) => { - //! TODO get this using get() when migrated into slice const { eventsSlice, dataQuery: dataQuerySlice, @@ -227,6 +226,28 @@ export const createQueryPanelSlice = (set, get) => ({ executeWorkflow, executeMultilineJS, } = queryPanel; + const queryUpdatePromise = dataQuerySlice.queryUpdates[queryId]; + if (queryUpdatePromise) { + setResolvedQuery(queryId, { + isLoading: true, + }); + return queryUpdatePromise.then(() => + get().queryPanel.runQuery( + queryId, + queryName, + confirmed, + mode, + userSuppliedParameters, + component, + eventId, + shouldSetPreviewData, + isOnLoad, + moduleId + ) + ); + } + //! TODO get this using get() when migrated into slice + const { onEvent } = eventsSlice; const { queryConfirmationList } = dataQuerySlice; @@ -494,6 +515,14 @@ export const createQueryPanelSlice = (set, get) => ({ executeMultilineJS, setIsPreviewQueryLoading, } = queryPanel; + const queryUpdatePromise = get().dataQuery.queryUpdates[query?.id]; + if (queryUpdatePromise) { + setPreviewLoading(true); + setIsPreviewQueryLoading(true); + return queryUpdatePromise.then(() => + get().queryPanel.previewQuery(query, calledFromQuery, userSuppliedParameters, moduleId) + ); + } const { onEvent } = eventsSlice; let parameters = userSuppliedParameters; diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index a01ed895e0..feb3087920 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -15,6 +15,7 @@ import Label from '@/_ui/Label'; import cx from 'classnames'; import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor } from './utils'; import { isMobileDevice } from '@/_helpers/appUtils'; +import useStore from '@/AppBuilder/_stores/store'; const { DropdownIndicator, ClearIndicator } = components; const INDICATOR_CONTAINER_WIDTH = 60; @@ -39,7 +40,7 @@ export const CustomDropdownIndicator = (props) => { export const CustomClearIndicator = (props) => { return ( - + ); }; @@ -88,6 +89,7 @@ export const DropdownV2 = ({ padding, } = styles; const isInitialRender = useRef(true); + const [isMenuOpen, setIsMenuOpen] = useState(false); const [currentValue, setCurrentValue] = useState(() => findDefaultItem(schema)); const isMandatory = validation?.mandatory ?? false; const options = properties?.options; @@ -95,11 +97,14 @@ export const DropdownV2 = ({ const { isValid, validationError } = validationStatus; const ref = React.useRef(null); const dropdownRef = React.useRef(null); + const selectRef = React.useRef(null); const [visibility, setVisibility] = useState(properties.visibility); const [isDropdownLoading, setIsDropdownLoading] = useState(dropdownLoadingState); const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState); const [searchInputValue, setSearchInputValue] = useState(''); const [userInteracted, setUserInteracted] = useState(false); + const currentMode = useStore((state) => state.currentMode); + const isEditor = currentMode === 'edit'; const _height = padding === 'default' ? `${height}px` : `${height + 4}px`; const labelRef = useRef(); @@ -166,6 +171,12 @@ export const DropdownV2 = ({ setExposedVariable('isValid', validationStatus?.isValid); }; + const handleClickInEditor = (e) => { + if (e.target.className.includes('clear-indicator') || isMenuOpen) return; + e.stopPropagation(); + selectRef.current?.onControlMouseDown(e); + }; + useEffect(() => { setInputValue(findDefaultItem(advanced ? schema : options)); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -353,15 +364,16 @@ export const DropdownV2 = ({ ...provided, padding: '0px', }), - option: (provided) => ({ + option: (provided, _state) => ({ ...provided, - backgroundColor: 'var(--surfaces-surface-01)', + backgroundColor: _state.isFocused ? 'var(--interactive-overlays-fill-hover)' : 'var(--surfaces-surface-01)', color: selectedTextColor !== '#1B1F24' ? selectedTextColor : isDropdownDisabled || isDropdownLoading ? 'var(--text-disabled)' : 'var(--text-primary)', + borderRadius: _state.isFocused && '8px', padding: '8px 6px 8px 38px', '&:hover': { backgroundColor: 'var(--interactive-overlays-fill-hover)', @@ -429,8 +441,14 @@ export const DropdownV2 = ({ _width={_width} top={'1px'} /> -
+
{ - fireEvent('onFocus'); setIsMultiselectOpen(true); + fireEvent('onFocus'); + }} + onMenuClose={() => { + setIsMultiselectOpen(false); + fireEvent('onBlur'); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' && !isMultiselectOpen) { + setIsMultiselectOpen(true); + e.preventDefault(); + } + if (e.key === 'Escape' && isMultiselectOpen) { + setIsMultiselectOpen(false); + e.preventDefault(); + } }} // select props icon={icon} diff --git a/frontend/src/Routes/AppsRoute.jsx b/frontend/src/Routes/AppsRoute.jsx index 7ed530162d..b84a2b04ff 100644 --- a/frontend/src/Routes/AppsRoute.jsx +++ b/frontend/src/Routes/AppsRoute.jsx @@ -8,6 +8,7 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { handleAppAccess } from '@/_helpers/handleAppAccess'; import { getQueryParams } from '@/_helpers/routes'; import queryString from 'query-string'; +import useStore from '@/AppBuilder/_stores/store'; export const AppsRoute = ({ children, componentType }) => { const params = useParams(); @@ -20,6 +21,7 @@ export const AppsRoute = ({ children, componentType }) => { }); const clonedElement = React.cloneElement(children, extraProps); const navigate = useNavigate(); + const switchPage = useStore((state) => state.switchPage); /* any extra logic specifc to the route can be done @@ -29,6 +31,10 @@ export const AppsRoute = ({ children, componentType }) => { if (isValidSession) { onValidSession(); } + + // handle back and forward navigation + window.addEventListener('popstate', handleBrowserNavigation); + return () => window.removeEventListener('popstate', handleBrowserNavigation); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isValidSession]); @@ -69,5 +75,10 @@ export const AppsRoute = ({ children, componentType }) => { } }; + const handleBrowserNavigation = (e) => { + const { id, handle } = e.state; + switchPage(id, handle, [], true); + }; + return {clonedElement}; };