From 8404a0184e60d50a58f4579024f146f554b8b25d Mon Sep 17 00:00:00 2001 From: VineetBala-AOT <90332175+VineetBala-AOT@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:10:08 -0700 Subject: [PATCH] [To Feature] DESENG-501 Added new content tabs to engagement form (#2427) * Added new content tabs to engagement form --- met-web/src/apiManager/endpoints/index.ts | 17 ++ .../engagement/form/ActionContext.tsx | 28 +++ .../EngagementContent/ContentTabModal.tsx | 187 +++++++++++++++++ .../EngagementContent/ContentTabs.tsx | 188 ++++++++++++++++++ .../EngagementContent/CustomTabContent.tsx | 50 +++++ .../EngagementContentContext.tsx | 129 ++++++++++++ .../EngagementContent/SummaryTabContent.tsx | 78 ++++++++ .../EngagementContent/index.tsx | 13 ++ .../EngagementFormTabs/EngagementForm.tsx | 30 +-- .../EngagementTabsContext.tsx | 138 ++++++++++++- .../src/components/engagement/form/types.ts | 6 +- .../engagement/view/ActionContext.tsx | 41 +++- .../engagement/view/EngagementContent.tsx | 6 +- .../engagement/view/PreviewBanner.tsx | 4 +- .../engagement/view/ScheduleModal/index.tsx | 4 +- met-web/src/models/engagementContent.ts | 28 +++ met-web/src/models/engagementCustomContent.ts | 7 + .../src/models/engagementSummaryContent.ts | 7 + .../engagementContentService/index.ts | 82 ++++++++ .../services/engagementCustomService/index.ts | 59 ++++++ .../src/services/engagementService/types.ts | 4 - .../engagementSummaryService/index.ts | 58 ++++++ .../edit/EngagementForm.Edit.One.test.tsx | 34 ++-- .../edit/EngagementForm.Edit.Two.test.tsx | 97 ++++++++- 24 files changed, 1234 insertions(+), 61 deletions(-) create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabModal.tsx create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabs.tsx create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/CustomTabContent.tsx create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/EngagementContentContext.tsx create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/SummaryTabContent.tsx create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/index.tsx create mode 100644 met-web/src/models/engagementContent.ts create mode 100644 met-web/src/models/engagementCustomContent.ts create mode 100644 met-web/src/models/engagementSummaryContent.ts create mode 100644 met-web/src/services/engagementContentService/index.ts create mode 100644 met-web/src/services/engagementCustomService/index.ts create mode 100644 met-web/src/services/engagementSummaryService/index.ts diff --git a/met-web/src/apiManager/endpoints/index.ts b/met-web/src/apiManager/endpoints/index.ts index 1d438ced2..db5861fe7 100644 --- a/met-web/src/apiManager/endpoints/index.ts +++ b/met-web/src/apiManager/endpoints/index.ts @@ -34,6 +34,23 @@ const Endpoints = { GET: `${AppConfig.apiUrl}/slugs/slug_id`, GET_ENG_ID: `${AppConfig.apiUrl}/slugs/engagements/engagement_id`, }, + EngagementContent: { + GET: `${AppConfig.apiUrl}/engagement/engagement_id/content`, + CREATE: `${AppConfig.apiUrl}/engagement/engagement_id/content`, + SORT: `${AppConfig.apiUrl}/engagement/engagement_id/content/sort_index`, + UPDATE: `${AppConfig.apiUrl}/engagement/engagement_id/content/content_id`, + DELETE: `${AppConfig.apiUrl}/engagement/engagement_id/content/content_id`, + }, + EngagementCustomContent: { + GET: `${AppConfig.apiUrl}/content/content_id/custom`, + CREATE: `${AppConfig.apiUrl}/content/content_id/custom`, + UPDATE: `${AppConfig.apiUrl}/content/content_id/custom`, + }, + EngagementSummaryContent: { + GET: `${AppConfig.apiUrl}/content/content_id/summary`, + CREATE: `${AppConfig.apiUrl}/content/content_id/summary`, + UPDATE: `${AppConfig.apiUrl}/content/content_id/summary`, + }, User: { GET: `${AppConfig.apiUrl}/user/user_id`, CREATE_UPDATE: `${AppConfig.apiUrl}/user/`, diff --git a/met-web/src/components/engagement/form/ActionContext.tsx b/met-web/src/components/engagement/form/ActionContext.tsx index ab320d7d7..75ce81609 100644 --- a/met-web/src/components/engagement/form/ActionContext.tsx +++ b/met-web/src/components/engagement/form/ActionContext.tsx @@ -1,9 +1,11 @@ import React, { createContext, useState, useEffect, useMemo } from 'react'; import { postEngagement, getEngagement, patchEngagement } from '../../../services/engagementService'; import { getEngagementMetadata, getMetadataTaxa } from '../../../services/engagementMetadataService'; +import { getEngagementContent } from 'services/engagementContentService'; import { useNavigate, useParams } from 'react-router-dom'; import { EngagementContext, EngagementForm, EngagementFormUpdate, EngagementParams } from './types'; import { createDefaultEngagement, Engagement, EngagementMetadata, MetadataTaxon } from '../../../models/engagement'; +import { createDefaultEngagementContent, EngagementContent } from 'models/engagementContent'; import { saveObject } from 'services/objectStorageService'; import { openNotification } from 'services/notificationService/notificationSlice'; import { useAppDispatch, useAppSelector } from 'hooks'; @@ -51,6 +53,13 @@ export const ActionContext = createContext({ setIsNewEngagement: () => { /* empty default method */ }, + contentTabs: [createDefaultEngagementContent()], + setContentTabs: () => { + return; + }, + fetchEngagementContents: async () => { + /* empty default method */ + }, }); export const ActionProvider = ({ children }: { children: JSX.Element }) => { @@ -70,6 +79,7 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { const [engagementMetadata, setEngagementMetadata] = useState([]); const [bannerImage, setBannerImage] = useState(); const [savedBannerImageFileName, setSavedBannerImageFileName] = useState(''); + const [contentTabs, setContentTabs] = useState([createDefaultEngagementContent()]); const isCreate = window.location.pathname.includes(CREATE); @@ -126,6 +136,20 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { } }; + const fetchEngagementContents = async () => { + if (isCreate) { + return; + } + + try { + const engagementContents = await getEngagementContent(Number(engagementId)); + setContentTabs(engagementContents); + } catch (err) { + console.log(err); + dispatch(openNotification({ severity: 'error', text: 'Error Fetching Engagement Contents' })); + } + }; + const taxonMetadata = useMemo(() => { const taxonMetadataMap = new Map(); engagementMetadata.forEach((metadata) => { @@ -173,6 +197,7 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { const loadData = async () => { await fetchEngagement(); await fetchEngagementMetadata(); + await fetchEngagementContents(); setLoadingSavedEngagement(false); }; @@ -275,6 +300,9 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { loadingAuthorization, isNewEngagement, setIsNewEngagement, + contentTabs, + setContentTabs, + fetchEngagementContents, }} > {children} diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabModal.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabModal.tsx new file mode 100644 index 000000000..8ca010ad6 --- /dev/null +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabModal.tsx @@ -0,0 +1,187 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { MenuItem, Modal, Grid, Stack, TextField, Select } from '@mui/material'; +import { modalStyle, MetHeader1, MetLabel, PrimaryButton } from 'components/common'; +import { useAppDispatch } from 'hooks'; +import { openNotification } from 'services/notificationService/notificationSlice'; +import { ActionContext } from '../../ActionContext'; +import { EngagementContentContext } from './EngagementContentContext'; +import { EngagementContent } from 'models/engagementContent'; +import { postEngagementContent, patchEngagementContent } from 'services/engagementContentService'; + +interface ContentTabModalProps { + open: boolean; + updateModal: (open: boolean) => void; + tabs: EngagementContent[]; + setTabs: React.Dispatch>; + selectedTabType?: string; + tabIndex?: number; +} + +const ContentTabModal = ({ open, updateModal, tabs, setTabs, selectedTabType, tabIndex }: ContentTabModalProps) => { + const { savedEngagement } = useContext(ActionContext); + const { isEditMode, setIsSummaryContentsLoading, setIsCustomContentsLoading } = + useContext(EngagementContentContext); + const dispatch = useAppDispatch(); + const [tabTitle, setTabTitle] = useState(''); + const [tabIcon, setTabIcon] = useState(''); + + useEffect(() => { + // Fetch tab details when modal is opened and selectedTabIndex changes + if (open && isEditMode && typeof tabIndex === 'number' && tabs[tabIndex]) { + setTabTitle(tabs[tabIndex].title); + setTabIcon(tabs[tabIndex].icon_name); + } else { + // If not in edit mode, initialize tabTitle and tabIcon with empty values + setTabTitle(''); + setTabIcon(''); + } + }, [open, isEditMode, tabIndex, tabs]); + + const fetchData = async () => { + setIsSummaryContentsLoading(true); + setIsCustomContentsLoading(true); + const newtab = isEditMode + ? await patchEngagementContent(savedEngagement.id, tabs[tabIndex || 0].id, { + title: tabTitle, + icon_name: tabIcon, + }) + : await postEngagementContent(savedEngagement.id, { + title: tabTitle, + icon_name: tabIcon, + content_type: selectedTabType, + engagement_id: savedEngagement.id, + }); + + if (isEditMode) { + if (newtab && Object.keys(newtab).length !== 0) { + setTabs((prevTabs) => { + const newTabs = [...prevTabs]; + newTabs[tabIndex || 0] = newtab; + return newTabs; + }); + } + } else { + if (newtab && Object.keys(newtab).length !== 0) { + // Update the state by adding the new tab to the existing tabs + setTabs((prevTabs) => [...prevTabs, newtab]); + } + } + + dispatch( + openNotification({ + severity: 'success', + text: `Content tab successfully ${isEditMode ? 'updated' : 'created'}. Proceed to ${ + isEditMode ? 'edit' : 'add' + } details.`, + }), + ); + setIsSummaryContentsLoading(false); + setIsCustomContentsLoading(false); + handleModalClose(); + }; + + const handleModalClose = () => { + updateModal(false); + setTabTitle(''); + setTabIcon(''); + }; + + return ( + updateModal(false)}> + + + + + {isEditMode ? 'Edit the engagement content tab' : 'Add a new engagement content tab'} + + + + + + + Tab Title: + + + + + setTabTitle(e.target.value)} + error={tabTitle.length > 50} + helperText={ + tabTitle.length > 50 + ? 'Title must not exceed 50 characters' + : 'Title must be specified' + } + size="small" + /> + + + + + + + Tab Icon: + + + + + + + + + + + {isEditMode ? 'Update Tab' : 'Add Tab'} + + + + + ); +}; + +export default ContentTabModal; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabs.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabs.tsx new file mode 100644 index 000000000..c77acee0a --- /dev/null +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabs.tsx @@ -0,0 +1,188 @@ +import React, { useContext, useState } from 'react'; +import { Box, Button, IconButton, MenuItem, Tooltip, Menu, Skeleton, Grid, Divider } from '@mui/material'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTrash, faPenToSquare, faPlus } from '@fortawesome/free-solid-svg-icons'; +import CustomTabContent from './CustomTabContent'; +import SummaryTabContent from './SummaryTabContent'; +import { MetTab, MetTabList, MetTabPanel } from '../../StyledTabComponents'; +import TabContext from '@mui/lab/TabContext'; +import { CONTENT_TYPE } from 'models/engagementContent'; +import { deleteEngagementContent } from 'services/engagementContentService'; +import { ActionContext } from '../../ActionContext'; +import { EngagementContentContext } from './EngagementContentContext'; +import { If, Then, Else } from 'react-if'; +import ContentTabModal from './ContentTabModal'; + +export const ContentTabs: React.FC = () => { + const { fetchEngagementContents, contentTabs, setContentTabs, savedEngagement } = useContext(ActionContext); + const { setIsEditMode, isSummaryContentsLoading } = useContext(EngagementContentContext); + const [tabIndex, setTabIndex] = useState(0); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedTabType, setSelectedTabType] = useState(''); + const [anchorEl, setAnchorEl] = useState(null); + const [customTabAdded, setCustomTabAdded] = useState(false); + const isAllTabTypesPresent = () => { + const tabTypes = Object.values(CONTENT_TYPE); + for (const tabType of tabTypes) { + if (!contentTabs.some((tab) => tab.content_type === tabType)) { + return false; // At least one tab type is missing + } + } + return true; // All tab types are present + }; + + const handleSetTabIndex = (_event: React.SyntheticEvent, newValue: number) => { + setTabIndex(newValue); + }; + + const handleModalOpen = () => { + setIsModalOpen(true); + }; + + const handleModalClose = () => { + setIsModalOpen(false); + }; + + const handleDeleteTab = async (index: number) => { + const tab_id = contentTabs[index].id; + if (!tab_id) { + return; + } + await deleteEngagementContent(savedEngagement.id, tab_id); + await fetchEngagementContents(); + setTabIndex(0); + + // Check if the deleted tab was a custom tab and reset customTabAdded state + if (contentTabs[index].content_type === CONTENT_TYPE.CUSTOM) { + setCustomTabAdded(false); + } + }; + + const handleMenuOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + + const handleMenuClick = (type: string) => { + setIsEditMode(false); + setSelectedTabType(type); + handleModalOpen(); + handleMenuClose(); + + // Set customTabAdded to true when a custom tab is added + if (type === CONTENT_TYPE.CUSTOM) { + setCustomTabAdded(true); + } + }; + + const handleEditTab = (index: number) => { + setIsEditMode(true); + setTabIndex(index); + setIsModalOpen(true); + }; + + return ( + + + + + + + + + + + + {contentTabs?.map((tab, index) => ( + + {tab.title} + + handleEditTab(index)} aria-label="edit"> + + + + {index !== 0 && ( + + handleDeleteTab(index)} + aria-label="delete" + > + + + + )} + + } + /> + ))} + + + + + + handleMenuClick(CONTENT_TYPE.CUSTOM)} + data-testid="add-new-custom-tab" + > + Add new custom tab + + + + + + {contentTabs && contentTabs.length > 0 ? ( + (() => { + if (contentTabs[tabIndex]) { + switch (contentTabs[tabIndex].content_type) { + case CONTENT_TYPE.SUMMARY: + return ; + case CONTENT_TYPE.CUSTOM: + return ; + default: + return ( +
+ Custom tab content for {contentTabs[tabIndex].content_type} +
+ ); + } + } else { + return
Invalid tab index
; + } + })() + ) : ( +
No tabs available
+ )} +
+
+ + +
+
+ ); +}; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/CustomTabContent.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/CustomTabContent.tsx new file mode 100644 index 000000000..b3bc525ad --- /dev/null +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/CustomTabContent.tsx @@ -0,0 +1,50 @@ +import React, { useEffect, useState, useContext } from 'react'; +import { Grid } from '@mui/material'; +import { MetDescription, MetLabel } from 'components/common'; +import RichTextEditor from 'components/common/RichTextEditor'; +import { EngagementTabsContext } from '../EngagementTabsContext'; + +const CustomTabContent = () => { + const [initialRichContent, setInitialRichContent] = useState(''); + + const { setCustomTextContent, customJsonContent, setCustomJsonContent } = useContext(EngagementTabsContext); + + const handleContentChange = (rawText: string) => { + setCustomTextContent(rawText); + }; + + const handleRichContentChange = (newState: string) => { + setCustomJsonContent(newState); + }; + + useEffect(() => { + setInitialRichContent(customJsonContent); + }, []); + + return ( + + + Engagement - Page Custom Content + + This is the additional content of the engagement page. + + + + + ); +}; + +export default CustomTabContent; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/EngagementContentContext.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/EngagementContentContext.tsx new file mode 100644 index 000000000..d7341f8e7 --- /dev/null +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/EngagementContentContext.tsx @@ -0,0 +1,129 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { CONTENT_TYPE } from 'models/engagementContent'; +import { getSummaryContent } from 'services/engagementSummaryService'; +import { getCustomContent } from 'services/engagementCustomService'; +import { ActionContext } from '../../ActionContext'; +import { EngagementTabsContext } from '../EngagementTabsContext'; +import { useAppDispatch } from 'hooks'; +import { openNotification } from 'services/notificationService/notificationSlice'; + +export interface EngagementContentProps { + isSummaryContentsLoading: boolean; + setIsSummaryContentsLoading: React.Dispatch>; + isCustomContentsLoading: boolean; + setIsCustomContentsLoading: React.Dispatch>; + isEditMode: boolean; + setIsEditMode: React.Dispatch>; +} + +export const EngagementContentContext = createContext({ + isSummaryContentsLoading: false, + setIsSummaryContentsLoading: async () => { + /* empty default method */ + }, + isCustomContentsLoading: false, + setIsCustomContentsLoading: async () => { + /* empty default method */ + }, + isEditMode: false, + setIsEditMode: async () => { + /* empty default method */ + }, +}); + +export const EngagementContextProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => { + const dispatch = useAppDispatch(); + const { contentTabs, savedEngagement } = useContext(ActionContext); + const { + setRichContent, + engagementFormData, + setEngagementFormData, + setEngagementSummaryContent, + setEngagementCustomContent, + setCustomTextContent, + setCustomJsonContent, + } = useContext(EngagementTabsContext); + const [isSummaryContentsLoading, setIsSummaryContentsLoading] = useState(true); + const [isCustomContentsLoading, setIsCustomContentsLoading] = useState(true); + const [isEditMode, setIsEditMode] = useState(false); + const summaryItem = contentTabs.find((item) => item.content_type === CONTENT_TYPE.SUMMARY); + const customItem = contentTabs.find((item) => item.content_type === CONTENT_TYPE.CUSTOM); + + const fetchEngagementSummaryContent = async () => { + if (!savedEngagement.id || !summaryItem) { + setIsSummaryContentsLoading(false); + return; + } + try { + const result = await getSummaryContent(summaryItem.id); + setEngagementSummaryContent(result[0]); + setRichContent(result[0].rich_content); + setEngagementFormData({ + ...engagementFormData, + content: result[0].content, + }); + setIsSummaryContentsLoading(false); + } catch (error) { + setIsSummaryContentsLoading(false); + dispatch( + openNotification({ + severity: 'error', + text: 'Error occurred while fetching engagement summary content', + }), + ); + } + }; + + const fetchEngagementCustomContent = async () => { + if (!savedEngagement.id || !customItem) { + setIsCustomContentsLoading(false); + return; + } + try { + const result = await getCustomContent(customItem.id); + setEngagementCustomContent(result[0]); + setCustomTextContent(result[0].custom_text_content); + setCustomJsonContent(result[0].custom_json_content); + setIsCustomContentsLoading(false); + } catch (error) { + setIsCustomContentsLoading(false); + dispatch( + openNotification({ + severity: 'error', + text: 'Error occurred while fetching engagement custom content', + }), + ); + } + }; + + useEffect(() => { + const fetchData = async () => { + await fetchEngagementSummaryContent(); + }; + + fetchData(); + }, [summaryItem]); + + useEffect(() => { + const fetchData = async () => { + await fetchEngagementCustomContent(); + }; + + fetchData(); + }, [customItem]); + + return ( + + {children} + + ); +}; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/SummaryTabContent.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/SummaryTabContent.tsx new file mode 100644 index 000000000..ab998a942 --- /dev/null +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/SummaryTabContent.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState, useContext } from 'react'; +import { Grid } from '@mui/material'; +import { SurveyBlock } from '../SurveyBlock'; +import { MetDescription, MetLabel } from 'components/common'; +import RichTextEditor from 'components/common/RichTextEditor'; +import { EngagementTabsContext } from '../EngagementTabsContext'; +import { ActionContext } from '../../ActionContext'; + +const SummaryTabContent = () => { + const { savedEngagement } = useContext(ActionContext); + const [initialRichContent, setInitialRichContent] = useState(''); + const [editorDisabled, setEditorDisabled] = useState(false); + + const { engagementFormData, setEngagementFormData, richContent, setRichContent } = + useContext(EngagementTabsContext); + + const handleContentChange = (rawText: string) => { + setEngagementFormData({ + ...engagementFormData, + content: rawText, + }); + }; + + const handleRichContentChange = (newState: string) => { + setRichContent(newState); + }; + + useEffect(() => { + setInitialRichContent(richContent || savedEngagement.rich_content); + setEditorDisabled(false); + }, []); + + return ( + + + Engagement - Page Content + + This is the main content of the engagement page. + +
+ + {/* Overlay to prevent user interaction */} + {editorDisabled && ( +
+ )} +
+ + + + + + ); +}; + +export default SummaryTabContent; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/index.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/index.tsx new file mode 100644 index 000000000..05e9f5550 --- /dev/null +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { EngagementContextProvider } from './EngagementContentContext'; +import { ContentTabs } from './ContentTabs'; + +const EngagementContentTabs = () => { + return ( + + + + ); +}; + +export default EngagementContentTabs; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementForm.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementForm.tsx index 3784853d4..63077a121 100644 --- a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementForm.tsx +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementForm.tsx @@ -3,13 +3,13 @@ import { Typography, Grid, TextField, Stack, Box } from '@mui/material'; import { MetPaper, MetLabel, PrimaryButton, SecondaryButton, MetDescription } from '../../../common'; import { ActionContext } from '../ActionContext'; import ImageUpload from 'components/imageUpload'; -import { SurveyBlock } from './SurveyBlock'; import { EngagementTabsContext } from './EngagementTabsContext'; import { EngagementStatus } from 'constants/engagementStatus'; import DayCalculatorModal from '../DayCalculator'; import { ENGAGEMENT_CROPPER_ASPECT_RATIO, ENGAGEMENT_UPLOADER_HEIGHT } from './constants'; import RichTextEditor from 'components/common/RichTextEditor'; import { getTextFromDraftJsContentState } from 'components/common/RichTextEditor/utils'; +import EngagementContentTabs from './EngagementContent/'; const EngagementForm = () => { const { isSaving, savedEngagement, handleAddBannerImage, setIsNewEngagement } = useContext(ActionContext); @@ -22,14 +22,11 @@ const EngagementForm = () => { handleSaveAndExitEngagement, richDescription, setRichDescription, - richContent, - setRichContent, engagementFormError, setEngagementFormError, } = useContext(EngagementTabsContext); const [initialRichDescription, setInitialRichDescription] = useState(''); - const [initialRichContent, setInitialRichContent] = useState(''); const [descriptionCharCount, setDescriptionCharCount] = useState(0); const [isOpen, setIsOpen] = useState(false); @@ -39,7 +36,6 @@ const EngagementForm = () => { useEffect(() => { const initialDescription = getTextFromDraftJsContentState(richDescription || savedEngagement.rich_description); setInitialRichDescription(richDescription || savedEngagement.rich_description); - setInitialRichContent(richContent || savedEngagement.rich_content); setDescriptionCharCount(initialDescription.length); }, []); @@ -81,21 +77,10 @@ const EngagementForm = () => { }); }; - const handleContentChange = (rawText: string) => { - setEngagementFormData({ - ...engagementFormData, - content: rawText, - }); - }; - const handleRichDescriptionChange = (newState: string) => { setRichDescription(newState); }; - const handleRichContentChange = (newState: string) => { - setRichContent(newState); - }; - const isDateFieldDisabled = [EngagementStatus.Closed, EngagementStatus.Unpublished].includes( savedEngagement.status_id, ); @@ -228,20 +213,9 @@ const EngagementForm = () => { Character Count: {descriptionCharCount} - - Engagement - Page Content - - This is the main content of the engagement page. - - - - + >; + engagementSummaryContent: EngagementSummaryContent; + setEngagementSummaryContent: React.Dispatch>; + engagementCustomContent: EngagementCustomContent; + setEngagementCustomContent: React.Dispatch>; + customTextContent: string; + setCustomTextContent: React.Dispatch>; + customJsonContent: string; + setCustomJsonContent: React.Dispatch>; } export const EngagementTabsContext = createContext({ @@ -171,6 +199,22 @@ export const EngagementTabsContext = createContext({ setSlug: () => { /* empty default method */ }, + engagementSummaryContent: initialEngagementSummaryContent, + setEngagementSummaryContent: () => { + /* empty default method */ + }, + engagementCustomContent: initialEngagementCustomContent, + setEngagementCustomContent: () => { + /* empty default method */ + }, + customTextContent: '', + setCustomTextContent: () => { + /* empty default method */ + }, + customJsonContent: '', + setCustomJsonContent: () => { + /* empty default method */ + }, }); export const EngagementTabsContextProvider = ({ children }: { children: React.ReactNode }) => { @@ -192,8 +236,15 @@ export const EngagementTabsContextProvider = ({ children }: { children: React.Re consent_message: savedEngagement.consent_message || '', }); const [richDescription, setRichDescription] = useState(savedEngagement?.rich_description || ''); - const [richContent, setRichContent] = useState(savedEngagement?.rich_content || ''); + const [richContent, setRichContent] = useState(''); const [richConsentMessage, setRichConsentMessage] = useState(savedEngagement?.consent_message || ''); + const [customTextContent, setCustomTextContent] = useState(''); + const [customJsonContent, setCustomJsonContent] = useState(''); + const [engagementSummaryContent, setEngagementSummaryContent] = useState( + initialEngagementSummaryContent, + ); + const [engagementCustomContent, setEngagementCustomContent] = + useState(initialEngagementCustomContent); const [engagementFormError, setEngagementFormError] = useState(initialFormError); const metadataFormRef = useRef(null); @@ -301,6 +352,80 @@ export const EngagementTabsContextProvider = ({ children }: { children: React.Re } }; + const updateSummaryContent = async () => { + setSaving(true); + try { + const updatedSummaryContent = updatedDiff( + { + content: engagementSummaryContent.content, + rich_content: engagementSummaryContent.rich_content, + }, + { + content: engagementFormData.content, + rich_content: richContent, + }, + ) as PatchSummaryContentRequest; + + if (Object.keys(updatedSummaryContent).length === 0) { + setSaving(false); + return; + } + const result = await patchSummaryContent( + engagementSummaryContent.engagement_content_id, + updatedSummaryContent, + ); + setEngagementSummaryContent(result); + setRichContent(result.rich_content); + setSaving(false); + return; + } catch (error) { + dispatch( + openNotification({ + severity: 'error', + text: 'Error occurred while trying to update summary content, please refresh the page or try again at a later time', + }), + ); + setSaving(false); + } + }; + + const updateCustomContent = async () => { + setSaving(true); + try { + const updatedCustomContent = updatedDiff( + { + custom_text_content: engagementCustomContent.custom_text_content, + custom_json_content: engagementCustomContent.custom_json_content, + }, + { + custom_text_content: customTextContent, + custom_json_content: customJsonContent, + }, + ) as PatchCustomContentRequest; + + if (Object.keys(updatedCustomContent).length === 0) { + setSaving(false); + return; + } + const result = await patchCustomContent( + engagementCustomContent.engagement_content_id, + updatedCustomContent, + ); + setEngagementCustomContent(result); + setCustomJsonContent(result.custom_json_content); + setSaving(false); + return; + } catch (error) { + dispatch( + openNotification({ + severity: 'error', + text: 'Error occurred while trying to update custom content, please refresh the page or try again at a later time', + }), + ); + setSaving(false); + } + }; + const [savedSlug, setSavedSlug] = useState(''); const [slug, setSlug] = useState(initialEngagementSettingsSlugData); @@ -411,12 +536,13 @@ export const EngagementTabsContextProvider = ({ children }: { children: React.Re : await handleUpdateEngagementRequest({ ...engagementFormData, rich_description: richDescription, - rich_content: richContent, status_block: surveyBlockList, }); if (!isNewEngagement) { await updateEngagementSettings(sendReport); + await updateSummaryContent(); + await updateCustomContent(); await handleSaveSlug(slug); await handleSaveEngagementMetadata(); } @@ -486,6 +612,14 @@ export const EngagementTabsContextProvider = ({ children }: { children: React.Re hasBeenOpened, slug, setSlug, + engagementSummaryContent, + setEngagementSummaryContent, + engagementCustomContent, + setEngagementCustomContent, + customTextContent, + setCustomTextContent, + customJsonContent, + setCustomJsonContent, }} > {children} diff --git a/met-web/src/components/engagement/form/types.ts b/met-web/src/components/engagement/form/types.ts index 3f3b1c225..25a453ecc 100644 --- a/met-web/src/components/engagement/form/types.ts +++ b/met-web/src/components/engagement/form/types.ts @@ -1,6 +1,7 @@ import React from 'react'; import { Engagement, EngagementMetadata, MetadataTaxon } from '../../../models/engagement'; import { EngagementStatusBlock } from '../../../models/engagementStatusBlock'; +import { EngagementContent } from 'models/engagementContent'; export interface EngagementContext { handleCreateEngagementRequest: (_engagement: EngagementForm) => Promise; @@ -21,6 +22,9 @@ export interface EngagementContext { loadingAuthorization: boolean; isNewEngagement: boolean; setIsNewEngagement: React.Dispatch>; + contentTabs: EngagementContent[]; + setContentTabs: React.Dispatch>; + fetchEngagementContents: () => void; } export interface Widget { @@ -47,8 +51,6 @@ export interface EngagementFormUpdate { rich_description?: string; start_date?: string; end_date?: string; - content?: string; - rich_content?: string; is_internal?: boolean; status_block?: EngagementStatusBlock[]; consent_message?: string; diff --git a/met-web/src/components/engagement/view/ActionContext.tsx b/met-web/src/components/engagement/view/ActionContext.tsx index a205405c9..df8e08a2e 100644 --- a/met-web/src/components/engagement/view/ActionContext.tsx +++ b/met-web/src/components/engagement/view/ActionContext.tsx @@ -1,7 +1,10 @@ import React, { createContext, useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { getEngagement, patchEngagement } from '../../../services/engagementService'; +import { getEngagementContent } from 'services/engagementContentService'; +import { getSummaryContent } from 'services/engagementSummaryService'; import { createDefaultEngagement, Engagement } from '../../../models/engagement'; +import { EngagementContent, CONTENT_TYPE } from 'models/engagementContent'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; import { Widget } from 'models/widget'; @@ -31,6 +34,8 @@ export interface EngagementViewContext { widgets: Widget[]; mockStatus: SubmissionStatus; updateMockStatus: (status: SubmissionStatus) => void; + content: string; + richContent: string; } export type EngagementParams = { @@ -54,6 +59,8 @@ export const ActionContext = createContext({ updateMockStatus: (status: SubmissionStatus) => { /* nothing returned */ }, + content: '', + richContent: '', }); export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => { @@ -68,6 +75,8 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme const [widgets, setWidgets] = useState([]); const [isEngagementLoading, setEngagementLoading] = useState(true); const [isWidgetsLoading, setIsWidgetsLoading] = useState(true); + const [content, setContent] = useState(''); + const [richContent, setRichContent] = useState(''); const [getWidgetsTrigger] = useLazyGetWidgetsQuery(); @@ -150,7 +159,6 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme try { const result = await getEngagement(Number(engagementId)); setSavedEngagement({ ...result }); - setEngagementLoading(false); } catch (error) { dispatch( openNotification({ @@ -180,6 +188,34 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme } }; + const fetchContents = async () => { + if (!savedEngagement.id) { + return; + } + try { + //TODO needs to changed along with the changes for tabs for public page + const engagementContents = await getEngagementContent(Number(engagementId)); + const summaryItemId = await getSummaryItemId(engagementContents); + const summaryContent = await getSummaryContent(summaryItemId); + setContent(summaryContent[0].content); + setRichContent(summaryContent[0].rich_content); + setEngagementLoading(false); + } catch (error) { + setEngagementLoading(false); + dispatch( + openNotification({ + severity: 'error', + text: 'Error occurred while fetching Engagement contents', + }), + ); + } + }; + + const getSummaryItemId = async (tabs: EngagementContent[]) => { + const summaryItem = tabs.find((item) => item.content_type === CONTENT_TYPE.SUMMARY); + return summaryItem?.id || 0; // Return null if summary item is not found + }; + const handleFetchEngagementIdBySlug = async () => { if (!slug) { return; @@ -202,6 +238,7 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme useEffect(() => { fetchWidgets(); + fetchContents(); }, [savedEngagement]); return ( @@ -215,6 +252,8 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme updateMockStatus, mockStatus, unpublishEngagement, + content, + richContent, }} > {children} diff --git a/met-web/src/components/engagement/view/EngagementContent.tsx b/met-web/src/components/engagement/view/EngagementContent.tsx index b1f683ce6..78fcc45b7 100644 --- a/met-web/src/components/engagement/view/EngagementContent.tsx +++ b/met-web/src/components/engagement/view/EngagementContent.tsx @@ -6,9 +6,7 @@ import { Skeleton } from '@mui/material'; import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; export const EngagementContent = () => { - const { savedEngagement, isEngagementLoading } = useContext(ActionContext); - - const { rich_content } = savedEngagement; + const { richContent, isEngagementLoading } = useContext(ActionContext); if (isEngagementLoading) { return ; @@ -16,7 +14,7 @@ export const EngagementContent = () => { return ( - + ); }; diff --git a/met-web/src/components/engagement/view/PreviewBanner.tsx b/met-web/src/components/engagement/view/PreviewBanner.tsx index 8de9603ea..7785c31ae 100644 --- a/met-web/src/components/engagement/view/PreviewBanner.tsx +++ b/met-web/src/components/engagement/view/PreviewBanner.tsx @@ -22,7 +22,7 @@ export const PreviewBanner = () => { const navigate = useNavigate(); const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); const [isUnpublishModalOpen, setIsUnpublishModalOpen] = useState(false); - const { isEngagementLoading, savedEngagement, updateMockStatus, mockStatus } = useContext(ActionContext); + const { content, isEngagementLoading, savedEngagement, updateMockStatus, mockStatus } = useContext(ActionContext); const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); const isDraft = savedEngagement.status_id === EngagementStatus.Draft; const engagementId = savedEngagement.id || ''; @@ -122,7 +122,7 @@ export const PreviewBanner = () => { - + { const [scheduledDate, setScheduledDate] = useState(dayjs(Date.now())); - const { savedEngagement, scheduleEngagement } = useContext(ActionContext); + const { content, savedEngagement, scheduleEngagement } = useContext(ActionContext); const dispatch = useAppDispatch(); const isEngagementReady = () => { - return savedEngagement.content && savedEngagement.description && savedEngagement.banner_url; + return content && savedEngagement.description && savedEngagement.banner_url; }; const handleChange = (newDate: Dayjs | null) => { diff --git a/met-web/src/models/engagementContent.ts b/met-web/src/models/engagementContent.ts new file mode 100644 index 000000000..14e705198 --- /dev/null +++ b/met-web/src/models/engagementContent.ts @@ -0,0 +1,28 @@ +export type EngagementContentTypes = 'Summary' | 'Custom'; + +export const CONTENT_TYPE: { [status: string]: EngagementContentTypes } = { + SUMMARY: 'Summary', + CUSTOM: 'Custom', +}; + +export interface EngagementContent { + id: number; + title: string; + icon_name: string; + content_type: string; + engagement_id: number; + sort_index: number; + is_internal: boolean; +} + +export const createDefaultEngagementContent = (): EngagementContent => { + return { + id: 0, + title: 'Summary', + icon_name: '', + content_type: CONTENT_TYPE.SUMMARY, + engagement_id: 0, + sort_index: 1, + is_internal: false, + }; +}; diff --git a/met-web/src/models/engagementCustomContent.ts b/met-web/src/models/engagementCustomContent.ts new file mode 100644 index 000000000..d915f7702 --- /dev/null +++ b/met-web/src/models/engagementCustomContent.ts @@ -0,0 +1,7 @@ +export interface EngagementCustomContent { + id: number; + custom_text_content: string; + custom_json_content: string; + engagement_id: number; + engagement_content_id: number; +} diff --git a/met-web/src/models/engagementSummaryContent.ts b/met-web/src/models/engagementSummaryContent.ts new file mode 100644 index 000000000..7c58eaa08 --- /dev/null +++ b/met-web/src/models/engagementSummaryContent.ts @@ -0,0 +1,7 @@ +export interface EngagementSummaryContent { + id: number; + content: string; + rich_content: string; + engagement_id: number; + engagement_content_id: number; +} diff --git a/met-web/src/services/engagementContentService/index.ts b/met-web/src/services/engagementContentService/index.ts new file mode 100644 index 000000000..d51356fe8 --- /dev/null +++ b/met-web/src/services/engagementContentService/index.ts @@ -0,0 +1,82 @@ +import http from 'apiManager/httpRequestHandler'; +import { EngagementContent } from 'models/engagementContent'; +import Endpoints from 'apiManager/endpoints'; +import { replaceAllInURL, replaceUrl } from 'helper'; + +export const getEngagementContent = async (engagementId: number): Promise => { + const url = replaceUrl(Endpoints.EngagementContent.GET, 'engagement_id', String(engagementId)); + if (!engagementId || isNaN(Number(engagementId))) { + return Promise.reject('Invalid Engagement Id ' + engagementId); + } + const response = await http.GetRequest(url); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to fetch engagement content'); +}; + +interface PostEngagementContent { + title?: string; + icon_name?: string; + content_type?: string; + sort_index?: number; + is_internal?: boolean; + engagement_id?: number; +} +export const postEngagementContent = async ( + engagement_id: number, + data: PostEngagementContent, +): Promise => { + try { + const url = replaceUrl(Endpoints.EngagementContent.CREATE, 'engagement_id', String(engagement_id)); + const response = await http.PostRequest(url, data); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to create engagement content'); + } catch (err) { + return Promise.reject(err); + } +}; + +export interface PatchEngagementContentRequest { + title?: string; + icon_name?: string; +} +export const patchEngagementContent = async ( + engagementId: number, + contentId: number, + data: PatchEngagementContentRequest, +): Promise => { + const url = replaceAllInURL({ + URL: Endpoints.EngagementContent.UPDATE, + params: { + engagement_id: String(engagementId), + content_id: String(contentId), + }, + }); + const response = await http.PatchRequest(url, data); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to update engagement content'); +}; + +export const deleteEngagementContent = async (engagementId: number, contentId: number): Promise => { + try { + const url = replaceAllInURL({ + URL: Endpoints.EngagementContent.DELETE, + params: { + engagement_id: String(engagementId), + content_id: String(contentId), + }, + }); + const response = await http.DeleteRequest(url); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to delete engagement content'); + } catch (err) { + return Promise.reject(err); + } +}; diff --git a/met-web/src/services/engagementCustomService/index.ts b/met-web/src/services/engagementCustomService/index.ts new file mode 100644 index 000000000..41359d726 --- /dev/null +++ b/met-web/src/services/engagementCustomService/index.ts @@ -0,0 +1,59 @@ +import http from 'apiManager/httpRequestHandler'; +import { EngagementCustomContent } from 'models/engagementCustomContent'; +import Endpoints from 'apiManager/endpoints'; +import { replaceAllInURL, replaceUrl } from 'helper'; + +export const getCustomContent = async (contentId: number): Promise => { + const url = replaceUrl(Endpoints.EngagementCustomContent.GET, 'content_id', String(contentId)); + if (!contentId || isNaN(Number(contentId))) { + return Promise.reject('Invalid content Id ' + contentId); + } + const response = await http.GetRequest(url); // Notice the change here + if (response.data) { + return response.data; + } + return Promise.reject('Failed to fetch engagement Custom content'); +}; + +interface PostCustomContentRequest { + custom_text_content?: string; + custom_json_content?: string; + engagement_id?: number; + engagement_content_id?: number; +} +export const postCustomContent = async ( + contentId: number, + data: PostCustomContentRequest, +): Promise => { + try { + const url = replaceUrl(Endpoints.EngagementCustomContent.CREATE, 'content_id', String(contentId)); + const response = await http.PostRequest(url, data); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to create engagement Custom content'); + } catch (err) { + return Promise.reject(err); + } +}; + +export interface PatchCustomContentRequest { + custom_text_content?: string; + custom_json_content?: string; +} +export const patchCustomContent = async ( + contentId: number, + data: PatchCustomContentRequest, +): Promise => { + const url = replaceAllInURL({ + URL: Endpoints.EngagementCustomContent.UPDATE, + params: { + content_id: String(contentId), + }, + }); + const response = await http.PatchRequest(url, data); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to update engagement Custom content'); +}; diff --git a/met-web/src/services/engagementService/types.ts b/met-web/src/services/engagementService/types.ts index 14561c40f..04dfd3733 100644 --- a/met-web/src/services/engagementService/types.ts +++ b/met-web/src/services/engagementService/types.ts @@ -23,8 +23,6 @@ export interface PutEngagementRequest { end_date: string; description: string; rich_description: string; - content: string; - rich_content: string; banner_filename?: string; status_block?: unknown[]; } @@ -37,8 +35,6 @@ export interface PatchEngagementRequest { end_date?: string; description?: string; rich_description?: string; - content?: string; - rich_content?: string; banner_filename?: string; status_block?: unknown[]; } diff --git a/met-web/src/services/engagementSummaryService/index.ts b/met-web/src/services/engagementSummaryService/index.ts new file mode 100644 index 000000000..7454f107b --- /dev/null +++ b/met-web/src/services/engagementSummaryService/index.ts @@ -0,0 +1,58 @@ +import http from 'apiManager/httpRequestHandler'; +import { EngagementSummaryContent } from 'models/engagementSummaryContent'; +import Endpoints from 'apiManager/endpoints'; +import { replaceAllInURL, replaceUrl } from 'helper'; + +export const getSummaryContent = async (contentId: number): Promise => { + const url = replaceUrl(Endpoints.EngagementSummaryContent.GET, 'content_id', String(contentId)); + if (!contentId || isNaN(Number(contentId))) { + return Promise.reject('Invalid content Id ' + contentId); + } + const response = await http.GetRequest(url); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to fetch engagement summary content'); +}; + +interface PostSummaryContentRequest { + content?: string; + rich_content?: string; + engagement_id?: number; +} +export const postSummaryContent = async ( + contentId: number, + data: PostSummaryContentRequest, +): Promise => { + try { + const url = replaceUrl(Endpoints.EngagementSummaryContent.CREATE, 'content_id', String(contentId)); + const response = await http.PostRequest(url, data); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to create engagement summary content'); + } catch (err) { + return Promise.reject(err); + } +}; + +export interface PatchSummaryContentRequest { + content?: string; + rich_content?: string; +} +export const patchSummaryContent = async ( + contentId: number, + data: PatchSummaryContentRequest, +): Promise => { + const url = replaceAllInURL({ + URL: Endpoints.EngagementSummaryContent.UPDATE, + params: { + content_id: String(contentId), + }, + }); + const response = await http.PatchRequest(url, data); + if (response.data) { + return response.data; + } + return Promise.reject('Failed to update engagement summary content'); +}; diff --git a/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.One.test.tsx b/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.One.test.tsx index a67edb906..c910f31d9 100644 --- a/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.One.test.tsx +++ b/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.One.test.tsx @@ -127,18 +127,20 @@ describe('Engagement form page tests', () => { test('Engagement form with saved engagement should display saved info', async () => { useParamsMock.mockReturnValue({ engagementId: '1' }); - render(); + const { getByTestId, getByText, getByDisplayValue } = render(); await waitFor(() => { expect(screen.getByDisplayValue('Test Engagement')).toBeInTheDocument(); }); - expect(getEngagementMock).toHaveBeenCalledOnce(); - expect(getEngagementMetadataMock).toHaveBeenCalledOnce(); - expect(screen.getByTestId('save-engagement-button')).toBeVisible(); - expect(screen.getByDisplayValue('2022-09-01')).toBeInTheDocument(); - expect(screen.getByDisplayValue('2022-09-30')).toBeInTheDocument(); - expect(screen.getByText('Survey 1')).toBeInTheDocument(); + await waitFor(() => { + expect(getEngagementMock).toHaveBeenCalledOnce(); + expect(getEngagementMetadataMock).toHaveBeenCalledOnce(); + expect(getByTestId('save-engagement-button')).toBeVisible(); + expect(getByDisplayValue('2022-09-01')).toBeInTheDocument(); + expect(getByDisplayValue('2022-09-30')).toBeInTheDocument(); + expect(getByText('Survey 1')).toBeInTheDocument(); + }); }); test('Save engagement button should trigger patch call', async () => { @@ -163,14 +165,17 @@ describe('Engagement form page tests', () => { test('Modal with warning appears when removing survey', async () => { useParamsMock.mockReturnValue({ engagementId: '1' }); - render(); + const { getByTestId, getByDisplayValue } = render(); await waitFor(() => { - expect(screen.getByDisplayValue('Test Engagement')).toBeInTheDocument(); + expect(getByDisplayValue('Test Engagement')).toBeInTheDocument(); }); - const removeSurveyButton = screen.getByTestId(`survey-widget/remove-${survey.id}`); + await waitFor(() => { + expect(getByTestId(`survey-widget/remove-${survey.id}`)); + }); + const removeSurveyButton = getByTestId(`survey-widget/remove-${survey.id}`); fireEvent.click(removeSurveyButton); expect(openNotificationModalMock).toHaveBeenCalledOnce(); @@ -184,13 +189,16 @@ describe('Engagement form page tests', () => { surveys: surveys, }), ); - render(); + const { getByText, getByDisplayValue } = render(); await waitFor(() => { - expect(screen.getByDisplayValue('Test Engagement')).toBeInTheDocument(); + expect(getByDisplayValue('Test Engagement')).toBeInTheDocument(); }); getEngagementMetadataMock.mockReturnValueOnce(Promise.resolve([engagementMetadata])); - expect(screen.getByText('Add Survey')).toBeDisabled(); + + await waitFor(() => { + expect(getByText('Add Survey')).toBeDisabled(); + }); }); test('Can move to settings tab', async () => { diff --git a/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.Two.test.tsx b/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.Two.test.tsx index d47693129..28233fa13 100644 --- a/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.Two.test.tsx +++ b/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.Two.test.tsx @@ -118,15 +118,17 @@ describe('Engagement form page tests', () => { }), ); - render(); + const { getByTestId, getByText } = render(); await waitFor(() => { expect(screen.getByDisplayValue('Test Engagement')).toBeInTheDocument(); }); - expect(screen.getByText('Survey 1')).toBeInTheDocument(); + await waitFor(() => { + expect(getByText('Survey 1')).toBeInTheDocument(); + }); - const removeSurveyButton = screen.getByTestId(`survey-widget/remove-${survey.id}`); + const removeSurveyButton = getByTestId(`survey-widget/remove-${survey.id}`); fireEvent.click(removeSurveyButton); @@ -284,4 +286,93 @@ describe('Engagement form page tests', () => { expect(endDate.value).toBe('2022-12-25'); }); }); + + test('Engagement summary tab appears', async () => { + useParamsMock.mockReturnValue({ engagementId: '1' }); + const { getByTestId, container, getByText } = render(); + + await waitFor(() => { + expect(screen.getByDisplayValue('Test Engagement')).toBeInTheDocument(); + expect(container.querySelector('span.MuiSkeleton-root')).toBeNull(); + }); + + await waitFor(() => { + expect(getByText('Summary')).toBeInTheDocument(); + expect(getByTestId('add-tab-menu')).toBeVisible(); + expect(getByText('Test content')).toBeInTheDocument(); + }); + }); + + test('Add new custom tab Modal appears', async () => { + useParamsMock.mockReturnValue({ engagementId: '1' }); + const { getByTestId, container, getByText } = render(); + + await waitFor(() => { + expect(screen.getByDisplayValue('Test Engagement')).toBeInTheDocument(); + expect(container.querySelector('span.MuiSkeleton-root')).toBeNull(); + }); + + const addNewTabButton = getByTestId('add-tab-menu'); + fireEvent.click(addNewTabButton); + + await waitFor(() => { + expect(getByTestId('add-new-custom-tab')).toBeVisible(); + }); + + const addNewCustomTab = getByTestId('add-new-custom-tab'); + fireEvent.click(addNewCustomTab); + + await waitFor(() => { + expect(getByTestId('add-tab-button')).toBeVisible(); + expect(getByText('Tab Title:')).toBeInTheDocument(); + expect(getByText('Tab Icon:')).toBeInTheDocument(); + }); + }); + + test('Edit tab Modal appears', async () => { + useParamsMock.mockReturnValue({ engagementId: '1' }); + const { getByTestId, container, getByText } = render(); + + await waitFor(() => { + expect(screen.getByDisplayValue('Test Engagement')).toBeInTheDocument(); + expect(container.querySelector('span.MuiSkeleton-root')).toBeNull(); + }); + + const addNewTabButton = getByTestId('edit-tab-details'); + fireEvent.click(addNewTabButton); + + await waitFor(() => { + expect(getByTestId('update-tab-button')).toBeVisible(); + expect(getByText('Edit the engagement content tab')).toBeInTheDocument(); + }); + }); + + test('Test cannot create tab with empty fields', async () => { + const handleCreateTab = jest.fn(); + useParamsMock.mockReturnValue({ engagementId: '1' }); + const { getByTestId, container } = render(); + + await waitFor(() => { + expect(screen.getByDisplayValue('Test Engagement')).toBeInTheDocument(); + expect(container.querySelector('span.MuiSkeleton-root')).toBeNull(); + }); + + const addNewTabButton = getByTestId('add-tab-menu'); + fireEvent.click(addNewTabButton); + + await waitFor(() => { + expect(getByTestId('add-new-custom-tab')).toBeVisible(); + }); + + const addNewCustomTab = getByTestId('add-new-custom-tab'); + fireEvent.click(addNewCustomTab); + + await waitFor(() => { + expect(getByTestId('add-tab-button')).toBeVisible(); + }); + + const addButton = getByTestId('add-tab-button'); + fireEvent.click(addButton); + expect(handleCreateTab).not.toHaveBeenCalled(); + }); });