Skip to content

Commit

Permalink
feat: implement item flagging
Browse files Browse the repository at this point in the history
  • Loading branch information
abdallah75 committed Jul 27, 2021
1 parent 82c7ad5 commit 27545fc
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 1 deletion.
27 changes: 27 additions & 0 deletions cypress/fixtures/flags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// eslint-disable-next-line import/prefer-default-export
export const SAMPLE_FLAGS = [
{
id: '053d9c35-182e-41d8-9f1f-2f5b443d0fbd',
name: 'Inappropriate Content',
},
{
id: '69f652a7-9c04-4346-b963-004e63c478b9',
name: 'Hate speech',
},
{
id: 'a1ebd159-416a-404b-b893-02a7064454db',
name: 'Fraud / Plagiarism',
},
{
id: '7463afaa-a74e-4a5c-810c-44f9642c87c5',
name: 'Spam',
},
{
id: 'ca6d1841-fabc-4444-b86e-b76af41263c1',
name: 'Targeted Harrasment',
},
{
id: '9baecb0e-3dc3-4191-bbf7-2a89d304600b',
name: 'False Information',
},
];
63 changes: 63 additions & 0 deletions cypress/integration/item/flag/flagItem.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { SAMPLE_ITEMS } from '../../../fixtures/items';
import { HOME_PATH } from '../../../../src/config/paths';
import {
buildFlagListItemId,
buildItemMenu,
buildItemsTableRowId,
FLAG_ITEM_BUTTON_ID,
ITEM_MENU_BUTTON_CLASS,
ITEM_MENU_FLAG_BUTTON_CLASS,
} from '../../../../src/config/selectors';
import { SAMPLE_FLAGS } from '../../../fixtures/flags';

const openFlagItemModal = (itemId) => {
const menuSelector = `#${buildItemsTableRowId(
itemId,
)} .${ITEM_MENU_BUTTON_CLASS}`;

cy.get(menuSelector).click();

const menuFlagButton = cy.get(
`#${buildItemMenu(itemId)} .${ITEM_MENU_FLAG_BUTTON_CLASS}`,
);

menuFlagButton.click();
};

const flagItem = (itemId, flagId) => {
openFlagItemModal(itemId);

const flagListItem = cy.get(`#${buildFlagListItemId(flagId)}`);

flagListItem.click();

const flagItemButton = cy.get(`#${FLAG_ITEM_BUTTON_ID}`);

flagItemButton.click();
};

describe('Flag Item', () => {
beforeEach(() => {
cy.setUpApi(SAMPLE_ITEMS);
cy.visit(HOME_PATH);
});

it('flag item', () => {
const item = SAMPLE_ITEMS.items[0];
const flag = SAMPLE_FLAGS[0];

flagItem(item.id, flag.id);

cy.wait('@postItemFlag').then(
({
request: {
url,
body: { flagId },
},
}) => {
expect(flagId).to.equal(flag.id);
expect(url).to.contain(item.id);
},
);
});
});
9 changes: 9 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ import {
mockGetPublicItem,
mockGetPublicChildren,
mockGetItems,
mockGetFlags,
mockPostItemFlag,
} from './server';
import './commands/item';
import './commands/navigation';
import { CURRENT_USER, MEMBERS } from '../fixtures/members';
import { SAMPLE_FLAGS } from '../fixtures/flags';

Cypress.Commands.add(
'setUpApi',
Expand All @@ -53,6 +56,7 @@ Cypress.Commands.add(
members = Object.values(MEMBERS),
currentMember = CURRENT_USER,
tags = [],
flags = SAMPLE_FLAGS,
deleteItemError = false,
deleteItemsError = false,
postItemError = false,
Expand All @@ -71,6 +75,7 @@ Cypress.Commands.add(
postItemLoginError = false,
putItemLoginError = false,
editMemberError = false,
postItemFlagError = false,
} = {}) => {
const cachedItems = JSON.parse(JSON.stringify(items));
const cachedMembers = JSON.parse(JSON.stringify(members));
Expand Down Expand Up @@ -138,6 +143,10 @@ Cypress.Commands.add(

mockDeleteItemMembershipForItem(items);

mockGetFlags(flags);

mockPostItemFlag(items, postItemFlagError);

mockGetPublicItem(items);

mockGetPublicChildren(items);
Expand Down
30 changes: 30 additions & 0 deletions cypress/support/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ const {
SHARE_ITEM_WITH_ROUTE,
buildEditItemMembershipRoute,
buildDeleteItemMembershipRoute,
buildPostItemFlagRoute,
GET_FLAGS_ROUTE,
} = API_ROUTES;

const API_HOST = Cypress.env('API_HOST');
Expand Down Expand Up @@ -796,3 +798,31 @@ export const mockPostItemTag = (items, shouldThrowError) => {
},
).as('postItemTag');
};

export const mockGetFlags = (flags) => {
cy.intercept(
{
method: DEFAULT_GET.method,
url: new RegExp(`${API_HOST}/${parseStringToRegExp(GET_FLAGS_ROUTE)}$`),
},
({ reply }) => {
reply(flags);
},
).as('getFlags');
};

export const mockPostItemFlag = (items, shouldThrowError) => {
cy.intercept(
{
method: DEFAULT_POST.method,
url: new RegExp(`${API_HOST}/${buildPostItemFlagRoute(ID_FORMAT)}$`),
},
({ reply, body }) => {
if (shouldThrowError) {
return reply({ statusCode: StatusCodes.BAD_REQUEST });
}

return reply(body);
},
).as('postItemFlag');
};
115 changes: 115 additions & 0 deletions src/components/context/FlagItemModalContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MUTATION_KEYS } from '@graasp/query-client';
import PropTypes from 'prop-types';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import List from '@material-ui/core/List';
import { ListItem, ListItemText, makeStyles } from '@material-ui/core';
import Typography from '@material-ui/core/Typography';
import { useMutation, hooks } from '../../config/queryClient';
import {
buildFlagListItemId,
FLAG_ITEM_BUTTON_ID,
} from '../../config/selectors';

const { useFlags } = hooks;

const FlagItemModalContext = React.createContext();

const useStyles = makeStyles(() => ({
list: {
width: '100%',
overflow: 'auto',
maxHeight: 250,
},
listTitle: {
fontSize: 'small',
},
flagItemButton: {
color: 'red',
},
}));

const FlagItemModalProvider = ({ children }) => {
const { t } = useTranslation();
const classes = useStyles();
const { mutate: postFlagItem } = useMutation(MUTATION_KEYS.POST_ITEM_FLAG);
const [open, setOpen] = useState(false);
const [selectedFlag, setSelectedFlag] = useState(false);
const [itemId, setItemId] = useState(false);

const { data: flags } = useFlags();

const openModal = (newItemId) => {
setOpen(true);
setItemId(newItemId);
};

const onClose = () => {
setOpen(false);
setItemId(null);
};

const handleSelect = (flag) => () => setSelectedFlag(flag);

const onFlag = () => {
postFlagItem({
flagId: selectedFlag.id,
itemId,
});
onClose();
};

return (
<FlagItemModalContext.Provider value={{ openModal }}>
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{t('Flag Item')}</DialogTitle>
<DialogContent>
<Typography variant="h6" className={classes.listTitle}>
{`${t('Select reason for flagging this item')}:`}
</Typography>
<List component="nav" className={classes.list}>
{flags?.map((flag) => (
<ListItem
id={buildFlagListItemId(flag.id)}
button
selected={selectedFlag.id === flag.id}
onClick={handleSelect(flag)}
>
<ListItemText primary={flag.name} />
</ListItem>
))}
</List>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
{t('Cancel')}
</Button>
<Button
onClick={onFlag}
className={classes.flagItemButton}
id={FLAG_ITEM_BUTTON_ID}
disabled={!selectedFlag}
>
{t('Flag')}
</Button>
</DialogActions>
</Dialog>
{children}
</FlagItemModalContext.Provider>
);
};

FlagItemModalProvider.propTypes = {
children: PropTypes.node,
};

FlagItemModalProvider.defaultProps = {
children: null,
};

export { FlagItemModalProvider, FlagItemModalContext };
3 changes: 2 additions & 1 deletion src/components/context/ModalProviders.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MoveItemModalProvider } from './MoveItemModalContext';
import { ShareItemModalProvider } from './ShareItemModalContext';
import { LayoutContextProvider } from './LayoutContext';
import { CreateShortcutModalProvider } from './CreateShortcutModalContext';
import { FlagItemModalProvider } from './FlagItemModalContext';

const ModalProviders = ({ children }) => (
<LayoutContextProvider>
Expand All @@ -14,7 +15,7 @@ const ModalProviders = ({ children }) => (
<MoveItemModalProvider>
<ShareItemModalProvider>
<CreateShortcutModalProvider>
{children}
<FlagItemModalProvider>{children}</FlagItemModalProvider>
</CreateShortcutModalProvider>
</ShareItemModalProvider>
</MoveItemModalProvider>
Expand Down
11 changes: 11 additions & 0 deletions src/components/main/ItemMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
buildItemMenu,
ITEM_MENU_BUTTON_CLASS,
ITEM_MENU_COPY_BUTTON_CLASS,
ITEM_MENU_FLAG_BUTTON_CLASS,
ITEM_MENU_MOVE_BUTTON_CLASS,
ITEM_MENU_SHORTCUT_BUTTON_CLASS,
} from '../../config/selectors';
import { CopyItemModalContext } from '../context/CopyItemModalContext';
import { CreateShortcutModalContext } from '../context/CreateShortcutModalContext';
import { MoveItemModalContext } from '../context/MoveItemModalContext';
import { FlagItemModalContext } from '../context/FlagItemModalContext';

const ItemMenu = ({ item }) => {
const [anchorEl, setAnchorEl] = React.useState(null);
Expand All @@ -24,6 +26,7 @@ const ItemMenu = ({ item }) => {
const { openModal: openCreateShortcutModal } = useContext(
CreateShortcutModalContext,
);
const { openModal: openFlagModal } = useContext(FlagItemModalContext);

const handleClick = (event) => {
setAnchorEl(event.currentTarget);
Expand All @@ -48,6 +51,11 @@ const ItemMenu = ({ item }) => {
handleClose();
};

const handleFlag = () => {
openFlagModal(item.id);
handleClose();
};

return (
<>
<IconButton className={ITEM_MENU_BUTTON_CLASS} onClick={handleClick}>
Expand All @@ -72,6 +80,9 @@ const ItemMenu = ({ item }) => {
>
{t('Create Shortcut')}
</MenuItem>
<MenuItem onClick={handleFlag} className={ITEM_MENU_FLAG_BUTTON_CLASS}>
{t('Flag')}
</MenuItem>
</Menu>
</>
);
Expand Down
4 changes: 4 additions & 0 deletions src/config/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export const HOME_ERROR_ALERT_ID = 'homeErrorAlert';
export const SHARED_ITEMS_ERROR_ALERT_ID = 'sharedItemsErrorAlert';
export const FAVORITE_ITEMS_ERROR_ALERT_ID = 'favoriteItemsErrorAlert';
export const ITEM_MENU_SHORTCUT_BUTTON_CLASS = 'itemMenuShortcutButton';
export const ITEM_MENU_FAVORITE_BUTTON_CLASS = 'itemMenuFavoriteButton';
export const ITEM_MENU_FLAG_BUTTON_CLASS = 'itemMenuFlagButton';
export const buildFlagListItemId = (id) => `flagListItem-${id}`;
export const FLAG_ITEM_BUTTON_ID = 'flagItemButton';
export const CREATE_ITEM_DOCUMENT_ID = 'createItemDocument';
export const ITEM_FORM_DOCUMENT_TEXT_ID = 'itemFormDocumentText';
export const ITEM_FORM_DOCUMENT_TEXT_SELECTOR = `#${ITEM_FORM_DOCUMENT_TEXT_ID} .ql-editor`;
Expand Down
3 changes: 3 additions & 0 deletions src/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@
"This item is empty.": "This item is empty.",
"Search…": "Search…",
"No search results found.": "No search results found.",
"Flag Item": "Flag Item",
"Flag": "Flag",
"Select reason for flagging this item": "Select reason for flagging this item",
"Items per page": "Items per page:",
"perform view": "perform view",
"Show Perform View": "Show Perform View",
Expand Down
3 changes: 3 additions & 0 deletions src/langs/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@
"This item is empty.": "Cet élément est vide.",
"Search…": "Recherche…",
"No search results found.": "Aucun résultat ne correspond à la recherche.",
"Flag Item": "Signaler l'élément",
"Flag": "Signaler",
"Select reason for flagging this item": "Sélectionner la raison du signalement de cet élément",
"Items per page": "Eléments par page:",
"perform view": "vue perform",
"Show Perform View": "Montrer la Vue Perform",
Expand Down

0 comments on commit 27545fc

Please sign in to comment.