Skip to content

Commit

Permalink
MC-1341: Add "Filter by Topics" to prospecting page (#1218)
Browse files Browse the repository at this point in the history
* MC-1342: add filter by topics in prospecting page

* handle approvedCorpusItem topic filtering logic
  • Loading branch information
katerinachinnappan authored Oct 1, 2024
1 parent 75083bf commit c045562
Show file tree
Hide file tree
Showing 14 changed files with 676 additions and 236 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '@mui/material/styles';
import theme from '../../../theme';
import { ScheduleDayFilter } from '..';
import { DropDownFilter } from '..';
import { Maybe } from '../../../api/generatedTypes';
import { getDisplayTopic, getGroupedTopicData } from '../../helpers/topics';
import { scheduledItems } from '../../integration-test-mocks/getScheduledItems';

describe('The ScheduleDayFilter component', () => {
describe('The DropDownFilter component', () => {
// Extract all topics from scheduled item data
const topics =
scheduledItems.map(
Expand All @@ -23,7 +23,7 @@ describe('The ScheduleDayFilter component', () => {
render(
<MemoryRouter>
<ThemeProvider theme={theme}>
<ScheduleDayFilter
<DropDownFilter
filterData={topicList}
filterName={'Topics'}
itemCount={60}
Expand All @@ -33,34 +33,32 @@ describe('The ScheduleDayFilter component', () => {
</MemoryRouter>,
);

// This is the name of the filter: it is visible on the page
// Check for the button text
const button = screen.getByText(/Topics/i);
expect(button).toBeInTheDocument();

// Let's open up the dropdown
// Open the dropdown
userEvent.click(button);

// Check that each topic displayed has a name
// Verify topic menu items
topicList.forEach((topic) => {
const menuOption = screen.getByText(new RegExp(topic.name, 'i'));
expect(menuOption).toBeInTheDocument();

// If there are no stories for a given topic, this menu option
// should be disabled.
// Check if the topic option is disabled if no stories for topic found
if (topic.count === 0) {
expect(menuOption).toHaveAttribute('aria-disabled');
} else {
expect(menuOption).not.toHaveAttribute('aria-disabled');
}
});
});

it('should call the "setFilters" function when a menu option is chosen', () => {
it('should call the "setFilters" function', async () => {
const setFilters = jest.fn();
render(
<MemoryRouter>
<ThemeProvider theme={theme}>
<ScheduleDayFilter
<DropDownFilter
filterData={topicList}
filterName={'Topics'}
itemCount={60}
Expand All @@ -70,12 +68,19 @@ describe('The ScheduleDayFilter component', () => {
</MemoryRouter>,
);

// Let's open up the dropdown
userEvent.click(screen.getByText(/Topics/i));
const button = screen.getByText(/Topics/i);
expect(button).toBeInTheDocument();

// And click on a topic
userEvent.click(screen.getByText(/Personal Finance/i));
// Open the dropdown
userEvent.click(button);

expect(setFilters).toHaveBeenCalled();
// Check each topic option for its state and click if enabled
topicList.forEach((topic) => {
const menuOption = screen.getByText(new RegExp(topic.name, 'i'));
if (!menuOption.closest('li')?.hasAttribute('aria-disabled')) {
userEvent.click(menuOption);
expect(setFilters).toHaveBeenCalled();
}
});
});
});
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
import React, { ReactElement } from 'react';
import React, { ReactElement, useState } from 'react';
import { Button } from '../../../_shared/components';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';

import { ScheduleDayFilterOptions, ScheduleSummary } from '../';
import { ScheduleDayFilterOptions, StoriesSummary } from '../';
import { ProspectFilterOptions } from '../ProspectFilters/ProspectFilters';
import { StyledMenu } from '../../../_shared/styled';
import { MenuItem } from '@mui/material';

interface ScheduleDayFilterProps {
interface DropDownFilterProps {
/**
* Options to show in the filter dropdown.
*/
filterData: ScheduleSummary[];

filterData: StoriesSummary[];
/**
* The copy to show on the filter button, e.g. "Topics" or "Publishers"
*/
filterName: string;

/**
* For the "All items" option (default): how many items there are.
*/
itemCount: number;

/**
* Callback to set filters on the Schedule Page
* Callback to set filters on the Schedule Page or Prospecting Page
*/
setFilters: React.Dispatch<React.SetStateAction<ScheduleDayFilterOptions>>;
setFilters: React.Dispatch<
React.SetStateAction<ScheduleDayFilterOptions | ProspectFilterOptions>
>;
}

/**
Expand All @@ -37,17 +36,25 @@ interface ScheduleDayFilterProps {
* @param props
* @constructor
*/
export const ScheduleDayFilter: React.FC<ScheduleDayFilterProps> = (
export const DropDownFilter: React.FC<DropDownFilterProps> = (
props,
): ReactElement => {
const { filterData, filterName, itemCount, setFilters } = props;

// State management for the dropdown menu options
// (lifted from the docs: https://mui.com/material-ui/react-menu/)
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [selectedIndex, setSelectedIndex] = React.useState(-1);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [, setSelectedOption] = useState<string>('All');
const open = Boolean(anchorEl);

// Function to get the total number of available filter options (don't include filters with 0 data points)
const getAvailableFilterCount = () => {
return filterData.filter((filter) => filter.count > 0).length;
};

const availableFilterCount = getAvailableFilterCount();

const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
Expand All @@ -58,12 +65,13 @@ export const ScheduleDayFilter: React.FC<ScheduleDayFilterProps> = (
value: string,
) => {
setSelectedIndex(index);
setSelectedOption(value);

setFilters((filters: any) => {
// Reset each filter to 'All' before applying the current filter
for (const prop in filters) {
filters[prop] = 'All';
}

// Apply the current filter
return { ...filters, [filterName]: value };
});
Expand All @@ -87,7 +95,8 @@ export const ScheduleDayFilter: React.FC<ScheduleDayFilterProps> = (
onClick={handleClick}
sx={{ textTransform: 'capitalize' }}
>
{filterName} {filterData.length}
{/* Display filter name and count */}
{filterName} {availableFilterCount}
</Button>

<StyledMenu
Expand All @@ -109,29 +118,25 @@ export const ScheduleDayFilter: React.FC<ScheduleDayFilterProps> = (
>
<MenuItem
disableRipple
selected={-1 === selectedIndex}
selected={selectedIndex === -1}
onClick={(event) => handleMenuItemClick(event, -1, 'All')}
>
All {itemCount}
</MenuItem>

{filterData.map((filter, index) => {
return (
<MenuItem
disabled={filter.count < 1}
disableRipple
key={filter.name}
selected={index === selectedIndex}
onClick={(event) =>
handleMenuItemClick(event, index, filter.name)
}
>
{/* Capitalise the ML filter only - as we filter by an object property (string),
{filterData.map((filter, index) => (
<MenuItem
disabled={filter.count < 1}
disableRipple
key={filter.name}
selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index, filter.name)}
>
{/* Capitalise the ML filter only - as we filter by an object property (string),
we need to keep the type name ("Ml") as it comes from the graph everywhere else */}
{filter.name === 'Ml' ? 'ML' : filter.name} {filter.count}
</MenuItem>
);
})}
{filter.name === 'Ml' ? 'ML' : filter.name} {filter.count}
</MenuItem>
))}
</StyledMenu>
</>
);
Expand Down
169 changes: 169 additions & 0 deletions src/curated-corpus/components/ProspectFilters/ProspectFilters.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ProspectFilters } from './ProspectFilters';
import {
CorpusItemSource,
CorpusLanguage,
CuratedStatus,
Prospect,
Topics,
} from '../../../api/generatedTypes';
import { StoriesSummary } from '../ScheduleSummaryCard/ScheduleSummaryCard';
import { DateTime } from 'luxon';

describe('The ProspectFilters component', () => {
const prospects: Prospect[] = [
{
id: '123-abc',
prospectId: '456-dfg',
title: 'How To Win Friends And Influence People with DynamoDB',
scheduledSurfaceGuid: 'NEW_TAB_EN_US',
prospectType: 'organic-timespent',
url: 'http://www.test.com/how-to',
imageUrl: 'https://placeimg.com/640/480/people?random=495',
excerpt:
'Everything You Wanted to Know About DynamoDB and Were Afraid To Ask',
language: CorpusLanguage.En,
publisher: 'Amazing Inventions',
authors: 'Charles Dickens,O. Henry',
topic: Topics.Technology,
saveCount: 111222,
isSyndicated: false,
},
{
id: '456-def',
prospectId: '123-bc',
title:
'How We Discovered That People Who Are Colorblind Are Less Likely to Be Picky Eaters',
scheduledSurfaceGuid: 'NEW_TAB_EN_US',
prospectType: 'organic-timespent',
url: 'http://www.test.com/how-to',
imageUrl: 'https://placeimg.com/640/480/people?random=495',
excerpt:
'The seventh season of Julia Child’s “The French Chef,” the first of the television series to ' +
'air in color, revealed how color can change the experience of food.',
language: CorpusLanguage.En,
publisher: 'The Conversation',
authors: 'Jason Parham',
topic: Topics.Food,
saveCount: 111222,
isSyndicated: false,
},
{
id: '523-zcf',
prospectId: '764-fgh',
title: 'The Strange Theft of a Priceless Churchill Portrait',
scheduledSurfaceGuid: 'NEW_TAB_EN_US',
prospectType: 'organic-timespent',
url: 'https://thewalrus.ca/churchill-portrait/',
imageUrl: 'https://placeimg.com/640/480/people?random=495',
excerpt:
'The inside story of one of Canada’s most brazen, baffling, and mysterious art heists and how the police cracked it.',
language: CorpusLanguage.En,
publisher: 'The Walrus',
authors: 'Brett Popplewell',
saveCount: 111222,
isSyndicated: false,
approvedCorpusItem: {
externalId: '123-abc',
createdBy: 'test-user',
hasTrustedDomain: true,
isTimeSensitive: false,
source: CorpusItemSource.Manual,
status: CuratedStatus.Recommendation,
createdAt: DateTime.local().millisecond,
updatedAt: DateTime.local().millisecond,
scheduledSurfaceHistory: [],
authors: [{ name: 'Brett Popplewell', sortOrder: 0 }],
title: 'The Strange Theft of a Priceless Churchill Portrait',
url: 'https://thewalrus.ca/churchill-portrait/',
excerpt:
'The inside story of one of Canada’s most brazen, baffling, and mysterious art heists and how the police cracked it.',
imageUrl: 'https://placeimg.com/640/480/people?random=495',
language: CorpusLanguage.En,
publisher: 'The Walrus',
topic: Topics.Education,
isCollection: false,
isSyndicated: false,
},
},
];
let excludePublisherSwitch = true;
const filterByPublisher = '';
const setFilterByPublisher = jest.fn();
const onChange = jest.fn();
const onSortByPublishedDate = jest.fn();
const sortByPublishedDate = false;
const sortByTimeToRead = false;
const handleSortByTimeToRead = jest.fn();

// const mockSetFilters = jest.fn();
//
// const filterData: StoriesSummary[] = [
// { name: 'Topic 1', count: 10 },
// { name: 'Topic 2', count: 20 },
// { name: 'Topic 3', count: 0 },
// ];
const renderComponent = () => {
render(
<ProspectFilters
setProspectMetadataFilters={jest.fn()}
prospects={prospects}
excludePublisherSwitch={excludePublisherSwitch}
filterByPublisher={filterByPublisher}
setFilterByPublisher={setFilterByPublisher}
onChange={onChange}
onSortByPublishedDate={onSortByPublishedDate}
sortByPublishedDate={sortByPublishedDate}
sortByTimeToRead={sortByTimeToRead}
handleSortByTimeToRead={handleSortByTimeToRead}
/>,
);
};

it('renders the "exclude" label correctly', () => {
renderComponent();

// The default switch label is present
const switchLabel = screen.getByText(/exclude/i);
expect(switchLabel).toBeInTheDocument();

// the text field is present
const filterByField = screen.getByLabelText(/filter by publisher/i);
expect(filterByField).toBeInTheDocument();
});

it('renders the "include" label correctly', () => {
excludePublisherSwitch = false;

renderComponent();

// The "include" switch label is present
const switchLabel = screen.getByText(/include/i);
expect(switchLabel).toBeInTheDocument();
});

it('should render the sort by published date filter', () => {
renderComponent();

const publishedDate = screen.getByText(/Published Date/i);
expect(publishedDate).toBeInTheDocument();
});

it('should render the sort by time to read filter', () => {
renderComponent();

const timeToRead = screen.getByText(/Time to Read/i);
expect(timeToRead).toBeInTheDocument();
});

it('should render the topic filter', () => {
renderComponent();

// should be 3 different topics
// last prospect has prospect.topic set to null, but has an
// approvedCorpusItem which has the topic set.
const topicFilter = screen.getByText(/Topics 3/i);
expect(topicFilter).toBeInTheDocument();
});
});
Loading

0 comments on commit c045562

Please sign in to comment.