From da88a8ae480e1736c8347cea07aeb70cdb0b2b5b Mon Sep 17 00:00:00 2001 From: Jareth Whitney Date: Thu, 19 Sep 2024 22:24:56 -0700 Subject: [PATCH] feature/deseng692: Made revisions to PR as per Nat and Alex's comments, fixed issues with deferred data aborted errors. --- CHANGELOG.MD | 5 + .../create/authoring/AuthoringContext.tsx | 9 +- .../create/authoring/AuthoringSummary.tsx | 36 ++--- .../create/authoring/AuthoringTemplate.tsx | 49 ++++--- .../form/EngagementWidgets/Video/Form.tsx | 5 +- .../widgets/Video/VideoWidgetView.tsx | 130 ++++++++---------- met-web/src/routes/AuthenticatedRoutes.tsx | 4 +- 7 files changed, 108 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index acd65576a..5e80b1a5e 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -5,6 +5,11 @@ - Created custom layover bar for videos that shows video provider and has a custom logo - Transcripts will not be implemented at this time +- Summary page in authoring section [🎟️ DESENG-671](https://citz-gdx.atlassian.net/browse/DESENG-671) + - Streamlined data loaders and actions to account for page changes + - Updated data structure so that summary data is pulled from engagement table + - Added 'description_title' column to engagement table + ## September 12, 2024 - **Feature** New Summary page in authoring section [🎟️ DESENG-671](https://citz-gdx.atlassian.net/browse/DESENG-671) diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx index 35af10549..d5c116d3c 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx @@ -82,7 +82,7 @@ export const AuthoringContext = () => { title: data.title, icon_name: data.icon_name, metadata_value: data.metadata_value, - send_report: data.send_report ? getSendReportValue(data.send_report) : '', + send_report: (data.send_report || '').toString(), slug: data.slug, request_type: data.request_type, text_content: data.text_content, @@ -95,13 +95,6 @@ export const AuthoringContext = () => { ); }; - const getSendReportValue = (valueToInterpret: boolean) => { - if (undefined === valueToInterpret) { - return ''; - } - return valueToInterpret ? 'true' : 'false'; - }; - return ( diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx index 03f4cc729..b2f34c65b 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx @@ -1,6 +1,6 @@ import { Grid } from '@mui/material'; import React, { useEffect } from 'react'; -import { useOutletContext, useRouteLoaderData, useNavigate } from 'react-router-dom'; +import { useLoaderData, useOutletContext } from 'react-router-dom'; import { TextField } from 'components/common/Input'; import { AuthoringTemplateOutletContext } from './types'; import { colors } from 'styles/Theme'; @@ -16,20 +16,9 @@ import { EngagementUpdateData } from './AuthoringContext'; import { Engagement } from 'models/engagement'; const AuthoringSummary = () => { - const { setValue, control, reset, getValues, setDefaultValues, fetcher, slug }: AuthoringTemplateOutletContext = + const { setValue, control, reset, getValues, setDefaultValues, fetcher }: AuthoringTemplateOutletContext = useOutletContext(); // Access the form functions and values from the authoring template. - // Update the loader data when the authoring section is changed, by triggering navigation(). - const navigate = useNavigate(); - useEffect(() => { - engagementData.then(() => navigate('.', { replace: true })); - }, [slug]); - - // Get the engagement data - const { engagement: engagementData } = useRouteLoaderData('single-engagement') as { - engagement: Promise; - }; - // Check if the form has succeeded or failed after a submit, and issue a message to the user. const dispatch = useAppDispatch(); useEffect(() => { @@ -47,6 +36,8 @@ const AuthoringSummary = () => { } }, [fetcher.data]); + const { engagement } = useLoaderData() as { engagement: Promise }; + const untouchedDefaultValues: EngagementUpdateData = { id: 0, status_id: 0, @@ -74,21 +65,22 @@ const AuthoringSummary = () => { // Reset values to default and retrieve relevant content from loader. useEffect(() => { reset(untouchedDefaultValues); - engagementData.then((engagement) => { - setValue('id', Number(engagement.id)); + engagement.then((eng) => { + setValue('id', Number(eng.id)); // Make sure it is valid JSON. - if (tryParse(engagement.rich_description)) { - setValue('rich_description', engagement.rich_description); + if (tryParse(eng.rich_description)) { + setValue('rich_description', eng.rich_description); } - setValue('description_title', engagement.description_title || 'Hello world'); - setValue('description', engagement.description); + setValue('description_title', eng.description_title || 'Hello world'); + setValue('description', eng.description); setValue( 'summary_editor_state', - EditorState.createWithContent(convertFromRaw(JSON.parse(engagement.rich_description))), + EditorState.createWithContent(convertFromRaw(JSON.parse(eng.rich_description))), ); - setDefaultValues(getValues()); // Update default values so that our loaded values are default. + // Update default values so that our loaded values are default. + setDefaultValues(getValues()); }); - }, [engagementData]); + }, [engagement]); // Define the styles const metBigLabelStyles = { diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx index 95ab178d2..851247147 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx @@ -1,17 +1,15 @@ import React, { Suspense, useMemo, useState } from 'react'; -import { useOutletContext, Form, useParams, useRouteLoaderData, Await, Outlet } from 'react-router-dom'; +import { useOutletContext, Form, useParams, Await, Outlet, useLoaderData } from 'react-router-dom'; import AuthoringBottomNav from './AuthoringBottomNav'; import { EngagementUpdateData } from './AuthoringContext'; import { useFormContext } from 'react-hook-form'; import UnsavedWorkConfirmation from 'components/common/Navigation/UnsavedWorkConfirmation'; import { AuthoringContextType, StatusLabelProps } from './types'; -import { Engagement } from 'models/engagement'; import { AutoBreadcrumbs } from 'components/common/Navigation/Breadcrumb'; import { ResponsiveContainer } from 'components/common/Layout'; import { EngagementStatus } from 'constants/engagementStatus'; import { EyebrowText, Header2 } from 'components/common/Typography'; import { useAppSelector } from 'hooks'; -import { getTenantLanguages } from 'services/languageService'; import { Language } from 'models/language'; import { getAuthoringRoutes } from './AuthoringNavElements'; import { FormControlLabel, Grid, Radio, RadioGroup } from '@mui/material'; @@ -20,6 +18,8 @@ import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; import { When } from 'react-if'; import WidgetPicker from '../widgets'; import { WidgetLocation } from 'models/widget'; +import { Engagement } from 'models/engagement'; +import { getTenantLanguages } from 'services/languageService'; export const StatusLabel = ({ text, completed }: StatusLabelProps) => { const statusLabelStyle = { @@ -39,7 +39,7 @@ export const getLanguageValue = (currentLanguage: string, languages: Language[]) const AuthoringTemplate = () => { const { onSubmit, defaultValues, setDefaultValues, fetcher }: AuthoringContextType = useOutletContext(); const { engagementId } = useParams() as { engagementId: string }; // We need the engagement ID quickly, so let's grab it from useParams - const { engagement } = useRouteLoaderData('single-engagement') as { engagement: Engagement }; + const { engagement } = useLoaderData() as { engagement: Promise }; const [currentLanguage, setCurrentLanguage] = useState(useAppSelector((state) => state.language.id)); const [contentTabsEnabled, setContentTabsEnabled] = useState('false'); // todo: replace default value with stored value in engagement. const defaultTabValues = { @@ -50,6 +50,7 @@ const AuthoringTemplate = () => { }; const [tabs, setTabs] = useState([defaultTabValues]); const [singleContentValues, setSingleContentValues] = useState({ ...defaultTabValues, heading: '' }); + const tenant = useAppSelector((state) => state.tenant); const languages = useMemo(() => getTenantLanguages(tenant.id), [tenant.id]); // todo: Using tenant language list until language data is integrated with the engagement. const authoringRoutes = getAuthoringRoutes(Number(engagementId), tenant); @@ -61,6 +62,7 @@ const AuthoringTemplate = () => { const pathSlug = pathArray[pathArray.length - 1]; return pathSlug === slug; })?.name; + const { handleSubmit, setValue, @@ -95,17 +97,17 @@ const AuthoringTemplate = () => { return ( - - - {(engagement: Engagement) => ( -
- - {/* todo: For the section status label when it's ready */} - {/* */} -
- )} -
-
+
+ + + {(engagement: Engagement) => ( + + )} + + + {/* todo: For the section status label when it's ready */} + {/* */} +

{pageTitle}

Under construction - the settings in this section have no effect. @@ -151,13 +153,16 @@ const AuthoringTemplate = () => { - - - - {(languages: Language[]) => getLanguageValue(currentLanguage, languages) + ' Content'} - - - + + + {(languages: Language[]) => ( + + {`${getLanguageValue(currentLanguage, languages)} Content`} + + )} + + +
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx index 70af54106..bddd953be 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx @@ -136,7 +136,7 @@ const Form = () => { - + { name="title" variant="outlined" label=" " + aria-label="Title: optional." InputLabelProps={{ shrink: false, }} @@ -162,6 +163,7 @@ const Form = () => { name="description" variant="outlined" label=" " + aria-label="Description: optional." InputLabelProps={{ shrink: false, }} @@ -179,6 +181,7 @@ const Form = () => { name="videoUrl" variant="outlined" label=" " + aria-label="Video URL: required." InputLabelProps={{ shrink: false, }} diff --git a/met-web/src/components/engagement/old-view/widgets/Video/VideoWidgetView.tsx b/met-web/src/components/engagement/old-view/widgets/Video/VideoWidgetView.tsx index e09f96854..11d228069 100644 --- a/met-web/src/components/engagement/old-view/widgets/Video/VideoWidgetView.tsx +++ b/met-web/src/components/engagement/old-view/widgets/Video/VideoWidgetView.tsx @@ -19,13 +19,25 @@ import { faMixcloud, faDailymotion, } from '@fortawesome/free-brands-svg-icons'; -import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import { faQuestionCircle, IconDefinition } from '@fortawesome/free-solid-svg-icons'; import { Palette } from 'styles/Theme'; interface VideoWidgetProps { widget: Widget; } +interface WidgetVideoSource { + domain: string; + name: string; + icon: IconDefinition; +} + +interface VideoOverlayProps { + videoOverlayTitle: string; + source: string; + videoSources: WidgetVideoSource[]; +} + const VideoWidgetView = ({ widget }: VideoWidgetProps) => { const theme = useTheme(); const dispatch = useAppDispatch(); @@ -63,6 +75,29 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { } }; + useEffect(() => { + fetchVideo(); + }, [widget]); + + if (isLoading) { + return ( + + + + + + + + + + + ); + } + + if (!videoWidget) { + return null; + } + const formatVideoDuration = (duration: number) => { let minutes = 0; let hours = 0; @@ -83,26 +118,18 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { } }; - const getVideoSource = (url: string) => { - const hostname = new URL(url).hostname; - if ('youtube.com' === hostname || 'youtu.be' === hostname) { - return 'YouTube'; - } else if ('vimeo.com' === hostname) { - return 'Vimeo'; - } else if ('facebook.com' === hostname) { - return 'Facebook'; - } else if ('twitch.tv' === hostname) { - return 'Twitch'; - } else if ('soundcloud.com' === hostname) { - return 'SoundCloud'; - } else if ('mixcloud.com' === hostname) { - return 'Mixcloud'; - } else if ('dailymotion.com' === hostname) { - return 'DailyMotion'; - } else { - return 'Unknown'; - } - }; + const videoSources: WidgetVideoSource[] = [ + { domain: 'youtu.be', name: 'YouTube', icon: faYoutube }, + { domain: 'vimeo.com', name: 'Vimeo', icon: faVimeo }, + { domain: 'twitch.tv', name: 'Twitch', icon: faTwitch }, + { domain: 'soundcloud.com', name: 'SoundCloud', icon: faSoundcloud }, + { domain: 'mixcloud.com', name: 'Mixcloud', icon: faMixcloud }, + { domain: 'dailymotion.com', name: 'DailyMotion', icon: faDailymotion }, + { domain: 'facebook.com', name: 'Facebook', icon: faFacebookF }, + ]; + + const hostname = new URL(videoWidget.video_url).hostname; + const videoSource = videoSources.find((source) => source.domain === hostname)?.name || 'Unknown'; const getVideoTitle = (player: ReactPlayer, source: string, widgetTitle: string) => { if ('YouTube' === source) { @@ -118,29 +145,6 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { } }; - useEffect(() => { - fetchVideo(); - }, [widget]); - - if (isLoading) { - return ( - - - - - - - - - - - ); - } - - if (!videoWidget) { - return null; - } - return ( @@ -164,11 +168,12 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { {/* Overlay covers play button for Mixcloud so it shouldn't be displayed */} - + @@ -181,7 +186,6 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { url={videoWidget.video_url} controls onReady={(player) => { - const videoSource = getVideoSource(videoWidget.video_url); setVideoOverlayTitle(getVideoTitle(player, videoSource, videoWidget.title)); }} onPlay={() => setShowOverlay(false)} @@ -200,7 +204,7 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { {videoDuration} @@ -209,31 +213,7 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { ); }; -interface VideoOverlayProps { - videoOverlayTitle: string; - source: string; -} - -const VideoOverlay = ({ videoOverlayTitle, source }: VideoOverlayProps) => { - const getIcon = (source: string) => { - if ('YouTube' === source) { - return faYoutube; - } else if ('Vimeo' === source) { - return faVimeo; - } else if ('Facebook' === source) { - return faFacebookF; - } else if ('Twitch' === source) { - return faTwitch; - } else if ('SoundCloud' === source) { - return faSoundcloud; - } else if ('Mixcloud' === source) { - return faMixcloud; - } else if ('DailyMotion' === source) { - return faDailymotion; - } else { - return faQuestionCircle; - } - }; +const VideoOverlay = ({ videoOverlayTitle, source, videoSources }: VideoOverlayProps) => { return ( { }} > vs.name === source)?.icon || faQuestionCircle} style={{ fontSize: '1.9rem', paddingRight: '0.65rem', color: '#FCBA19' }} />{' '} - {videoOverlayTitle} + {videoOverlayTitle} ); diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index 8651ea503..6150a41ff 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -52,7 +52,6 @@ import AuthoringFeedback from 'components/engagement/admin/create/authoring/Auth import AuthoringResults from 'components/engagement/admin/create/authoring/AuthoringResults'; import AuthoringSubscribe from 'components/engagement/admin/create/authoring/AuthoringSubscribe'; import AuthoringMore from 'components/engagement/admin/create/authoring/AuthoringMore'; -import { authoringLoader } from 'components/engagement/admin/create/authoring/authoringLoader'; const AuthenticatedRoutes = () => { return ( @@ -131,7 +130,7 @@ const AuthenticatedRoutes = () => { element={} > }> - } loader={authoringLoader} id="authoring-loader"> + } id="authoring-loader" loader={engagementLoader}> } @@ -145,6 +144,7 @@ const AuthenticatedRoutes = () => { /> } handle={{