diff --git a/src/PivotTableUI.jsx b/src/PivotTableUI.jsx index 0de055f..0010c0f 100644 --- a/src/PivotTableUI.jsx +++ b/src/PivotTableUI.jsx @@ -210,6 +210,7 @@ export class Dropdown extends React.PureComponent { this.props.toggle(); } else { this.props.setValue(r); + this.props.toggle(); // Close the dropdown after selection } }} className={ @@ -284,12 +285,38 @@ class PivotTableUI extends React.PureComponent { this.setState(newState); } + handleDuplicates(newAttributes, existingAttributes) { + if (!newAttributes || !existingAttributes) { + return existingAttributes || []; + } + const duplicates = newAttributes.filter(item => existingAttributes.includes(item)); + return duplicates.length > 0 + ? existingAttributes.filter(item => !duplicates.includes(item)) + : existingAttributes; + } + sendPropUpdate(command) { this.props.onChange(update(this.props, command)); } propUpdater(key) { - return value => this.sendPropUpdate({[key]: {$set: value}}); + return value => { + const update = {[key]: {$set: value}}; + + if (key === 'rows') { + const updatedCols = this.handleDuplicates(value, this.props.cols); + if (updatedCols.length !== this.props.cols.length) { + update.cols = {$set: updatedCols}; + } + } else if (key === 'cols') { + const updatedRows = this.handleDuplicates(value, this.props.rows); + if (updatedRows.length !== this.props.rows.length) { + update.rows = {$set: updatedRows}; + } + } + + this.sendPropUpdate(update); + }; } setValuesInFilter(attribute, values) { @@ -502,7 +529,6 @@ class PivotTableUI extends React.PureComponent { !this.props.hiddenAttributes.includes(e) && !this.props.hiddenFromDragDrop.includes(e) ); - const colAttrsCell = this.makeDnDCell( colAttrs, this.propUpdater('cols'), @@ -519,6 +545,7 @@ class PivotTableUI extends React.PureComponent { this.propUpdater('rows'), 'pvtAxisContainer pvtVertList pvtRows' ); + const outputCell = ( { + describe('handleDuplicates', () => { + // Create a minimal instance of PivotTableUI for testing + const getInstance = () => { + const pivotTableUI = new PivotTableUI({ + onChange: () => {}, + renderers: {}, + aggregators: {}, + rows: [], + cols: [], + rendererName: '', + aggregatorName: '', + vals: [], + valueFilter: {}, + rowOrder: 'key_a_to_z', + colOrder: 'key_a_to_z', + derivedAttributes: {}, + data: [] + }); + return pivotTableUI; + }; + + it('returns existingAttributes when newAttributes is null or undefined', () => { + const instance = getInstance(); + expect(instance.handleDuplicates(null, ['a', 'b'])).toEqual(['a', 'b']); + expect(instance.handleDuplicates(undefined, ['a', 'b'])).toEqual(['a', 'b']); + }); + + it('returns empty array when both inputs are null or undefined', () => { + const instance = getInstance(); + expect(instance.handleDuplicates(null, null)).toEqual([]); + expect(instance.handleDuplicates(undefined, undefined)).toEqual([]); + }); + + it('returns existingAttributes when there are no duplicates', () => { + const instance = getInstance(); + const newAttributes = ['a', 'b', 'c']; + const existingAttributes = ['d', 'e', 'f']; + expect(instance.handleDuplicates(newAttributes, existingAttributes)) + .toEqual(existingAttributes); + }); + + it('removes duplicates from existingAttributes', () => { + const instance = getInstance(); + const newAttributes = ['a', 'b', 'c']; + const existingAttributes = ['b', 'c', 'd']; + // 'b' and 'c' are duplicates and should be removed + expect(instance.handleDuplicates(newAttributes, existingAttributes)) + .toEqual(['d']); + }); + + it('handles empty newAttributes', () => { + const instance = getInstance(); + const newAttributes = []; + const existingAttributes = ['a', 'b', 'c']; + expect(instance.handleDuplicates(newAttributes, existingAttributes)) + .toEqual(existingAttributes); + }); + + it('handles empty existingAttributes', () => { + const instance = getInstance(); + const newAttributes = ['a', 'b', 'c']; + const existingAttributes = []; + expect(instance.handleDuplicates(newAttributes, existingAttributes)) + .toEqual([]); + }); + + it('handles case with all attributes being duplicates', () => { + const instance = getInstance(); + const newAttributes = ['a', 'b', 'c']; + const existingAttributes = ['a', 'b', 'c']; + expect(instance.handleDuplicates(newAttributes, existingAttributes)) + .toEqual([]); + }); + }); + + describe('propUpdater', () => { + // We'll use a mock to check if sendPropUpdate is called with the right arguments + let mockSendPropUpdate; + let instance; + + beforeEach(() => { + instance = new PivotTableUI({ + onChange: () => {}, + renderers: {}, + aggregators: {}, + rows: ['gender', 'age'], + cols: ['country', 'year'], + rendererName: '', + aggregatorName: '', + vals: [], + valueFilter: {}, + rowOrder: 'key_a_to_z', + colOrder: 'key_a_to_z', + derivedAttributes: {}, + data: [] + }); + // Mock the sendPropUpdate method + mockSendPropUpdate = jest.spyOn(instance, 'sendPropUpdate').mockImplementation(() => {}); + // Mock the handleDuplicates method to control its return value + jest.spyOn(instance, 'handleDuplicates'); + }); + + afterEach(() => { + mockSendPropUpdate.mockRestore(); + instance.handleDuplicates.mockRestore(); + }); + + it('calls handleDuplicates when key is "rows"', () => { + const newRows = ['gender', 'name']; + const updater = instance.propUpdater('rows'); + + // Set up the mock to return the same cols (no duplicates found) + instance.handleDuplicates.mockReturnValueOnce(instance.props.cols); + + updater(newRows); + + expect(instance.handleDuplicates).toHaveBeenCalledWith(newRows, instance.props.cols); + expect(mockSendPropUpdate).toHaveBeenCalledWith({ + rows: { $set: newRows } + }); + }); + + it('calls handleDuplicates when key is "cols"', () => { + const newCols = ['country', 'city']; + const updater = instance.propUpdater('cols'); + + // Set up the mock to return the same rows (no duplicates found) + instance.handleDuplicates.mockReturnValueOnce(instance.props.rows); + + updater(newCols); + + expect(instance.handleDuplicates).toHaveBeenCalledWith(newCols, instance.props.rows); + expect(mockSendPropUpdate).toHaveBeenCalledWith({ + cols: { $set: newCols } + }); + }); + + it('updates cols when duplicate is found in rows update', () => { + const newRows = ['gender', 'country']; // 'country' is duplicate + const updater = instance.propUpdater('rows'); + + // 'country' is removed from cols + const updatedCols = ['year']; + instance.handleDuplicates.mockReturnValueOnce(updatedCols); + + updater(newRows); + + expect(mockSendPropUpdate).toHaveBeenCalledWith({ + rows: { $set: newRows }, + cols: { $set: updatedCols } + }); + }); + + it('updates rows when duplicate is found in cols update', () => { + const newCols = ['country', 'gender']; // 'gender' is duplicate + const updater = instance.propUpdater('cols'); + + // 'gender' is removed from rows + const updatedRows = ['age']; + instance.handleDuplicates.mockReturnValueOnce(updatedRows); + + updater(newCols); + + expect(mockSendPropUpdate).toHaveBeenCalledWith({ + cols: { $set: newCols }, + rows: { $set: updatedRows } + }); + }); + + it('does not update the other attribute if no duplicates found', () => { + const newRows = ['gender', 'name']; + const updater = instance.propUpdater('rows'); + + // No change to cols (same array reference) + instance.handleDuplicates.mockReturnValueOnce(instance.props.cols); + + updater(newRows); + + expect(mockSendPropUpdate).toHaveBeenCalledWith({ + rows: { $set: newRows } + }); + // We shouldn't have cols in the update + expect(mockSendPropUpdate.mock.calls[0][0].cols).toBeUndefined(); + }); + }); +});