Skip to content

Commit eba73bc

Browse files
MC-1659: display section items per section (#1229)
1 parent 3663309 commit eba73bc

File tree

10 files changed

+357
-134
lines changed

10 files changed

+357
-134
lines changed

src/_shared/components/CardActionButtonRow/CardActionButtonRow.test.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,35 @@ describe('The CardActionButtonRow component', () => {
5151
userEvent.click(moveToBottomButton);
5252
expect(onMoveToBottom).toHaveBeenCalled();
5353

54+
//assert for reject button and onReject callback
55+
const rejectButton = screen.getByRole('button', {
56+
name: 'reject',
57+
});
58+
expect(rejectButton).toBeInTheDocument();
59+
userEvent.click(rejectButton);
60+
expect(onReject).toHaveBeenCalled();
61+
});
62+
it('should only render card actions that are passed', () => {
63+
render(<CardActionButtonRow onEdit={onEdit} onReject={onReject} />);
64+
65+
// assert edit button is present and calls its callback
66+
const editButton = screen.getByRole('button', { name: 'edit' });
67+
expect(editButton).toBeInTheDocument();
68+
userEvent.click(editButton);
69+
expect(onEdit).toHaveBeenCalled();
70+
71+
// assert unscheduleButton button is NOT present
72+
const unscheduleButton = screen.queryByLabelText('unschedule');
73+
expect(unscheduleButton).not.toBeInTheDocument();
74+
75+
// assert re-schedule button is NOT present
76+
const rescheduleButton = screen.queryByLabelText('re-schedule');
77+
expect(rescheduleButton).not.toBeInTheDocument();
78+
79+
// assert move to bottom button is is NOT present
80+
const moveToBottomButton = screen.queryByLabelText('move to bottom');
81+
expect(moveToBottomButton).not.toBeInTheDocument();
82+
5483
//assert for reject button and onReject callback
5584
const rejectButton = screen.getByRole('button', {
5685
name: 'reject',

src/_shared/components/CardActionButtonRow/CardActionButtonRow.tsx

+36-30
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ interface CardActionButtonRowProps {
1212
/**
1313
* Callback for the "Unschedule" button
1414
*/
15-
onUnschedule: VoidFunction;
15+
onUnschedule?: VoidFunction;
1616

1717
/**
1818
* Callback for the "Reschedule" button
1919
*/
20-
onReschedule: VoidFunction;
20+
onReschedule?: VoidFunction;
2121

2222
/**
2323
* Callback for the "Edit" button
@@ -27,7 +27,7 @@ interface CardActionButtonRowProps {
2727
/**
2828
* Callback for the "Move to bottom" button
2929
*/
30-
onMoveToBottom: VoidFunction;
30+
onMoveToBottom?: VoidFunction;
3131

3232
/**
3333
* Callback for the "Reject" (trash) button
@@ -60,15 +60,17 @@ export const CardActionButtonRow: React.FC<CardActionButtonRowProps> = (
6060
</IconButton>
6161
</Tooltip>
6262

63-
<Tooltip title="Move to bottom" placement="bottom">
64-
<IconButton
65-
aria-label="move to bottom"
66-
onClick={onMoveToBottom}
67-
sx={{ color: curationPalette.jetBlack }}
68-
>
69-
<KeyboardDoubleArrowDownOutlinedIcon />
70-
</IconButton>
71-
</Tooltip>
63+
{onMoveToBottom && (
64+
<Tooltip title="Move to bottom" placement="bottom">
65+
<IconButton
66+
aria-label="move to bottom"
67+
onClick={onMoveToBottom}
68+
sx={{ color: curationPalette.jetBlack }}
69+
>
70+
<KeyboardDoubleArrowDownOutlinedIcon />
71+
</IconButton>
72+
</Tooltip>
73+
)}
7274

7375
<Tooltip title="Edit" placement="bottom">
7476
<IconButton
@@ -80,27 +82,31 @@ export const CardActionButtonRow: React.FC<CardActionButtonRowProps> = (
8082
</IconButton>
8183
</Tooltip>
8284

83-
<Tooltip title="Re-schedule" placement="bottom">
84-
<IconButton
85-
aria-label="re-schedule"
86-
onClick={onReschedule}
87-
sx={{ color: curationPalette.jetBlack }}
88-
>
89-
<ScheduleIcon />
90-
</IconButton>
91-
</Tooltip>
85+
{onReschedule && (
86+
<Tooltip title="Re-schedule" placement="bottom">
87+
<IconButton
88+
aria-label="re-schedule"
89+
onClick={onReschedule}
90+
sx={{ color: curationPalette.jetBlack }}
91+
>
92+
<ScheduleIcon />
93+
</IconButton>
94+
</Tooltip>
95+
)}
9296
</Stack>
9397

9498
<Stack direction="row" justifyContent="flex-start">
95-
<Tooltip title="Unschedule" placement="bottom">
96-
<IconButton
97-
aria-label="unschedule"
98-
onClick={onUnschedule}
99-
sx={{ color: curationPalette.jetBlack }}
100-
>
101-
<EventBusyOutlinedIcon />
102-
</IconButton>
103-
</Tooltip>
99+
{onUnschedule && (
100+
<Tooltip title="Unschedule" placement="bottom">
101+
<IconButton
102+
aria-label="unschedule"
103+
onClick={onUnschedule}
104+
sx={{ color: curationPalette.jetBlack }}
105+
>
106+
<EventBusyOutlinedIcon />
107+
</IconButton>
108+
</Tooltip>
109+
)}
104110
</Stack>
105111
</Stack>
106112
);

src/curated-corpus/components/ScheduledItemCardWrapper/ScheduledItemCardWrapper.tsx

+12-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
} from '../../../api/generatedTypes';
77

88
import { StyledScheduledItemCard } from '../../../_shared/styled';
9-
import { SuggestedScheduleItemListCard } from '../SuggestedScheduleItemListCard/SuggestedScheduleItemListCard';
9+
import { StoryItemListCard } from '../StoryItemListCard/StoryItemListCard';
10+
import { CardActionButtonRow } from '../../../_shared/components';
1011

1112
interface ScheduledItemCardWrapperProps {
1213
/**
@@ -67,16 +68,20 @@ export const ScheduledItemCardWrapper: React.FC<
6768
return (
6869
<Grid item xs={12} sm={6} md={3}>
6970
<StyledScheduledItemCard variant="outlined">
70-
<SuggestedScheduleItemListCard
71+
<StoryItemListCard
7172
item={item.approvedItem}
73+
cardActionButtonRow={
74+
<CardActionButtonRow
75+
onEdit={onEdit}
76+
onUnschedule={onUnschedule}
77+
onReschedule={onReschedule}
78+
onMoveToBottom={onMoveToBottom}
79+
onReject={onReject}
80+
/>
81+
}
7282
isMlScheduled={item.source === ActivitySource.Ml}
7383
currentScheduledDate={currentScheduledDate}
7484
scheduledSurfaceGuid={scheduledSurfaceGuid}
75-
onEdit={onEdit}
76-
onUnschedule={onUnschedule}
77-
onReschedule={onReschedule}
78-
onMoveToBottom={onMoveToBottom}
79-
onReject={onReject}
8085
/>
8186
</StyledScheduledItemCard>
8287
</Grid>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { SectionDetails } from './SectionDetails';
4+
import {
5+
ActivitySource,
6+
ApprovedCorpusItem,
7+
Section,
8+
} from '../../../api/generatedTypes';
9+
import { getTestApprovedItem } from '../../helpers/approvedItem';
10+
11+
describe('The SectionDetails component', () => {
12+
const item: ApprovedCorpusItem = getTestApprovedItem();
13+
const mockSections: Section[] = [
14+
{
15+
externalId: '1',
16+
title: 'Section 1',
17+
active: true,
18+
sectionItems: [
19+
{
20+
externalId: 'item-1',
21+
approvedItem: item,
22+
createdAt: Date.now(),
23+
updatedAt: Date.now(),
24+
},
25+
],
26+
createdAt: Date.now(),
27+
updatedAt: Date.now(),
28+
createSource: ActivitySource.Ml,
29+
scheduledSurfaceGuid: 'NEW_TAB_EN_US',
30+
},
31+
{
32+
externalId: '2',
33+
title: 'Section 2',
34+
active: false,
35+
sectionItems: [
36+
{
37+
externalId: 'item-2',
38+
approvedItem: item,
39+
createdAt: Date.now(),
40+
updatedAt: Date.now(),
41+
},
42+
],
43+
createdAt: Date.now(),
44+
updatedAt: Date.now(),
45+
createSource: ActivitySource.Ml,
46+
scheduledSurfaceGuid: 'NEW_TAB_EN_US',
47+
},
48+
];
49+
50+
it('should render all sections when currentSection is "all"', () => {
51+
render(
52+
<SectionDetails
53+
sections={mockSections}
54+
currentSection="all"
55+
currentScheduledSurfaceGuid="NEW_TAB_EN_US"
56+
/>,
57+
);
58+
59+
expect(screen.getByText('Section 1')).toBeInTheDocument();
60+
expect(screen.getByText('Section 2')).toBeInTheDocument();
61+
});
62+
63+
it('should render only the selected section', () => {
64+
render(
65+
<SectionDetails
66+
sections={mockSections}
67+
currentSection="Section 1"
68+
currentScheduledSurfaceGuid="NEW_TAB_EN_US"
69+
/>,
70+
);
71+
72+
expect(screen.getByText('Section 1')).toBeInTheDocument();
73+
expect(screen.queryByText('Section 2')).not.toBeInTheDocument();
74+
});
75+
76+
it('should not render sections when there are no matching sections', () => {
77+
render(
78+
<SectionDetails
79+
sections={mockSections}
80+
currentSection="section-does-not-exist"
81+
currentScheduledSurfaceGuid="NEW_TAB_EN_US"
82+
/>,
83+
);
84+
85+
expect(screen.queryByText('Section 1')).not.toBeInTheDocument();
86+
expect(screen.queryByText('Section 2')).not.toBeInTheDocument();
87+
});
88+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, { ReactElement } from 'react';
2+
import { Section, SectionItem } from '../../../api/generatedTypes';
3+
import { Box, Grid } from '@mui/material';
4+
import { SectionItemCardWrapper } from '../SectionItemCardWrapper/SectionItemCardWrapper';
5+
6+
interface SectionDetailsProps {
7+
/**
8+
* All sections for a selected scheduled surface
9+
*/
10+
sections: Section[];
11+
/**
12+
* The current section selected by user
13+
*/
14+
currentSection: string;
15+
/**
16+
* The current surface selected by user
17+
*/
18+
currentScheduledSurfaceGuid: string;
19+
}
20+
export const SectionDetails: React.FC<SectionDetailsProps> = (
21+
props,
22+
): ReactElement => {
23+
const { sections, currentSection, currentScheduledSurfaceGuid } = props;
24+
return (
25+
<>
26+
{sections &&
27+
sections
28+
.filter(
29+
(section) =>
30+
currentSection === 'all' || section.title === currentSection,
31+
)
32+
.map((section: Section) => (
33+
<>
34+
<Box mt={9} mb={3}>
35+
<h2>{section.title}</h2>
36+
<p>{section.active}</p>
37+
</Box>
38+
<Grid
39+
container
40+
alignItems="stretch"
41+
spacing={3}
42+
justifyContent="flex-start"
43+
key={section.externalId}
44+
>
45+
<Grid item xs={12}>
46+
<Grid container spacing={2}>
47+
{section.sectionItems.map((item: SectionItem) => {
48+
{
49+
return (
50+
<SectionItemCardWrapper
51+
key={item.externalId}
52+
item={item.approvedItem}
53+
// TODO: wire up edit action https://mozilla-hub.atlassian.net/browse/MC-1656
54+
onEdit={() => {}}
55+
// TODO: wire up reject/remove action https://mozilla-hub.atlassian.net/browse/MC-1656
56+
onReject={() => {}}
57+
scheduledSurfaceGuid={currentScheduledSurfaceGuid}
58+
/>
59+
);
60+
}
61+
})}
62+
</Grid>
63+
</Grid>
64+
</Grid>
65+
</>
66+
))}
67+
</>
68+
);
69+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { ReactElement } from 'react';
2+
import { Grid } from '@mui/material';
3+
import {
4+
ApprovedCorpusItem,
5+
CorpusItemSource,
6+
} from '../../../api/generatedTypes';
7+
8+
import { StyledScheduledItemCard } from '../../../_shared/styled';
9+
import { StoryItemListCard } from '../StoryItemListCard/StoryItemListCard';
10+
import { CardActionButtonRow } from '../../../_shared/components';
11+
12+
interface SectionItemCardWrapperProps {
13+
/**
14+
* An approved corpus item
15+
*/
16+
item: ApprovedCorpusItem;
17+
18+
/**
19+
* Callback for the "Edit" button
20+
*/
21+
onEdit: VoidFunction;
22+
23+
/**
24+
* Callback for the "Reject" (trash) button
25+
*/
26+
onReject: VoidFunction;
27+
28+
/**
29+
* The surface the card is displayed on, e.g. EN_US
30+
*/
31+
scheduledSurfaceGuid: string;
32+
}
33+
34+
export const SectionItemCardWrapper: React.FC<SectionItemCardWrapperProps> = (
35+
props,
36+
): ReactElement => {
37+
const { item, onEdit, onReject, scheduledSurfaceGuid } = props;
38+
39+
return (
40+
<Grid item xs={12} sm={6} md={3}>
41+
<StyledScheduledItemCard variant="outlined">
42+
<StoryItemListCard
43+
item={item}
44+
cardActionButtonRow={
45+
<CardActionButtonRow onEdit={onEdit} onReject={onReject} />
46+
}
47+
isMlScheduled={item.source === CorpusItemSource.Ml}
48+
scheduledSurfaceGuid={scheduledSurfaceGuid}
49+
/>
50+
</StyledScheduledItemCard>
51+
</Grid>
52+
);
53+
};

0 commit comments

Comments
 (0)