diff --git a/index.js b/index.js index 13a57bc7..a70394da 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,8 @@ export { default as CustomMetaSection } from './lib/CustomMetaSection'; export { default as DateFilter } from './lib/DateFilter'; export { default as DocumentCard } from './lib/DocumentCard'; export { default as DocumentFilter } from './lib/DocumentFilter'; +export { default as DocumentFilterForm } from './lib/DocumentFilter/DocumentFilterForm'; +export { default as DocumentFilterFieldArray } from './lib/DocumentFilter/DocumentFilterFieldArray'; export { default as DocumentsFieldArray } from './lib/DocumentsFieldArray'; export { default as DuplicateModal } from './lib/DuplicateModal'; export { default as EditCard } from './lib/EditCard'; diff --git a/lib/DocumentFilter/DocumentFilter.js b/lib/DocumentFilter/DocumentFilter.js index 531ff6d1..d654ec22 100644 --- a/lib/DocumentFilter/DocumentFilter.js +++ b/lib/DocumentFilter/DocumentFilter.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { useState } from 'react'; + import { Accordion, FilterAccordionHeader, @@ -8,18 +8,26 @@ import { import { FormattedMessage } from 'react-intl'; import { - deparseKiwtQueryFilters, parseKiwtQueryFilters, } from '@k-int/stripes-kint-components'; import DocumentFilterForm from './DocumentFilterForm'; -const DocumentFilter = ({ activeFilters, atTypeValues = [], filterHandlers }) => { - // atTypeValues are only passed for SupplementaryDocumentFilter - const filterType = atTypeValues.length > 0 ? 'supplementaryDocuments' : 'documents'; - const [editingFilters, setEditingFilters] = useState(false); - const openEditModal = () => setEditingFilters(true); - const closeEditModal = () => setEditingFilters(false); +const DocumentFilter = ({ + activeFilters, + atTypeValues = [], // DEPRECATED + categoryValues = [], // Use this instead of atTypeValues + filterHandlers, + filterLabel, + filterModalProps = {}, + filterName = 'docs' +}) => { + let categoryValuesToUse = categoryValues; + if (categoryValuesToUse.length === 0 && atTypeValues.length !== 0) { + // eslint-disable-next-line no-console + console.error('atTypeValues have been deprecated from Ramsons. Use categoryValues instead'); + categoryValuesToUse = atTypeValues; + } // Due to how filters are handled within SearchAndSortQuery the filter string needs to be parsed back into a usual object const parseQueryString = (filterArray) => { @@ -55,52 +63,16 @@ const DocumentFilter = ({ activeFilters, atTypeValues = [], filterHandlers }) => return []; }; - const parsedFilterData = parseQueryString(activeFilters?.[filterType] || []); - - const handleSubmit = (values) => { - // In order to convert the form values into the shape for them to be deparsed we do the inverse of the above - // Adding a || operator between all elements of the filters array and a && operator between all elements of the nested arrays - // With special logic to ensure that operators are not added infront of the first elements of any arrays, to ensure no grouping errors - const kiwtQueryShape = values?.filters?.reduce((acc, curr, index) => { - let newAcc = [...acc]; - - if (index !== 0) { - newAcc = [...newAcc, '||']; - } - - newAcc = [ - ...newAcc, - curr.rules.reduce((a, c, i) => { - return [ - ...a, - i !== 0 ? '&&' : null, // Don't group on first entry - c, - ].filter(Boolean); - }, []), - ]; - - return newAcc; - }, []); - - filterHandlers.state({ - ...activeFilters, - [filterType]: [ - // Currently the deparse function returns a query string containing whitespace which leads to grouping errors - // This regex removes all whitespace from the querystring - deparseKiwtQueryFilters(kiwtQueryShape), - ], - }); - setEditingFilters(false); - }; + const parsedFilterData = parseQueryString(activeFilters?.[filterName] || []); return ( } - onClearFilter={() => filterHandlers.state({ ...activeFilters, [filterType]: [] }) + id={`clickable-agreement-${filterName}-filter`} + label={filterLabel ?? } + onClearFilter={() => filterHandlers.state({ ...activeFilters, [filterName]: [] }) } separator={false} > @@ -113,14 +85,12 @@ const DocumentFilter = ({ activeFilters, atTypeValues = [], filterHandlers }) => )} ); @@ -129,7 +99,13 @@ const DocumentFilter = ({ activeFilters, atTypeValues = [], filterHandlers }) => DocumentFilter.propTypes = { activeFilters: PropTypes.object, atTypeValues: PropTypes.arrayOf(PropTypes.object), + categoryValues: PropTypes.arrayOf(PropTypes.object), filterHandlers: PropTypes.object, + filterLabel: PropTypes.string, + filterName: PropTypes.string, + filterModalProps: PropTypes.shape({ + label: PropTypes.string, + }), }; export default DocumentFilter; diff --git a/lib/DocumentFilter/DocumentFilter.test.js b/lib/DocumentFilter/DocumentFilter.test.js new file mode 100644 index 00000000..b9ba7779 --- /dev/null +++ b/lib/DocumentFilter/DocumentFilter.test.js @@ -0,0 +1,143 @@ +import { MemoryRouter } from 'react-router-dom'; +import { + Button, + Accordion, + renderWithIntl, +} from '@folio/stripes-erm-testing'; + +import { Button as MockStripesButton } from '@folio/stripes/components'; + +import { waitFor } from '@folio/jest-config-stripes/testing-library/react'; +import { translationsProperties } from '../../test/jest/helpers'; +import DocumentFilter from './DocumentFilter'; + +import { documentFilterParsing } from './testResources'; + +const mockPFD = jest.fn(); + +jest.mock('./DocumentFilterForm', () => ({ + filters, +}) => { + return ( +
+ mockPFD(filters)}> + Test parsed filter data + + DocumentFilterForm +
+ ); +}); + +const stateMock = jest.fn(); +const filterHandlers = { + state: stateMock, + checkbox: () => {}, + clear: () => {}, + clearGroup: () => {}, + reset: () => {}, +}; + + +// Todo these should be centralised +const categoryValues = [ + { + 'id': '2c9180a09262108601926219be050022', + 'value': 'consortium_negotiation_document', + 'label': 'Consortium negotiation document', + }, + { + 'id': '2c9180a09262108601926219bdfc0020', + 'value': 'license', + 'label': 'License', + }, + { + 'id': '2c9180a09262108601926219be010021', + 'value': 'misc', + 'label': 'Misc', + }, +]; + + +let renderComponent; + +describe('DocumentFilter', () => { + describe.each([ + [ + 'with active filters', + documentFilterParsing.find(dfp => dfp.name === 'simple').deparsed, + '1 document filters applied', + documentFilterParsing.find(dfp => dfp.name === 'simple').parsed, + ], + [ + 'without active filters', + { docs: [] }, + null, + [] + ], + [ + 'with multiple active filters', + documentFilterParsing.find(dfp => dfp.name === 'complex').deparsed, + '2 document filters applied', + documentFilterParsing.find(dfp => dfp.name === 'complex').parsed, + ], + ])('ActiveFilter tests', ( + activeFilterTitle, + activeFilterState, + expectedLayoutText, + expectedParsedFilterData + ) => { + describe(activeFilterTitle, () => { + beforeEach(() => { + renderComponent = renderWithIntl( + + , + , + translationsProperties + ); + }); + + test('renders the document filters accordion', async () => { + await Accordion('Documents').exists(); + }); + + test('renders expected filters text', async () => { + const baseLayoutText = /document filters applied/; + const { queryByText } = renderComponent; + if (expectedLayoutText != null) { + expect(queryByText(expectedLayoutText)).toBeInTheDocument(); + } else { + expect(queryByText(baseLayoutText)).not.toBeInTheDocument(); + } + }); + + it('renders DocumentFilterForm component', () => { + const { getByText } = renderComponent; + expect(getByText('DocumentFilterForm')).toBeInTheDocument(); + }); + + // TODO + // Add test for clearing accordion, whether clear button exists etc (mock filterHandlers.state) + // and check the right value are passed when it's clicked + + describe('testing parsedFilterData', () => { + beforeEach(async () => { + mockPFD.mockClear(); + await waitFor(async () => { + await Button('Test parsed filter data').click(); + }); + }); + + test('parsed filter data is as expected', () => { + expect(mockPFD.mock.calls[0][0]).toEqual(expectedParsedFilterData); + }); + }); + }); + }); + + // TODO + // Add test for passing in/not passing in labels +}); diff --git a/lib/DocumentFilter/DocumentFilterField.js b/lib/DocumentFilter/DocumentFilterField.js index 63b7cc75..f835a68f 100644 --- a/lib/DocumentFilter/DocumentFilterField.js +++ b/lib/DocumentFilter/DocumentFilterField.js @@ -5,7 +5,12 @@ import { FieldArray } from 'react-final-form-arrays'; import { FormattedMessage } from 'react-intl'; import DocumentFilterRule from './DocumentFilterRule'; -const DocumentFilterField = ({ atTypeValues, index, name }) => { +const DocumentFilterField = ({ + categoryValues, + filterName, + index, + name +}) => { const { mutators: { push }, } = useForm(); @@ -15,7 +20,8 @@ const DocumentFilterField = ({ atTypeValues, index, name }) => { ruleFields.remove(ruleFieldIndex)} @@ -63,9 +69,10 @@ const DocumentFilterField = ({ atTypeValues, index, name }) => { }; DocumentFilterField.propTypes = { - atTypeValues: PropTypes.arrayOf(PropTypes.object), + categoryValues: PropTypes.arrayOf(PropTypes.object), index: PropTypes.number, name: PropTypes.string, + filterName: PropTypes.string, }; export default DocumentFilterField; diff --git a/lib/DocumentFilter/DocumentFilterField.test.js b/lib/DocumentFilter/DocumentFilterField.test.js new file mode 100644 index 00000000..c4314393 --- /dev/null +++ b/lib/DocumentFilter/DocumentFilterField.test.js @@ -0,0 +1,96 @@ +import { MemoryRouter } from 'react-router-dom'; +import { FieldArray } from 'react-final-form-arrays'; + +import { renderWithIntl, Button, TestForm } from '@folio/stripes-erm-testing'; + +import { translationsProperties } from '../../test/jest/helpers'; +import DocumentFilterField from './DocumentFilterField'; +import { documentFilterParsing } from './testResources'; + +const onSubmit = jest.fn(); + +// These should be centralised +const categoryValues = [ + { + 'id': '2c9180a09262108601926219be050022', + 'value': 'consortium_negotiation_document', + 'label': 'Consortium negotiation document', + }, + { + 'id': '2c9180a09262108601926219bdfc0020', + 'value': 'license', + 'label': 'License', + }, + { + 'id': '2c9180a09262108601926219be010021', + 'value': 'misc', + 'label': 'Misc', + }, +]; + +let renderComponent; +describe('DocumentFilterField', () => { + describe.each([ + { + initalFilters: documentFilterParsing.find(dfp => dfp.name === 'complex').parsed, + expectedFields: 2 + }, + { + initalFilters: documentFilterParsing.find(dfp => dfp.name === 'simple').parsed, + expectedFields: 1 + }, + { + initalFilters: [], + expectedFields: 0 + } + ])('Render DocumentFilterField with $expectedFields in the array', ({ initalFilters, expectedFields }) => { + beforeEach(() => { + renderComponent = renderWithIntl( + + + + {({ fields }) => fields.map((name, index) => ( + + ))} + + + , + , + translationsProperties + ); + }); + + it('displays attibute label(s)', () => { + const { queryAllByText } = renderComponent; + expect(queryAllByText('Attribute')).toHaveLength(expectedFields); + }); + + it('displays operator label(s)', () => { + const { queryAllByText } = renderComponent; + expect(queryAllByText('Operator')).toHaveLength(expectedFields); + }); + + it('displays value label(s)', () => { + const { queryAllByText } = renderComponent; + expect(queryAllByText('Value')).toHaveLength(expectedFields); + }); + + if (expectedFields > 0) { + test('renders the add rule button', async () => { + await Button('Add rule').exists(); + }); + // TODO Mock DocumentFilterRule and check that you can add rules, that the right number show up and that they're separated by ANDs + } + }); +}); diff --git a/lib/DocumentFilter/DocumentFilterFieldArray.js b/lib/DocumentFilter/DocumentFilterFieldArray.js index 64fd52aa..a55d9ced 100644 --- a/lib/DocumentFilter/DocumentFilterFieldArray.js +++ b/lib/DocumentFilter/DocumentFilterFieldArray.js @@ -13,10 +13,14 @@ import { FormattedMessage } from 'react-intl'; import DocumentFilterField from './DocumentFilterField'; const propTypes = { - atTypeValues: PropTypes.arrayOf(PropTypes.object), + categoryValues: PropTypes.arrayOf(PropTypes.object), + filterName: PropTypes.string }; -const DocumentFilterFieldArray = ({ atTypeValues }) => { +const DocumentFilterFieldArray = ({ + categoryValues, + filterName +}) => { const { mutators: { push }, } = useForm(); @@ -63,8 +67,9 @@ const DocumentFilterFieldArray = ({ atTypeValues }) => { marginBottom0={index !== fields.length - 1} > diff --git a/lib/DocumentFilter/DocumentFilterFieldArray.test.js b/lib/DocumentFilter/DocumentFilterFieldArray.test.js new file mode 100644 index 00000000..2e74fe0e --- /dev/null +++ b/lib/DocumentFilter/DocumentFilterFieldArray.test.js @@ -0,0 +1,53 @@ +import { MemoryRouter } from 'react-router-dom'; + +import { renderWithIntl, Button, TestForm } from '@folio/stripes-erm-testing'; + +import { translationsProperties } from '../../test/jest/helpers'; +import DocumentFilterFieldArray from './DocumentFilterFieldArray'; + +const onSubmit = jest.fn(); + +const categoryValues = [ + { + 'id': '2c9180a09262108601926219be050022', + 'value': 'consortium_negotiation_document', + 'label': 'Consortium negotiation document', + }, + { + 'id': '2c9180a09262108601926219bdfc0020', + 'value': 'license', + 'label': 'License', + }, + { + 'id': '2c9180a09262108601926219be010021', + 'value': 'misc', + 'label': 'Misc', + }, +]; + +describe('DocumentFilterFieldArray', () => { + beforeEach(() => { + renderWithIntl( + + + + + , + , + translationsProperties + ); + }); + + test('renders the document filters button', async () => { + await Button('Add filter').exists(); + }); + + // TODO mock DocumentFilterField + // Test adding renders card as expected + // Test card delete removes an item + // Test card heading shows right label + // Test multiples are separated by OR (check this scales with right number of ORs for 2/3/4 items) +}); diff --git a/lib/DocumentFilter/DocumentFilterForm.js b/lib/DocumentFilter/DocumentFilterForm.js index 502640b5..f8a3e14b 100644 --- a/lib/DocumentFilter/DocumentFilterForm.js +++ b/lib/DocumentFilter/DocumentFilterForm.js @@ -1,18 +1,64 @@ +import { useState } from 'react'; import PropTypes from 'prop-types'; -import { Button } from '@folio/stripes/components'; -import arrayMutators from 'final-form-arrays'; -import { FormModal } from '@k-int/stripes-kint-components'; import { FormattedMessage } from 'react-intl'; + +import arrayMutators from 'final-form-arrays'; + +import { deparseKiwtQueryFilters, FormModal } from '@k-int/stripes-kint-components'; + +import { Button } from '@folio/stripes/components'; + import DocumentFilterFieldArray from './DocumentFilterFieldArray'; const DocumentFilterForm = ({ - atTypeValues, - editingFilters, + activeFilters, + categoryValues, + filterHandlers, + filterModalProps, + filterName, filters, - handlers: { openEditModal, closeEditModal }, - onSubmit, }) => { - const filterBuilder = atTypeValues.length > 0 ? 'supplementaryDocumentFilterBuilder' : 'coreDocumentFilterBuilder'; + // categoryValues are only passed for SupplementaryDocumentFilter + const [editingFilters, setEditingFilters] = useState(false); + const openEditModal = () => setEditingFilters(true); + const closeEditModal = () => setEditingFilters(false); + + const handleSubmit = (values) => { + // In order to convert the form values into the shape for them to be deparsed we do the inverse of the above + // Adding a || operator between all elements of the filters array and a && operator between all elements of the nested arrays + // With special logic to ensure that operators are not added infront of the first elements of any arrays, to ensure no grouping errors + const kiwtQueryShape = values?.filters?.reduce((acc, curr, index) => { + let newAcc = [...acc]; + + if (index !== 0) { + newAcc = [...newAcc, '||']; + } + + newAcc = [ + ...newAcc, + curr.rules.reduce((a, c, i) => { + return [ + ...a, + i !== 0 ? '&&' : null, // Don't group on first entry + c, + ].filter(Boolean); + }, []), + ]; + + return newAcc; + }, []); + + filterHandlers.state({ + ...activeFilters, + [filterName]: [ + // Currently the deparse function returns a query string containing whitespace which leads to grouping errors + // This regex removes all whitespace from the querystring + deparseKiwtQueryFilters(kiwtQueryShape), + ], + }); + closeEditModal(); + }; + return ( <>