Skip to content

Commit

Permalink
Merge pull request #239 from opossum-tool/multiselect
Browse files Browse the repository at this point in the history
Add multiselect in place of checkbox for the filters
  • Loading branch information
nicarl authored Dec 8, 2021
2 parents 69eefa8 + d94d33a commit 8a67c80
Show file tree
Hide file tree
Showing 13 changed files with 447 additions and 170 deletions.
2 changes: 2 additions & 0 deletions src/Frontend/Components/AttributionList/AttributionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const useStyles = makeStyles({
marginLeft: 5,
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
});

Expand Down
34 changes: 5 additions & 29 deletions src/Frontend/Components/AttributionView/AttributionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@ import makeStyles from '@mui/styles/makeStyles';
import React, { ReactElement } from 'react';
import { useAppDispatch, useAppSelector } from '../../state/hooks';
import { Attributions } from '../../../shared/shared-types';
import { FilterType, PackagePanelTitle } from '../../enums/enums';
import { PackagePanelTitle } from '../../enums/enums';
import { changeSelectedAttributionIdOrOpenUnsavedPopup } from '../../state/actions/popup-actions/popup-actions';
import {
getAttributionIdMarkedForReplacement,
getManualAttributions,
} from '../../state/selectors/all-views-resource-selectors';
import { getSelectedAttributionId } from '../../state/selectors/attribution-view-resource-selectors';
import { provideFollowUpFilter } from '../../util/provide-follow-up-filter';
import { useFilters } from '../../util/use-filters';
import { useWindowHeight } from '../../util/use-window-height';
import { AttributionDetailsViewer } from '../AttributionDetailsViewer/AttributionDetailsViewer';
import { AttributionList } from '../AttributionList/AttributionList';
import { Checkbox } from '../Checkbox/Checkbox';
import { OpossumColors } from '../../shared-styles';
import { topBarHeight } from '../TopBar/TopBar';
import { getActiveFilters } from '../../state/selectors/view-selector';
import { FilterMultiSelect } from '../Filter/FilterMultiSelect';

const countAndSearchOffset = 119;

Expand All @@ -35,11 +34,6 @@ const useStyles = makeStyles({
width: '30%',
margin: 5,
},
checkBox: {
display: 'flex',
alignItems: 'center',
marginLeft: 'auto',
},
});

export function AttributionView(): ReactElement {
Expand All @@ -52,26 +46,15 @@ export function AttributionView(): ReactElement {
const attributionIdMarkedForReplacement: string = useAppSelector(
getAttributionIdMarkedForReplacement
);
const activeFilters = useAppSelector(getActiveFilters);
const filterForFollowUp = activeFilters.has(FilterType.OnlyFollowUp);

const { handleFilterChange, getFilteredAttributions } = provideFollowUpFilter(
filterForFollowUp,
dispatch
);

function onCardClick(attributionId: string): void {
dispatch(changeSelectedAttributionIdOrOpenUnsavedPopup(attributionId));
}

const filteredAttributions = getFilteredAttributions(attributions);
const filteredAttributions = useFilters(attributions);
const title = `${PackagePanelTitle.AllAttributions} (${
Object.keys(attributions).length
})`;
const checkBoxLabel = `Show only follow-up (${
Object.values(attributions).filter((attribution) => attribution.followUp)
.length
})`;

return (
<div className={classes.root}>
Expand All @@ -83,14 +66,7 @@ export function AttributionView(): ReactElement {
className={classes.attributionList}
maxHeight={useWindowHeight() - topBarHeight - countAndSearchOffset}
title={title}
topRightElement={
<Checkbox
label={checkBoxLabel}
checked={filterForFollowUp}
onChange={handleFilterChange}
className={classes.checkBox}
/>
}
topRightElement={<FilterMultiSelect />}
/>
<AttributionDetailsViewer />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import {
FollowUp,
ResourcesToAttributions,
} from '../../../../shared/shared-types';
import { View } from '../../../enums/enums';
import { FilterType, View } from '../../../enums/enums';
import { loadFromFile } from '../../../state/actions/resource-actions/load-actions';
import { navigateToView } from '../../../state/actions/view-actions/view-actions';
import { renderComponentWithStore } from '../../../test-helpers/render-component-with-store';
import {
clickOnCheckbox,
clickOnFilter,
getParsedInputFileEnrichedWithTestData,
openDropDown,
} from '../../../test-helpers/general-test-helpers';
import { AttributionView } from '../AttributionView';
import { IpcRenderer } from 'electron';
Expand Down Expand Up @@ -49,6 +50,7 @@ describe('The Attribution View', () => {
packageVersion: '1.0',
copyright: 'Copyright John Doe',
licenseText: 'Some license text',
firstParty: true,
};
testResourcesToManualAttributions['test resource'] = [testManualUuid];
testManualAttributions[testOtherManualUuid] = {
Expand Down Expand Up @@ -103,9 +105,61 @@ describe('The Attribution View', () => {
expect(screen.getByText('Test package, 1.0'));
expect(screen.getByText('Test other package, 2.0'));

clickOnCheckbox(screen, 'Show only follow-up (1)');
openDropDown(screen);
clickOnFilter(screen, FilterType.OnlyFollowUp);

expect(screen.getByText('Test other package, 2.0'));
expect(screen.queryByText('Test package, 1.0')).toBe(null);
});

test('filters Only First Party', () => {
const { store } = renderComponentWithStore(<AttributionView />);
store.dispatch(
loadFromFile(
getParsedInputFileEnrichedWithTestData({
resources: { ['test resource']: 1 },
manualAttributions: testManualAttributions,
resourcesToManualAttributions: testResourcesToManualAttributions,
})
)
);
store.dispatch(navigateToView(View.Attribution));
expect(screen.getByText('All Attributions (2)'));
expect(screen.getByText('Test package, 1.0'));
expect(screen.getByText('Test other package, 2.0'));

openDropDown(screen);
clickOnFilter(screen, FilterType.OnlyFirstParty);

expect(screen.getByText('Test package, 1.0'));
expect(screen.queryByText('Test other package, 2.0')).toBe(null);
});

test('filters Only First Party and follow ups and then hide first party and follow ups', () => {
const { store } = renderComponentWithStore(<AttributionView />);
store.dispatch(
loadFromFile(
getParsedInputFileEnrichedWithTestData({
resources: { ['test resource']: 1 },
manualAttributions: testManualAttributions,
resourcesToManualAttributions: testResourcesToManualAttributions,
})
)
);
store.dispatch(navigateToView(View.Attribution));
expect(screen.getByText('All Attributions (2)'));
expect(screen.getByText('Test package, 1.0'));
expect(screen.getByText('Test other package, 2.0'));

openDropDown(screen);
clickOnFilter(screen, FilterType.OnlyFirstParty);
clickOnFilter(screen, FilterType.OnlyFollowUp);

expect(screen.queryByText('Test package, 1.0')).toBe(null);
expect(screen.queryByText('Test other package, 2.0')).toBe(null);

clickOnFilter(screen, FilterType.HideFirstParty);
expect(screen.getByText('Test other package, 2.0'));
expect(screen.queryByText('Test package, 1.0')).toBe(null);
});
});
117 changes: 117 additions & 0 deletions src/Frontend/Components/Filter/FilterMultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: Facebook, Inc. and its affiliates
// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
//
// SPDX-License-Identifier: Apache-2.0

import React, { ReactElement } from 'react';
import MuiFormControl from '@mui/material/FormControl';
import MuiInputLabel from '@mui/material/InputLabel';
import MuiMenuItem from '@mui/material/MenuItem';
import MuiSelect from '@mui/material/Select';
import MuiChip from '@mui/material/Chip';
import MuiBox from '@mui/material/Box';
import MuiOutlinedInput from '@mui/material/OutlinedInput';
import { FilterType } from '../../enums/enums';
import { useAppDispatch, useAppSelector } from '../../state/hooks';
import { updateActiveFilters } from '../../state/actions/view-actions/view-actions';
import { getActiveFilters } from '../../state/selectors/view-selector';
import { makeStyles } from '@mui/styles';
import { OpossumColors } from '../../shared-styles';

const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const FILTERS = [
FilterType.OnlyFollowUp,
FilterType.OnlyFirstParty,
FilterType.HideFirstParty,
];

const useStyles = makeStyles({
dropDownForm: {
width: '170px',
margin: '12px 4px',
backgroundColor: OpossumColors.white,
'& fieldset': {
borderRadius: 0,
},
'& label': {
backgroundColor: OpossumColors.white,
padding: '1px 3px',
},
},
dropDownSelect: {
height: '65px',
},
dropDownBox: {
display: 'flex',
flexWrap: 'wrap',
gap: 0.5,
},
});

export function FilterMultiSelect(): ReactElement {
const classes = useStyles();
const dispatch = useAppDispatch();
const activeFilters = Array.from(useAppSelector(getActiveFilters));

const updateFilters = (filter: FilterType): void => {
dispatch(updateActiveFilters(filter));
};

function getMenuItems(): Array<ReactElement> {
return FILTERS.map((filter) => (
<MuiMenuItem
dense
aria-label={filter}
key={filter}
value={filter}
onClick={(event): void => {
updateFilters(event.currentTarget.textContent as FilterType);
}}
>
{filter}
</MuiMenuItem>
));
}

return (
<MuiFormControl className={classes.dropDownForm} size="small">
<MuiInputLabel shrink={true}>Filter</MuiInputLabel>
<MuiSelect
className={classes.dropDownSelect}
data-testid="test-id-filter-multi-select"
multiple
value={activeFilters}
input={<MuiOutlinedInput />}
renderValue={(selectedFilters): ReactElement => (
<MuiBox className={classes.dropDownBox}>
{selectedFilters.map((filter) => (
<MuiChip
key={filter}
label={filter}
size="small"
onMouseDown={(event): void => {
event.preventDefault();
event.stopPropagation();
}}
onDelete={(): void => {
updateFilters(filter);
}}
/>
))}
</MuiBox>
)}
MenuProps={{
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 170,
},
},
}}
>
{getMenuItems()}
</MuiSelect>
</MuiFormControl>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Facebook, Inc. and its affiliates
// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
//
// SPDX-License-Identifier: Apache-2.0

import { FilterMultiSelect } from '../FilterMultiSelect';
import React from 'react';
import { render, screen } from '@testing-library/react';
import {
expectFilterIsShown,
openDropDown,
} from '../../../test-helpers/general-test-helpers';
import { FilterType } from '../../../enums/enums';
import { Provider } from 'react-redux';
import { createAppStore } from '../../../state/configure-store';

describe('FilterMultiSelect', () => {
test('renders the filters in a dropdown', () => {
const store = createAppStore();
render(
<Provider store={store}>
<FilterMultiSelect />
</Provider>
);
openDropDown(screen);
expectFilterIsShown(screen, FilterType.OnlyFirstParty);
expectFilterIsShown(screen, FilterType.OnlyFollowUp);
expectFilterIsShown(screen, FilterType.HideFirstParty);
});
});
Loading

0 comments on commit 8a67c80

Please sign in to comment.