Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DATAP-1510 Conversion of AggregationBranch from Class to Functional Component #530

Merged
merged 20 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import './AggregationBranch.less';
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedNumber } from 'react-intl';
import {
coalesce,
getAllFilters,
sanitizeHtmlId,
slugify,
} from '../../../../utils';
import {
removeMultipleFilters,
replaceFilters,
} from '../../../../actions/filter';
import { selectQueryState } from '../../../../reducers/query/selectors';
import AggregationItem from '../AggregationItem/AggregationItem';
import getIcon from '../../../iconMap';
import { SLUG_SEPARATOR } from '../../../../constants';

export const UNCHECKED = 'UNCHECKED';
export const INDETERMINATE = 'INDETERMINATE';
export const CHECKED = 'CHECKED';

export const AggregationBranch = ({ fieldName, item, subitems }) => {
const query = useSelector(selectQueryState);
const dispatch = useDispatch();
const [isOpen, setOpen] = useState(false);

// Find all query filters that refer to the field name
const allFilters = coalesce(query, fieldName, []);

// Do any of these values start with the key?
const keyFilters = allFilters.filter(
(aFilter) => aFilter.indexOf(item.key) === 0,
);

// Does the key contain the separator?
const activeChildren = keyFilters.filter(
(key) => key.indexOf(SLUG_SEPARATOR) !== -1,
);

const activeParent = keyFilters.filter((key) => key === item.key);

let checkedState = UNCHECKED;
if (activeParent.length === 0 && activeChildren.length > 0) {
checkedState = INDETERMINATE;
} else if (activeParent.length > 0) {
checkedState = CHECKED;
}

// Fix up the subitems to prepend the current item key
const buckets = subitems.map((sub) => ({
disabled: item.isDisabled,
key: slugify(item.key, sub.key),
value: sub.key,
// eslint-disable-next-line camelcase
doc_count: sub.doc_count,
}));

const liStyle = 'parent m-form-field m-form-field--checkbox body-copy';
const id = sanitizeHtmlId(`${fieldName} ${item.key}`);

const toggleParent = () => {
const subItemFilters = getAllFilters(item.key, subitems);

// Add the active filters (that might be hidden)
activeChildren.forEach((child) => subItemFilters.add(child));

if (checkedState === CHECKED) {
dispatch(removeMultipleFilters(fieldName, [...subItemFilters]));
} else {
// remove all of the child filters
const replacementFilters = allFilters.filter(
(filter) => filter.indexOf(item.key + SLUG_SEPARATOR) === -1,
);
// add self/ parent filter
replacementFilters.push(item.key);
dispatch(replaceFilters(fieldName, [...replacementFilters]));
}
};

if (buckets.length === 0) {
return <AggregationItem item={item} key={item.key} fieldName={fieldName} />;
}

return (
<>
<li
className={`aggregation-branch ${sanitizeHtmlId(item.key)} ${liStyle}`}
>
<input
type="checkbox"
aria-label={item.key}
disabled={item.isDisabled}
checked={checkedState === CHECKED}
className="flex-fixed a-checkbox"
id={id}
onChange={toggleParent}
/>
<label
className={`toggle a-label ${checkedState === INDETERMINATE ? ' indeterminate' : ''}`}
htmlFor={id}
>
<span className="u-visually-hidden">{item.key}</span>
</label>
<button
className="flex-all a-btn a-btn--link"
onClick={() => setOpen(!isOpen)}
>
{item.key}
{isOpen ? getIcon('up') : getIcon('down')}
</button>
<span className="flex-fixed parent-count">
<FormattedNumber value={item.doc_count} />
</span>
</li>
{isOpen ? (
<ul className="children">
{buckets.map((bucket) => (
<AggregationItem
item={bucket}
key={bucket.key}
fieldName={fieldName}
/>
))}
</ul>
) : null}
</>
);
};

AggregationBranch.propTypes = {
fieldName: PropTypes.string.isRequired,
item: PropTypes.shape({
// eslint-disable-next-line camelcase
doc_count: PropTypes.number.isRequired,
key: PropTypes.string.isRequired,
value: PropTypes.string,
isDisabled: PropTypes.bool,
}).isRequired,
subitems: PropTypes.array.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { testRender as render, screen } from '../../../../testUtils/test-utils';
import userEvent from '@testing-library/user-event';
import { merge } from '../../../../testUtils/functionHelpers';
import { defaultQuery } from '../../../../reducers/query/query';
import * as filter from '../../../../actions/filter';
import { AggregationBranch } from './AggregationBranch';

const fieldName = 'abc';

const item = {
key: 'foo',
doc_count: 99,
};

const subitems = [
{ key: 'bar', doc_count: 90 },
{ key: 'baz', doc_count: 5 },
{ key: 'qaz', doc_count: 4 },
];

const renderComponent = (props, newQueryState) => {
merge(newQueryState, defaultQuery);

const data = {
query: newQueryState,
};

return render(<AggregationBranch {...props} />, {
preloadedState: data,
});
};

let props;

describe('component::AggregationBranch', () => {
beforeEach(() => {
props = {
fieldName,
item,
subitems,
};
});

describe('initial state', () => {
test('renders as list item button with unchecked state when one or more subitems are present', () => {
renderComponent(props);

expect(screen.getByRole('checkbox')).not.toBeChecked();
expect(screen.getByLabelText(props.item.key)).toBeInTheDocument();
expect(
screen.getByText(props.item.key, { selector: 'button' }),
).toBeInTheDocument();
expect(screen.getByText(props.item.doc_count)).toBeInTheDocument();
expect(screen.queryByRole('list')).not.toBeInTheDocument();
});

test('renders list item button with disabled checkbox when item property is disabled', () => {
const aitem = { ...item, isDisabled: true };
props.item = aitem;

renderComponent(props);

expect(screen.getByRole('checkbox')).toBeDisabled();
});

test('renders AggregationItem when no subitems are present', () => {
props.subitems = [];

renderComponent(props);

//list item doesn't render with toggle button;
//no need to test rendering of values, since it's covered by AggregationItem tests
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});

test('renders with checkbox in checked state', () => {
const query = {
abc: [props.item.key],
};

renderComponent(props, query);
expect(screen.getByRole('checkbox')).toBeChecked();
});

test('renders with checkbox in indeterminate state', () => {
const query = {
abc: [`${props.item.key}•${props.subitems[0].key}`],
};

renderComponent(props, query);
expect(
screen.getByRole('checkbox', { indeterminate: true }),
).toBeInTheDocument();
});
});

describe('toggle states', () => {
const user = userEvent.setup({ delay: null });

let replaceFiltersFn, removeMultipleFiltersFn;

beforeEach(() => {
replaceFiltersFn = jest.spyOn(filter, 'replaceFilters');
removeMultipleFiltersFn = jest.spyOn(filter, 'removeMultipleFilters');
});

afterEach(() => {
jest.restoreAllMocks();
});

test('should properly check the component', async () => {
renderComponent(props);

await user.click(screen.getByRole('checkbox'));

expect(replaceFiltersFn).toHaveBeenCalledWith(props.fieldName, ['foo']);
});

test('should properly uncheck the component', async () => {
const query = {
abc: [props.item.key],
};

renderComponent(props, query);

await user.click(screen.getByRole('checkbox'));

expect(removeMultipleFiltersFn).toHaveBeenCalledWith(props.fieldName, [
'foo',
'foo•bar',
'foo•baz',
'foo•qaz',
]);
});

test('should show children list items on button click', async () => {
renderComponent(props);

await user.click(screen.getByRole('button'));

expect(screen.getByRole('list')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { FormattedNumber } from 'react-intl';
import { filterPatch, SLUG_SEPARATOR } from '../../../constants';
import { coalesce, sanitizeHtmlId } from '../../../utils';
import { arrayEquals } from '../../../utils/compare';
import { replaceFilters, toggleFilter } from '../../../actions/filter';
import { getUpdatedFilters } from '../../../utils/filters';
import { selectAggsState } from '../../../reducers/aggs/selectors';
import { selectQueryState } from '../../../reducers/query/selectors';
import { filterPatch, SLUG_SEPARATOR } from '../../../../constants';
import { coalesce, sanitizeHtmlId } from '../../../../utils';
import { arrayEquals } from '../../../../utils/compare';
import { replaceFilters, toggleFilter } from '../../../../actions/filter';
import { getUpdatedFilters } from '../../../../utils/filters';
import { selectAggsState } from '../../../../reducers/aggs/selectors';
import { selectQueryState } from '../../../../reducers/query/selectors';

const appliedFilters = ({ fieldName, item, aggs, filters }) => {
// We should find the parent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { testRender as render, screen } from '../../../testUtils/test-utils';
import { merge } from '../../../testUtils/functionHelpers';
import { testRender as render, screen } from '../../../../testUtils/test-utils';
import { merge } from '../../../../testUtils/functionHelpers';
import userEvent from '@testing-library/user-event';
import * as filter from '../../../actions/filter';
import * as utils from '../../../utils';
import { slugify } from '../../../utils';
import { defaultAggs } from '../../../reducers/aggs/aggs';
import { defaultQuery } from '../../../reducers/query/query';
import * as filter from '../../../../actions/filter';
import * as utils from '../../../../utils';
import { slugify } from '../../../../utils';
import { defaultAggs } from '../../../../reducers/aggs/aggs';
import { defaultQuery } from '../../../../reducers/query/query';
import AggregationItem from './AggregationItem';

const defaultTestProps = {
Expand Down
Loading
Loading