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'}
/>
-
+
diff --git a/frontend/src/Editor/Components/MultiselectV2/CustomValueContainer.jsx b/frontend/src/Editor/Components/MultiselectV2/CustomValueContainer.jsx
index 2e6078d5c6..2901abc106 100644
--- a/frontend/src/Editor/Components/MultiselectV2/CustomValueContainer.jsx
+++ b/frontend/src/Editor/Components/MultiselectV2/CustomValueContainer.jsx
@@ -4,7 +4,7 @@ import * as Icons from '@tabler/icons-react';
const { ValueContainer, Placeholder } = components;
import './multiselectV2.scss';
-const CustomValueContainer = ({ ...props }) => {
+const CustomValueContainer = ({ children, ...props }) => {
const selectProps = props.selectProps;
const values = Array.isArray(selectProps?.value) && selectProps?.value?.map((option) => option.label);
const isAllOptionsSelected = selectProps?.value.length === selectProps.options.length;
@@ -39,6 +39,13 @@ const CustomValueContainer = ({ ...props }) => {
{isAllOptionsSelected ? 'All items are selected.' : values.join(', ')}
)}
+ {/* Rendering children except Placeholder component to preserve the default behavior of react-select like focus
+ handling */}
+ {React.Children.map(children, (child) => {
+ if (child.type !== Placeholder) {
+ return child;
+ }
+ })}
diff --git a/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx b/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx
index 6ef45e84d9..4665430d00 100644
--- a/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx
+++ b/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx
@@ -12,6 +12,7 @@ import Label from '@/_ui/Label';
const tinycolor = require('tinycolor2');
import { CustomDropdownIndicator, CustomClearIndicator } from '../DropdownV2/DropdownV2';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor } from '../DropdownV2/utils';
+import useStore from '@/AppBuilder/_stores/store';
export const MultiselectV2 = ({
id,
@@ -62,6 +63,7 @@ export const MultiselectV2 = ({
const isMandatory = validation?.mandatory ?? false;
const multiselectRef = React.useRef(null);
const labelRef = React.useRef(null);
+ const selectRef = React.useRef(null);
const [validationStatus, setValidationStatus] = useState(
validate(selected?.length ? selected?.map((option) => option.value) : null)
);
@@ -74,6 +76,8 @@ export const MultiselectV2 = ({
const [searchInputValue, setSearchInputValue] = useState('');
const _height = padding === 'default' ? `${height}px` : `${height + 4}px`;
const [userInteracted, setUserInteracted] = useState(false);
+ const currentMode = useStore((state) => state.currentMode);
+ const isEditor = currentMode === 'edit';
const [isMultiselectOpen, setIsMultiselectOpen] = useState(false);
useEffect(() => {
@@ -281,6 +285,12 @@ export const MultiselectV2 = ({
}
};
+ const handleClickInEditor = (e) => {
+ if (e.target.className.includes('clear-indicator') || isMultiselectOpen) return;
+ e.stopPropagation();
+ selectRef.current?.onControlMouseDown(e);
+ };
+
const setInputValue = (values) => {
setSelected(values);
setExposedVariables({
@@ -386,7 +396,7 @@ export const MultiselectV2 = ({
}),
option: (provided, _state) => ({
...provided,
- backgroundColor: 'var(--surfaces-surface-01)',
+ backgroundColor: _state.isFocused ? 'var(--interactive-overlays-fill-hover)' : 'var(--surfaces-surface-01)',
color: _state.isDisabled
? 'var(_--text-disbled)'
: selectedTextColor !== '#1B1F24'
@@ -394,6 +404,7 @@ export const MultiselectV2 = ({
: isMultiSelectDisabled || isMultiSelectLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
+ borderRadius: _state.isFocused && '8px',
padding: '8px 6px 8px 12px',
'&:hover': {
backgroundColor: 'var(--interactive-overlays-fill-hover)',
@@ -456,16 +467,9 @@ export const MultiselectV2 = ({
_width={_width}
top={'1px'}
/>
- {
- if (!isMultiSelectDisabled) {
- fireEvent('onFocus');
- setIsMultiselectOpen(!isMultiselectOpen);
- }
- }}
- >
+