mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Fix : Bugs from aggregate feature (#10239)
* Bug fix: if option is undefined select dropdown is breaking application * Hide Select section when one aggregate is added * Deleting single aggregate will delete all the available group by values * Wrapped showSelectSelection withing useCallback to avoid unnecessary re-renders * Improved code readability of constructAggregateValue function * Change the name of variable to columnList * Updated the requested changes * Removed duplicate condition * Updated requested change * Fixed : In Join operation, unable to select columns in aggregate condition * Showing tooltip for truncated table name in group by section * Disable the group by condition and and show tooltip if tableName is falsy value * Fixed width issue of table name in group by section Do not show dropdown if joining table is empty for the same * show delete confirmation modal, when we are deleting last filled value and all other are half filled or empty, then discard all group by conditions * fix : deleting aggregate with confirmation always deletes last avaialable aggregate whether its filled or empty * Fix: editing aggregate change was not consistent
This commit is contained in:
parent
7997801851
commit
c63abeb168
4 changed files with 188 additions and 100 deletions
|
|
@ -24,7 +24,7 @@ export const SelectBox = ({
|
||||||
<DropDownSelect
|
<DropDownSelect
|
||||||
customBorder={false}
|
customBorder={false}
|
||||||
showPlaceHolder
|
showPlaceHolder
|
||||||
options={options}
|
options={options || []}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Confirm } from '@/Editor/Viewer/Confirm';
|
import { Confirm } from '@/Editor/Viewer/Confirm';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { ToolTip } from '@/_components';
|
||||||
export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
|
|
@ -46,6 +46,7 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
}, [operation, handleOptionsChange, joinTableOptionsChange]);
|
}, [operation, handleOptionsChange, joinTableOptionsChange]);
|
||||||
|
|
||||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||||
|
const [currentAggregateKeyForDeleteConfirmation, setCurrentAggregateKeyForDeleteConfirmation] = useState(null);
|
||||||
|
|
||||||
const addNewAggregateOption = () => {
|
const addNewAggregateOption = () => {
|
||||||
const currentAggregates = { ...(operationDetails?.aggregates || {}) };
|
const currentAggregates = { ...(operationDetails?.aggregates || {}) };
|
||||||
|
|
@ -79,11 +80,11 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = getValue(operation, optionToUpdate, selectedValue);
|
const value = getValue(operation, optionToUpdate, selectedValue);
|
||||||
const table_id = selectedValue.hasOwnProperty('tableId') ? selectedValue.tableId : selectedTableId;
|
const tableIdExist = selectedValue.hasOwnProperty('tableId');
|
||||||
const aggregateToUpdate = {
|
const aggregateToUpdate = {
|
||||||
...currentAggregates[key],
|
...currentAggregates[key],
|
||||||
[optionToUpdate]: value,
|
[optionToUpdate]: value,
|
||||||
table_id,
|
...(tableIdExist && { table_id: selectedValue.tableId }),
|
||||||
};
|
};
|
||||||
const updatedAggregates = {
|
const updatedAggregates = {
|
||||||
...currentAggregates,
|
...currentAggregates,
|
||||||
|
|
@ -92,48 +93,88 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
handleChange('aggregates', updatedAggregates);
|
handleChange('aggregates', updatedAggregates);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteAggregate = (aggregateKey) => {
|
const showConfirmationMoalForLastFilledValue = (currentAggregates, aggregateKey) => {
|
||||||
const currentAggregates = { ...(operationDetails?.aggregates || {}) };
|
if (!currentAggregates[aggregateKey]) {
|
||||||
const numberOfAggregates = Object.keys(currentAggregates).length;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (numberOfAggregates > 1) {
|
// Check if all values for the matched key are truthy
|
||||||
delete currentAggregates[aggregateKey];
|
const allValuesTruthy = Object.values(currentAggregates[aggregateKey]).every((value) => Boolean(value));
|
||||||
try {
|
if (!allValuesTruthy) {
|
||||||
handleChange('aggregates', currentAggregates);
|
return false;
|
||||||
toast.success('Aggregate function deleted successfully!');
|
}
|
||||||
return;
|
|
||||||
} catch (error) {
|
// Check if the rest of the keys have values that are either all empty or partially filled
|
||||||
return toast.error('Could not delete aggregate function. Please try again!');
|
const keys = Object.keys(currentAggregates);
|
||||||
}
|
for (const key of keys) {
|
||||||
} else {
|
if (key !== aggregateKey) {
|
||||||
const currentGroupBy = { ...(operationDetails?.group_by || {}) };
|
const values = Object.values(currentAggregates[key]);
|
||||||
const isValidGroupByPresent = Object.entries(currentGroupBy).some(([tableId, selectedColumn]) => {
|
const allEmpty = values.every((value) => value === '');
|
||||||
if (tableId && selectedColumn.length >= 1) {
|
const partiallyFilled = values.some((value) => value !== '') && values.some((value) => value === '');
|
||||||
return true;
|
|
||||||
}
|
if (!(allEmpty || partiallyFilled)) {
|
||||||
return false;
|
return false;
|
||||||
});
|
|
||||||
if (isValidGroupByPresent) {
|
|
||||||
setShowDeleteConfirmation(true);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
delete currentAggregates[aggregateKey];
|
|
||||||
handleChange('aggregates', currentAggregates);
|
|
||||||
toast.success('Aggregate function deleted successfully!');
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
|
||||||
return toast.error('Could not delete aggregate function. Please try again!');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const executeAggregateDeletion = (aggregateKey) => {
|
const handleDeleteAggregate = (aggregateKey) => {
|
||||||
|
const currentAggregates = { ...(operationDetails?.aggregates || {}) };
|
||||||
|
const numberOfAggregates = Object.keys(currentAggregates).length;
|
||||||
|
const currentGroupBy = { ...(operationDetails?.group_by || {}) };
|
||||||
|
const showConfirmationModal = showConfirmationMoalForLastFilledValue(currentAggregates, aggregateKey);
|
||||||
|
|
||||||
|
const isValidGroupByPresent = Object.values(currentGroupBy).some((selectedColumn) => selectedColumn.length >= 1);
|
||||||
|
|
||||||
|
const deleteAggregate = () => {
|
||||||
|
delete currentAggregates[aggregateKey];
|
||||||
|
handleChange('aggregates', currentAggregates);
|
||||||
|
return toast.success('Aggregate function deleted successfully!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const showError = () => {
|
||||||
|
return toast.error('Could not delete aggregate function. Please try again!');
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (numberOfAggregates > 1) {
|
||||||
|
if (showConfirmationModal && isValidGroupByPresent) {
|
||||||
|
setCurrentAggregateKeyForDeleteConfirmation(aggregateKey);
|
||||||
|
setShowDeleteConfirmation(true);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
deleteAggregate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isValidGroupByPresent) {
|
||||||
|
setCurrentAggregateKeyForDeleteConfirmation(aggregateKey);
|
||||||
|
setShowDeleteConfirmation(true);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
deleteAggregate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeAggregateDeletion = () => {
|
||||||
|
const aggregateKey = currentAggregateKeyForDeleteConfirmation || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!aggregateKey) {
|
||||||
|
throw new Error('Could not delete aggregate function. Please try again!');
|
||||||
|
}
|
||||||
const currentAggregates = { ...(operationDetails?.aggregates || {}) };
|
const currentAggregates = { ...(operationDetails?.aggregates || {}) };
|
||||||
delete currentAggregates[aggregateKey];
|
delete currentAggregates[aggregateKey];
|
||||||
const currentGroupBy = { ...(operationDetails?.group_by || {}) };
|
const currentGroupBy = {};
|
||||||
delete currentGroupBy?.[selectedTableId];
|
|
||||||
|
|
||||||
handleChange('group_by', currentGroupBy);
|
handleChange('group_by', currentGroupBy);
|
||||||
handleChange('aggregates', currentAggregates);
|
handleChange('aggregates', currentAggregates);
|
||||||
|
|
@ -244,35 +285,43 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const getJoinTableOption = (value, tableId) => {
|
||||||
|
const valueToFilter = `${value}-${tableId}`;
|
||||||
|
let foundOption = null; // Use a variable to store the found option
|
||||||
|
|
||||||
|
tableListOptions?.forEach((singleOption) => {
|
||||||
|
if (foundOption) return; // Exit early if foundOption is set
|
||||||
|
singleOption?.options?.some((option) => {
|
||||||
|
if (option.value === valueToFilter) {
|
||||||
|
foundOption = {
|
||||||
|
value: valueToFilter.split('-')[0],
|
||||||
|
label: option.tableName + '.' + option.label,
|
||||||
|
table: tableId,
|
||||||
|
};
|
||||||
|
return true; // Exit the some loop early
|
||||||
|
}
|
||||||
|
return false; // Continue the some loop
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return foundOption || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getListRowsOption = (value) => {
|
||||||
|
const option = columnAccessorsOptions?.find((option) => option?.value === value);
|
||||||
|
return option || {};
|
||||||
|
};
|
||||||
|
|
||||||
const constructAggregateValue = (value, operation, option, tableId = '') => {
|
const constructAggregateValue = (value, operation, option, tableId = '') => {
|
||||||
if (option === 'aggFx') {
|
if (option === 'aggFx') {
|
||||||
const option = aggFxOptions.find((option) => option?.value === value);
|
const option = aggFxOptions.find((option) => option?.value === value);
|
||||||
return option || {};
|
return option || {};
|
||||||
}
|
}
|
||||||
if (option === 'column') {
|
if (option === 'column') {
|
||||||
switch (operation) {
|
if (operation === 'joinTable') {
|
||||||
case 'joinTable': {
|
return getJoinTableOption(value, tableId);
|
||||||
const option = tableListOptions?.reduce((acc, singleOption) => {
|
} else if (operation === 'listRows') {
|
||||||
const valueToFilter = `${value}-${tableId}`;
|
return getListRowsOption(value);
|
||||||
singleOption?.options?.find((option) => {
|
|
||||||
if (option.value === valueToFilter) {
|
|
||||||
acc = {
|
|
||||||
value: valueToFilter.split('-')[0],
|
|
||||||
label: option.tableName + '.' + option.label,
|
|
||||||
table: tableId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
return option || {};
|
|
||||||
}
|
|
||||||
case 'listRows': {
|
|
||||||
const option = columnAccessorsOptions?.find((option) => option?.value === value);
|
|
||||||
return option || {};
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -288,6 +337,18 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedTableName = getTableName(selectedTableId);
|
||||||
|
|
||||||
|
const isGroupByTableNameTruncated = (text) => {
|
||||||
|
const container = document.querySelector('.truncate-container');
|
||||||
|
const textElement = document.createElement('span');
|
||||||
|
textElement.innerText = text;
|
||||||
|
document.body.append(textElement);
|
||||||
|
const isTruncated = textElement?.offsetWidth > container?.offsetWidth;
|
||||||
|
document.body.removeChild(textElement);
|
||||||
|
return isTruncated;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="d-flex" style={{ marginBottom: '1.5rem' }}>
|
<div className="d-flex" style={{ marginBottom: '1.5rem' }}>
|
||||||
|
|
@ -349,7 +410,9 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
'Deleting the aggregate function will also delete the group by conditions. Are you sure, you want to continue?'
|
'Deleting the aggregate function will also delete the group by conditions. Are you sure, you want to continue?'
|
||||||
}
|
}
|
||||||
// confirmButtonLoading={isDeletingQueryInProcess}
|
// confirmButtonLoading={isDeletingQueryInProcess}
|
||||||
onConfirm={() => executeAggregateDeletion(aggregateKey)}
|
onConfirm={() => {
|
||||||
|
executeAggregateDeletion();
|
||||||
|
}}
|
||||||
onCancel={() => setShowDeleteConfirmation(false)}
|
onCancel={() => setShowDeleteConfirmation(false)}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
/>
|
/>
|
||||||
|
|
@ -398,15 +461,22 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{operation === 'joinTable' && (
|
{operation === 'joinTable' && (
|
||||||
<div className="d-flex flex-column custom-gap-8">
|
<div className="d-flex flex-column custom-gap-8 join-group-bys">
|
||||||
<div className="border rounded d-flex">
|
<div className="border rounded d-flex">
|
||||||
<div
|
<ToolTip
|
||||||
style={{ width: '15%', padding: '4px 8px' }}
|
message={selectedTableName}
|
||||||
className="border border-only-right d-flex align-items-center"
|
placement="top"
|
||||||
|
tooltipClassName="tjdb-cell-tooltip"
|
||||||
|
show={isGroupByTableNameTruncated(selectedTableName)}
|
||||||
>
|
>
|
||||||
{getTableName(selectedTableId)}
|
<div
|
||||||
</div>
|
style={{ width: '25%', padding: '4px 8px' }}
|
||||||
<div style={{ width: '85%' }}>
|
className="border border-only-right d-block align-items-center text-truncate truncate-container"
|
||||||
|
>
|
||||||
|
{selectedTableName}
|
||||||
|
</div>
|
||||||
|
</ToolTip>
|
||||||
|
<div style={{ width: '75%' }}>
|
||||||
<SelectBox
|
<SelectBox
|
||||||
width="100%"
|
width="100%"
|
||||||
height="32"
|
height="32"
|
||||||
|
|
@ -422,30 +492,45 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{joinTableOptions?.joins?.map((table) => {
|
{joinTableOptions?.joins?.map((table) => {
|
||||||
return (
|
if (table.hasOwnProperty('table') && table.table) {
|
||||||
<div key={table.table} className="border rounded d-flex">
|
const tableName = getTableName(table.table); // Replace with your dynamic text
|
||||||
<div
|
const isTextTruncated = isGroupByTableNameTruncated(tableName);
|
||||||
style={{ width: '15%', padding: '4px 8px' }}
|
const tableNameEmpty = !tableName;
|
||||||
className="border border-only-right d-flex align-items-center"
|
const showTooltip = tableNameEmpty || isTextTruncated;
|
||||||
>
|
const toolTipMessage = tableNameEmpty ? 'Please select joining table to see its name' : tableName;
|
||||||
{getTableName(table.table)}
|
return (
|
||||||
|
<div key={table.table} className="border rounded d-flex">
|
||||||
|
<ToolTip
|
||||||
|
message={toolTipMessage}
|
||||||
|
placement="top"
|
||||||
|
tooltipClassName="tjdb-cell-tooltip"
|
||||||
|
show={showTooltip}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ width: '25%', padding: '4px 8px' }}
|
||||||
|
className="border border-only-right d-block align-items-center text-truncate group-by-trunate"
|
||||||
|
>
|
||||||
|
{tableName}
|
||||||
|
</div>
|
||||||
|
</ToolTip>
|
||||||
|
<div style={{ width: '75%' }}>
|
||||||
|
<SelectBox
|
||||||
|
width="100%"
|
||||||
|
height="32"
|
||||||
|
value={constructGroupByValue(operationDetails?.group_by?.[table.table])}
|
||||||
|
options={getColumnsDetails(table.table)}
|
||||||
|
placeholder={`Select column(s) to group by`}
|
||||||
|
isMulti={true}
|
||||||
|
handleChange={(value) => handleGroupByChange(table.table, value)}
|
||||||
|
disabled={disableGroupBy() || tableNameEmpty}
|
||||||
|
darkMode={darkMode}
|
||||||
|
showTooltip={disableGroupBy()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '85%' }}>
|
);
|
||||||
<SelectBox
|
}
|
||||||
width="100%"
|
return null;
|
||||||
height="32"
|
|
||||||
value={constructGroupByValue(operationDetails?.group_by?.[table.table])}
|
|
||||||
options={getColumnsDetails(table.table)}
|
|
||||||
placeholder={`Select column(s) to group by`}
|
|
||||||
isMulti={true}
|
|
||||||
handleChange={(value) => handleGroupByChange(table.table, value)}
|
|
||||||
disabled={disableGroupBy()}
|
|
||||||
darkMode={darkMode}
|
|
||||||
showTooltip={disableGroupBy()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useContext } from 'react';
|
import React, { useCallback, useContext } from 'react';
|
||||||
import { Col, Container, Row } from 'react-bootstrap';
|
import { Col, Container, Row } from 'react-bootstrap';
|
||||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||||
import Trash from '@/_ui/Icon/solidIcons/Trash';
|
import Trash from '@/_ui/Icon/solidIcons/Trash';
|
||||||
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
|
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
|
||||||
import { clone } from 'lodash';
|
import { clone, isEmpty } from 'lodash';
|
||||||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||||
import DropDownSelect from './DropDownSelect';
|
import DropDownSelect from './DropDownSelect';
|
||||||
import JoinConstraint from './JoinConstraint';
|
import JoinConstraint from './JoinConstraint';
|
||||||
|
|
@ -87,11 +87,14 @@ const SelectTableMenu = ({ darkMode }) => {
|
||||||
return cleanedJoin;
|
return cleanedJoin;
|
||||||
};
|
};
|
||||||
|
|
||||||
const showSelectSection = () => {
|
const showSelectSection = useCallback(() => {
|
||||||
const groupBy = joinTableOptions?.group_by || {};
|
const groupBy = joinTableOptions?.group_by || {};
|
||||||
const isGroupByUsed = Object?.values(groupBy)?.some((condition) => condition?.length >= 1);
|
const aggregates = joinTableOptions?.aggregates || {};
|
||||||
return isGroupByUsed ? false : true;
|
const isGroupByUsed = Object?.values(groupBy)?.some((columnList) => columnList?.length >= 1);
|
||||||
};
|
//checking if isGroupby is valid or aggregates is not empty then hide select or else show select options
|
||||||
|
return isGroupByUsed || !isEmpty(aggregates) ? false : true;
|
||||||
|
}, [joinTableOptions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Join Section */}
|
{/* Join Section */}
|
||||||
|
|
|
||||||
|
|
@ -197,10 +197,10 @@ function DataSourceSelect({
|
||||||
closePopup && !isMulti && closePopup();
|
closePopup && !isMulti && closePopup();
|
||||||
};
|
};
|
||||||
|
|
||||||
let optionsCount = options.length;
|
let optionsCount = options?.length;
|
||||||
|
|
||||||
options.forEach((item) => {
|
options?.forEach((item) => {
|
||||||
if (item.options && item.options.length > 0) {
|
if (item?.options && item?.options?.length > 0) {
|
||||||
optionsCount += item.options.length;
|
optionsCount += item.options.length;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue