From 1fd5af74d763f4b0ed45f8a85d868819628688e0 Mon Sep 17 00:00:00 2001
From: Jacob Capps <99674188+jcbcapps@users.noreply.github.com>
Date: Tue, 24 Jan 2023 14:35:29 -0600
Subject: [PATCH] refactor: Refactor modals (#898)
* Remove unused line
* Create modalContext
* Update modalContext and add CustomModal component
* Update modalContext w/ setComponent
* Initial display of CustomModal component
* Refactor NewsWidget and RemoveSectionModal
* Updates to refactor CustomCollection and RemoveCustomCollectionModal
* Temporarily add handleRemoveCollection back
* Updates to add AddCustomLinkModal functionality to CustomModal
* Move functionality of EditCustomLinkModal to CustomModal
* Clean up
* Clean up
* Clean up
* Update renderWithModalRoot
* Update props for storybook components
* Update
* Update w/ defaultMockModalContext
* Fix NewsWidget tests
* Update tests
* Update tests
* Remove comments
* Update tests
* Add initial tests for modalContext
* Update test
* Update test
* Fix add bookmark bug and add test in modalContext
* Update tests w/ added mocks
* Update test to save updated bookmark
* Update tests
* Update to click Delete button in modal
* Update test and add test for deleting custom bookmark
* Ignore functions that should not be factored into test coverage
* Add check to close dropdown on modal close
* Update conditional render
* Update test
* Update type
* Remove useRef
* Remove/add comments
* Add comment
* Update test
* Update tests
---
src/__fixtures__/operations/addBookmark.ts | 31 ++
src/__fixtures__/operations/editBookmark.ts | 32 ++
src/__fixtures__/operations/removeBookmark.ts | 27 ++
.../operations/removeCollection.ts | 26 ++
src/__fixtures__/operations/removeWidget.ts | 22 ++
.../CustomBookmark/CustomBookmark.test.tsx | 99 ++---
.../CustomBookmark/CustomBookmark.tsx | 58 ++-
.../CustomBookmark/CustomBookmarkForm.tsx | 5 +-
.../CustomCollection.stories.tsx | 6 -
.../CustomCollection.test.tsx | 316 ++-------------
.../CustomCollection/CustomCollection.tsx | 88 ++---
src/components/CustomModal/CustomModal.tsx | 86 ++++
src/components/MySpace/MySpace.test.tsx | 68 ++--
src/components/MySpace/MySpace.tsx | 27 +-
.../NewsWidget/NewsWidget.stories.tsx | 5 +-
src/components/NewsWidget/NewsWidget.test.tsx | 116 ++----
src/components/NewsWidget/NewsWidget.tsx | 48 +--
.../modals/AddCustomLinkModal.test.tsx | 2 +-
src/layout/DefaultLayout/DefaultLayout.tsx | 3 +
src/pages/_app.tsx | 235 +++++------
src/stores/analyticsContext.tsx | 4 +-
src/stores/authContext.tsx | 6 +-
src/stores/modalContext.test.tsx | 368 ++++++++++++++++++
src/stores/modalContext.tsx | 249 ++++++++++++
src/testHelpers.tsx | 57 ++-
25 files changed, 1253 insertions(+), 731 deletions(-)
create mode 100644 src/__fixtures__/operations/addBookmark.ts
create mode 100644 src/__fixtures__/operations/editBookmark.ts
create mode 100644 src/__fixtures__/operations/removeBookmark.ts
create mode 100644 src/__fixtures__/operations/removeCollection.ts
create mode 100644 src/__fixtures__/operations/removeWidget.ts
create mode 100644 src/components/CustomModal/CustomModal.tsx
create mode 100644 src/stores/modalContext.test.tsx
create mode 100644 src/stores/modalContext.tsx
diff --git a/src/__fixtures__/operations/addBookmark.ts b/src/__fixtures__/operations/addBookmark.ts
new file mode 100644
index 000000000..73ea3fc9d
--- /dev/null
+++ b/src/__fixtures__/operations/addBookmark.ts
@@ -0,0 +1,31 @@
+import { ObjectId } from 'mongodb'
+import { AddBookmarkDocument } from 'operations/portal/mutations/addBookmark.g'
+
+const mockBookmark = {
+ url: 'example.com',
+ label: 'My Custom Label',
+ cmsId: null,
+ isRemoved: null,
+}
+
+export const mockCollectionId = ObjectId()
+
+export const addBookmarkMock = [
+ {
+ request: {
+ query: AddBookmarkDocument,
+ variables: {
+ collectionId: mockCollectionId,
+ url: mockBookmark.url,
+ label: mockBookmark.label,
+ },
+ },
+ result: {
+ data: {
+ _id: ObjectId(),
+ url: mockBookmark.url,
+ label: mockBookmark.label,
+ },
+ },
+ },
+]
diff --git a/src/__fixtures__/operations/editBookmark.ts b/src/__fixtures__/operations/editBookmark.ts
new file mode 100644
index 000000000..fcc4dfbda
--- /dev/null
+++ b/src/__fixtures__/operations/editBookmark.ts
@@ -0,0 +1,32 @@
+import { ObjectId } from 'mongodb'
+import { EditBookmarkDocument } from 'operations/portal/mutations/editBookmark.g'
+import { Bookmark } from 'types'
+
+export const mockBookmark: Bookmark = {
+ _id: ObjectId(),
+ url: 'example.com',
+ label: 'Custom Label',
+}
+
+export const mockCollectionIdForEditBookmark = ObjectId()
+
+export const editBookmarkMock = [
+ {
+ request: {
+ query: EditBookmarkDocument,
+ variables: {
+ _id: mockBookmark._id,
+ collectionId: mockCollectionIdForEditBookmark,
+ url: mockBookmark.url,
+ label: mockBookmark.label,
+ },
+ },
+ result: {
+ data: {
+ _id: mockBookmark._id,
+ label: 'Updated Label',
+ url: mockBookmark.url,
+ },
+ },
+ },
+]
diff --git a/src/__fixtures__/operations/removeBookmark.ts b/src/__fixtures__/operations/removeBookmark.ts
new file mode 100644
index 000000000..05c792f0b
--- /dev/null
+++ b/src/__fixtures__/operations/removeBookmark.ts
@@ -0,0 +1,27 @@
+import { ObjectId } from 'mongodb'
+import { RemoveBookmarkDocument } from 'operations/portal/mutations/removeBookmark.g'
+
+export const mockRemoveBookmark = {
+ _id: ObjectId(),
+ url: 'example.com',
+ label: 'Remove me',
+}
+
+export const mockRemoveBookmarkCollectionId = ObjectId()
+
+export const removeBookmarkMock = [
+ {
+ request: {
+ query: RemoveBookmarkDocument,
+ variables: {
+ _id: mockRemoveBookmark._id,
+ collectionId: mockRemoveBookmarkCollectionId,
+ },
+ },
+ result: {
+ data: {
+ _id: mockRemoveBookmark._id,
+ },
+ },
+ },
+]
diff --git a/src/__fixtures__/operations/removeCollection.ts b/src/__fixtures__/operations/removeCollection.ts
new file mode 100644
index 000000000..e7876126e
--- /dev/null
+++ b/src/__fixtures__/operations/removeCollection.ts
@@ -0,0 +1,26 @@
+import { ObjectId } from 'mongodb'
+import { RemoveCollectionDocument } from 'operations/portal/mutations/removeCollection.g'
+import { Collection } from 'types'
+
+export const mockCollection: Collection = {
+ _id: ObjectId(),
+ title: 'Test Collection',
+ type: 'Collection',
+ bookmarks: [],
+}
+
+export const removeCollectionMock = [
+ {
+ request: {
+ query: RemoveCollectionDocument,
+ variables: {
+ _id: mockCollection._id,
+ },
+ },
+ result: {
+ data: {
+ _id: mockCollection._id,
+ },
+ },
+ },
+]
diff --git a/src/__fixtures__/operations/removeWidget.ts b/src/__fixtures__/operations/removeWidget.ts
new file mode 100644
index 000000000..e7e6352e4
--- /dev/null
+++ b/src/__fixtures__/operations/removeWidget.ts
@@ -0,0 +1,22 @@
+import { ObjectId } from 'mongodb'
+import { RemoveWidgetDocument } from 'operations/portal/mutations/removeWidget.g'
+
+export const mockWidget = {
+ _id: ObjectId(),
+}
+
+export const removeWidgetMock = [
+ {
+ request: {
+ query: RemoveWidgetDocument,
+ variables: {
+ _id: mockWidget._id,
+ },
+ },
+ result: {
+ data: {
+ _id: mockWidget._id,
+ },
+ },
+ },
+]
diff --git a/src/components/CustomCollection/CustomBookmark/CustomBookmark.test.tsx b/src/components/CustomCollection/CustomBookmark/CustomBookmark.test.tsx
index f94bed6ff..7bef687b1 100644
--- a/src/components/CustomCollection/CustomBookmark/CustomBookmark.test.tsx
+++ b/src/components/CustomCollection/CustomBookmark/CustomBookmark.test.tsx
@@ -17,10 +17,9 @@ const testBookmark = {
label: 'Webmail',
}
-const testHandlers = {
- onSave: jest.fn(),
- onDelete: jest.fn(),
-}
+const testWidgetId = ObjectId()
+
+const testCollectionTitle = 'Test Collection Title'
describe('CustomBookmark component', () => {
afterEach(() => {
@@ -29,7 +28,11 @@ describe('CustomBookmark component', () => {
it('renders a bookmark with an edit handler', async () => {
renderWithModalRoot(
-
+
)
await screen.findByText(testBookmark.label)
@@ -45,76 +48,58 @@ describe('CustomBookmark component', () => {
url: 'https://example.com',
}
- render()
+ render(
+
+ )
await screen.findByText(testBookmarkNoLabel.url)
expect(screen.getByRole('link')).toHaveTextContent(testBookmarkNoLabel.url)
})
- it('can save the bookmark', async () => {
+ it('calls each function associated with editing a bookmark', async () => {
const user = userEvent.setup()
+ const mockUpdateModalId = jest.fn()
+ const mockUpdateModalText = jest.fn()
+ const mockUpdateWidget = jest.fn()
+ const mockUpdateBookmark = jest.fn()
+ const mockModalRef = jest.spyOn(React, 'useRef')
renderWithModalRoot(
-
+ ,
+ {
+ updateModalId: mockUpdateModalId,
+ updateModalText: mockUpdateModalText,
+ updateWidget: mockUpdateWidget,
+ updateBookmark: mockUpdateBookmark,
+ modalRef: mockModalRef,
+ }
)
const editButton = await screen.findByRole('button', {
name: 'Edit this link',
})
await user.click(editButton)
- expect(
- screen.getByRole('dialog', { name: 'Edit custom link' })
- ).not.toHaveClass('is-hidden')
-
- const saveButton = screen.getByRole('button', { name: 'Save custom link' })
- expect(saveButton).toBeInTheDocument()
- await user.click(saveButton)
- expect(testHandlers.onSave).toHaveBeenCalled()
- })
-
- it('can delete the bookmark', async () => {
- const user = userEvent.setup()
-
- renderWithModalRoot(
-
- )
- const editButton = await screen.findByRole('button', {
- name: 'Edit this link',
+ expect(mockUpdateModalId).toHaveBeenCalledWith('editCustomLinkModal')
+ expect(mockUpdateModalText).toHaveBeenCalledWith({
+ headingText: 'Edit custom link',
})
- await user.click(editButton)
- expect(
- screen.getByRole('dialog', { name: 'Edit custom link' })
- ).not.toHaveClass('is-hidden')
-
- const deleteButton = screen.getByRole('button', { name: 'Delete' })
- expect(deleteButton).toBeInTheDocument()
- await user.click(deleteButton)
- expect(testHandlers.onDelete).toHaveBeenCalled()
- })
- it('can start editing and cancel', async () => {
- const user = userEvent.setup()
-
- renderWithModalRoot(
-
- )
-
- const editButton = await screen.findByRole('button', {
- name: 'Edit this link',
+ expect(mockUpdateWidget).toHaveBeenCalledWith({
+ _id: testWidgetId,
+ title: testCollectionTitle,
+ type: 'Collection',
})
- await user.click(editButton)
- expect(
- screen.getByRole('dialog', { name: 'Edit custom link' })
- ).not.toHaveClass('is-hidden')
+ expect(mockUpdateBookmark).toHaveBeenCalledWith(testBookmark)
- const cancelButton = screen.getByRole('button', { name: 'Cancel' })
- expect(cancelButton).toBeInTheDocument()
- await user.click(cancelButton)
- expect(
- screen.getByRole('dialog', { name: 'Edit custom link' })
- ).toHaveClass('is-hidden')
- expect(testHandlers.onSave).not.toHaveBeenCalled()
- expect(testHandlers.onDelete).not.toHaveBeenCalled()
+ expect(mockModalRef).toHaveBeenCalled()
})
})
diff --git a/src/components/CustomCollection/CustomBookmark/CustomBookmark.tsx b/src/components/CustomCollection/CustomBookmark/CustomBookmark.tsx
index 6fce898ba..ef761943c 100644
--- a/src/components/CustomCollection/CustomBookmark/CustomBookmark.tsx
+++ b/src/components/CustomCollection/CustomBookmark/CustomBookmark.tsx
@@ -1,38 +1,42 @@
-import React, { useRef } from 'react'
-import { ModalRef } from '@trussworks/react-uswds'
+import React from 'react'
+import type { ObjectId } from 'bson'
import styles from '../CustomCollection.module.scss'
import Bookmark from 'components/Bookmark/Bookmark'
import type { Bookmark as BookmarkType } from 'types/index'
-import EditCustomLinkModal from 'components/modals/EditCustomLinkModal'
+import { useModalContext } from 'stores/modalContext'
export const CustomBookmark = ({
bookmark,
- onSave,
- onDelete,
+ widgetId,
+ collectionTitle,
}: {
bookmark: BookmarkType
- onSave: (url: string, label: string) => void
- onDelete: () => void
+ widgetId: ObjectId
+ collectionTitle: string
}) => {
- const editCustomLinkModal = useRef(null)
const { url, label } = bookmark
-
- const handleEditLink = () =>
- editCustomLinkModal.current?.toggleModal(undefined, true)
-
- const handleSaveLink = (label: string, url: string) => {
- editCustomLinkModal.current?.toggleModal(undefined, false)
- onSave(url, label)
- }
-
- const handleCancel = () =>
- editCustomLinkModal.current?.toggleModal(undefined, false)
-
- const handleDeleteLink = () => {
- editCustomLinkModal.current?.toggleModal(undefined, false)
- onDelete()
+ const {
+ updateModalId,
+ updateModalText,
+ modalRef,
+ updateBookmark,
+ updateWidget,
+ } = useModalContext()
+
+ const handleEditLink = () => {
+ updateModalId('editCustomLinkModal')
+ updateModalText({
+ headingText: 'Edit custom link',
+ })
+
+ // The collectionTitle isn't needed here, but adding it in to maintain the Widget type
+ // while performing operations against the bookmark
+ updateWidget({ _id: widgetId, title: collectionTitle, type: 'Collection' })
+ updateBookmark(bookmark)
+
+ modalRef?.current?.toggleModal(undefined, true)
}
return (
@@ -43,14 +47,6 @@ export const CustomBookmark = ({
className={styles.customLink}>
{label || url}
-
-
>
)
}
diff --git a/src/components/CustomCollection/CustomBookmark/CustomBookmarkForm.tsx b/src/components/CustomCollection/CustomBookmark/CustomBookmarkForm.tsx
index d6caad0fb..650617654 100644
--- a/src/components/CustomCollection/CustomBookmark/CustomBookmarkForm.tsx
+++ b/src/components/CustomCollection/CustomBookmark/CustomBookmarkForm.tsx
@@ -10,6 +10,7 @@ import {
Alert,
ErrorMessage,
} from '@trussworks/react-uswds'
+import { useModalContext } from 'stores/modalContext'
type CustomBookmarkFormProps = {
onSave: (url: string, label: string) => void
@@ -34,6 +35,8 @@ export const CustomBookmarkForm = ({
// #TODO: Integrate Formik into our forms following the
// wrapper component pattern used in other Truss projects
+ const { isAddingLinkContext } = useModalContext()
+
const formik = useFormik({
initialValues: {
// We have to provide label and url as possible initial values
@@ -147,7 +150,7 @@ export const CustomBookmarkForm = ({
onClick={handleOnCancel}>
Cancel
- {onDelete && (
+ {isAddingLinkContext ? null : (
)}
-
>
)
@@ -462,12 +461,8 @@ const CustomCollection = ({
{
- handleEditBookmark(bookmark._id, url, label)
- }}
- onDelete={() =>
- handleRemoveBookmark(bookmark._id)
- }
+ widgetId={_id}
+ collectionTitle={title}
/>
@@ -481,13 +476,6 @@ const CustomCollection = ({
)}
-
)
}
diff --git a/src/components/CustomModal/CustomModal.tsx b/src/components/CustomModal/CustomModal.tsx
new file mode 100644
index 000000000..379055a63
--- /dev/null
+++ b/src/components/CustomModal/CustomModal.tsx
@@ -0,0 +1,86 @@
+import React, { useRef } from 'react'
+import {
+ Modal,
+ ModalHeading,
+ ModalFooter,
+ ButtonGroup,
+ Button,
+} from '@trussworks/react-uswds'
+import styles from '../modals/modal.module.scss'
+import ModalPortal from 'components/util/ModalPortal'
+import { CustomBookmarkForm } from 'components/CustomCollection/CustomBookmark/CustomBookmarkForm'
+import { useModalContext } from 'stores/modalContext'
+
+const CustomModal = ({ ...props }) => {
+ const {
+ modalRef,
+ modalHeadingText,
+ additionalText,
+ closeModal,
+ onSave,
+ onDelete,
+ bookmark,
+ customLinkLabel,
+ showAddWarning,
+ isAddingLinkContext,
+ } = useModalContext()
+
+ const nameInputRef = useRef(null)
+ const urlInputRef = useRef(null)
+
+ // TO DO - id, aria-labelledby, and aria-describedby were previously combined w/ the
+ // modalId of each modal component to create a unique value (e.g. aria-labelledby=addCustomLinkModal-heading).
+ // Now that we only have one modal component, do we need just one unique value? Or something similar to before?
+ return (
+
+
+ {modalHeadingText}
+
+ {additionalText && (
+
+ )}
+
+ {bookmark || isAddingLinkContext ? (
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+ )
+}
+
+export default CustomModal
diff --git a/src/components/MySpace/MySpace.test.tsx b/src/components/MySpace/MySpace.test.tsx
index 0478a89c2..fa31002f1 100644
--- a/src/components/MySpace/MySpace.test.tsx
+++ b/src/components/MySpace/MySpace.test.tsx
@@ -402,6 +402,9 @@ describe('My Space Component', () => {
it('handles the remove collection operation', async () => {
const user = userEvent.setup()
+ const mockUpdateModalId = jest.fn()
+ const mockUpdateModalText = jest.fn()
+ const mockUpdateWidget = jest.fn()
let collectionRemoved = false
const collectionId = getMySpaceMock[0].result.data.mySpace[0]._id
@@ -429,7 +432,12 @@ describe('My Space Component', () => {
renderWithModalRoot(
-
+ ,
+ {
+ updateModalId: mockUpdateModalId,
+ updateModalText: mockUpdateModalText,
+ updateWidget: mockUpdateWidget,
+ }
)
const dropdownMenu = await screen.findAllByRole('button', {
@@ -440,21 +448,16 @@ describe('My Space Component', () => {
await user.click(
screen.getByRole('button', { name: 'Delete this collection' })
)
- const removeCollectionModals = screen.getAllByRole('dialog', {
- name: 'Are you sure you’d like to delete this collection from My Space?',
- })
- const removeCollectionModal = removeCollectionModals[0]
- expect(removeCollectionModal).toHaveClass('is-visible')
- await user.click(
- within(removeCollectionModal).getByRole('button', { name: 'Delete' })
+ expect(mockUpdateModalId).toHaveBeenCalledWith(
+ 'removeCustomCollectionModal'
)
-
- await act(
- async () => await new Promise((resolve) => setTimeout(resolve, 0))
- ) // wait for response
-
- expect(collectionRemoved).toBe(true)
+ expect(mockUpdateModalText).toHaveBeenCalledWith({
+ headingText:
+ 'Are you sure you’d like to delete this collection from My Space?',
+ descriptionText: 'This action cannot be undone.',
+ })
+ expect(mockUpdateWidget).toHaveBeenCalled()
})
it('handles the add collection operation', async () => {
@@ -505,6 +508,10 @@ describe('My Space Component', () => {
it('handles the edit bookmark operation', async () => {
const user = userEvent.setup()
+ const mockUpdateModalId = jest.fn()
+ const mockUpdateModalText = jest.fn()
+ const mockUpdateWidget = jest.fn()
+ const mockUpdateBookmark = jest.fn()
let bookmarkEdited = false
const bookmarkId =
@@ -540,7 +547,13 @@ describe('My Space Component', () => {
renderWithModalRoot(
-
+ ,
+ {
+ updateModalId: mockUpdateModalId,
+ updateModalText: mockUpdateModalText,
+ updateWidget: mockUpdateWidget,
+ updateBookmark: mockUpdateBookmark,
+ }
)
const editButton = await screen.findByRole('button', {
@@ -548,26 +561,11 @@ describe('My Space Component', () => {
})
await user.click(editButton)
- const editModal = await screen.findByRole('dialog', {
- name: 'Edit custom link',
+ expect(mockUpdateModalId).toHaveBeenCalledWith('editCustomLinkModal')
+ expect(mockUpdateModalText).toHaveBeenCalledWith({
+ headingText: 'Edit custom link',
})
-
- expect(editModal).toBeVisible()
- const nameInput = within(editModal).getByLabelText('Name')
- const urlInput = within(editModal).getByLabelText('URL')
-
- await user.clear(nameInput)
- await user.clear(urlInput)
- await user.type(nameInput, 'Yahoo')
- await user.type(urlInput, '{clear}https://www.yahoo.com')
- await user.click(
- within(editModal).getByRole('button', { name: 'Save custom link' })
- )
-
- await act(
- async () => await new Promise((resolve) => setTimeout(resolve, 0))
- )
-
- expect(bookmarkEdited).toBe(true)
+ expect(mockUpdateWidget).toHaveBeenCalled()
+ expect(mockUpdateBookmark).toHaveBeenCalled()
})
})
diff --git a/src/components/MySpace/MySpace.tsx b/src/components/MySpace/MySpace.tsx
index 8f64a890e..9f83f7391 100644
--- a/src/components/MySpace/MySpace.tsx
+++ b/src/components/MySpace/MySpace.tsx
@@ -8,12 +8,10 @@ import styles from './MySpace.module.scss'
import { useAddBookmarkMutation } from 'operations/portal/mutations/addBookmark.g'
import { useAddCollectionMutation } from 'operations/portal/mutations/addCollection.g'
import { useAddWidgetMutation } from 'operations/portal/mutations/addWidget.g'
-import { useEditBookmarkMutation } from 'operations/portal/mutations/editBookmark.g'
import { useGetMySpaceQuery } from 'operations/portal/queries/getMySpace.g'
import { useEditCollectionMutation } from 'operations/portal/mutations/editCollection.g'
import { useRemoveBookmarkMutation } from 'operations/portal/mutations/removeBookmark.g'
import { useRemoveCollectionMutation } from 'operations/portal/mutations/removeCollection.g'
-import { useRemoveWidgetMutation } from 'operations/portal/mutations/removeWidget.g'
import {
MySpaceWidget,
@@ -44,13 +42,11 @@ const MySpace = ({ bookmarks }: { bookmarks: BookmarkRecords }) => {
const mySpace = (data?.mySpace || []) as MySpaceWidget[]
const [handleAddWidget] = useAddWidgetMutation()
- const [handleRemoveWidget] = useRemoveWidgetMutation()
const [handleRemoveBookmark] = useRemoveBookmarkMutation()
const [handleAddBookmark] = useAddBookmarkMutation()
const [handleRemoveCollection] = useRemoveCollectionMutation()
const [handleEditCollection] = useEditCollectionMutation()
const [handleAddCollection] = useAddCollectionMutation()
- const [handleEditBookmark] = useEditBookmarkMutation()
if (error) return Error
@@ -109,21 +105,13 @@ const MySpace = ({ bookmarks }: { bookmarks: BookmarkRecords }) => {
key={`widget_${widget._id}`}
tabletLg={{ col: 6 }}
desktopLg={{ col: 4 }}>
- {widget.type === 'News' && (
- {
- handleRemoveWidget({
- variables: { _id: widget._id },
- refetchQueries: [`getMySpace`],
- })
- }}
- />
- )}
+ {widget.type === 'News' && }
{isCollection(widget) && (
{
@@ -196,17 +184,6 @@ const MySpace = ({ bookmarks }: { bookmarks: BookmarkRecords }) => {
refetchQueries: [`getMySpace`],
})
}}
- handleEditBookmark={(id, url, label) => {
- handleEditBookmark({
- variables: {
- _id: id,
- collectionId: widget._id,
- url,
- label,
- },
- refetchQueries: [`getMySpace`],
- })
- }}
/>
)}
diff --git a/src/components/NewsWidget/NewsWidget.stories.tsx b/src/components/NewsWidget/NewsWidget.stories.tsx
index 5eee8f5bd..bc673991f 100644
--- a/src/components/NewsWidget/NewsWidget.stories.tsx
+++ b/src/components/NewsWidget/NewsWidget.stories.tsx
@@ -4,12 +4,13 @@ import { Meta } from '@storybook/react'
import NewsWidget from './NewsWidget'
import { mockRssFeedTwo } from '__mocks__/news-rss'
import { SPACEFORCE_NEWS_RSS_URL } from 'constants/index'
+import { Widget } from 'types'
// Load 2 items
const RSS_URL = `${SPACEFORCE_NEWS_RSS_URL}&max=2`
type StorybookArgTypes = {
- onRemove: () => void
+ widget: Widget
}
export default {
@@ -40,5 +41,5 @@ export default {
} as Meta
export const SpaceForceRSS = (argTypes: StorybookArgTypes) => (
-
+
)
diff --git a/src/components/NewsWidget/NewsWidget.test.tsx b/src/components/NewsWidget/NewsWidget.test.tsx
index 08712fee1..77a7a26da 100644
--- a/src/components/NewsWidget/NewsWidget.test.tsx
+++ b/src/components/NewsWidget/NewsWidget.test.tsx
@@ -5,9 +5,11 @@ import React from 'react'
import { render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import axios from 'axios'
+import { ObjectId } from 'mongodb'
import { renderWithModalRoot } from '../../testHelpers'
import NewsWidget from './NewsWidget'
+import { Widget } from 'types/index'
import { mockRssFeedTen } from '__mocks__/news-rss'
@@ -15,9 +17,13 @@ jest.mock('axios')
const mockedAxios = axios as jest.Mocked
-describe('NewsWidget component', () => {
- const mockHandleRemove = jest.fn()
+const mockNewsWidget: Widget = {
+ _id: ObjectId(),
+ title: 'Recent News',
+ type: 'News',
+}
+describe('NewsWidget component', () => {
mockedAxios.get.mockImplementation(() => {
return Promise.resolve({ data: mockRssFeedTen })
})
@@ -27,7 +33,7 @@ describe('NewsWidget component', () => {
})
it('renders a widget that displays RSS items and a link to the News page', async () => {
- render()
+ render()
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(
'Recent News'
@@ -42,7 +48,7 @@ describe('NewsWidget component', () => {
it('renders a settings menu', async () => {
const user = userEvent.setup()
- render()
+ render()
user.click(
screen.getByRole('button', {
@@ -57,38 +63,18 @@ describe('NewsWidget component', () => {
expect(removeButton).toBeInTheDocument()
})
- it('clicking the remove section button opens the confirmation modal', async () => {
+ it('clicking in settings to remove section passes correct values to modalContext', async () => {
const user = userEvent.setup()
- renderWithModalRoot()
-
- await user.click(
- screen.getByRole('button', {
- name: 'Section Settings',
- })
- )
-
- const removeButton = await screen.findByRole('button', {
- name: 'Remove this section',
+ const mockUpdateModalId = jest.fn()
+ const mockUpdateModalText = jest.fn()
+ const mockUpdateWidget = jest.fn()
+
+ renderWithModalRoot(, {
+ updateModalId: mockUpdateModalId,
+ updateModalText: mockUpdateModalText,
+ updateWidget: mockUpdateWidget,
})
- expect(removeButton).toBeInTheDocument()
-
- await user.click(removeButton)
-
- // Open modal
- expect(
- screen.getByRole('dialog', {
- name: 'Are you sure you’d like to delete this section?',
- })
- ).toHaveClass('is-visible')
-
- expect(mockHandleRemove).not.toHaveBeenCalled()
- })
-
- it('clicking the cancel button in the modal closes the confirmation modal', async () => {
- const user = userEvent.setup()
- renderWithModalRoot()
-
await user.click(
screen.getByRole('button', {
name: 'Section Settings',
@@ -101,64 +87,12 @@ describe('NewsWidget component', () => {
})
)
- // Open modal
- const confirmationModal = screen.getByRole('dialog', {
- name: 'Are you sure you’d like to delete this section?',
- })
-
- expect(confirmationModal).toHaveClass('is-visible')
- const cancelButton = within(confirmationModal).getByRole('button', {
- name: 'Cancel',
+ expect(mockUpdateModalId).toHaveBeenCalledWith('removeSectionModal')
+ expect(mockUpdateModalText).toHaveBeenCalledWith({
+ headingText: 'Are you sure you’d like to delete this section?',
+ descriptionText:
+ 'You can re-add it to your My Space from the Add Section menu.',
})
- await user.click(cancelButton)
-
- expect(mockHandleRemove).toHaveBeenCalledTimes(0)
- expect(
- screen.getByRole('dialog', {
- name: 'Are you sure you’d like to delete this section?',
- })
- ).toHaveClass('is-hidden')
- })
-
- it('clicking the confirm button in the modal calls the remove handler and closes the confirmation modal', async () => {
- const user = userEvent.setup()
- renderWithModalRoot()
-
- expect(
- screen.getByRole('dialog', {
- name: 'Are you sure you’d like to delete this section?',
- })
- ).toHaveClass('is-hidden')
-
- await user.click(
- screen.getByRole('button', {
- name: 'Section Settings',
- })
- )
-
- await user.click(
- await screen.findByRole('button', {
- name: 'Remove this section',
- })
- )
-
- // Open modal
- const confirmationModal = screen.getByRole('dialog', {
- name: 'Are you sure you’d like to delete this section?',
- })
-
- expect(confirmationModal).toHaveClass('is-visible')
- await user.click(
- within(confirmationModal).getByRole('button', {
- name: 'Delete',
- })
- )
-
- expect(mockHandleRemove).toHaveBeenCalledTimes(1)
- expect(
- screen.getByRole('dialog', {
- name: 'Are you sure you’d like to delete this section?',
- })
- ).toHaveClass('is-hidden')
+ expect(mockUpdateWidget).toHaveBeenCalled()
})
})
diff --git a/src/components/NewsWidget/NewsWidget.tsx b/src/components/NewsWidget/NewsWidget.tsx
index c0418e934..284159bee 100644
--- a/src/components/NewsWidget/NewsWidget.tsx
+++ b/src/components/NewsWidget/NewsWidget.tsx
@@ -1,5 +1,5 @@
-import React, { useEffect, useRef } from 'react'
-import { Button, ModalRef } from '@trussworks/react-uswds'
+import React, { useEffect } from 'react'
+import { Button } from '@trussworks/react-uswds'
import styles from './NewsWidget.module.scss'
@@ -10,16 +10,20 @@ import NewsItem from 'components/NewsItem/NewsItem'
import type { RSSNewsItem } from 'types'
import { validateNewsItems, formatRssToArticle } from 'helpers/index'
import { SPACEFORCE_NEWS_RSS_URL } from 'constants/index'
-import RemoveSectionModal from 'components/modals/RemoveSectionModal'
-import { useAnalytics } from 'stores/analyticsContext'
+import { useModalContext } from 'stores/modalContext'
+import { Widget } from 'types/index'
// Load 2 items
const RSS_URL = `${SPACEFORCE_NEWS_RSS_URL}&max=2`
-const NewsWidget = ({ onRemove }: { onRemove: () => void }) => {
+type NewsWidgetProps = {
+ widget: Widget
+}
+
+const NewsWidget = (widget: NewsWidgetProps) => {
+ const { updateModalId, updateModalText, modalRef, updateWidget } =
+ useModalContext()
const { items, fetchItems } = useRSSFeed(RSS_URL)
- const removeSectionModal = useRef(null)
- const { trackEvent } = useAnalytics()
useEffect(() => {
fetchItems()
@@ -28,19 +32,22 @@ const NewsWidget = ({ onRemove }: { onRemove: () => void }) => {
/** Remove section */
// Show confirmation modal
const handleConfirmRemoveSection = () => {
- removeSectionModal.current?.toggleModal(undefined, true)
- }
+ updateModalId('removeSectionModal')
+ updateModalText({
+ headingText: 'Are you sure you’d like to delete this section?',
+ descriptionText:
+ 'You can re-add it to your My Space from the Add Section menu.',
+ })
- // After confirming remove, trigger the mutation and close the modal
- const handleRemoveSection = () => {
- trackEvent('Section settings', 'Remove this section', 'News')
- onRemove()
- removeSectionModal.current?.toggleModal(undefined, false)
- }
+ const widgetState: Widget = {
+ _id: widget.widget._id,
+ title: widget.widget.title,
+ type: 'News',
+ }
+
+ updateWidget(widgetState)
- // Cancel removing
- const handleCancelRemoveSection = () => {
- removeSectionModal.current?.toggleModal(undefined, false)
+ modalRef?.current?.toggleModal(undefined, true)
}
return (
@@ -76,11 +83,6 @@ const NewsWidget = ({ onRemove }: { onRemove: () => void }) => {
-
>
)
}
diff --git a/src/components/modals/AddCustomLinkModal.test.tsx b/src/components/modals/AddCustomLinkModal.test.tsx
index c43cf3dc2..69ec541ff 100644
--- a/src/components/modals/AddCustomLinkModal.test.tsx
+++ b/src/components/modals/AddCustomLinkModal.test.tsx
@@ -36,7 +36,7 @@ describe('AddCustomLinkModal', () => {
const deleteLinkButton = screen.queryByRole('button', {
name: 'Delete',
})
- expect(deleteLinkButton).not.toBeInTheDocument()
+ expect(deleteLinkButton).toBeInTheDocument()
})
it('can cancel out of the modal', async () => {
diff --git a/src/layout/DefaultLayout/DefaultLayout.tsx b/src/layout/DefaultLayout/DefaultLayout.tsx
index 360a8e6a9..b4b7f02ac 100644
--- a/src/layout/DefaultLayout/DefaultLayout.tsx
+++ b/src/layout/DefaultLayout/DefaultLayout.tsx
@@ -9,6 +9,7 @@ import PageHeader from 'components/PageHeader/PageHeader'
import PageNav from 'components/PageNav/PageNav'
import FeedbackCard from 'components/FeedbackCard/FeedbackCard'
import Footer from 'components/Footer/Footer'
+import CustomModal from 'components/CustomModal/CustomModal'
const DefaultLayout = ({
displayFeedbackCard = true,
@@ -53,6 +54,8 @@ const DefaultLayout = ({
+
+
>
)
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 931f0bd51..5d2c3b5c8 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -3,7 +3,7 @@ import App from 'next/app'
import Head from 'next/head'
import type { AppProps, AppContext } from 'next/app'
import { useRouter } from 'next/router'
-import type { ReactNode, ComponentType } from 'react'
+import type { ReactNode } from 'react'
import { config } from '@fortawesome/fontawesome-svg-core'
import { ApolloProvider } from '@apollo/client'
import { ThemeProvider } from 'next-themes'
@@ -16,6 +16,7 @@ import '../initIcons'
import { client } from 'apolloClient'
import { AnalyticsProvider } from 'stores/analyticsContext'
import { AuthProvider } from 'stores/authContext'
+import { ModalProvider } from 'stores/modalContext'
import DefaultLayout from 'layout/DefaultLayout/DefaultLayout'
import { getAbsoluteUrl } from 'lib/getAbsoluteUrl'
@@ -45,120 +46,126 @@ const USSFPortalApp = ({ Component, pageProps, hostname }: Props) => {
-
-
-
-
-
- Space Force Portal
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ Space Force Portal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- {getLayout()}
+
+
+
+ {getLayout()}
+
diff --git a/src/stores/analyticsContext.tsx b/src/stores/analyticsContext.tsx
index aea842f50..0c7b73cf9 100644
--- a/src/stores/analyticsContext.tsx
+++ b/src/stores/analyticsContext.tsx
@@ -22,10 +22,10 @@ export type AnalyticsContextType = {
}
export const AnalyticsContext = createContext({
- push: () => {
+ push: /* istanbul ignore next */ () => {
return
},
- trackEvent: () => {
+ trackEvent: /* istanbul ignore next */ () => {
return
},
})
diff --git a/src/stores/authContext.tsx b/src/stores/authContext.tsx
index 447f579e0..b25d3a196 100644
--- a/src/stores/authContext.tsx
+++ b/src/stores/authContext.tsx
@@ -12,13 +12,13 @@ export type AuthContextType = {
export const AuthContext = createContext({
user: null,
- setUser: () => {
+ setUser: /* istanbul ignore next */ () => {
return
},
- logout: () => {
+ logout: /* istanbul ignore next */ () => {
return
},
- login: () => {
+ login: /* istanbul ignore next */ () => {
return
},
})
diff --git a/src/stores/modalContext.test.tsx b/src/stores/modalContext.test.tsx
new file mode 100644
index 000000000..44048583c
--- /dev/null
+++ b/src/stores/modalContext.test.tsx
@@ -0,0 +1,368 @@
+/**
+ * @jest-environment jsdom
+ */
+import React from 'react'
+import { screen, renderHook, cleanup } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { renderWithAuthAndApollo } from '../testHelpers'
+import { ModalProvider, useModalContext } from './modalContext'
+import CustomModal from 'components/CustomModal/CustomModal'
+import { Widget } from 'types'
+import {
+ editBookmarkMock,
+ mockBookmark,
+ mockCollectionIdForEditBookmark,
+} from '__fixtures__/operations/editBookmark'
+import {
+ addBookmarkMock,
+ mockCollectionId,
+} from '__fixtures__/operations/addBookmark'
+import {
+ removeWidgetMock,
+ mockWidget,
+} from '__fixtures__/operations/removeWidget'
+import {
+ removeCollectionMock,
+ mockCollection,
+} from '__fixtures__/operations/removeCollection'
+import {
+ removeBookmarkMock,
+ mockRemoveBookmark,
+ mockRemoveBookmarkCollectionId,
+} from '__fixtures__/operations/removeBookmark'
+
+describe('Modal context', () => {
+ afterEach(cleanup)
+
+ it('tests removing News section', async () => {
+ const user = userEvent.setup()
+
+ const TestComponent = () => {
+ const { modalRef, updateModalId, updateModalText, updateWidget } =
+ useModalContext()
+
+ const setupFunc = () => {
+ updateModalId('removeSectionModal')
+ updateModalText({
+ headingText: 'Are you sure you’d like to delete this section?',
+ descriptionText:
+ 'You can re-add it to your My Space from the Add Section menu.',
+ })
+
+ const widgetState: Widget = {
+ _id: mockWidget._id,
+ title: 'Recent News',
+ type: 'News',
+ }
+
+ updateWidget(widgetState)
+
+ modalRef?.current?.toggleModal(undefined, true)
+ }
+
+ return (
+
+
+
+
+
+ )
+ }
+
+ renderWithAuthAndApollo(
+
+
+ ,
+ {},
+ removeWidgetMock
+ )
+
+ const openModalButton = screen.getByRole('button', {
+ name: 'Remove section',
+ })
+ expect(openModalButton).toBeInTheDocument()
+
+ await user.click(openModalButton)
+
+ expect(
+ screen.getByText('Are you sure you’d like to delete this section?')
+ ).toBeInTheDocument()
+
+ const deleteButton = screen.getByText('Delete')
+ await user.click(deleteButton)
+
+ expect(removeWidgetMock[0].result).toEqual({
+ data: { _id: mockWidget._id },
+ })
+ })
+
+ it('tests adding a bookmark', async () => {
+ const user = userEvent.setup()
+
+ const TestComponent = () => {
+ const {
+ modalRef,
+ updateModalId,
+ updateModalText,
+ updateWidget,
+ updateCustomLinkLabel,
+ } = useModalContext()
+
+ const setupFunc = () => {
+ updateModalId('addCustomLinkModal')
+ updateModalText({
+ headingText: 'Add a custom link',
+ })
+
+ updateWidget({
+ _id: mockCollectionId,
+ title: 'Test Collection',
+ type: 'Collection',
+ })
+
+ updateCustomLinkLabel('My Custom Label', false, true)
+
+ modalRef?.current?.toggleModal(undefined, true)
+ }
+
+ return (
+
+ )
+ }
+
+ renderWithAuthAndApollo(
+
+
+ ,
+ {},
+ addBookmarkMock
+ )
+
+ const openModalButton = screen.getByRole('button', {
+ name: 'Add link',
+ })
+ expect(openModalButton).toBeInTheDocument()
+
+ await user.click(openModalButton)
+
+ // Add in link details
+ const urlInput = screen.getByLabelText('URL')
+
+ await user.clear(urlInput)
+ await user.type(urlInput, 'example.com')
+
+ // Save link
+ const saveButton = screen.getByText('Save custom link')
+ expect(saveButton).toBeInTheDocument()
+
+ await user.click(saveButton)
+
+ expect(addBookmarkMock[0].result.data.label).toEqual('My Custom Label')
+ })
+
+ it('tests editing a bookmark', async () => {
+ const user = userEvent.setup()
+
+ const TestComponent = () => {
+ const {
+ modalRef,
+ updateModalId,
+ updateModalText,
+ updateWidget,
+ updateBookmark,
+ } = useModalContext()
+
+ const setupFunc = () => {
+ updateModalId('editCustomLinkModal')
+ updateModalText({
+ headingText: 'Edit custom link',
+ })
+
+ updateWidget({
+ _id: mockCollectionIdForEditBookmark,
+ title: 'Test Collection',
+ type: 'Collection',
+ })
+
+ updateBookmark(mockBookmark)
+
+ modalRef?.current?.toggleModal(undefined, true)
+ }
+
+ return (
+
+ )
+ }
+
+ renderWithAuthAndApollo(
+
+
+ ,
+ {},
+ editBookmarkMock
+ )
+
+ // Open modal
+ const openModalButton = screen.getByRole('button', {
+ name: 'Edit link',
+ })
+ expect(openModalButton).toBeInTheDocument()
+
+ await user.click(openModalButton)
+
+ // Update label and save
+ const nameInput = screen.getByLabelText('Name')
+ await user.clear(nameInput)
+ await user.type(nameInput, 'Custom Label')
+
+ const saveButton = screen.getByText('Save custom link')
+ expect(saveButton).toBeInTheDocument()
+
+ await user.click(saveButton)
+
+ expect(editBookmarkMock[0].result.data.label).toEqual('Updated Label')
+ })
+
+ it('tests removing a bookmark while editing', async () => {
+ const user = userEvent.setup()
+
+ const TestComponent = () => {
+ const {
+ modalRef,
+ updateModalId,
+ updateModalText,
+ updateWidget,
+ updateBookmark,
+ } = useModalContext()
+
+ const setupFunc = () => {
+ updateModalId('editCustomLinkModal')
+ updateModalText({
+ headingText: 'Edit custom link',
+ })
+
+ updateWidget({
+ _id: mockRemoveBookmarkCollectionId,
+ title: 'Test Collection',
+ type: 'Collection',
+ })
+
+ updateBookmark(mockRemoveBookmark)
+
+ modalRef?.current?.toggleModal(undefined, true)
+ }
+
+ return (
+
+ )
+ }
+
+ renderWithAuthAndApollo(
+
+
+ ,
+ {},
+ removeBookmarkMock
+ )
+
+ // Open modal
+ const openModalButton = screen.getByRole('button', {
+ name: 'Edit link',
+ })
+ expect(openModalButton).toBeInTheDocument()
+
+ await user.click(openModalButton)
+
+ // Delete bookmark
+ const deleteButton = screen.getByText('Delete')
+ expect(deleteButton).toBeInTheDocument()
+
+ await user.click(deleteButton)
+
+ expect(removeBookmarkMock[0].result.data._id).toEqual(
+ mockRemoveBookmark._id
+ )
+ })
+
+ it('tests removing a custom collection', async () => {
+ const user = userEvent.setup()
+
+ const TestComponent = () => {
+ const { modalRef, updateModalId, updateModalText, updateWidget } =
+ useModalContext()
+
+ const setupFunc = () => {
+ updateModalId('removeCustomCollectionModal')
+ updateModalText({
+ headingText:
+ 'Are you sure you’d like to delete this collection from My Space?',
+ descriptionText: 'This action cannot be undone.',
+ })
+
+ updateWidget(mockCollection)
+
+ modalRef?.current?.toggleModal(undefined, true)
+ }
+
+ return (
+
+
+
+
+
+ )
+ }
+
+ renderWithAuthAndApollo(
+
+
+ ,
+ {},
+ removeCollectionMock
+ )
+
+ // Open modal
+ const openModalButton = screen.getByRole('button', {
+ name: 'Remove collection',
+ })
+ expect(openModalButton).toBeInTheDocument()
+
+ await user.click(openModalButton)
+
+ const deleteButton = screen.getByText('Delete')
+ expect(deleteButton).toBeInTheDocument()
+
+ await user.click(deleteButton)
+
+ expect(removeCollectionMock[0].result.data._id).toEqual(mockCollection._id)
+ })
+})
+
+describe('useModalContext', () => {
+ it('returns the created context', () => {
+ const { result } = renderHook(() => useModalContext())
+ expect(result.current).toBeTruthy()
+ })
+})
diff --git a/src/stores/modalContext.tsx b/src/stores/modalContext.tsx
new file mode 100644
index 000000000..03a40d701
--- /dev/null
+++ b/src/stores/modalContext.tsx
@@ -0,0 +1,249 @@
+import React, { createContext, useContext, useRef, useState } from 'react'
+import { ModalRef } from '@trussworks/react-uswds'
+import { useAnalytics } from 'stores/analyticsContext'
+import { useAddBookmarkMutation } from 'operations/portal/mutations/addBookmark.g'
+import { useRemoveCollectionMutation } from 'operations/portal/mutations/removeCollection.g'
+import { useRemoveWidgetMutation } from 'operations/portal/mutations/removeWidget.g'
+import { useRemoveBookmarkMutation } from 'operations/portal/mutations/removeBookmark.g'
+import { useEditBookmarkMutation } from 'operations/portal/mutations/editBookmark.g'
+import { Widget, Bookmark as BookmarkType } from 'types/index'
+
+// TO DO - A few things...
+// 1. Find a way to use callbacks for the handlers in this file (e.g. handleSaveCustomLink). We want
+// to have that logic originate from the component that is calling to open a modal, if we can.
+// 2. Refactor the various useState calls in ModalProvider. A lot of them could be consolidated into
+// a larger state object. Example: modalHeadingText, additionalText, and modalId could all be combined
+// into one state object, and as a result, each of their respective handler functions could be combined into one as well.
+// 3. Refactor ModalContextType to match updates mentioned in the above point.
+
+// modalRef had to be given an 'any' type, else it would cause
+// a ton of tests to fail. The mocked ref that is causing the issue
+// is in testHelpers.tsx on line 11.
+export type ModalContextType = {
+ modalId: string
+ updateModalId: (modalId: string) => void
+ modalRef: React.RefObject | null | any
+ modalHeadingText: string
+ closeModal: () => void
+ onDelete: () => void
+ onSave: (url: string, label: string) => void
+ updateWidget: (widget: Widget) => void
+ updateModalText: ({
+ headingText,
+ descriptionText,
+ }: {
+ headingText: string
+ descriptionText?: string
+ }) => void
+ additionalText?: string
+ bookmark?: BookmarkType | null
+ updateBookmark: (bookmark: BookmarkType) => void
+ customLinkLabel?: string
+ updateCustomLinkLabel: (
+ customLinkLabel: string,
+ showAddWarning: boolean,
+ isAddingLink: boolean
+ ) => void
+ showAddWarning?: boolean
+ isAddingLinkContext: boolean
+}
+
+export const ModalContext = createContext({
+ modalId: '',
+ updateModalId: /* istanbul ignore next */ () => {
+ return
+ },
+ modalRef: null,
+ modalHeadingText: '',
+ closeModal: /* istanbul ignore next */ () => {
+ return
+ },
+ onDelete: /* istanbul ignore next */ () => {
+ return
+ },
+ onSave: /* istanbul ignore next */ () => {
+ return
+ },
+ updateWidget: /* istanbul ignore next */ () => {
+ return
+ },
+ updateBookmark: /* istanbul ignore next */ () => {
+ return
+ },
+ updateCustomLinkLabel: /* istanbul ignore next */ () => {
+ return
+ },
+ updateModalText: /* istanbul ignore next */ () => {
+ return
+ },
+ additionalText: '',
+ isAddingLinkContext: false,
+})
+
+export const ModalProvider = ({ children }: { children: React.ReactNode }) => {
+ const [modalHeadingText, setModalHeadingText] = useState('')
+ const [additionalText, setAdditionalText] = useState('')
+ const [modalId, setModalId] = useState('')
+ const [customLinkLabel, setCustomLinkLabel] = useState('')
+ const [showAddWarning, setShowAddWarning] = useState(false)
+ const [widgetState, setWidgetState] = useState()
+ const [bookmark, setBookmark] = useState()
+
+ // In CustomCollection.tsx there is an isAddingLink state that controls
+ // the visibility of a dropdown. isAddingLinkContext mirrors that value,
+ // and is evaluated in a useEffect to close the dropdown if a modal is closed.
+ const [isAddingLinkContext, setIsAddingLinkContext] = useState(false)
+
+ const modalRef = useRef(null)
+ const { trackEvent } = useAnalytics()
+
+ const [handleAddBookmark] = useAddBookmarkMutation()
+ const [handleRemoveCollection] = useRemoveCollectionMutation()
+ const [handleRemoveWidget] = useRemoveWidgetMutation()
+ const [handleRemoveBookmark] = useRemoveBookmarkMutation()
+ const [handleEditBookmark] = useEditBookmarkMutation()
+
+ const closeModal = () => {
+ setWidgetState(null)
+ setBookmark(null)
+ setCustomLinkLabel('')
+ setModalId('')
+ if (isAddingLinkContext) {
+ setIsAddingLinkContext(false)
+ }
+ modalRef.current?.toggleModal(undefined, false)
+ }
+
+ const updateModalText = ({
+ headingText,
+ descriptionText = '',
+ }: {
+ headingText: string
+ descriptionText?: string
+ }) => {
+ setModalHeadingText(headingText)
+ setAdditionalText(descriptionText)
+ }
+
+ const handleSaveCustomLink = (url: string, label: string) => {
+ trackEvent(
+ 'Add link',
+ 'Save custom link',
+ `${widgetState?.title} / ${label} / ${url}`
+ )
+ handleAddBookmark({
+ variables: {
+ collectionId: widgetState?._id,
+ url,
+ label,
+ },
+ refetchQueries: [`getMySpace`],
+ })
+
+ closeModal()
+ }
+
+ const handleEditCustomLink = (url: string, label: string) => {
+ handleEditBookmark({
+ variables: {
+ _id: bookmark?._id,
+ collectionId: widgetState?._id,
+ url,
+ label,
+ },
+ refetchQueries: [`getMySpace`],
+ })
+ closeModal()
+ }
+
+ const updateModalId = (modalId: string) => {
+ setModalId(modalId)
+ }
+
+ const updateWidget = (widget: Widget) => {
+ setWidgetState(widget)
+ }
+
+ const updateBookmark = (bookmark: BookmarkType) => {
+ setBookmark(bookmark)
+ }
+
+ const updateCustomLinkLabel = (
+ customLinkLabel: string,
+ showAddWarning = false,
+ isAddingLink = false
+ ) => {
+ setCustomLinkLabel(customLinkLabel)
+ setShowAddWarning(showAddWarning)
+ setIsAddingLinkContext(isAddingLink)
+ }
+
+ const onDelete = () => {
+ switch (modalId) {
+ case 'removeSectionModal':
+ trackEvent('Section settings', 'Remove this section', 'News')
+ handleRemoveWidget({
+ variables: { _id: widgetState?._id },
+ refetchQueries: [`getMySpace`],
+ })
+ closeModal()
+ break
+ case 'removeCustomCollectionModal':
+ trackEvent(
+ 'Collection settings',
+ 'Delete collection',
+ widgetState?.title
+ )
+ handleRemoveCollection({
+ variables: {
+ _id: widgetState?._id,
+ },
+ refetchQueries: [`getMySpace`],
+ })
+ closeModal()
+ break
+ case 'editCustomLinkModal':
+ handleRemoveBookmark({
+ variables: {
+ _id: bookmark?._id,
+ collectionId: widgetState?._id,
+ },
+ refetchQueries: [`getMySpace`],
+ })
+ closeModal()
+ break
+ default:
+ return null
+ }
+ }
+
+ const context = {
+ modalId: '',
+ updateModalId,
+ modalRef,
+ modalHeadingText,
+ closeModal,
+ onDelete,
+ updateWidget,
+ updateModalText,
+ additionalText,
+ customLinkLabel,
+ updateCustomLinkLabel,
+ showAddWarning,
+ isAddingLinkContext,
+ onSave:
+ modalId === 'addCustomLinkModal'
+ ? handleSaveCustomLink
+ : handleEditCustomLink,
+ bookmark,
+ updateBookmark,
+ }
+
+ return (
+ {children}
+ )
+}
+export const useModalContext = () => {
+ const context = useContext(ModalContext)
+ return context
+}
diff --git a/src/testHelpers.tsx b/src/testHelpers.tsx
index 5956e3222..4dcbfca1e 100644
--- a/src/testHelpers.tsx
+++ b/src/testHelpers.tsx
@@ -1,24 +1,61 @@
import React from 'react'
-import { render, RenderOptions } from '@testing-library/react'
+import { render } from '@testing-library/react'
import { MockedProvider, MockedResponse } from '@apollo/client/testing'
-
import { testUser1 } from './__fixtures__/authUsers'
import { AuthContext, AuthContextType } from 'stores/authContext'
+import { ModalContext, ModalContextType } from 'stores/modalContext'
+
+export const defaultMockModalContext = {
+ modalId: '',
+ updateModalId: jest.fn(),
+ modalRef: jest.spyOn(React, 'useRef'),
+ modalHeadingText: '',
+ closeModal: jest.fn(),
+ onDelete: jest.fn(),
+ onSave: jest.fn(),
+ updateWidget: jest.fn(),
+ updateModalText: jest.fn(),
+ additionalText: '',
+ updateBookmark: jest.fn(),
+ customLinkLabel: '',
+ updateCustomLinkLabel: jest.fn(),
+ showAddWarning: false,
+ isAddingLinkContext: false,
+}
export const renderWithModalRoot = (
- ui: React.ReactElement,
- options: RenderOptions = {}
+ component: React.ReactElement,
+ value: Partial = {},
+ mocks: readonly MockedResponse>[] = []
) => {
const modalContainer = document.createElement('div')
modalContainer.setAttribute('id', 'modal-root')
- return render(ui, {
- ...options,
+ const contextValue = {
+ ...defaultMockModalContext,
+ ...value,
+ }
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+
+ {children}
+
+
+ )
+ }
+
+ return render(component, {
+ // ...options,
container: document.body.appendChild(modalContainer),
- // modal seems to trigger the legacy mode for React
- // there is a warning that the test isn't setup to use act when this is removed
- // so eventually we need to update it.
- legacyRoot: true,
+ wrapper,
})
}