Skip to content

Commit a457df2

Browse files
cdmh219cdhenley219flacoman91
authored
DATAP-1510 Conversion of AggregationBranch from Class to Functional Component (#530)
* Implemented initial version of CollapsibleItem functional component * removed old CollapsibleItem component and associated styles * updated references to CollapsibleFilter in calling components, updated associated snapshots * initial implementation of AggregationBranch functional component * Moved all UI returns to correct position in component * Took out unnecessary functionality to toggle component based on presence/absence of children (can be closed by default). Also removed old snapshot. * Fixed Redux state mutation issue with query, which was causing issues with unit tests * Completed unit tests * removed console log * removed console message in AggregationBranch * Removed references to hasChildren again (were brought back in due to merge conflicts) * took out return of render function * Eliminated default exports for component * Added in CollapsibleFilter updates that were unknowingly deleted via merge conflict * temporarily updated code coverage thresholds * no message * Revert "no message" This reverts commit 62c1e2b. * Revert "temporarily updated code coverage thresholds" This reverts commit 685e9e8. * test to get coveralls to pass --------- Co-authored-by: cdhenley219 <[email protected]> Co-authored-by: Richard Dinh <[email protected]>
1 parent f3aad46 commit a457df2

File tree

19 files changed

+319
-706
lines changed

19 files changed

+319
-706
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
run: yarn run test
2626

2727
- name: Update Coveralls
28-
uses: coverallsapp/github-action@main
28+
uses: coverallsapp/github-action@v2
2929
with:
3030
github-token: ${{ secrets.GITHUB_TOKEN }}
3131

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@
135135
],
136136
"coverageThreshold": {
137137
"global": {
138-
"branches": 80,
139-
"functions": 88,
138+
"branches": 81,
139+
"functions": 89,
140140
"lines": 89,
141141
"statements": 89
142142
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import './AggregationBranch.less';
2+
import { useState } from 'react';
3+
import { useSelector, useDispatch } from 'react-redux';
4+
import PropTypes from 'prop-types';
5+
import { FormattedNumber } from 'react-intl';
6+
import {
7+
coalesce,
8+
getAllFilters,
9+
sanitizeHtmlId,
10+
slugify,
11+
} from '../../../../utils';
12+
import {
13+
removeMultipleFilters,
14+
replaceFilters,
15+
} from '../../../../actions/filter';
16+
import { selectQueryState } from '../../../../reducers/query/selectors';
17+
import AggregationItem from '../AggregationItem/AggregationItem';
18+
import getIcon from '../../../iconMap';
19+
import { SLUG_SEPARATOR } from '../../../../constants';
20+
21+
export const UNCHECKED = 'UNCHECKED';
22+
export const INDETERMINATE = 'INDETERMINATE';
23+
export const CHECKED = 'CHECKED';
24+
25+
export const AggregationBranch = ({ fieldName, item, subitems }) => {
26+
const query = useSelector(selectQueryState);
27+
const dispatch = useDispatch();
28+
const [isOpen, setOpen] = useState(false);
29+
30+
// Find all query filters that refer to the field name
31+
const allFilters = coalesce(query, fieldName, []);
32+
33+
// Do any of these values start with the key?
34+
const keyFilters = allFilters.filter(
35+
(aFilter) => aFilter.indexOf(item.key) === 0,
36+
);
37+
38+
// Does the key contain the separator?
39+
const activeChildren = keyFilters.filter(
40+
(key) => key.indexOf(SLUG_SEPARATOR) !== -1,
41+
);
42+
43+
const activeParent = keyFilters.filter((key) => key === item.key);
44+
45+
let checkedState = UNCHECKED;
46+
if (activeParent.length === 0 && activeChildren.length > 0) {
47+
checkedState = INDETERMINATE;
48+
} else if (activeParent.length > 0) {
49+
checkedState = CHECKED;
50+
}
51+
52+
// Fix up the subitems to prepend the current item key
53+
const buckets = subitems.map((sub) => ({
54+
disabled: item.isDisabled,
55+
key: slugify(item.key, sub.key),
56+
value: sub.key,
57+
// eslint-disable-next-line camelcase
58+
doc_count: sub.doc_count,
59+
}));
60+
61+
const liStyle = 'parent m-form-field m-form-field--checkbox body-copy';
62+
const id = sanitizeHtmlId(`${fieldName} ${item.key}`);
63+
64+
const toggleParent = () => {
65+
const subItemFilters = getAllFilters(item.key, subitems);
66+
67+
// Add the active filters (that might be hidden)
68+
activeChildren.forEach((child) => subItemFilters.add(child));
69+
70+
if (checkedState === CHECKED) {
71+
dispatch(removeMultipleFilters(fieldName, [...subItemFilters]));
72+
} else {
73+
// remove all of the child filters
74+
const replacementFilters = allFilters.filter(
75+
(filter) => filter.indexOf(item.key + SLUG_SEPARATOR) === -1,
76+
);
77+
// add self/ parent filter
78+
replacementFilters.push(item.key);
79+
dispatch(replaceFilters(fieldName, [...replacementFilters]));
80+
}
81+
};
82+
83+
if (buckets.length === 0) {
84+
return <AggregationItem item={item} key={item.key} fieldName={fieldName} />;
85+
}
86+
87+
return (
88+
<>
89+
<li
90+
className={`aggregation-branch ${sanitizeHtmlId(item.key)} ${liStyle}`}
91+
>
92+
<input
93+
type="checkbox"
94+
aria-label={item.key}
95+
disabled={item.isDisabled}
96+
checked={checkedState === CHECKED}
97+
className="flex-fixed a-checkbox"
98+
id={id}
99+
onChange={toggleParent}
100+
/>
101+
<label
102+
className={`toggle a-label ${checkedState === INDETERMINATE ? ' indeterminate' : ''}`}
103+
htmlFor={id}
104+
>
105+
<span className="u-visually-hidden">{item.key}</span>
106+
</label>
107+
<button
108+
className="flex-all a-btn a-btn--link"
109+
onClick={() => setOpen(!isOpen)}
110+
>
111+
{item.key}
112+
{isOpen ? getIcon('up') : getIcon('down')}
113+
</button>
114+
<span className="flex-fixed parent-count">
115+
<FormattedNumber value={item.doc_count} />
116+
</span>
117+
</li>
118+
{isOpen ? (
119+
<ul className="children">
120+
{buckets.map((bucket) => (
121+
<AggregationItem
122+
item={bucket}
123+
key={bucket.key}
124+
fieldName={fieldName}
125+
/>
126+
))}
127+
</ul>
128+
) : null}
129+
</>
130+
);
131+
};
132+
133+
AggregationBranch.propTypes = {
134+
fieldName: PropTypes.string.isRequired,
135+
item: PropTypes.shape({
136+
// eslint-disable-next-line camelcase
137+
doc_count: PropTypes.number.isRequired,
138+
key: PropTypes.string.isRequired,
139+
value: PropTypes.string,
140+
isDisabled: PropTypes.bool,
141+
}).isRequired,
142+
subitems: PropTypes.array.isRequired,
143+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { testRender as render, screen } from '../../../../testUtils/test-utils';
2+
import userEvent from '@testing-library/user-event';
3+
import { merge } from '../../../../testUtils/functionHelpers';
4+
import { defaultQuery } from '../../../../reducers/query/query';
5+
import * as filter from '../../../../actions/filter';
6+
import { AggregationBranch } from './AggregationBranch';
7+
8+
const fieldName = 'abc';
9+
10+
const item = {
11+
key: 'foo',
12+
doc_count: 99,
13+
};
14+
15+
const subitems = [
16+
{ key: 'bar', doc_count: 90 },
17+
{ key: 'baz', doc_count: 5 },
18+
{ key: 'qaz', doc_count: 4 },
19+
];
20+
21+
const renderComponent = (props, newQueryState) => {
22+
merge(newQueryState, defaultQuery);
23+
24+
const data = {
25+
query: newQueryState,
26+
};
27+
28+
return render(<AggregationBranch {...props} />, {
29+
preloadedState: data,
30+
});
31+
};
32+
33+
let props;
34+
35+
describe('component::AggregationBranch', () => {
36+
beforeEach(() => {
37+
props = {
38+
fieldName,
39+
item,
40+
subitems,
41+
};
42+
});
43+
44+
describe('initial state', () => {
45+
test('renders as list item button with unchecked state when one or more subitems are present', () => {
46+
renderComponent(props);
47+
48+
expect(screen.getByRole('checkbox')).not.toBeChecked();
49+
expect(screen.getByLabelText(props.item.key)).toBeInTheDocument();
50+
expect(
51+
screen.getByText(props.item.key, { selector: 'button' }),
52+
).toBeInTheDocument();
53+
expect(screen.getByText(props.item.doc_count)).toBeInTheDocument();
54+
expect(screen.queryByRole('list')).not.toBeInTheDocument();
55+
});
56+
57+
test('renders list item button with disabled checkbox when item property is disabled', () => {
58+
const aitem = { ...item, isDisabled: true };
59+
props.item = aitem;
60+
61+
renderComponent(props);
62+
63+
expect(screen.getByRole('checkbox')).toBeDisabled();
64+
});
65+
66+
test('renders AggregationItem when no subitems are present', () => {
67+
props.subitems = [];
68+
69+
renderComponent(props);
70+
71+
//list item doesn't render with toggle button;
72+
//no need to test rendering of values, since it's covered by AggregationItem tests
73+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
74+
});
75+
76+
test('renders with checkbox in checked state', () => {
77+
const query = {
78+
abc: [props.item.key],
79+
};
80+
81+
renderComponent(props, query);
82+
expect(screen.getByRole('checkbox')).toBeChecked();
83+
});
84+
85+
test('renders with checkbox in indeterminate state', () => {
86+
const query = {
87+
abc: [`${props.item.key}${props.subitems[0].key}`],
88+
};
89+
90+
renderComponent(props, query);
91+
expect(
92+
screen.getByRole('checkbox', { indeterminate: true }),
93+
).toBeInTheDocument();
94+
});
95+
});
96+
97+
describe('toggle states', () => {
98+
const user = userEvent.setup({ delay: null });
99+
100+
let replaceFiltersFn, removeMultipleFiltersFn;
101+
102+
beforeEach(() => {
103+
replaceFiltersFn = jest.spyOn(filter, 'replaceFilters');
104+
removeMultipleFiltersFn = jest.spyOn(filter, 'removeMultipleFilters');
105+
});
106+
107+
afterEach(() => {
108+
jest.restoreAllMocks();
109+
});
110+
111+
test('should properly check the component', async () => {
112+
renderComponent(props);
113+
114+
await user.click(screen.getByRole('checkbox'));
115+
116+
expect(replaceFiltersFn).toHaveBeenCalledWith(props.fieldName, ['foo']);
117+
});
118+
119+
test('should properly uncheck the component', async () => {
120+
const query = {
121+
abc: [props.item.key],
122+
};
123+
124+
renderComponent(props, query);
125+
126+
await user.click(screen.getByRole('checkbox'));
127+
128+
expect(removeMultipleFiltersFn).toHaveBeenCalledWith(props.fieldName, [
129+
'foo',
130+
'foo•bar',
131+
'foo•baz',
132+
'foo•qaz',
133+
]);
134+
});
135+
136+
test('should show children list items on button click', async () => {
137+
renderComponent(props);
138+
139+
await user.click(screen.getByRole('button'));
140+
141+
expect(screen.getByRole('list')).toBeInTheDocument();
142+
});
143+
});
144+
});

src/components/Filters/AggregationItem/AggregationItem.js src/components/Filters/Aggregation/AggregationItem/AggregationItem.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import PropTypes from 'prop-types';
22
import { useSelector, useDispatch } from 'react-redux';
33
import { FormattedNumber } from 'react-intl';
4-
import { filterPatch, SLUG_SEPARATOR } from '../../../constants';
5-
import { coalesce, sanitizeHtmlId } from '../../../utils';
6-
import { arrayEquals } from '../../../utils/compare';
7-
import { replaceFilters, toggleFilter } from '../../../actions/filter';
8-
import { getUpdatedFilters } from '../../../utils/filters';
9-
import { selectAggsState } from '../../../reducers/aggs/selectors';
10-
import { selectQueryState } from '../../../reducers/query/selectors';
4+
import { filterPatch, SLUG_SEPARATOR } from '../../../../constants';
5+
import { coalesce, sanitizeHtmlId } from '../../../../utils';
6+
import { arrayEquals } from '../../../../utils/compare';
7+
import { replaceFilters, toggleFilter } from '../../../../actions/filter';
8+
import { getUpdatedFilters } from '../../../../utils/filters';
9+
import { selectAggsState } from '../../../../reducers/aggs/selectors';
10+
import { selectQueryState } from '../../../../reducers/query/selectors';
1111

1212
const appliedFilters = ({ fieldName, item, aggs, filters }) => {
1313
// We should find the parent

src/components/Filters/AggregationItem/AggregationItem.spec.js src/components/Filters/Aggregation/AggregationItem/AggregationItem.spec.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { testRender as render, screen } from '../../../testUtils/test-utils';
2-
import { merge } from '../../../testUtils/functionHelpers';
1+
import { testRender as render, screen } from '../../../../testUtils/test-utils';
2+
import { merge } from '../../../../testUtils/functionHelpers';
33
import userEvent from '@testing-library/user-event';
4-
import * as filter from '../../../actions/filter';
5-
import * as utils from '../../../utils';
6-
import { slugify } from '../../../utils';
7-
import { defaultAggs } from '../../../reducers/aggs/aggs';
8-
import { defaultQuery } from '../../../reducers/query/query';
4+
import * as filter from '../../../../actions/filter';
5+
import * as utils from '../../../../utils';
6+
import { slugify } from '../../../../utils';
7+
import { defaultAggs } from '../../../../reducers/aggs/aggs';
8+
import { defaultQuery } from '../../../../reducers/query/query';
99
import AggregationItem from './AggregationItem';
1010

1111
const defaultTestProps = {

0 commit comments

Comments
 (0)