Skip to content

Commit

Permalink
ERM-3332, Add documents filter to agreement line search (#704)
Browse files Browse the repository at this point in the history
* Add translations

* ERM-3332

* feat: DocumentFilter labelling

Allow for default and custom labelling of DocumentFilters. Also added centralised "core documents" and "supplementary documents" labels as we use those terms in multiple modules

ERM-3332

* feat: Pass filterName (defaulted to docs) all the way down

Ensure that we're passing down filterName so that supplementary vs core vs docs can all be set up in the generic component

* style: Rename atTypeValues to categoryValues

Deprecated atTypeValues (but still in place anyway. Use categoryValues instead

* Refactor change orders of props and imports

* fix: Category value field not shifting to Select

 No longer relying on "supplementary docs" to be the name of the field. Instead have `ifCategory` decide whether or not to render the field, then use the passed filterName to work out whether to diplsay value as TextField or Select

* lint

* Add test coverage to DocumentFilters

* lint

* fix: Render OrganizationSelection placeholder (Was never rendering when no option selected)

* fix render Internal contacts placeholder, tweak tests

* test: WIP! Broken the tests to demonstrate a progression of jest testing for the heirachy

* refactor: Move  a bunch of logic down a level to avoid weird component boundaries

* test: Fixed FocumentFilter test (Leaving some for cleanup later)

* test: Added documentFilterParsing to combined testResource file, so that both tests can make use. Improved DocumentForm test

* test: Add test comments for DocumentFilterFieldArray

* test: DocumentFilterField test (Leaving rules testing to M)

* test: Testing DocumentFilterRule (With comments to expand on this later

* chore: Linting

* chore: Prop validation

---------

Co-authored-by: Ethan Freestone <[email protected]>
  • Loading branch information
MonireRasouli and EthanFreestone authored Oct 29, 2024
1 parent dc404b6 commit 7879c28
Show file tree
Hide file tree
Showing 19 changed files with 854 additions and 142 deletions.
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
86 changes: 31 additions & 55 deletions lib/DocumentFilter/DocumentFilter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import { useState } from 'react';

import {
Accordion,
FilterAccordionHeader,
Expand All @@ -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) => {
Expand Down Expand Up @@ -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 (
<Accordion
closedByDefault
displayClearButton={!!parsedFilterData?.length}
header={FilterAccordionHeader}
id={`clickable-agreement-${filterType}-filter`}
label={<FormattedMessage id={`stripes-erm-components.documentFilter.${filterType}`} />}
onClearFilter={() => filterHandlers.state({ ...activeFilters, [filterType]: [] })
id={`clickable-agreement-${filterName}-filter`}
label={filterLabel ?? <FormattedMessage id="stripes-erm-components.documentFilter.documents" />}
onClearFilter={() => filterHandlers.state({ ...activeFilters, [filterName]: [] })
}
separator={false}
>
Expand All @@ -113,14 +85,12 @@ const DocumentFilter = ({ activeFilters, atTypeValues = [], filterHandlers }) =>
</Layout>
)}
<DocumentFilterForm
atTypeValues={atTypeValues}
editingFilters={editingFilters}
activeFilters={activeFilters}
categoryValues={categoryValuesToUse}
filterHandlers={filterHandlers}
filterModalProps={filterModalProps}
filterName={filterName}
filters={parsedFilterData}
handlers={{
closeEditModal,
openEditModal,
}}
onSubmit={handleSubmit}
/>
</Accordion>
);
Expand All @@ -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;
143 changes: 143 additions & 0 deletions lib/DocumentFilter/DocumentFilter.test.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<MockStripesButton onClick={() => mockPFD(filters)}>
Test parsed filter data
</MockStripesButton>
DocumentFilterForm
</div>
);
});

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(
<MemoryRouter>
<DocumentFilter
activeFilters={activeFilterState}
categoryValues={categoryValues}
filterHandlers={filterHandlers}
/>,
</MemoryRouter>,
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
});
13 changes: 10 additions & 3 deletions lib/DocumentFilter/DocumentFilterField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -15,7 +20,8 @@ const DocumentFilterField = ({ atTypeValues, index, name }) => {
<DocumentFilterRule
key={ruleFieldName}
ariaLabelledby={`selected-document-item-name-${index}`}
atTypeValues={atTypeValues}
categoryValues={categoryValues}
filterName={filterName}
index={ruleFieldIndex}
name={ruleFieldName}
onDelete={() => ruleFields.remove(ruleFieldIndex)}
Expand Down Expand Up @@ -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;
Loading

0 comments on commit 7879c28

Please sign in to comment.