diff --git a/public/brokenImage.svg b/public/brokenImage.svg new file mode 100644 index 0000000..80feb31 --- /dev/null +++ b/public/brokenImage.svg @@ -0,0 +1 @@ + diff --git a/src/App.tsx b/src/App.tsx index f95c849..9f0b5c4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -88,7 +88,7 @@ function App(): JSX.Element { diff --git a/src/components/AddStory/AddStory.tsx b/src/components/AddStory/AddStory.tsx index 59cacd7..48026e1 100644 --- a/src/components/AddStory/AddStory.tsx +++ b/src/components/AddStory/AddStory.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Divider, Button, TextField } from '@material-ui/core'; +import { Box, Divider, Button, TextField, Grid } from '@material-ui/core'; import { useForm } from 'react-hook-form'; interface AddStoryProps { @@ -8,7 +8,7 @@ interface AddStoryProps { * * @param data */ - onSubmit: (data: { url: string }) => void; + onSubmit: (data: AddStoryFormData) => void; } export interface AddStoryFormData { @@ -57,7 +57,11 @@ export const AddStory: React.FC = (props): JSX.Element => { }} variant="outlined" /> - + + + + + - - + + + diff --git a/src/components/EditAndApproveStory/EditAndApproveStory.test.tsx b/src/components/EditAndApproveStory/EditAndApproveStory.test.tsx index 2a7b910..3bd7f75 100644 --- a/src/components/EditAndApproveStory/EditAndApproveStory.test.tsx +++ b/src/components/EditAndApproveStory/EditAndApproveStory.test.tsx @@ -1,56 +1,73 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { EditAndApproveStory } from './EditAndApproveStory'; +import { act, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { + EditAndApproveStory, + EditAndApproveStoryFormData, +} from './EditAndApproveStory'; +import { Prospect } from '../../services/types/Prospect'; describe('The EditAndApproveStory component', () => { - beforeEach(() => { - render( - - ); + let mockSubmit: any; + let mockProspect: Prospect; + + beforeEach(async () => { + mockProspect = { + id: 'abcdefg', + altText: 'Test alt text', + author: 'Test author', + excerpt: 'This is a short description', + feedId: 'abcdefg', + imageUrl: + 'https://assets.getpocket.com/web/yir/2020/images/mostread-1@2x.d849a2bbcf7ce894c8e5d01bc6a73052.jpg', + publisher: 'Test publisher', + source: 'Test source', + snoozedUntil: null, + title: 'Test title', + topic: 'Business', + url: 'https://getpocket.com', + }; + + mockSubmit = jest.fn((data: EditAndApproveStoryFormData) => { + return Promise.resolve(data); + }); + + await waitFor(() => { + render( + + ); + }); }); - it('renders successfully', () => { - // there is at least a heading and nothing falls over - const heading = screen.getByRole('heading'); - expect(heading).toBeInTheDocument(); + it('renders successfully', async () => { + // there is at least a form and nothing falls over + const form = screen.getByRole('form'); + expect(form).toBeInTheDocument(); }); - it('has the requisite buttons', () => { + it('has the requisite buttons', async () => { // check that all action buttons are present - expect(screen.getByText('Cancel')).toBeInTheDocument(); expect(screen.getByText('Reject')).toBeInTheDocument(); expect(screen.getByText('Snooze')).toBeInTheDocument(); expect(screen.getByText('Approve')).toBeInTheDocument(); }); - it('shows basic story data', () => { + it('shows basic story data', async () => { const storyUrlField = screen.getByLabelText('Story URL'); expect(storyUrlField).toBeInTheDocument(); expect(storyUrlField).toBeDisabled(); const publisherField = screen.getByLabelText('Publisher'); expect(publisherField).toBeInTheDocument(); - expect(publisherField).toBeDisabled(); const authorField = screen.getByLabelText('Author'); expect(authorField).toBeInTheDocument(); - expect(authorField).toBeDisabled(); expect(screen.getByLabelText('Headline')).toBeInTheDocument(); expect(screen.getByLabelText('Excerpt')).toBeInTheDocument(); }); - it('shows thumbnail and associated data', () => { + it('shows thumbnail and associated data', async () => { const image = screen.getByRole('img'); expect(image).toBeInTheDocument(); @@ -58,7 +75,7 @@ describe('The EditAndApproveStory component', () => { expect(screen.getByLabelText('Alt Text')).toBeInTheDocument(); }); - it('shows additional information about the story', () => { + it('shows additional information about the story', async () => { const sourceField = screen.getByLabelText('Source'); expect(sourceField).toBeInTheDocument(); expect(sourceField).toBeDisabled(); @@ -66,4 +83,67 @@ describe('The EditAndApproveStory component', () => { expect(screen.getByLabelText('Topic')).toBeInTheDocument(); expect(screen.getByLabelText('History')).toBeInTheDocument(); }); + + it('displays errors when required fields are empty', async () => { + userEvent.clear(screen.getByLabelText(/headline/i)); + userEvent.clear(screen.getByLabelText(/excerpt/i)); + userEvent.selectOptions(screen.getByLabelText(/topic/i), ''); + + await waitFor(() => { + userEvent.click(screen.getByText(/approve/i)); + }); + + expect(screen.getByText(/please enter a title/i)).toBeInTheDocument(); + expect( + screen.getByText(/please enter a short description/i) + ).toBeInTheDocument(); + expect(screen.getByText(/please choose a topic/i)).toBeInTheDocument(); + expect(mockSubmit).not.toBeCalled(); + }); + + it('displays matching error when thumbnail URL is malformed', async () => { + const input = screen.getByLabelText(/thumbnail url/i) as HTMLInputElement; + + userEvent.clear(input); + userEvent.type(input, 'This is not a valid link!!!'); + + await waitFor(() => { + userEvent.click(screen.getByText(/approve/i)); + }); + + expect(screen.getByText(/please enter a valid url/i)).toBeInTheDocument(); + expect(mockSubmit).not.toBeCalled(); + }); + + it('proceeds with form submission if all fields are valid', async () => { + await waitFor(() => { + userEvent.click(screen.getByText(/approve/i)); + }); + + expect(screen.queryByText(/please enter a title/i)).not.toBeInTheDocument(); + expect( + screen.queryByText(/please enter a short description/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/please choose a topic/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/please enter a valid url/i) + ).not.toBeInTheDocument(); + expect(mockSubmit).toBeCalled(); + }); + + it('shows a broken image icon if thumbnail URL is invalid', async () => { + const input = screen.getByLabelText(/thumbnail url/i) as HTMLInputElement; + + userEvent.clear(input); + userEvent.type(input, 'http//:www.not-a-valid-domain$/.com/image.png'); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + const thumbnailImage = document.querySelector('img') as HTMLImageElement; + expect(thumbnailImage.src).toContain('not-a-valid-domain'); + }); }); diff --git a/src/components/EditAndApproveStory/EditAndApproveStory.tsx b/src/components/EditAndApproveStory/EditAndApproveStory.tsx index eef3487..632ebcc 100644 --- a/src/components/EditAndApproveStory/EditAndApproveStory.tsx +++ b/src/components/EditAndApproveStory/EditAndApproveStory.tsx @@ -1,19 +1,20 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useState } from 'react'; import { Box, Card, CardMedia, Divider, + FormControl, FormHelperText, Grid, - MenuItem, + InputLabel, + Select, TextField, - Typography, } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; - +import { useForm } from 'react-hook-form'; import { Button } from '../Button/Button'; +import { Prospect } from '../../services/types/Prospect'; const useStyles = makeStyles((theme: Theme) => ({ alignRight: { @@ -22,49 +23,68 @@ const useStyles = makeStyles((theme: Theme) => ({ greyLink: { color: theme.palette.grey[600], }, + thumbnail: { + maxHeight: 200, + }, + formControl: { + width: '100%', + }, })); export interface EditAndApproveStoryProps { /** - * The name of the publisher + * The Prospect object that holds most of the data we need to display. */ - publisher: string; + prospect: Prospect; + /** - * The name of the author + * The submit handler passed to this component receives form data as a prop. + * + * @param data */ - author: string; + onSubmit: (data: EditAndApproveStoryFormData) => void; +} + +export interface EditAndApproveStoryFormData { /** - * The URL of the story + * The alt-text property for the thumbnail image */ - url: string; + altText: string; + /** - * The title of the story + * The name of the author */ - title: string; + author: string; + /** - * A short summary of the story + * A short description of the article */ excerpt: string; /** - * Alternative text for the thumbnail image + * The URL for the thumbnail image that will accompany the published story */ - altText: string; + imageUrl: string; /** - * The URL of the thumbnail image + * The name of the publisher, i.e. 'CNN' */ - thumbnailUrl: string; + publisher: string; /** - * The source of the story, i.e. "Syndication" + * The title of the article ("headline" on the frontend) */ - source: string; + title: string; /** - * The topic of the story, i.e. "History" + * The topic this article most closely fits into */ topic: string; + + /** + * The action requested: save changes and... + */ + submitAction: 'snooze' | 'reject' | 'approve'; } /** @@ -74,236 +94,368 @@ export interface EditAndApproveStoryProps { * @param props: EditAndApproveStoryProps * @returns JSX.Element The rendered form */ -export const EditAndApproveStory = ( - props: EditAndApproveStoryProps +export const EditAndApproveStory: React.FC = ( + props ): JSX.Element => { const classes = useStyles(); + const { onSubmit } = props; + const [prospect, setProspect] = useState(props.prospect); + const [imageUrl, setImageUrl] = useState(prospect.imageUrl); + const { + handleSubmit, + register, + errors, + } = useForm(); + + /** + * Update form field values on change. + */ + const handleChange = ( + event: React.ChangeEvent + ): void => { + const name = event.target.name as keyof typeof prospect; + + setProspect({ + ...prospect, + [name]: event.target.value, + }); + }; + + /** + * When thumbnail URL is edited, give instant feedback on the page + * whether the image resolves or not. + * + * @param event + */ + const updateThumbnail = ( + event: React.ChangeEvent + ): void => { + // update thumbnail URL both in the form and on the page + setImageUrl(event.target.value); + + // update prospect data as well + handleChange(event); + }; + + /** + * Show a Material-UI broken image icon if the thumbnail URL + * is missing or invalid. + * + * @param event + */ + const handleMissingThumbnail = ( + event: React.SyntheticEvent + ): void => { + event.currentTarget.src = '/brokenImage.svg'; + }; + + /** + * Handle updates to topic select separately as event signatures + * for input and select elements do not match. + * + * @param event + */ + const handleTopicChange = ( + event: React.ChangeEvent<{ name?: string | undefined; value: unknown }> + ): void => { + const name = event.target.name as keyof typeof prospect; + + setProspect({ + ...prospect, + [name]: event.target.value, + }); + }; + + /** + * Append 'snooze' action name to form data so that the form handler knows + * which mutation to run. + */ + const onSnooze = handleSubmit((data: EditAndApproveStoryFormData): void => { + data.submitAction = 'snooze'; + onSubmit(data); + }); + + /** + * Append 'reject' action name to form data. + */ + const onReject = handleSubmit((data: EditAndApproveStoryFormData): void => { + data.submitAction = 'reject'; + onSubmit(data); + }); + + /** + * Append 'approve' action name to form data. + */ + const onApprove = handleSubmit((data: EditAndApproveStoryFormData): void => { + data.submitAction = 'approve'; + onSubmit(data); + }); return ( -
+
- - - Edit & Approve - + + - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Learn how to write alt text - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + Topic + + + {errors.topic ? errors.topic.message : null} + + + - + + + + + + - + + - - - - + + + + - - - + + + - - - - + + + - -
+ + ); }; - -EditAndApproveStory.propTypes = { - publisher: PropTypes.string.isRequired, - author: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - excerpt: PropTypes.string.isRequired, - altText: PropTypes.string.isRequired, - thumbnailUrl: PropTypes.string.isRequired, - source: PropTypes.string.isRequired, - topic: PropTypes.string.isRequired, -}; diff --git a/src/pages/AddStoryPage/AddStoryPage.test.tsx b/src/pages/AddStoryPage/AddStoryPage.test.tsx index bc8c044..19a2e17 100644 --- a/src/pages/AddStoryPage/AddStoryPage.test.tsx +++ b/src/pages/AddStoryPage/AddStoryPage.test.tsx @@ -112,7 +112,7 @@ describe('The AddStory page', () => { // redirect to the Edit & Approve form on success expect(history.location.pathname).toEqual( - `/en-US/prospects/article/edit-and-approve/${newProspectId}/` + `/en-US/prospects/${newProspectId}/edit-and-approve/` ); }); diff --git a/src/pages/AddStoryPage/AddStoryPage.tsx b/src/pages/AddStoryPage/AddStoryPage.tsx index ad3b707..151a950 100644 --- a/src/pages/AddStoryPage/AddStoryPage.tsx +++ b/src/pages/AddStoryPage/AddStoryPage.tsx @@ -65,7 +65,7 @@ export const AddStoryPage = ({ .then((data) => { // Success! Move on to the full edit form history.push( - `/${feed?.name}/prospects/article/edit-and-approve/${data.data?.prospect.id}/`, + `/${feed?.name}/prospects/${data.data?.prospect.id}/edit-and-approve/`, { prospect: data.data?.prospect } ); }) diff --git a/src/pages/EditAndApproveStoryPage/EditAndApproveStoryPage.test.tsx b/src/pages/EditAndApproveStoryPage/EditAndApproveStoryPage.test.tsx new file mode 100644 index 0000000..10b0cf4 --- /dev/null +++ b/src/pages/EditAndApproveStoryPage/EditAndApproveStoryPage.test.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { MockedProvider } from '@apollo/client/testing'; +import { ApolloError } from '@apollo/client'; +import { render, screen, act, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { EditAndApproveStoryPage } from './EditAndApproveStoryPage'; +import { Prospect } from '../../services/types/Prospect'; +import { + approveProspect, + ApproveProspectData, + ApproveProspectVariables, +} from '../../services/mutations/approveProspect'; + +describe('The Edit And Approve Story page', () => { + let history: any; + let mockProspect: Prospect; + + beforeEach(() => { + mockProspect = { + id: '123a-456b-789c', + altText: 'Test alt text', + author: 'Test author', + excerpt: 'This is a short description', + feedId: 'abcdefg', + imageUrl: + 'https://assets.getpocket.com/web/yir/2020/images/mostread-1@2x.d849a2bbcf7ce894c8e5d01bc6a73052.jpg', + publisher: 'Test publisher', + snoozedUntil: null, + source: 'Test source', + title: 'Test title', + topic: 'Business', + url: 'https://getpocket.com', + }; + + // create a custom location history so that we can add the prospect + // to location state + history = createMemoryHistory(); + // start on the Snoozed tab + history.push('/en-US/prospects/snoozed/'); + // click "Edit & Approve" button on a prospect card and pass the prospect along + history.push('/en-US/prospects/123a-456b-789c/edit-and-approve/', { + prospect: mockProspect, + }); + }); + + it('shows an error if the API call was unsuccessful', async () => { + const mocksWithError = [ + { + request: { + query: approveProspect, + variables: { + id: 'abcdefg', + altText: 'Alt text', + excerpt: 'A very short description', + imageUrl: 'http://www.test.com/image.svg', + publisher: 'CNN', + title: 'Updated Prospect', + topic: 'Business', + } as ApproveProspectVariables, + }, + error: new ApolloError({ + networkError: new Error('An error occurred.'), + }), + }, + ]; + + render( + + + + + + ); + + // wait for form validation to complete + await waitFor(() => { + userEvent.click(screen.getByText(/^approve$/i)); + }); + + // wait for the API + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + // get response + expect(screen.getByText(/error/i)).toBeInTheDocument(); + }); + + it('shows a success message if prospect was successfully approved', async () => { + const approvedProspect = mockProspect; + approvedProspect.altText = 'Updated alt text'; + approvedProspect.excerpt = 'Updated excerpt'; + approvedProspect.title = 'Approved Prospect'; + + const mocks = [ + { + request: { + query: approveProspect, + variables: { + id: approvedProspect.id, + altText: approvedProspect.altText, + excerpt: approvedProspect.excerpt, + imageUrl: approvedProspect.imageUrl, + publisher: approvedProspect.publisher, + title: approvedProspect.title, + topic: approvedProspect.topic, + } as ApproveProspectVariables, + }, + result: { + data: { + prospect: approvedProspect, + } as ApproveProspectData, + }, + }, + ]; + + render( + + + + + + ); + + // wait for form validation to complete + await waitFor(() => { + userEvent.click(screen.getByText(/^approve$/i)); + }); + + // wait for the API + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + // see a success message + expect(screen.getByText(/story approved/i)).toBeInTheDocument(); + + // go back to previous tab on success + expect(history.location.pathname).toEqual('/en-US/prospects/snoozed/'); + }); + + describe('when Cancel button is clicked', () => { + it('it goes back to the previous page', () => { + history = createMemoryHistory({ + initialEntries: [ + '/en-US/prospects/', + '/en-US/prospects/123a-456b-789c/edit-and-approve/', + ], + initialIndex: 1, + }); + + render( + + + + + + ); + + userEvent.click(screen.getByText('Cancel')); + + expect(history.location.pathname).toEqual('/en-US/prospects/'); + }); + + it('it goes to home page if there is no browsing history', () => { + history = createMemoryHistory({ + initialEntries: ['/en-US/prospects/123a-456b-789c/edit-and-approve/'], + initialIndex: 0, + }); + + render( + + + + + + ); + + userEvent.click(screen.getByText('Cancel')); + + expect(history.location.pathname).toEqual('/'); + }); + }); +}); diff --git a/src/pages/EditAndApproveStoryPage/EditAndApproveStoryPage.tsx b/src/pages/EditAndApproveStoryPage/EditAndApproveStoryPage.tsx index 5b9b7f8..2179112 100644 --- a/src/pages/EditAndApproveStoryPage/EditAndApproveStoryPage.tsx +++ b/src/pages/EditAndApproveStoryPage/EditAndApproveStoryPage.tsx @@ -1,6 +1,27 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { Box, Grid, Snackbar, Typography } from '@material-ui/core'; +import { useMutation } from '@apollo/client'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import { + EditAndApproveStory, + EditAndApproveStoryFormData, +} from '../../components/EditAndApproveStory/EditAndApproveStory'; +import { Button } from '../../components/Button/Button'; import { Prospect } from '../../services/types/Prospect'; -import { useLocation } from 'react-router-dom'; +import { + approveProspect, + ApproveProspectData, + ApproveProspectVariables, +} from '../../services/mutations/approveProspect'; +import { HandleApiResponse } from '../../components/HandleApiResponse/HandleApiResponse'; +import { Alert } from '@material-ui/lab'; + +const useStyles = makeStyles((theme: Theme) => ({ + alignRight: { + textAlign: 'right', + }, +})); interface EditAndApproveStoryPageProps { prospect?: Prospect; @@ -9,15 +30,131 @@ interface EditAndApproveStoryPageProps { export const EditAndApproveStoryPage: React.FC = ( props ): JSX.Element => { + const history = useHistory(); + const classes = useStyles(); + const [success, setSuccess] = useState(false); + const [successMessage, setSuccessMessage] = useState(''); + /** * If a Prospect object was passed to the page from one of the other app pages, * let's extract it from the routing. */ - const location = useLocation<{ prospect?: Prospect }>(); + const location = useLocation(); + const prospect = location.state?.prospect; + + // prepare "approve" mutation + const [approveProspectMutation, { loading, error }] = useMutation< + ApproveProspectData, + ApproveProspectVariables + >(approveProspect); + + /** + * Collect form data, choose action and send it off to the API + * + * @param data + */ + const handleSubmit = (data: EditAndApproveStoryFormData) => { + // TODO: ensure there is always a prospect on the page by loading it from the API + // if it's not passed down from other pages + if (prospect) { + switch (data.submitAction) { + case 'approve': + approveProspectMutation({ + variables: { + id: prospect.id, + altText: data.altText, + excerpt: data.excerpt, + imageUrl: data.imageUrl, + publisher: data.publisher, + title: data.title, + topic: data.topic, + }, + }) + .then((data) => { + // Success! Show a toast notification + setSuccessMessage('Story approved!'); + setSuccess(true); + // go back to previous tab + history.goBack(); + }) + .catch((error) => { + // Do nothing. The errors are already destructured and shown on the frontend + // Yet if a catch() statement is missing an "Unhandled rejection" will break through + }); + break; + case 'snooze': + setSuccessMessage('Story snoozed for two weeks'); + setSuccess(true); + break; + case 'reject': + setSuccessMessage('Story rejected'); + setSuccess(true); + break; + } + } + }; + + /** + * Go back to previous page if there is anything to go back to, + * otherwise go to home page. + */ + const handleCancelAction = () => { + history.length > 1 ? history.goBack() : history.push('/'); + }; + + /** + * Close the toast notification + */ + const handleSuccessMessage = ( + event?: React.SyntheticEvent, + reason?: string + ) => { + if (reason === 'clickaway') { + return; + } + + setSuccess(false); + }; - // Use the variable or get a TypeScript error. - // To be replaced with actual logic for this page. - console.log(location.state.prospect); + return ( + <> + + + + + Edit & Approve + + + + + + + - return

Edit & Approve

; + { + // TODO: load prospect from the API if page is accessed directly + prospect && ( + + ) + } + + {success && ( + + + {successMessage} + + + )} + + ); }; diff --git a/src/pages/ProspectsPage/ProspectsPage.test.tsx b/src/pages/ProspectsPage/ProspectsPage.test.tsx index 1103cc9..426165e 100644 --- a/src/pages/ProspectsPage/ProspectsPage.test.tsx +++ b/src/pages/ProspectsPage/ProspectsPage.test.tsx @@ -60,24 +60,26 @@ describe('The Prospects page', () => { id: 'abc-123', altText: 'This is an image', author: 'Test author', - category: 'Health', excerpt: 'This is a short description', imageUrl: 'https://test.com/image.jpeg', publisher: 'Test publisher', source: 'Syndication', + snoozedUntil: null, title: 'Test title', + topic: 'Health', url: 'https://test.com/test-title/', }, { id: 'cde-345', altText: 'This is an image 2', author: 'Test author 2', - category: 'Art', excerpt: 'This is a short description 2', imageUrl: 'https://test.com/image2.jpeg', publisher: 'Test publisher 2', source: 'Syndication', + snoozedUntil: null, title: 'Test title 2', + topic: 'Art', url: 'https://test.com/test-title-2/', }, ], @@ -107,8 +109,7 @@ describe('The Prospects page', () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); - const cardTitle = await screen.findByText('/test title/i'); - console.log(cardTitle); + const cardTitle = await screen.queryByText('/test title/i'); expect(cardTitle).toBeInTheDocument(); }); }); diff --git a/src/pages/ProspectsPage/ProspectsPage.tsx b/src/pages/ProspectsPage/ProspectsPage.tsx index f4ed31f..6bb1cf8 100644 --- a/src/pages/ProspectsPage/ProspectsPage.tsx +++ b/src/pages/ProspectsPage/ProspectsPage.tsx @@ -99,7 +99,13 @@ export const ProspectsPage = ({ {data && ( {data.listProspects.items.map((prospect: Prospect) => { - return ; + return ( + + ); })} )} diff --git a/src/services/fragments/ProspectData.ts b/src/services/fragments/ProspectData.ts index 99f6f09..081af62 100644 --- a/src/services/fragments/ProspectData.ts +++ b/src/services/fragments/ProspectData.ts @@ -7,12 +7,14 @@ export const ProspectData = gql` fragment ProspectData on Prospect { id altText - category: topic excerpt + feedId imageUrl publisher source: sourceName + snoozedUntil title + topic url } `; diff --git a/src/services/mutations/approveProspect.ts b/src/services/mutations/approveProspect.ts new file mode 100644 index 0000000..8dda627 --- /dev/null +++ b/src/services/mutations/approveProspect.ts @@ -0,0 +1,51 @@ +import { gql } from '@apollo/client'; +import { Prospect } from '../types/Prospect'; +import { ProspectData } from '../fragments/ProspectData'; + +export interface ApproveProspectData { + prospect: Prospect; +} + +export interface ApproveProspectVariables { + id: string; + altText: string; + excerpt: string; + imageUrl: string; + publisher: string; + title: string; + topic: string; +} + +/** + * Update a prospect's properties; set state to APPROVED + * TODO: add a scheduled entry + */ +export const approveProspect = gql` + mutation approveProspect( + $id: ID! + $altText: String + $excerpt: String! + $imageUrl: String + $publisher: String + $title: String! + $topic: String! + ) { + prospect: updateProspect( + input: { + id: $id + altText: $altText + excerpt: $excerpt + imageUrl: $imageUrl + publisher: $publisher + snoozedUntil: null + state: APPROVED + title: $title + topic: $topic + updatedAt: null + } + ) { + ...ProspectData + } + } + ${ProspectData} +`; diff --git a/src/services/types/Prospect.ts b/src/services/types/Prospect.ts index e545159..7bf9f21 100644 --- a/src/services/types/Prospect.ts +++ b/src/services/types/Prospect.ts @@ -2,11 +2,13 @@ export interface Prospect { id: string; altText: string; author: string; - category: string; excerpt: string; + feedId: string; imageUrl: string; publisher: string; + snoozedUntil: string | null; source: string; title: string; + topic: string; url: string; }