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:
Manish Kushare 2024-07-05 19:48:39 +05:30 committed by GitHub
parent 7997801851
commit c63abeb168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 188 additions and 100 deletions

View file

@ -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}

View file

@ -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>
)} )}

View file

@ -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 */}

View file

@ -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;
} }
}); });