From d0273d8bd7b7c11613d97bceda08970f0ff074a6 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Mon, 19 Aug 2024 15:29:54 -0700 Subject: [PATCH 1/9] Router restructuring for engagement detail tabs --- .../engagement/admin/view/index.tsx | 24 +++++++--------- .../components/engagement/listing/index.tsx | 2 +- met-web/src/routes/AuthenticatedRoutes.tsx | 28 ++++++++++++------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/met-web/src/components/engagement/admin/view/index.tsx b/met-web/src/components/engagement/admin/view/index.tsx index 6455e998f..875036053 100644 --- a/met-web/src/components/engagement/admin/view/index.tsx +++ b/met-web/src/components/engagement/admin/view/index.tsx @@ -1,14 +1,13 @@ -import React, { Suspense, useState } from 'react'; -import { useRouteLoaderData, Await } from 'react-router-dom'; +import React, { Suspense } from 'react'; +import { useRouteLoaderData, Await, useMatches, UIMatch, Outlet } from 'react-router-dom'; import { Engagement } from 'models/engagement'; import { AutoBreadcrumbs } from 'components/common/Navigation/Breadcrumb'; import { EngagementStatus } from 'constants/engagementStatus'; import { Tab } from '@mui/material'; import { ResponsiveContainer } from 'components/common/Layout'; -import { ConfigSummary } from './ConfigSummary'; -import { AuthoringTab } from './AuthoringTab'; import { TabContext, TabList, TabPanel } from '@mui/lab'; import { EngagementLoaderData } from 'components/engagement/public/view'; +import { RouterLinkRenderer } from 'components/common/Navigation/Link'; export const AdminEngagementView = () => { const { engagement, teamMembers, slug } = useRouteLoaderData('single-engagement') as EngagementLoaderData; @@ -21,7 +20,8 @@ export const AdminEngagementView = () => { publish: 'Publishing', }; - const [currentTab, setCurrentTab] = useState(EngagementViewTabs.config); + const matches = useMatches() as UIMatch[]; + const currentTab = matches[matches.length - 1].pathname.split('/').pop() ?? ''; return ( @@ -50,7 +50,6 @@ export const AdminEngagementView = () => { setCurrentTab(newValue)} aria-label="Admin Engagement View Tabs" TabIndicatorProps={{ sx: { display: 'none' } }} sx={{ @@ -63,9 +62,11 @@ export const AdminEngagementView = () => { {Object.entries(EngagementViewTabs).map(([key, value]) => ( { ))} - + - + - - - - - ); diff --git a/met-web/src/components/engagement/listing/index.tsx b/met-web/src/components/engagement/listing/index.tsx index ea14b4fed..b43b3755c 100644 --- a/met-web/src/components/engagement/listing/index.tsx +++ b/met-web/src/components/engagement/listing/index.tsx @@ -131,7 +131,7 @@ const EngagementListing = () => { label: 'Engagement Name', allowSort: true, renderCell: (row: Engagement) => ( - + {row.name} ), diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index fcf94f828..9c487a72d 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -40,6 +40,8 @@ import EngagementCreationWizard from 'components/engagement/admin/config/wizard/ import engagementCreateAction from 'components/engagement/admin/config/EngagementCreateAction'; import EngagementConfigurationWizard from 'components/engagement/admin/config/wizard/ConfigWizard'; import engagementUpdateAction from 'components/engagement/admin/config/EngagementUpdateAction'; +import { ConfigSummary as ConfigTab } from 'components/engagement/admin/view/ConfigSummary'; +import { AuthoringTab } from 'components/engagement/admin/view/AuthoringTab'; const AuthenticatedRoutes = () => { return ( @@ -92,29 +94,35 @@ const AuthenticatedRoutes = () => { crumb: async (data: { engagement: Promise }) => { return data.engagement.then((engagement) => { return { - link: `/engagements/${engagement.id}/view`, + link: `/engagements/${engagement.id}/details/config`, name: engagement.name, }; }); }, }} > - } /> - }> - } /> + } /> + + } /> + {/* Wraps the tabs with the engagement title and TabContext */} + }> + } /> + } /> + } /> + } action={engagementUpdateAction} handle={{ - crumb: () => ({ - name: 'Configure', - }), + crumb: () => ({ name: 'Configure' }), }} /> - } /> - } /> + }> + } /> + + } /> } /> } /> From cb3439e8d4a300634e9dbca1fa5ce085e626c749 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Tue, 20 Aug 2024 13:33:18 -0700 Subject: [PATCH 2/9] Fixes to links between routes --- .../engagement/admin/config/EngagementUpdateAction.tsx | 2 +- .../engagement/admin/config/wizard/ConfigWizard.tsx | 4 ++-- .../src/components/engagement/admin/view/ConfigSummary.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx b/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx index b1f6df846..5656e367b 100644 --- a/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx +++ b/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx @@ -58,7 +58,7 @@ export const engagementUpdateAction: ActionFunction = async ({ request, params } console.error('Error updating team members', e); } - return redirect(`/engagements/${engagementId}/view`); + return redirect(`/engagements/${engagementId}/details/config`); }; export default engagementUpdateAction; diff --git a/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx b/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx index 3929b4ab1..1ee93f000 100644 --- a/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx +++ b/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx @@ -82,7 +82,7 @@ const ConfigForm = () => { }), { method: 'patch', - action: `/engagements/${engagement.id}/config/`, + action: `/engagements/${engagement.id}/details/config/edit`, }, ); }; @@ -131,7 +131,7 @@ const ConfigForm = () => { > - We're just looking over your configuration. + We're saving your configuration. diff --git a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx index d24a50906..8da2b2d6e 100644 --- a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx +++ b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx @@ -179,7 +179,7 @@ export const ConfigSummary = () => { - From 9b7a9903ab0b5421a14c77ee2ed9fafb788d1e5c Mon Sep 17 00:00:00 2001 From: Jareth Whitney Date: Fri, 23 Aug 2024 11:07:19 -0700 Subject: [PATCH 3/9] feature/deseng668: Integrated side nav, bottom nav, context, and base page for authoring section. --- CHANGELOG.MD | 7 + met-web/src/assets/images/pagePreview.png | Bin 0 -> 580 bytes .../create/authoring/AuthoringBanner.tsx | 31 ++ .../create/authoring/AuthoringBottomNav.tsx | 214 +++++++++++++ .../create/authoring/AuthoringContext.tsx | 82 +++++ .../create/authoring/AuthoringNavElements.tsx | 78 +++++ .../create/authoring/AuthoringSideNav.tsx | 281 ++++++++++++++++++ .../authoring/engagementUpdateAction.tsx | 105 +++++++ .../admin/create/authoring/types.ts | 19 ++ .../engagement/admin/view/AuthoringTab.tsx | 40 +-- .../admin/view/AuthoringTabElements.tsx | 76 +++++ .../feedback/FeedbackModal/index.tsx | 2 +- .../layout/Header/InternalHeader.tsx | 33 +- met-web/src/routes/AuthenticatedRoutes.tsx | 14 + met-web/src/styles/Theme.ts | 24 +- 15 files changed, 957 insertions(+), 49 deletions(-) create mode 100644 met-web/src/assets/images/pagePreview.png create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/engagementUpdateAction.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/types.ts create mode 100644 met-web/src/components/engagement/admin/view/AuthoringTabElements.tsx diff --git a/CHANGELOG.MD b/CHANGELOG.MD index fedf3eeac..1dbd7b70a 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,10 @@ +## August 23, 2024 +- **Feature** New authoring content section [🎟️ DESENG-668](https://citz-gdx.atlassian.net/browse/DESENG-668) + - Implemented authoring side nav + - Implemented authoring bottom nav + - Implemented authoring section context + - Still working on skeletons + ## August 15, 2024 - **Feature** New engagement authoring view tab [🎟️ DESENG-674](https://citz-gdx.atlassian.net/browse/DESENG-674) diff --git a/met-web/src/assets/images/pagePreview.png b/met-web/src/assets/images/pagePreview.png new file mode 100644 index 0000000000000000000000000000000000000000..9dcd251e40b3423250d0022cb34abb1b11a9af3f GIT binary patch literal 580 zcmV-K0=xZ*P)0SyxYs{ z*X$z3mJZMmK9ZE z&U~X0TU@{aaZIy^s&J@PW1~m${$%PRAJJxyOstvnw)LhRYy5>%5XV9IPEipjYK$pa zT1WRt7W#v}soT8IGIZga%$`U-Ejm4j7#Bd&M`jcVL>Atm(*e21x3b=sVC%FSC79oT*1%e&ai&~>~r3J8srfi`wGNR(S^sUN`q z8{qvkYm&lC%PqaM1TVasA?yuZ_7ml&^isRoJO^SGymND3!}4fH+W8Qfe`^X4vf#ui z`HW13^u`|byZ;M0;> S|Lbc20000 { + const { onSubmit }: AuthoringContextType = useOutletContext(); + const locationArray = useLocation().pathname.split('/'); + const engagementId = locationArray[2]; + + const { + handleSubmit, + formState: { isDirty, isValid, isSubmitting }, + } = useFormContext(); + + return ( + +
+ + + + +
+ ); +}; + +export default AuthoringBanner; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx new file mode 100644 index 000000000..ee8d149cf --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx @@ -0,0 +1,214 @@ +import React, { Suspense, useState } from 'react'; +import { Await, useAsyncValue, useLocation } from 'react-router-dom'; +import { AppBar, Theme, ThemeProvider, Box, useMediaQuery, Select, MenuItem, SelectChangeEvent } from '@mui/material'; +import { Palette, colors, DarkTheme, BaseTheme } from 'styles/Theme'; +import { When, Unless } from 'react-if'; +import { BodyText } from 'components/common/Typography'; +import { elevations } from 'components/common'; +import { Button } from 'components/common/Input'; +import { Link } from 'components/common/Navigation'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck } from '@fortawesome/pro-regular-svg-icons'; +import { useAppSelector } from 'hooks'; +import { getAuthoringRoutes } from './AuthoringNavElements'; +import { getTenantLanguages } from 'services/languageService'; +import { Language } from 'models/language'; +import { StatusCircle } from '../../view/AuthoringTab'; +import pagePreview from 'assets/images/pagePreview.png'; + +interface AuthoringBottomNavProps { + isDirty: boolean; + isValid: boolean; + isSubmitting: boolean; +} + +const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { + const { isDirty, isValid, isSubmitting } = props; + const isMediumScreenOrLarger = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); + const padding = { xs: '1rem 1rem', md: '1rem 1.5rem 1rem 2rem', lg: '1rem 3rem 1rem 2rem' }; + + const tenant = useAppSelector((state) => state.tenant); + const locationArray = useLocation().pathname.split('/'); + const pageSlug = locationArray[locationArray.length - 1]; + const engagementId = locationArray[2]; + + const languages = getTenantLanguages(tenant.id); // todo: Using tenant language list until language data is integrated with the engagement. + const [currentLanguage, setCurrentLanguage] = useState(useAppSelector((state) => state.language.id)); + + const getPageValue = () => { + const authoringRoutes = getAuthoringRoutes(Number(engagementId), tenant); + return authoringRoutes.find((route) => route.path.includes(pageSlug))?.name; + }; + + const getLanguageValue = (currentLanguage: string, allLanguages: Language[]) => { + return allLanguages.find((language) => language.code === currentLanguage)?.name; + }; + + const handleSelectChange = (event: SelectChangeEvent) => { + const newLanguageCode = event.target.value; + setCurrentLanguage(newLanguageCode); + }; + + const Selector = () => { + const languages = useAsyncValue() as Language[]; + return ( + + ); + }; + + const buttonStyles = { + height: '2.6rem', + borderRadius: '8px', + border: 'none', + padding: '0 1rem', + minWidth: '8.125rem', + fontSize: '0.9rem', + }; + + return ( + + + + + Currently Authoring + + {getPageValue()} + + {'\u2B24'} + + + + {(languages: Language[]) => ( + {getLanguageValue(currentLanguage, languages)} + )} + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default AuthoringBottomNav; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx new file mode 100644 index 000000000..d5d346300 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Dayjs } from 'dayjs'; +import { FormProvider, useForm } from 'react-hook-form'; +import { createSearchParams, useFetcher, Outlet, useLocation } from 'react-router-dom'; + +export interface EngagementUpdateData { + id: number; + name: string; + start_date: Dayjs; + end_date: Dayjs; + status_id: number; + description: string; + rich_description: string; + banner_filename: string; + status_block: string[]; + title: string; + icon_name: string; + metadata_value: string; + taxon_id: number; + send_report: boolean; + slug: string; + request_type: string; +} + +export const AuthoringContext = () => { + const fetcher = useFetcher(); + const locationArray = useLocation().pathname.split('/'); + const engagementUpdateForm = useForm({ + defaultValues: { + id: 0, + name: '', + start_date: undefined, + end_date: undefined, + status_id: 0, + description: '', + rich_description: '', + banner_filename: '', + status_block: [], + title: '', + icon_name: '', + metadata_value: '', + taxon_id: 0, + send_report: false, + slug: '', + request_type: '', + }, + mode: 'onSubmit', + reValidateMode: 'onChange', + }); + + const onSubmit = async (data: EngagementUpdateData) => { + fetcher.submit( + createSearchParams({ + id: data.id.toString(), + name: data.name, + start_date: data.start_date.format('YYYY-MM-DD'), + end_date: data.end_date.format('YYYY-MM-DD'), + description: data.description, + rich_description: data.rich_description, + banner_filename: data.banner_filename, + status_block: data.status_block, + title: data.title, + icon_name: data.icon_name, + metadata_value: data.metadata_value, + taxon_id: data.taxon_id.toString(), + send_report: data.send_report ? 'true' : 'false', + slug: data.slug, + request_type: data.request_type, + }), + { + method: 'post', + action: `/engagements/${data.id}/authoring/${locationArray[2]}`, + }, + ); + }; + + return ( + + + + ); +}; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx new file mode 100644 index 000000000..9c36b1a18 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx @@ -0,0 +1,78 @@ +import { USER_ROLES } from 'services/userService/constants'; +import { TenantState } from 'reduxSlices/tenantSlice'; + +export interface AuthoringRoute { + name: string; + path: string; + base: string; + authenticated: boolean; + allowedRoles: string[]; + required?: boolean; +} + +export const getAuthoringRoutes = (engagementId: number, tenant: TenantState): AuthoringRoute[] => [ + { + name: 'Engagement Home', + path: `/${tenant.id}/engagements/${engagementId}/view`, + base: `/${tenant.id}/engagements/${engagementId}/view`, + authenticated: false, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'Hero Banner', + path: `/engagements/${engagementId}/authoring/banner`, + base: `/engagements/${engagementId}/authoring/banner`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'Summary', + path: `/engagements/${engagementId}/authoring/summary`, + base: `/engagements/${engagementId}/authoring/summary`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'Details', + path: `/engagements/${engagementId}/authoring/details`, + base: `/engagements/${engagementId}/authoring/details`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'Provide Feedback', + path: `/engagements/${engagementId}/authoring/feedback`, + base: `/engagements/${engagementId}/authoring/feedback`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'View Results', + path: `/engagements/${engagementId}/authoring/results`, + base: `/engagements/${engagementId}/authoring/results`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: false, + }, + { + name: 'Subscribe', + path: `/engagements/${engagementId}/authoring/subscribe`, + base: `/engagements/${engagementId}/authoring/subscribe`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: false, + }, + { + name: 'More Engagements', + path: `/engagements/${engagementId}/authoring/more`, + base: `/engagements/${engagementId}/authoring/more`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: false, + }, +]; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx new file mode 100644 index 000000000..2cb79abde --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx @@ -0,0 +1,281 @@ +import React from 'react'; +import { + ListItemButton, + List, + ListItem, + Box, + Drawer, + Toolbar, + SwipeableDrawer, + Grid, + Avatar, + ThemeProvider, +} from '@mui/material'; +import { getAuthoringRoutes as getRoutes, AuthoringRoute as Route } from './AuthoringNavElements'; +import { DarkTheme, Palette, colors, ZIndex } from 'styles/Theme'; +import { AuthoringNavProps, DrawerBoxProps } from './types'; +import { When } from 'react-if'; +import { useAppSelector } from 'hooks'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPencil } from '@fortawesome/pro-light-svg-icons/faPencil'; +import { Link } from 'components/common/Navigation'; +import { BodyText } from 'components/common/Typography'; +import { USER_ROLES } from 'services/userService/constants'; +import UserService from 'services/userService'; +import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; +import { faArrowLeftLong } from '@fortawesome/pro-light-svg-icons'; +import { faCheck } from '@fortawesome/pro-regular-svg-icons'; +import { StatusCircle } from '../../view/AuthoringTab'; + +export const routeItemStyle = { + padding: 0, + backgroundColor: Palette.background.default, + '&:hover, &:focus': { + filter: 'brightness(96%)', + }, + '&:active': { + filter: 'brightness(92%)', + }, + borderRadius: '8px', +}; + +const DrawerBox = ({ isMediumScreenOrLarger, setOpen, engagementId }: DrawerBoxProps) => { + const permissions = useAppSelector((state) => state.user.roles); + const tenant = useAppSelector((state) => state.tenant); + + const currentBaseRoute = getRoutes(Number(engagementId), tenant) + .map((route) => route.base) + .filter((route) => location.pathname.includes(route)) + .reduce((prev, curr) => (prev.length > curr.length ? prev : curr)); + + const allowedRoutes = getRoutes(Number(engagementId), tenant).filter((route) => { + return !route.authenticated || route.allowedRoles.some((role) => permissions.includes(role)); + }); + + const renderListItem = (route: Route, itemType: string, key: number, engagementId: number) => { + const navLabelTextStyles: React.CSSProperties = { + display: 'flex', + textTransform: 'uppercase', + fontWeight: 'bold', + fontSize: '0.95rem', + color: Palette.text.primary, + marginTop: '1.4rem', + marginBottom: '1rem', + }; + return ( + <> + + + {'Hero Banner' === route.name ? 'Required' : 'Optional'} Sections + + + + { + setOpen(false); + }} + > + + + + + + + {route.name} + + + + + + + + + + + ); + }; + + return ( + + + {/* Engagement Home link */} + + + {getRoutes(Number(engagementId), tenant)[0].name} + +
+ {/* All other menu items */} + {allowedRoutes.map( + (route, index) => + 0 !== index && + renderListItem( + route, + currentBaseRoute === route.base ? 'selected' : 'other', + index, + Number(engagementId), + ), + )} +
+
+ ); +}; + +const AuthoringSideNav = ({ open, setOpen, isMediumScreen, engagementId }: AuthoringNavProps) => { + const currentUser = useAppSelector((state) => state.user.userDetail.user); + if (isMediumScreen) + return ( + + + + + ); + return ( + theme.zIndex.drawer + 3, // render above feedback button + }} + onOpen={() => setOpen(true)} + onClose={() => setOpen(false)} + anchor={'top'} + open={open} + disableEnforceFocus + disablePortal + > + + + + + + + {currentUser?.first_name[0]} + {currentUser?.last_name[0]} + + + + + Hello {currentUser?.first_name} + + + {currentUser?.roles.includes(USER_ROLES.SUPER_ADMIN) + ? 'Super Admin' + : currentUser?.main_role ?? 'User'} + + + + + Logout + + + + + + + + ); +}; + +export default AuthoringSideNav; diff --git a/met-web/src/components/engagement/admin/create/authoring/engagementUpdateAction.tsx b/met-web/src/components/engagement/admin/create/authoring/engagementUpdateAction.tsx new file mode 100644 index 000000000..8caff9eb4 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/engagementUpdateAction.tsx @@ -0,0 +1,105 @@ +import { ActionFunction, redirect } from 'react-router-dom'; +import { patchEngagement } from 'services/engagementService'; +import { patchEngagementContent } from 'services/engagementContentService'; +import { patchSummaryContent } from 'services/engagementSummaryService'; +import { patchEngagementMetadata } from 'services/engagementMetadataService'; +import { patchEngagementSettings } from 'services/engagementSettingService'; +import { patchEngagementSlug } from 'services/engagementSlugService'; +import { openNotification } from 'services/notificationService/notificationSlice'; + +export const engagementUpdateAction: ActionFunction = async ({ request }) => { + const formData = (await request.formData()) as FormData; + const errors = []; + const requestType = formData.get('request_type') as string; + const engagement = await patchEngagement({ + id: formData.get('id') as unknown as number, + name: (formData.get('name') as string) || undefined, + start_date: (formData.get('start_date') as string) || undefined, + status_id: (formData.get('status_id') as unknown as number) || undefined, + end_date: (formData.get('end_date') as string) || undefined, + description: (formData.get('description') as string) || undefined, + rich_description: (formData.get('rich_description') as string) || undefined, + banner_filename: (formData.get('banner_filename') as string) || undefined, + status_block: (formData.get('status_block') as unknown as unknown[]) || undefined, + }); + + // Update engagement content. + try { + await patchEngagementContent(engagement.id, formData.get('content_id') as unknown as number, { + title: formData.get('title') as string, + icon_name: formData.get('icon_name') as string, + }); + } catch (e) { + console.error('Error updating engagement', e); + errors.push(e); + } + + // Update engagement summary if necessary. + if (formData.get('content_id') && engagement.content && engagement.rich_content) { + try { + await patchSummaryContent(formData.get('content_id') as unknown as number, { + content: engagement.content, + rich_content: engagement.rich_content, + }); + } catch (e) { + console.error('Error updating engagement summary', e); + errors.push(e); + } + } + + // Update engagement metadata if necessary. + if (formData.get('metadata_value') && formData.get('taxon_id')) { + try { + await patchEngagementMetadata({ + value: formData.get('metadata_value') as string, + taxon_id: formData.get('taxon_id') as unknown as number, + engagement_id: engagement.id, + }); + } catch (e) { + console.error('Error updating engagement metadata', e); + errors.push(e); + } + } + + // Update engagement settings if necessary. + if (formData.get('send_report')) { + try { + await patchEngagementSettings({ + engagement_id: engagement.id, + send_report: formData.get('send_report') as unknown as boolean, + }); + } catch (e) { + console.error('Error updating engagement settings', e); + errors.push(e); + } + } + + // Update engagement slug if necessary. + if (formData.get('slug')) { + try { + await patchEngagementSlug({ + engagement_id: engagement.id, + slug: formData.get('slug') as string, + }); + } catch (e) { + console.error('Error updating engagement slug', e); + errors.push(e); + } + } + + if (0 === errors.length && 'preview' === requestType) { + return redirect(`/engagements/${engagement.id}/view`); + } else if (0 === errors.length && 'update' === requestType) { + openNotification({ + severity: 'success', + text: 'Engagement saved successfully.', + }); + } else { + openNotification({ + severity: 'error', + text: 'preview' === requestType ? 'Unable to preview engagement' : 'Unable to save engagement', + }); + } +}; + +export default engagementUpdateAction; diff --git a/met-web/src/components/engagement/admin/create/authoring/types.ts b/met-web/src/components/engagement/admin/create/authoring/types.ts new file mode 100644 index 000000000..238ba233c --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/types.ts @@ -0,0 +1,19 @@ +import { SubmitHandler } from 'react-hook-form'; +import { EngagementUpdateData } from './AuthoringContext'; + +export interface AuthoringNavProps { + open: boolean; + isMediumScreen: boolean; + setOpen: (open: boolean) => void; + engagementId: string; +} + +export interface DrawerBoxProps { + isMediumScreenOrLarger: boolean; + setOpen: (open: boolean) => void; + engagementId: string; +} + +export interface AuthoringContextType { + onSubmit: SubmitHandler; +} diff --git a/met-web/src/components/engagement/admin/view/AuthoringTab.tsx b/met-web/src/components/engagement/admin/view/AuthoringTab.tsx index 8f81689fa..7cb7b63a4 100644 --- a/met-web/src/components/engagement/admin/view/AuthoringTab.tsx +++ b/met-web/src/components/engagement/admin/view/AuthoringTab.tsx @@ -9,15 +9,18 @@ import { SystemMessage } from 'components/common/Layout/SystemMessage'; import { When } from 'react-if'; import { Grid, Link } from '@mui/material'; import { colors } from 'styles/Theme'; +import { getDefaultAuthoringTabValues } from './AuthoringTabElements'; -const StatusCircle = (props: StatusCircleProps) => { +export const StatusCircle = (props: StatusCircleProps) => { const statusCircleStyles = { width: '6px', height: '6px', borderRadius: '50%', backgroundColor: props.required ? colors.notification.danger.icon : colors.surface.gray[70], marginLeft: '0.3rem', - marginTop: '-1rem', + display: 'inline-block', + bottom: '0.5rem', + position: 'relative' as const, }; return ; }; @@ -68,38 +71,9 @@ const AuthoringButton = (props: AuthoringButtonProps) => { }; export const AuthoringTab = () => { - // Set default values - const mandatorySectionTitles = ['Hero Banner', 'Summary', 'Details', 'Provide Feedback']; - const optionalSectionTitles = ['View Results', 'Subscribe', 'More Engagements']; - const feedbackTitles = ['Survey', '3rd Party Feedback Method Link']; - const defaultAuthoringValue: AuthoringValue = { - id: 0, - title: '', - link: '#', - required: false, - completed: false, - }; - const getAuthoringValues = ( - defaultValues: AuthoringValue, - titles: string[], - required: boolean, - idOffset = 0, - ): AuthoringValue[] => { - return titles.map((title, index) => ({ - ...defaultValues, - title: title, - required: required, - id: index + idOffset, - })); - }; - const mandatorySectionValues = getAuthoringValues(defaultAuthoringValue, mandatorySectionTitles, true); - const optionalSectionValues = getAuthoringValues(defaultAuthoringValue, optionalSectionTitles, false, 100); - const defaultSectionValues = [...mandatorySectionValues, ...optionalSectionValues]; - const defaultFeedbackMethods = getAuthoringValues(defaultAuthoringValue, feedbackTitles, true, 1000); - // Set useStates. When data is imported, it will be set with setSectionValues and setFeedbackMethods. - const [sectionValues] = useState(defaultSectionValues); - const [feedbackMethods] = useState(defaultFeedbackMethods); + const [sectionValues] = useState(getDefaultAuthoringTabValues('sections')); + const [feedbackMethods] = useState(getDefaultAuthoringTabValues('feedback')); const [requiredSectionsCompleted, setRequiredSectionsCompleted] = useState(false); const [feedbackCompleted, setFeedbackCompleted] = useState(false); diff --git a/met-web/src/components/engagement/admin/view/AuthoringTabElements.tsx b/met-web/src/components/engagement/admin/view/AuthoringTabElements.tsx new file mode 100644 index 000000000..298259612 --- /dev/null +++ b/met-web/src/components/engagement/admin/view/AuthoringTabElements.tsx @@ -0,0 +1,76 @@ +import { AuthoringValue } from './types'; + +export const getDefaultAuthoringTabValues = (type: string): AuthoringValue[] => { + if ('sections' === type) { + // Return the default "section" items + return [ + { + id: 1, + title: 'Hero Banner', + link: `authoring/banner`, + required: true, + completed: false, + }, + { + id: 2, + title: 'Summary', + link: `authoring/summary`, + required: true, + completed: false, + }, + { + id: 3, + title: 'Details', + link: `authoring/details`, + required: true, + completed: false, + }, + { + id: 4, + title: 'Provide Feedback', + link: `authoring/feedback`, + required: true, + completed: false, + }, + { + id: 5, + title: 'View Results', + link: `authoring/results`, + required: false, + completed: false, + }, + { + id: 6, + title: 'Subscribe', + link: `authoring/subscribe`, + required: false, + completed: false, + }, + { + id: 7, + title: 'More Engagements', + link: `authoring/more`, + required: false, + completed: false, + }, + ]; + } else { + // Return the default "feedback" items + return [ + { + id: 101, + title: 'Survey', + link: `#`, + required: true, + completed: false, + }, + { + id: 102, + title: '3rd Party Feedback Method Link', + link: `#`, + required: true, + completed: false, + }, + ]; + } +}; diff --git a/met-web/src/components/feedback/FeedbackModal/index.tsx b/met-web/src/components/feedback/FeedbackModal/index.tsx index 19e497d19..8647f83dd 100644 --- a/met-web/src/components/feedback/FeedbackModal/index.tsx +++ b/met-web/src/components/feedback/FeedbackModal/index.tsx @@ -79,7 +79,7 @@ export const FeedbackModal = () => { const isCommentNotProvided = comment_type !== CommentTypeEnum.None && !comment; const theme = useTheme(); - const bottomSpacing = theme.spacing(40); + const bottomSpacing = theme.spacing(52); const rightSpacing = theme.spacing(0); return ( diff --git a/met-web/src/components/layout/Header/InternalHeader.tsx b/met-web/src/components/layout/Header/InternalHeader.tsx index 72b2f79ab..3df0b0e05 100644 --- a/met-web/src/components/layout/Header/InternalHeader.tsx +++ b/met-web/src/components/layout/Header/InternalHeader.tsx @@ -23,15 +23,16 @@ import { Link } from 'components/common/Navigation'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBars, faChevronDown, faClose, faSignOut } from '@fortawesome/pro-regular-svg-icons'; import UserService from 'services/userService'; -import { Await, useAsyncValue, useRouteLoaderData } from 'react-router-dom'; +import { Await, useAsyncValue, useRouteLoaderData, useLocation } from 'react-router-dom'; import { Tenant } from 'models/tenant'; -import { When, Unless } from 'react-if'; +import { When, Unless, If, Else, Then } from 'react-if'; import { Button } from 'components/common/Input'; import DropdownMenu, { dropdownMenuStyles } from 'components/common/Navigation/DropdownMenu'; import { elevations } from 'components/common'; import TrapFocus from '@mui/base/TrapFocus'; import SideNav from '../SideNav/SideNav'; import { USER_ROLES } from 'services/userService/constants'; +import AuthoringSideNav from '../../engagement/admin/create/authoring/AuthoringSideNav'; const InternalHeader = () => { const isMediumScreenOrLarger = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); @@ -58,6 +59,7 @@ const InternalHeader = () => { } }, [isMediumScreenOrLarger, sideNavOpen]); + const location = useLocation(); const { myTenants } = useRouteLoaderData('authenticated-root') as { myTenants: Tenant[] }; const sidePadding = { xs: '0 1em', md: '0 1.5em 0 2em', lg: '0 3em 0 2em' }; @@ -195,12 +197,27 @@ const InternalHeader = () => { - + + + <> + + + + + + + diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index 8179d4dfc..63b5ad9ea 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -38,6 +38,9 @@ import { languageLoader } from 'components/engagement/admin/create/languageLoade import { userSearchLoader } from 'components/userManagement/userSearchLoader'; import EngagementCreationWizard from 'components/engagement/admin/create'; import engagementCreateAction from 'components/engagement/admin/create/engagementCreateAction'; +import AuthoringBanner from 'components/engagement/admin/create/authoring/AuthoringBanner'; +import { engagementUpdateAction } from 'components/engagement/admin/create/authoring/engagementUpdateAction'; +import { AuthoringContext } from 'components/engagement/admin/create/authoring/AuthoringContext'; const AuthenticatedRoutes = () => { return ( @@ -90,6 +93,17 @@ const AuthenticatedRoutes = () => { > }> } /> + }> + } action={engagementUpdateAction}> + } /> + {/* } /> + } /> + } /> + } /> + } /> + } /> */} + + } /> Date: Fri, 23 Aug 2024 17:24:40 -0700 Subject: [PATCH 4/9] feature/deseng668: Revised some routing code after merge, fixed conflicting action names, changed some data fetches. --- .../create/authoring/AuthoringBanner.tsx | 5 +-- .../create/authoring/AuthoringBottomNav.tsx | 8 ++-- .../create/authoring/AuthoringContext.tsx | 7 +-- .../create/authoring/AuthoringNavElements.tsx | 32 +++++++------- ...sx => engagementAuthoringUpdateAction.tsx} | 4 +- .../engagement/admin/view/index.tsx | 2 +- .../layout/Header/InternalHeader.tsx | 21 +++++++-- met-web/src/routes/AuthenticatedRoutes.tsx | 43 +++++++++---------- 8 files changed, 65 insertions(+), 57 deletions(-) rename met-web/src/components/engagement/admin/create/authoring/{engagementUpdateAction.tsx => engagementAuthoringUpdateAction.tsx} (97%) diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx index 0a82ea645..72aaf31c5 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useOutletContext, Form, useLocation } from 'react-router-dom'; +import { useOutletContext, Form, useParams } from 'react-router-dom'; import AuthoringBottomNav from './AuthoringBottomNav'; import { EngagementUpdateData } from './AuthoringContext'; import { useFormContext } from 'react-hook-form'; @@ -9,8 +9,7 @@ import { AuthoringContextType } from './types'; const AuthoringBanner = () => { const { onSubmit }: AuthoringContextType = useOutletContext(); - const locationArray = useLocation().pathname.split('/'); - const engagementId = locationArray[2]; + const { engagementId } = useParams() as { engagementId: string }; const { handleSubmit, diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx index ee8d149cf..5410e9801 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx @@ -1,5 +1,5 @@ import React, { Suspense, useState } from 'react'; -import { Await, useAsyncValue, useLocation } from 'react-router-dom'; +import { Await, useAsyncValue, useParams } from 'react-router-dom'; import { AppBar, Theme, ThemeProvider, Box, useMediaQuery, Select, MenuItem, SelectChangeEvent } from '@mui/material'; import { Palette, colors, DarkTheme, BaseTheme } from 'styles/Theme'; import { When, Unless } from 'react-if'; @@ -28,16 +28,14 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { const padding = { xs: '1rem 1rem', md: '1rem 1.5rem 1rem 2rem', lg: '1rem 3rem 1rem 2rem' }; const tenant = useAppSelector((state) => state.tenant); - const locationArray = useLocation().pathname.split('/'); - const pageSlug = locationArray[locationArray.length - 1]; - const engagementId = locationArray[2]; + const { engagementId, slug } = useParams() as { engagementId: string; slug: string }; const languages = getTenantLanguages(tenant.id); // todo: Using tenant language list until language data is integrated with the engagement. const [currentLanguage, setCurrentLanguage] = useState(useAppSelector((state) => state.language.id)); const getPageValue = () => { const authoringRoutes = getAuthoringRoutes(Number(engagementId), tenant); - return authoringRoutes.find((route) => route.path.includes(pageSlug))?.name; + return authoringRoutes.find((route) => route.path.includes(slug))?.name; }; const getLanguageValue = (currentLanguage: string, allLanguages: Language[]) => { 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 d5d346300..bc0ce559f 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Dayjs } from 'dayjs'; import { FormProvider, useForm } from 'react-hook-form'; -import { createSearchParams, useFetcher, Outlet, useLocation } from 'react-router-dom'; +import { createSearchParams, useFetcher, Outlet } from 'react-router-dom'; export interface EngagementUpdateData { id: number; @@ -24,7 +24,8 @@ export interface EngagementUpdateData { export const AuthoringContext = () => { const fetcher = useFetcher(); - const locationArray = useLocation().pathname.split('/'); + const locationArray = window.location.href.split('/'); + const slug = locationArray[locationArray.length - 1]; const engagementUpdateForm = useForm({ defaultValues: { id: 0, @@ -69,7 +70,7 @@ export const AuthoringContext = () => { }), { method: 'post', - action: `/engagements/${data.id}/authoring/${locationArray[2]}`, + action: `/engagements/${data.id}/authoring/${slug}`, }, ); }; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx index 9c36b1a18..f49485544 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx @@ -13,64 +13,64 @@ export interface AuthoringRoute { export const getAuthoringRoutes = (engagementId: number, tenant: TenantState): AuthoringRoute[] => [ { name: 'Engagement Home', - path: `/${tenant.id}/engagements/${engagementId}/view`, - base: `/${tenant.id}/engagements/${engagementId}/view`, + path: `/${tenant.id}/engagements/${engagementId}/details/authoring`, + base: `/${tenant.id}/engagements`, authenticated: false, allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], required: true, }, { name: 'Hero Banner', - path: `/engagements/${engagementId}/authoring/banner`, - base: `/engagements/${engagementId}/authoring/banner`, + path: `/engagements/${engagementId}/details/authoring/banner`, + base: `/engagements`, authenticated: true, allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], required: true, }, { name: 'Summary', - path: `/engagements/${engagementId}/authoring/summary`, - base: `/engagements/${engagementId}/authoring/summary`, + path: `/engagements/${engagementId}/details/authoring/summary`, + base: `/engagements`, authenticated: true, allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], required: true, }, { name: 'Details', - path: `/engagements/${engagementId}/authoring/details`, - base: `/engagements/${engagementId}/authoring/details`, + path: `/engagements/${engagementId}/details/authoring/details`, + base: `/engagements`, authenticated: true, allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], required: true, }, { name: 'Provide Feedback', - path: `/engagements/${engagementId}/authoring/feedback`, - base: `/engagements/${engagementId}/authoring/feedback`, + path: `/engagements/${engagementId}/details/authoring/feedback`, + base: `/engagements`, authenticated: true, allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], required: true, }, { name: 'View Results', - path: `/engagements/${engagementId}/authoring/results`, - base: `/engagements/${engagementId}/authoring/results`, + path: `/engagements/${engagementId}/details/authoring/results`, + base: `/engagements`, authenticated: true, allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], required: false, }, { name: 'Subscribe', - path: `/engagements/${engagementId}/authoring/subscribe`, - base: `/engagements/${engagementId}/authoring/subscribe`, + path: `/engagements/${engagementId}/details/authoring/subscribe`, + base: `/engagements`, authenticated: true, allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], required: false, }, { name: 'More Engagements', - path: `/engagements/${engagementId}/authoring/more`, - base: `/engagements/${engagementId}/authoring/more`, + path: `/engagements/${engagementId}/details/authoring/more`, + base: `/engagements`, authenticated: true, allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], required: false, diff --git a/met-web/src/components/engagement/admin/create/authoring/engagementUpdateAction.tsx b/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx similarity index 97% rename from met-web/src/components/engagement/admin/create/authoring/engagementUpdateAction.tsx rename to met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx index 8caff9eb4..73c31dda8 100644 --- a/met-web/src/components/engagement/admin/create/authoring/engagementUpdateAction.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx @@ -7,7 +7,7 @@ import { patchEngagementSettings } from 'services/engagementSettingService'; import { patchEngagementSlug } from 'services/engagementSlugService'; import { openNotification } from 'services/notificationService/notificationSlice'; -export const engagementUpdateAction: ActionFunction = async ({ request }) => { +export const engagementAuthoringUpdateAction: ActionFunction = async ({ request }) => { const formData = (await request.formData()) as FormData; const errors = []; const requestType = formData.get('request_type') as string; @@ -101,5 +101,3 @@ export const engagementUpdateAction: ActionFunction = async ({ request }) => { }); } }; - -export default engagementUpdateAction; diff --git a/met-web/src/components/engagement/admin/view/index.tsx b/met-web/src/components/engagement/admin/view/index.tsx index 875036053..91b7de65a 100644 --- a/met-web/src/components/engagement/admin/view/index.tsx +++ b/met-web/src/components/engagement/admin/view/index.tsx @@ -14,7 +14,7 @@ export const AdminEngagementView = () => { const EngagementViewTabs = { config: 'Configuration', - author: 'Authoring', + authoring: 'Authoring', activity: 'Activity', results: 'Results', publish: 'Publishing', diff --git a/met-web/src/components/layout/Header/InternalHeader.tsx b/met-web/src/components/layout/Header/InternalHeader.tsx index 3df0b0e05..a3f1a28ad 100644 --- a/met-web/src/components/layout/Header/InternalHeader.tsx +++ b/met-web/src/components/layout/Header/InternalHeader.tsx @@ -23,7 +23,7 @@ import { Link } from 'components/common/Navigation'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBars, faChevronDown, faClose, faSignOut } from '@fortawesome/pro-regular-svg-icons'; import UserService from 'services/userService'; -import { Await, useAsyncValue, useRouteLoaderData, useLocation } from 'react-router-dom'; +import { Await, useAsyncValue, useRouteLoaderData, useParams } from 'react-router-dom'; import { Tenant } from 'models/tenant'; import { When, Unless, If, Else, Then } from 'react-if'; import { Button } from 'components/common/Input'; @@ -33,6 +33,7 @@ import TrapFocus from '@mui/base/TrapFocus'; import SideNav from '../SideNav/SideNav'; import { USER_ROLES } from 'services/userService/constants'; import AuthoringSideNav from '../../engagement/admin/create/authoring/AuthoringSideNav'; +import { getAuthoringRoutes } from '../../engagement/admin/create/authoring/AuthoringNavElements'; const InternalHeader = () => { const isMediumScreenOrLarger = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); @@ -59,7 +60,19 @@ const InternalHeader = () => { } }, [isMediumScreenOrLarger, sideNavOpen]); - const location = useLocation(); + // Get the authoring nav elements and current route so we can check their last two slugs against the current route's last two slugs. + // This will be used to determine which sidenav menu is displayed. + const pathname = window.location.href; + const { engagementId } = useParams() as { engagementId: string }; + const currentAuthoringSlug = pathname.split('/').slice(-2).join('/'); + const authoringRoutes = getAuthoringRoutes(Number(engagementId), tenant).map((route) => { + // skip the "Engagement Home" link + if ('Engagement Home' !== route.name) { + const pathArray = route.path.split('/'); + return pathArray.slice(-2).join('/'); + } + }); + const { myTenants } = useRouteLoaderData('authenticated-root') as { myTenants: Tenant[] }; const sidePadding = { xs: '0 1em', md: '0 1.5em 0 2em', lg: '0 3em 0 2em' }; @@ -197,7 +210,7 @@ const InternalHeader = () => { - + <> { setOpen={setSideNavOpen} data-testid="authoringnav-header" isMediumScreen={isMediumScreenOrLarger} - engagementId={location.pathname.split('/')[2]} + engagementId={engagementId} /> diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index 7c996dde0..af4f609a0 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -96,17 +96,6 @@ const AuthenticatedRoutes = () => { > }> } /> - }> - } action={engagementUpdateAction}> - } /> - {/* } /> - } /> - } /> - } /> - } /> - } /> */} - - } /> { {/* Wraps the tabs with the engagement title and TabContext */} }> } /> - } /> - } /> + }> + + }> + } action={engagementAuthoringUpdateAction}> + } /> + {/* } /> + } /> + } /> + } /> + } /> + } /> */} + - } - action={engagementUpdateAction} - handle={{ - crumb: () => ({ name: 'Configure' }), - }} - /> + } /> + } + action={engagementUpdateAction} + handle={{ + crumb: () => ({ name: 'Configure' }), + }} + /> }> } /> - } /> } /> } /> From e60b3064a3baf06de7e319235ea1d2b5225b4f3b Mon Sep 17 00:00:00 2001 From: Jareth Whitney Date: Tue, 27 Aug 2024 14:58:58 -0700 Subject: [PATCH 5/9] feature/deseng668: Revised routes and authoring components as per PR comments. --- .../create/authoring/AuthoringBottomNav.tsx | 139 +++++++++--------- .../engagementAuthoringUpdateAction.tsx | 2 +- .../admin/create/authoring/types.ts | 12 ++ met-web/src/routes/AuthenticatedRoutes.tsx | 41 +++--- met-web/src/styles/Theme.ts | 21 --- 5 files changed, 100 insertions(+), 115 deletions(-) diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx index 5410e9801..8fbf0b32b 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useState } from 'react'; +import React, { Suspense, useState, useMemo } from 'react'; import { Await, useAsyncValue, useParams } from 'react-router-dom'; import { AppBar, Theme, ThemeProvider, Box, useMediaQuery, Select, MenuItem, SelectChangeEvent } from '@mui/material'; import { Palette, colors, DarkTheme, BaseTheme } from 'styles/Theme'; @@ -15,12 +15,7 @@ import { getTenantLanguages } from 'services/languageService'; import { Language } from 'models/language'; import { StatusCircle } from '../../view/AuthoringTab'; import pagePreview from 'assets/images/pagePreview.png'; - -interface AuthoringBottomNavProps { - isDirty: boolean; - isValid: boolean; - isSubmitting: boolean; -} +import { AuthoringBottomNavProps, LanguageSelectorProps } from './types'; const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { const { isDirty, isValid, isSubmitting } = props; @@ -28,9 +23,12 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { const padding = { xs: '1rem 1rem', md: '1rem 1.5rem 1rem 2rem', lg: '1rem 3rem 1rem 2rem' }; const tenant = useAppSelector((state) => state.tenant); - const { engagementId, slug } = useParams() as { engagementId: string; slug: string }; + const { engagementId } = useParams() as { engagementId: string }; + const location = window.location.href; + const locationArray = location.split('/'); + const slug = locationArray[locationArray.length - 1]; - const languages = getTenantLanguages(tenant.id); // todo: Using tenant language list until language data is integrated with the engagement. + const languages = useMemo(() => getTenantLanguages(tenant.id), [tenant.id]); // todo: Using tenant language list until language data is integrated with the engagement. const [currentLanguage, setCurrentLanguage] = useState(useAppSelector((state) => state.language.id)); const getPageValue = () => { @@ -38,66 +36,8 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { return authoringRoutes.find((route) => route.path.includes(slug))?.name; }; - const getLanguageValue = (currentLanguage: string, allLanguages: Language[]) => { - return allLanguages.find((language) => language.code === currentLanguage)?.name; - }; - - const handleSelectChange = (event: SelectChangeEvent) => { - const newLanguageCode = event.target.value; - setCurrentLanguage(newLanguageCode); - }; - - const Selector = () => { - const languages = useAsyncValue() as Language[]; - return ( - - ); + const getLanguageValue = (currentLanguage: string, languages: Language[]) => { + return languages.find((language) => language.code === currentLanguage)?.name; }; const buttonStyles = { @@ -170,7 +110,10 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { - + @@ -209,4 +152,60 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { ); }; +const LanguageSelector = ({ currentLanguage, setCurrentLanguage }: LanguageSelectorProps) => { + const languages = useAsyncValue() as Language[]; + const handleSelectChange = (event: SelectChangeEvent) => { + const newLanguageCode = event.target.value; + setCurrentLanguage(newLanguageCode); + }; + return ( + + ); +}; + export default AuthoringBottomNav; diff --git a/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx b/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx index 73c31dda8..3630a01ff 100644 --- a/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx @@ -88,7 +88,7 @@ export const engagementAuthoringUpdateAction: ActionFunction = async ({ request } if (0 === errors.length && 'preview' === requestType) { - return redirect(`/engagements/${engagement.id}/view`); + return redirect(`/engagements/${engagement.id}/old-view`); } else if (0 === errors.length && 'update' === requestType) { openNotification({ severity: 'success', diff --git a/met-web/src/components/engagement/admin/create/authoring/types.ts b/met-web/src/components/engagement/admin/create/authoring/types.ts index 238ba233c..d90d0f457 100644 --- a/met-web/src/components/engagement/admin/create/authoring/types.ts +++ b/met-web/src/components/engagement/admin/create/authoring/types.ts @@ -1,5 +1,6 @@ import { SubmitHandler } from 'react-hook-form'; import { EngagementUpdateData } from './AuthoringContext'; +import { Dispatch, SetStateAction } from 'react'; export interface AuthoringNavProps { open: boolean; @@ -17,3 +18,14 @@ export interface DrawerBoxProps { export interface AuthoringContextType { onSubmit: SubmitHandler; } + +export interface LanguageSelectorProps { + currentLanguage: string; + setCurrentLanguage: Dispatch>; +} + +export interface AuthoringBottomNavProps { + isDirty: boolean; + isValid: boolean; + isSubmitting: boolean; +} diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index af4f609a0..a4fa5e303 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -93,26 +93,21 @@ const AuthenticatedRoutes = () => { id="single-engagement" errorElement={} loader={engagementLoader} + handle={{ + crumb: async (data: { engagement: Promise }) => { + return data.engagement.then((engagement) => { + return { + link: `/engagements/${engagement.id}/old-view`, + name: engagement.name, + }; + }); + }, + }} > }> } /> } /> - }) => { - return data.engagement.then((engagement) => { - return { - link: `/engagements/${engagement.id}/details/config`, - name: engagement.name, - }; - }); - }, - }} - element={} - /> } /> } /> @@ -133,15 +128,15 @@ const AuthenticatedRoutes = () => { } /> + } + action={engagementUpdateAction} + handle={{ + crumb: () => ({ name: 'Configure' }), + }} + /> - } - action={engagementUpdateAction} - handle={{ - crumb: () => ({ name: 'Configure' }), - }} - /> }> } /> diff --git a/met-web/src/styles/Theme.ts b/met-web/src/styles/Theme.ts index 08464ab94..819bad08d 100644 --- a/met-web/src/styles/Theme.ts +++ b/met-web/src/styles/Theme.ts @@ -354,27 +354,6 @@ export const DarkTheme = createTheme({ }, }, }, - MuiSelect: { - defaultProps: { - // MenuProps: { - // sx: { - // '& .MuiMenu-list .MuiMenuItem-root:hover, .MuiMenu-list .MuiMenuItem-root.Mui-focusVisible': { - // backgroundColor: DarkPalette.hover.light, - // }, - // '& .MuiMenu-list .MuiMenuItem-root.Mui-selected': { - // backgroundColor: colors.surface.blue[30], - // '&:hover, &.Mui-focusVisible': { - // backgroundColor: colors.surface.blue[40], - // }, - // }, - // '& .MuiMenu-list .MuiMenuItem-root.Mui-disabled': { - // backgroundColor: colors.surface.gray[10], - // color: colors.type.regular.disabled, - // }, - // }, - // }, - }, - }, MuiButton: { styleOverrides: { root: { From 0702a23ca8e9d59a31cb16cc6da83dac20f12625 Mon Sep 17 00:00:00 2001 From: Jareth Whitney Date: Tue, 3 Sep 2024 14:39:37 -0700 Subject: [PATCH 6/9] feature/deseng668: Added authoring template and skeletons. --- CHANGELOG.MD | 4 + .../common/RichTextEditor/index.tsx | 1 - .../create/authoring/AuthoringBanner.tsx | 318 +++++++++++- .../create/authoring/AuthoringBottomNav.tsx | 99 ++-- .../create/authoring/AuthoringContext.tsx | 43 +- .../create/authoring/AuthoringDetails.tsx | 461 ++++++++++++++++++ .../create/authoring/AuthoringFeedback.tsx | 281 +++++++++++ .../admin/create/authoring/AuthoringMore.tsx | 12 + .../create/authoring/AuthoringResults.tsx | 12 + .../create/authoring/AuthoringSideNav.tsx | 8 +- .../create/authoring/AuthoringSubscribe.tsx | 12 + .../create/authoring/AuthoringSummary.tsx | 207 ++++++++ .../create/authoring/AuthoringTemplate.tsx | 192 ++++++++ .../engagementAuthoringUpdateAction.tsx | 28 +- .../admin/create/authoring/types.ts | 49 +- met-web/src/routes/AuthenticatedRoutes.tsx | 94 +++- 16 files changed, 1706 insertions(+), 115 deletions(-) create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringDetails.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringFeedback.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringMore.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringResults.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringSubscribe.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx create mode 100644 met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx diff --git a/CHANGELOG.MD b/CHANGELOG.MD index da7a4feb2..59cec5479 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,7 @@ +## September 3, 2024 +- **Feature** New authoring content section [🎟️ DESENG-668](https://citz-gdx.atlassian.net/browse/DESENG-668) + - Added skeletons + ## August 23, 2024 - **Feature** New authoring content section [🎟️ DESENG-668](https://citz-gdx.atlassian.net/browse/DESENG-668) - Implemented authoring side nav diff --git a/met-web/src/components/common/RichTextEditor/index.tsx b/met-web/src/components/common/RichTextEditor/index.tsx index 9c2bbf3f9..ce28d24d2 100644 --- a/met-web/src/components/common/RichTextEditor/index.tsx +++ b/met-web/src/components/common/RichTextEditor/index.tsx @@ -67,7 +67,6 @@ const RichTextEditor = ({ onEditorStateChange={handleChange} handlePastedText={() => false} editorStyle={{ - height: '10em', padding: '1em', resize: 'vertical', }} diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx index 72aaf31c5..f129a9da9 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx @@ -1,29 +1,305 @@ -import React from 'react'; -import { useOutletContext, Form, useParams } 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 { Box } from '@mui/material'; -import { AuthoringContextType } from './types'; +import { FormControlLabel, Grid, MenuItem, Radio, RadioGroup, Select } from '@mui/material'; +import React, { useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { TextField } from 'components/common/Input'; +import { AuthoringTemplateOutletContext } from './types'; +import { colors } from 'styles/Theme'; +import { EyebrowText } from 'components/common/Typography'; +import { MetLabel, MetHeader3 } from 'components/common'; +import ImageUpload from 'components/imageUpload'; + +const ENGAGEMENT_UPLOADER_HEIGHT = '360px'; +const ENGAGEMENT_CROPPER_ASPECT_RATIO = 1920 / 700; const AuthoringBanner = () => { - const { onSubmit }: AuthoringContextType = useOutletContext(); - const { engagementId } = useParams() as { engagementId: string }; + const { setValue, watch, control, engagement }: AuthoringTemplateOutletContext = useOutletContext(); // Access the form functions and values from the authoring template + + const [bannerImage, setBannerImage] = useState(); + const [savedBannerImageFileName, setSavedBannerImageFileName] = useState(engagement.banner_filename || ''); + + const [openCtaExternalURLEnabled, setOpenCtaExternalURLEnabled] = useState(false); + const [openCtaSectionSelectEnabled, setOpenCtaSectionSelectEnabled] = useState(true); + const [viewResultsCtaExternalURLEnabled, setViewResultsCtaExternalURLEnabled] = useState(false); + const [viewResultsSectionSelectEnabled, setViewResultsSectionSelectEnabled] = useState(true); - const { - handleSubmit, - formState: { isDirty, isValid, isSubmitting }, - } = useFormContext(); + //Define the styles + const metHeader3Styles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + }; + const eyebrowTextStyles = { + fontSize: '0.9rem', + marginBottom: '1.5rem', + }; + const metLabelStyles = { + fontSize: '0.95rem', + }; + const formItemContainerStyles = { + padding: '2rem 1.4rem !important', + margin: '1rem 0', + borderRadius: '16px', + }; + const conditionalSelectStyles = { + width: '100%', + backgroundColor: colors.surface.white, + borderRadius: '8px', + // boxShadow: '0 0 0 1px #7A7876 inset', + lineHeight: '1.4375em', + height: '48px', + marginTop: '8px', + padding: '0', + '&:disabled': { + boxShadow: 'none', + }, + }; + + const handleAddBannerImage = (files: File[]) => { + if (files.length > 0) { + setBannerImage(files[0]); + return; + } + setBannerImage(null); + setSavedBannerImageFileName(''); + }; + + const handleRadioSelectChange = (event: React.FormEvent, source: string) => { + const newRadioValue = event.currentTarget.value; + if ('ctaLinkType' === source) { + setOpenCtaExternalURLEnabled('section' === newRadioValue ? false : true); + setOpenCtaSectionSelectEnabled('section' === newRadioValue ? true : false); + } else { + setViewResultsCtaExternalURLEnabled('section' === newRadioValue ? false : true); + setViewResultsSectionSelectEnabled('section' === newRadioValue ? true : false); + } + }; return ( - -
- - - - -
+ + + + + + + + + + + + Hero Image (Required) + + + Please ensure you use high quality images that help to communicate the topic of your engagement. You + must ensure that any important subject matter is positioned on the right side. + + + + + + Engagement State Content Variants + + The content in this section of your engagement may be changed based on the state or status of your + engagement. Select the Section Preview or Page Preview button to see each of these states. + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx index 8fbf0b32b..e322eded7 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx @@ -1,45 +1,29 @@ -import React, { Suspense, useState, useMemo } from 'react'; -import { Await, useAsyncValue, useParams } from 'react-router-dom'; +import React from 'react'; import { AppBar, Theme, ThemeProvider, Box, useMediaQuery, Select, MenuItem, SelectChangeEvent } from '@mui/material'; import { Palette, colors, DarkTheme, BaseTheme } from 'styles/Theme'; import { When, Unless } from 'react-if'; import { BodyText } from 'components/common/Typography'; import { elevations } from 'components/common'; import { Button } from 'components/common/Input'; -import { Link } from 'components/common/Navigation'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck } from '@fortawesome/pro-regular-svg-icons'; -import { useAppSelector } from 'hooks'; -import { getAuthoringRoutes } from './AuthoringNavElements'; -import { getTenantLanguages } from 'services/languageService'; -import { Language } from 'models/language'; import { StatusCircle } from '../../view/AuthoringTab'; import pagePreview from 'assets/images/pagePreview.png'; import { AuthoringBottomNavProps, LanguageSelectorProps } from './types'; +import { getLanguageValue } from './AuthoringTemplate'; -const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { - const { isDirty, isValid, isSubmitting } = props; +const AuthoringBottomNav = ({ + isDirty, + isValid, + isSubmitting, + currentLanguage, + setCurrentLanguage, + languages, + pageTitle, +}: AuthoringBottomNavProps) => { const isMediumScreenOrLarger = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); const padding = { xs: '1rem 1rem', md: '1rem 1.5rem 1rem 2rem', lg: '1rem 3rem 1rem 2rem' }; - const tenant = useAppSelector((state) => state.tenant); - const { engagementId } = useParams() as { engagementId: string }; - const location = window.location.href; - const locationArray = location.split('/'); - const slug = locationArray[locationArray.length - 1]; - - const languages = useMemo(() => getTenantLanguages(tenant.id), [tenant.id]); // todo: Using tenant language list until language data is integrated with the engagement. - const [currentLanguage, setCurrentLanguage] = useState(useAppSelector((state) => state.language.id)); - - const getPageValue = () => { - const authoringRoutes = getAuthoringRoutes(Number(engagementId), tenant); - return authoringRoutes.find((route) => route.path.includes(slug))?.name; - }; - - const getLanguageValue = (currentLanguage: string, languages: Language[]) => { - return languages.find((language) => language.code === currentLanguage)?.name; - }; - const buttonStyles = { height: '2.6rem', borderRadius: '8px', @@ -80,24 +64,18 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { Currently Authoring - {getPageValue()} + {pageTitle} {'\u2B24'} - - - {(languages: Language[]) => ( - {getLanguageValue(currentLanguage, languages)} - )} - - + {getLanguageValue(currentLanguage, languages)} { }} > - - - - - + @@ -152,10 +135,24 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => { ); }; -const LanguageSelector = ({ currentLanguage, setCurrentLanguage }: LanguageSelectorProps) => { - const languages = useAsyncValue() as Language[]; +const LanguageSelector = ({ + currentLanguage, + setCurrentLanguage, + languages, + isDirty, + isSubmitting, +}: LanguageSelectorProps) => { const handleSelectChange = (event: SelectChangeEvent) => { const newLanguageCode = event.target.value; + if (isDirty && !isSubmitting) + // todo: Replace this message with our stylized modal message. + window.confirm( + `Are you sure you want to switch to ${ + getLanguageValue(newLanguageCode, languages) || 'another language' + }? You have unsaved changes for the ${ + getLanguageValue(currentLanguage, languages) || 'current' + } language.`, + ); setCurrentLanguage(newLanguageCode); }; return ( 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 bc0ce559f..e80e48f16 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx @@ -1,14 +1,16 @@ import React from 'react'; -import { Dayjs } from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { FormProvider, useForm } from 'react-hook-form'; import { createSearchParams, useFetcher, Outlet } from 'react-router-dom'; export interface EngagementUpdateData { id: number; + status_id: number; + taxon_id: number; + content_id: number; name: string; start_date: Dayjs; end_date: Dayjs; - status_id: number; description: string; rich_description: string; banner_filename: string; @@ -16,7 +18,6 @@ export interface EngagementUpdateData { title: string; icon_name: string; metadata_value: string; - taxon_id: number; send_report: boolean; slug: string; request_type: string; @@ -26,13 +27,16 @@ export const AuthoringContext = () => { const fetcher = useFetcher(); const locationArray = window.location.href.split('/'); const slug = locationArray[locationArray.length - 1]; + const defaultDateValue = dayjs(new Date(1970, 0, 1)); const engagementUpdateForm = useForm({ defaultValues: { id: 0, - name: '', - start_date: undefined, - end_date: undefined, status_id: 0, + taxon_id: 0, + content_id: 0, + name: '', + start_date: defaultDateValue, + end_date: defaultDateValue, description: '', rich_description: '', banner_filename: '', @@ -40,22 +44,25 @@ export const AuthoringContext = () => { title: '', icon_name: '', metadata_value: '', - taxon_id: 0, - send_report: false, + send_report: undefined, slug: '', request_type: '', }, mode: 'onSubmit', reValidateMode: 'onChange', }); - const onSubmit = async (data: EngagementUpdateData) => { fetcher.submit( createSearchParams({ - id: data.id.toString(), + id: 0 === data.id ? '' : data.id.toString(), + status_id: 0 === data.status_id ? '' : data.status_id.toString(), + taxon_id: 0 === data.taxon_id ? '' : data.taxon_id.toString(), + content_id: 0 === data.content_id ? '' : data.content_id.toString(), name: data.name, - start_date: data.start_date.format('YYYY-MM-DD'), - end_date: data.end_date.format('YYYY-MM-DD'), + start_date: + '1970-01-01' === data.start_date.format('YYYY-MM-DD') ? '' : data.start_date.format('YYYY-MM-DD'), + end_date: + '1970-01-01' === data.start_date.format('YYYY-MM-DD') ? '' : data.end_date.format('YYYY-MM-DD'), description: data.description, rich_description: data.rich_description, banner_filename: data.banner_filename, @@ -63,18 +70,24 @@ export const AuthoringContext = () => { title: data.title, icon_name: data.icon_name, metadata_value: data.metadata_value, - taxon_id: data.taxon_id.toString(), - send_report: data.send_report ? 'true' : 'false', + send_report: getSendReportValue(data.send_report), slug: data.slug, request_type: data.request_type, }), { method: 'post', - action: `/engagements/${data.id}/authoring/${slug}`, + action: `/engagements/${data.id}/details/authoring/${slug}`, }, ); }; + const getSendReportValue = (valueToInterpret: boolean) => { + if (undefined === valueToInterpret) { + return ''; + } + return valueToInterpret ? 'true' : 'false'; + }; + return ( diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringDetails.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringDetails.tsx new file mode 100644 index 000000000..f3194e292 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringDetails.tsx @@ -0,0 +1,461 @@ +import { Grid, IconButton, MenuItem, Select, SelectChangeEvent, Tab, Tabs } from '@mui/material'; +import { colors, MetHeader3, MetLabel } from 'components/common'; +import { EyebrowText } from 'components/common/Typography'; +import { Button, TextField } from 'components/common/Input'; +import React, { SyntheticEvent, useState } from 'react'; +import { RichTextArea } from 'components/common/Input/RichTextArea'; +import { AuthoringTemplateOutletContext, DetailsTabProps, TabValues } from './types'; +import { useOutletContext } from 'react-router-dom'; +import { Palette } from 'styles/Theme'; +import { Unless, When } from 'react-if'; +import { TabContext, TabPanel } from '@mui/lab'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faX } from '@fortawesome/pro-regular-svg-icons'; +import { EditorState } from 'draft-js'; + +const handleDuplicateTabNames = (newTabs: TabValues[], newTabName: string) => { + // Will add a sequencial number suffix for up to 10 numbers if there is a duplicate, then add (copy) if none of those are available. + for (let i = 2; i < 12; i++) { + if (!newTabs.find((tab) => tab.heading === `${newTabName} (${i})`)) { + return `${newTabName} (${i})`; + } + } + return `${newTabName} (copy)`; +}; + +const AuthoringDetails = () => { + const { + setValue, + watch, // Optional form control prop + control, // Optional form control prop + engagement, // Optional form control prop + contentTabsEnabled, + tabs, + setTabs, + setSingleContentValues, + setContentTabsEnabled, + singleContentValues, + defaultTabValues, + }: AuthoringTemplateOutletContext = useOutletContext(); // Access the form functions and values from the authoring template + const [currentTab, setCurrentTab] = useState(tabs[0]); + + const tabsStyles = { + borderBottom: `2px solid ${colors.surface.gray[60]}`, + overflow: 'hidden', + '& .MuiTabs-flexContainer': { + justifyContent: 'flex-start', + width: 'max-content', + }, + }; + const tabStyles = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '48px', + padding: '4px 18px 2px 18px', + fontSize: '14px', + borderRadius: '0px 16px 0px 0px', + border: `1px solid ${colors.surface.gray[60]}`, + borderBottom: 'none', + boxShadow: + '0px 2px 5px 0px rgba(0, 0, 0, 0.12), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.20)', + backgroundColor: 'gray.10', + color: 'text.secondary', + fontWeight: 'normal', + '&.Mui-selected': { + backgroundColor: 'primary.main', + borderColor: 'primary.main', + color: 'white', + fontWeight: 'bold', + }, + outlineOffset: '-4px', + '&:focus-visible': { + outline: `2px solid`, + outlineColor: '#12508F', + border: '4px solid', + borderColor: '#12508F', + padding: '0px 20px 0px 14px', + }, + }; + + const handleCloseTab = (e: React.MouseEvent, tab: TabValues) => { + e.stopPropagation(); + const index = tabs.findIndex((t) => t.heading === tab.heading); + if (-1 < index) { + const newTabs = [...tabs]; + newTabs.splice(index, 1); + if (1 === newTabs.length) { + // If we're switching back to single content mode + 'Tab 1' !== tabs[0].heading + ? setSingleContentValues(tabs[0]) + : setSingleContentValues({ ...tabs[0], heading: '' }); // If the current Section Heading is "Tab 1" then change it to a blank value. + setTabs([tabs[0]]); + setContentTabsEnabled('false'); + } else { + setTabs(newTabs); + tab === currentTab && setCurrentTab(newTabs[index - 1]); // Switch tabs if you're closing the current one + } + } + }; + + const handleAddTab = () => { + const newTabs = [...tabs]; + const newTabName = 'Tab ' + (newTabs.length + 1); + newTabs.find((tab) => newTabName === tab.heading) + ? newTabs.push({ ...defaultTabValues, heading: handleDuplicateTabNames(newTabs, newTabName) }) + : newTabs.push({ ...defaultTabValues, heading: newTabName }); // Don't create duplicate entries + setTabs(newTabs); + setCurrentTab(newTabs[newTabs.length - 1]); + }; + + return ( + + + + tab.heading === currentTab.heading) + ? tabs[tabs.findIndex((t) => t.heading === currentTab.heading)].heading + : tabs[tabs.length - 1].heading + } + > + tab.heading === currentTab.heading) + ? tabs[tabs.findIndex((t) => t.heading === currentTab.heading)].heading + : tabs[tabs.length - 1].heading + } + onChange={(event: SyntheticEvent, value: string) => + tabs.find((tab) => tab.heading === value) && + setCurrentTab(tabs[tabs.findIndex((tab) => tab.heading === value)]) + } + > + {tabs.map((tab, key) => { + return ( + + {tab.heading} + + handleCloseTab(e, tab)} + > + + + + + } + key={key} + value={tab.heading} + disableFocusRipple + /> + ); + })} + + + {tabs.map((tab, key) => { + return ( + + + + ); + })} + + + + + + + + ); +}; + +export default AuthoringDetails; + +const DetailsTab = ({ + setValue, + setTabs, + setCurrentTab, + setSingleContentValues, + tabs, + tabIndex, + singleContentValues, + defaultTabValues, +}: DetailsTabProps) => { + // Define the styles + const metHeader3Styles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + }; + const eyebrowTextStyles = { + fontSize: '0.9rem', + marginBottom: '1.5rem', + }; + const formItemContainerStyles = { + padding: '2rem 1.4rem !important', + margin: '1rem 0', + borderRadius: '16px', + }; + const metLabelStyles = { + fontSize: '0.95rem', + }; + const conditionalSelectStyles = { + width: '100%', + backgroundColor: colors.surface.white, + borderRadius: '8px', + boxShadow: '0 0 0 1px #7A7876 inset', + lineHeight: '1.4375em', + height: '48px', + marginTop: '8px', + padding: '0', + }; + const widgetPreviewStyles = { + margin: '2rem 4rem 4rem', + display: 'flex', + minHeight: '18rem', + border: '2px dashed rgb(122, 120, 118)', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '16px', + }; + const buttonStyles = { + height: '2.6rem', + borderRadius: '8px', + border: 'none', + padding: '0 1rem', + minWidth: '8.125rem', + fontSize: '0.9rem', + }; + + const toolbar = { + options: ['inline', 'list', 'link', 'blockType', 'history'], + inline: { + options: ['bold', 'italic', 'underline', 'superscript', 'subscript'], + }, + blockType: { options: ['Normal', 'H2', 'H3', 'Blockquote'] }, + list: { options: ['unordered', 'ordered'] }, + }; + + const handleSectionHeadingChange = (value: string) => { + const newHeading = value; + if (2 > tabs.length && 0 === tabIndex) { + // If there are no tabs + setSingleContentValues({ ...singleContentValues, heading: newHeading }); + setTabs([{ ...defaultTabValues, heading: newHeading }]); + } else { + // If there are tabs + const newTabs = [...tabs]; + newTabs[tabIndex].heading = newTabs.find((tab) => tab.heading === newHeading) + ? handleDuplicateTabNames(newTabs, newHeading) + : newHeading; // If the new name is the same as an existing one, rename it + setSingleContentValues(newTabs[0]); + setTabs([...newTabs]); + setCurrentTab(newTabs[tabIndex]); + } + }; + + const handleWidgetChange = (event: SelectChangeEvent) => { + const newWidget = event.target.value; + const newTabs = [...tabs]; + newTabs[tabIndex].widget = newWidget; + setTabs(newTabs); + }; + + const handleRemoveWidget = () => { + if ('' === tabs[tabIndex].widget) { + return; + } else { + const newTabs = [...tabs]; + newTabs[tabIndex].widget = ''; + setTabs(newTabs); + } + }; + + const handleBodyTextChange = (newEditorState: EditorState) => { + const plainText = newEditorState.getCurrentContent().getPlainText(); + const newTabs = [...tabs]; + newTabs[tabIndex].bodyCopyEditorState = newEditorState; + newTabs[tabIndex].bodyCopyPlainText = plainText; + setTabs(newTabs); + }; + + return ( + + + Primary Content (Required) + + Primary content will display on the left two thirds of the page on large screens and full width on + small screens. (If you add optional supporting content in the section below, on small screens, your + primary content will display first (on top) followed by your supporting content (underneath). + + + + + + + + + + + + + Supporting Content (Optional) + + You may use a widget to add supporting content to your primary content. On large screens this + content will be displayed to the right of your primary content. On small screens this content will + be displayed below your primary content. + + + + + + + + + + {tabs[tabIndex].widget} {tabs[tabIndex].widget && 'Widget'} + + + {/* todo: show a preview of the widget here */} + Widget Preview + + + + + + ); +}; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringFeedback.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringFeedback.tsx new file mode 100644 index 000000000..4d62da0ad --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringFeedback.tsx @@ -0,0 +1,281 @@ +import { Grid, MenuItem, Select, SelectChangeEvent } from '@mui/material'; +import { colors, MetHeader3, MetLabel } from 'components/common'; +import { Button, TextField } from 'components/common/Input'; +import { RichTextArea } from 'components/common/Input/RichTextArea'; +import { EyebrowText } from 'components/common/Typography'; +import { EditorState } from 'draft-js'; +import React, { useState } from 'react'; +import { Palette } from 'styles/Theme'; + +const AuthoringFeedback = () => { + const [sectionHeading, setSectionHeading] = useState(''); + const [bodyText, setBodyText] = useState(''); + const [editorState, setEditorState] = useState(); + const [surveyButtonText, setSurveyButtonText] = useState(''); + const [thirdPartyCtaText, setThirdPartyCtaText] = useState(''); + const [thirdPartyCtaLink, setThirdPartyCtaLink] = useState(''); + const [currentWidget, setCurrentWidget] = useState(''); + + // Define the styles + const metHeader3Styles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + }; + const eyebrowTextStyles = { + fontSize: '0.9rem', + marginBottom: '1.5rem', + }; + const formItemContainerStyles = { + padding: '2rem 1.4rem !important', + margin: '1rem 0', + borderRadius: '16px', + }; + const metLabelStyles = { + fontSize: '0.95rem', + }; + const buttonStyles = { + height: '2.6rem', + borderRadius: '8px', + border: 'none', + padding: '0 1rem', + minWidth: '8.125rem', + fontSize: '0.9rem', + }; + const widgetPreviewStyles = { + margin: '2rem 4rem 4rem', + display: 'flex', + minHeight: '18rem', + border: '2px dashed rgb(122, 120, 118)', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '16px', + }; + const conditionalSelectStyles = { + width: '100%', + backgroundColor: colors.surface.white, + borderRadius: '8px', + boxShadow: '0 0 0 1px #7A7876 inset', + lineHeight: '1.4375em', + height: '48px', + marginTop: '8px', + padding: '0', + }; + const toolbar = { + options: ['inline', 'list', 'link', 'blockType', 'history'], + inline: { + options: ['bold', 'italic', 'underline', 'superscript', 'subscript'], + }, + blockType: { options: ['Normal', 'H2', 'H3', 'Blockquote'] }, + list: { options: ['unordered', 'ordered'] }, + }; + + const handleEditorChange = (newEditorState: EditorState) => { + const plainText = newEditorState.getCurrentContent().getPlainText(); + setEditorState(newEditorState); + setBodyText(plainText); + }; + + const handleWidgetChange = (event: SelectChangeEvent) => { + setCurrentWidget(event.target.value); + }; + + const handleRemoveWidget = () => { + if ('' === currentWidget) { + return; + } else { + setCurrentWidget(''); + } + }; + + return ( + + + Primary Content (Required) + + This section of content should provide a brief overview of what your engagement is about and what + you would like your audience to do. + + + + + + + + + + + + + + + + + + + + + + Supporting Content (Optional) + + You may use a widget to add supporting content to your primary content. On large screens this + content will be displayed to the right of your primary content. On small screens this content will + be displayed below your primary content. + + + + + + + + + + {currentWidget} {currentWidget && 'Widget'} + + + {/* todo: show a preview of the widget here */} + Widget Preview + + + + + + ); +}; + +export default AuthoringFeedback; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringMore.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringMore.tsx new file mode 100644 index 000000000..12e713506 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringMore.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const AuthoringMore = () => { + return ( + <> +

Hello world

+ + + ); +}; + +export default AuthoringMore; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringResults.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringResults.tsx new file mode 100644 index 000000000..6a7d282e7 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringResults.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const AuthoringResults = () => { + return ( + <> +

Hello world

+ + + ); +}; + +export default AuthoringResults; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx index 2cb79abde..76fc7fc85 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx @@ -43,8 +43,8 @@ const DrawerBox = ({ isMediumScreenOrLarger, setOpen, engagementId }: DrawerBoxP const permissions = useAppSelector((state) => state.user.roles); const tenant = useAppSelector((state) => state.tenant); - const currentBaseRoute = getRoutes(Number(engagementId), tenant) - .map((route) => route.base) + const currentRoutePath = getRoutes(Number(engagementId), tenant) + .map((route) => route.path) .filter((route) => location.pathname.includes(route)) .reduce((prev, curr) => (prev.length > curr.length ? prev : curr)); @@ -116,7 +116,7 @@ const DrawerBox = ({ isMediumScreenOrLarger, setOpen, engagementId }: DrawerBoxP {route.name} - + { + return ( + <> +

Hello world

+ + + ); +}; + +export default AuthoringSubscribe; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx new file mode 100644 index 000000000..52a0e0384 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx @@ -0,0 +1,207 @@ +import { Grid, MenuItem, Select, SelectChangeEvent } from '@mui/material'; +import React, { useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { TextField } from 'components/common/Input'; +import { AuthoringTemplateOutletContext } from './types'; +import { colors, Palette } from 'styles/Theme'; +import { EyebrowText } from 'components/common/Typography'; +import { MetLabel, MetHeader3 } from 'components/common'; +import { Button } from 'components/common/Input'; +import { RichTextArea } from 'components/common/Input/RichTextArea'; +import { EditorState } from 'draft-js'; + +const AuthoringBanner = () => { + const { + setValue, // Optional form control prop + watch, // Optional form control prop + control, // Optional form control prop + engagement, // Engagement for populating values + }: AuthoringTemplateOutletContext = useOutletContext(); // Access the form functions and values from the authoring template + + const [editorState, setEditorState] = useState(); + const [bodyText, setBodyText] = useState(''); + const [currentWidget, setCurrentWidget] = useState(''); + + //Define the styles + const metHeader3Styles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + }; + const eyebrowTextStyles = { + fontSize: '0.9rem', + marginBottom: '1.5rem', + }; + const metLabelStyles = { + fontSize: '0.95rem', + }; + const formItemContainerStyles = { + padding: '2rem 1.4rem !important', + margin: '1rem 0', + borderRadius: '16px', + }; + const conditionalSelectStyles = { + width: '100%', + backgroundColor: colors.surface.white, + borderRadius: '8px', + boxShadow: '0 0 0 1px #7A7876 inset', + lineHeight: '1.4375em', + height: '48px', + marginTop: '8px', + padding: '0', + }; + const widgetPreviewStyles = { + margin: '2rem 4rem 4rem', + display: 'flex', + minHeight: '18rem', + border: '2px dashed rgb(122, 120, 118)', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '16px', + }; + const buttonStyles = { + height: '2.6rem', + borderRadius: '8px', + border: 'none', + padding: '0 1rem', + minWidth: '8.125rem', + fontSize: '0.9rem', + }; + + const toolbar = { + options: ['inline', 'list', 'link', 'blockType', 'history'], + inline: { + options: ['bold', 'italic', 'underline', 'superscript', 'subscript'], + }, + blockType: { options: ['Normal', 'H2', 'H3', 'Blockquote'] }, + list: { options: ['unordered', 'ordered'] }, + }; + + const handleWidgetSelectChange = (event: SelectChangeEvent) => { + const newWidgetValue = event.target.value; + setCurrentWidget(newWidgetValue); + }; + + const handleEditorChange = (newEditorState: EditorState) => { + const plainText = newEditorState.getCurrentContent().getPlainText(); + setEditorState(newEditorState); + setBodyText(plainText); + }; + + return ( + + + + + + + + + + + Supporting Content (Optional) + + You may use a widget to add supporting content to your primary content. + + + + + + + + + + {currentWidget} {currentWidget && 'Widget'} + + + {/* todo: show a preview of the widget here */} + Widget Preview + + + + + + ); +}; + +export default AuthoringBanner; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx new file mode 100644 index 000000000..921694d22 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx @@ -0,0 +1,192 @@ +import React, { Suspense, useMemo, useState } from 'react'; +import { useOutletContext, Form, useParams, useRouteLoaderData, Await, Outlet } 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'; +import { SystemMessage } from 'components/common/Layout/SystemMessage'; +import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; + +export const StatusLabel = ({ text, completed }: StatusLabelProps) => { + const statusLabelStyle = { + background: true === completed ? '#42814A' : '#CE3E39', + padding: '0.2rem 0.75rem', + color: '#ffffff', + borderRadius: '3px', + fontSize: '0.8rem', + }; + return {text}; +}; + +export const getLanguageValue = (currentLanguage: string, languages: Language[]) => { + return languages.find((language) => language.code === currentLanguage)?.name; +}; + +const AuthoringTemplate = () => { + const { onSubmit }: 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 [currentLanguage, setCurrentLanguage] = useState(useAppSelector((state) => state.language.id)); + const [contentTabsEnabled, setContentTabsEnabled] = useState('false'); // todo: replace default value with stored value in engagement. + const defaultTabValues = { + heading: 'Tab 1', + bodyCopyPlainText: '', + bodyCopyEditorState: getEditorStateFromRaw(''), + widget: '', + }; + 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); + const location = window.location.href; + const locationArray = location.split('/'); + const slug = locationArray[locationArray.length - 1]; + const pageTitle = authoringRoutes.find((route) => { + const pathArray = route.path.split('/'); + const pathSlug = pathArray[pathArray.length - 1]; + return pathSlug === slug; + })?.name; + const { + handleSubmit, + setValue, + watch, + control, + formState: { isDirty, isValid, isSubmitting }, + } = useFormContext(); + + // Set hidden values for form data + setValue('id', Number(engagementId)); + + const eyebrowTextStyles = { + fontSize: '0.9rem', + marginBottom: '1rem', + }; + + // If switching from single to multiple, add "tab 2". If switching from multiple to single, remove all values but index 0. + const handleTabsRadio = (event: React.ChangeEvent) => { + const newTabsRadioValue = event.target.value; + if ('true' === newTabsRadioValue) { + setTabs([ + { ...tabs[0], heading: singleContentValues.heading ? singleContentValues.heading : 'Tab 1' }, + { ...defaultTabValues, heading: 'Tab 2' }, + ]); + setContentTabsEnabled('true'); + } else { + setSingleContentValues({ ...tabs[0], heading: 'Tab 1' !== tabs[0].heading ? tabs[0].heading : '' }); + setTabs([tabs[0]]); + setContentTabsEnabled('false'); + } + }; + + return ( + + + + + {(engagement: Engagement) => ( +
+ + {/* todo: For the section status label when it's ready */} + {/* */} +
+ )} +
+
+

{pageTitle}

+ + Under construction - the settings in this section have no effect. + + {'details' === slug && ( + + + Content Configuration + + + In the Details Section of your engagement, you have the option to display your content in a + normal, static page section view (No Tabs), or for lengthy content, use Tabs. You may wish to + use tabs if your content is quite lengthy so you can organize it into smaller, more digestible + chunks and reduce the length of your engagement page. + + + + } label="No Tabs" /> + + + } label="Tabs (2 Minimum)" /> + + + + )} + + + + {(languages: Language[]) => getLanguageValue(currentLanguage, languages) + ' Content'} + + + + +
+ + + {(engagement: Engagement) => ( + + )} + + + + + + + {(languages: Language[]) => ( + + )} + + + +
+ ); +}; + +export default AuthoringTemplate; diff --git a/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx b/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx index 3630a01ff..d8324e210 100644 --- a/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx @@ -12,10 +12,10 @@ export const engagementAuthoringUpdateAction: ActionFunction = async ({ request const errors = []; const requestType = formData.get('request_type') as string; const engagement = await patchEngagement({ - id: formData.get('id') as unknown as number, + id: Number(formData.get('id')) as unknown as number, name: (formData.get('name') as string) || undefined, start_date: (formData.get('start_date') as string) || undefined, - status_id: (formData.get('status_id') as unknown as number) || undefined, + status_id: (Number(formData.get('status_id')) as unknown as number) || undefined, end_date: (formData.get('end_date') as string) || undefined, description: (formData.get('description') as string) || undefined, rich_description: (formData.get('rich_description') as string) || undefined, @@ -24,20 +24,22 @@ export const engagementAuthoringUpdateAction: ActionFunction = async ({ request }); // Update engagement content. - try { - await patchEngagementContent(engagement.id, formData.get('content_id') as unknown as number, { - title: formData.get('title') as string, - icon_name: formData.get('icon_name') as string, - }); - } catch (e) { - console.error('Error updating engagement', e); - errors.push(e); + if ((formData.get('title') || formData.get('icon_name')) && '0' !== formData.get('content_id')) { + try { + await patchEngagementContent(engagement.id, Number(formData.get('content_id')) as unknown as number, { + title: formData.get('title') as string, + icon_name: formData.get('icon_name') as string, + }); + } catch (e) { + console.error('Error updating engagement', e); + errors.push(e); + } } // Update engagement summary if necessary. if (formData.get('content_id') && engagement.content && engagement.rich_content) { try { - await patchSummaryContent(formData.get('content_id') as unknown as number, { + await patchSummaryContent(Number(formData.get('content_id')) as unknown as number, { content: engagement.content, rich_content: engagement.rich_content, }); @@ -52,7 +54,7 @@ export const engagementAuthoringUpdateAction: ActionFunction = async ({ request try { await patchEngagementMetadata({ value: formData.get('metadata_value') as string, - taxon_id: formData.get('taxon_id') as unknown as number, + taxon_id: Number(formData.get('taxon_id')) as unknown as number, engagement_id: engagement.id, }); } catch (e) { @@ -66,7 +68,7 @@ export const engagementAuthoringUpdateAction: ActionFunction = async ({ request try { await patchEngagementSettings({ engagement_id: engagement.id, - send_report: formData.get('send_report') as unknown as boolean, + send_report: 'true' === formData.get('send_report') ? true : false, }); } catch (e) { console.error('Error updating engagement settings', e); diff --git a/met-web/src/components/engagement/admin/create/authoring/types.ts b/met-web/src/components/engagement/admin/create/authoring/types.ts index d90d0f457..810fc667b 100644 --- a/met-web/src/components/engagement/admin/create/authoring/types.ts +++ b/met-web/src/components/engagement/admin/create/authoring/types.ts @@ -1,6 +1,9 @@ -import { SubmitHandler } from 'react-hook-form'; +import { Control, SubmitHandler, UseFormSetValue, UseFormWatch } from 'react-hook-form'; import { EngagementUpdateData } from './AuthoringContext'; import { Dispatch, SetStateAction } from 'react'; +import { Language } from 'models/language'; +import { Engagement } from 'models/engagement'; +import { EditorState } from 'draft-js'; export interface AuthoringNavProps { open: boolean; @@ -22,10 +25,54 @@ export interface AuthoringContextType { export interface LanguageSelectorProps { currentLanguage: string; setCurrentLanguage: Dispatch>; + languages: Language[]; + isDirty: boolean; + isSubmitting: boolean; } export interface AuthoringBottomNavProps { isDirty: boolean; isValid: boolean; isSubmitting: boolean; + currentLanguage: string; + setCurrentLanguage: Dispatch>; + languages: Language[]; + pageTitle: string; +} + +export interface StatusLabelProps { + text: string; + completed: boolean; +} + +export interface AuthoringTemplateOutletContext { + setValue: UseFormSetValue; + watch: UseFormWatch; + control: Control; + engagement: Engagement; + contentTabsEnabled: string; + tabs: TabValues[]; + setTabs: Dispatch>; + singleContentValues: TabValues; + setSingleContentValues: Dispatch>; + setContentTabsEnabled: Dispatch>; + defaultTabValues: TabValues; +} + +export interface DetailsTabProps { + setValue: UseFormSetValue; + setTabs: Dispatch>; + setCurrentTab: Dispatch>; + setSingleContentValues: Dispatch>; + tabs: TabValues[]; + tabIndex: number; + singleContentValues: TabValues; + defaultTabValues: TabValues; +} + +export interface TabValues { + heading: string; + bodyCopyPlainText: string; + bodyCopyEditorState: EditorState; + widget: string; } diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index a4fa5e303..636615435 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -45,6 +45,13 @@ import { AuthoringTab } from 'components/engagement/admin/view/AuthoringTab'; import AuthoringBanner from 'components/engagement/admin/create/authoring/AuthoringBanner'; import { engagementAuthoringUpdateAction } from 'components/engagement/admin/create/authoring/engagementAuthoringUpdateAction'; import { AuthoringContext } from 'components/engagement/admin/create/authoring/AuthoringContext'; +import AuthoringTemplate from 'components/engagement/admin/create/authoring/AuthoringTemplate'; +import AuthoringSummary from 'components/engagement/admin/create/authoring/AuthoringSummary'; +import AuthoringDetails from 'components/engagement/admin/create/authoring/AuthoringDetails'; +import AuthoringFeedback from 'components/engagement/admin/create/authoring/AuthoringFeedback'; +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'; const AuthenticatedRoutes = () => { return ( @@ -116,15 +123,84 @@ const AuthenticatedRoutes = () => { } /> }> - }> - } action={engagementAuthoringUpdateAction}> - } /> - {/* } /> - } /> - } /> - } /> - } /> - } /> */} + ({ name: 'Authoring' }) }} + element={} + > + }> + } action={engagementAuthoringUpdateAction}> + } + handle={{ + crumb: () => ({ + link: `banner`, + name: 'Hero Banner', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `summary`, + name: 'Summary', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `details`, + name: 'Details', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `feedback`, + name: 'Provide Feedback', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `results`, + name: 'View Results', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `subscribe`, + name: 'Subscribe', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `more`, + name: 'More Engagements', + }), + }} + /> + } /> From 66191a91949374362ccfdc181f15244952e48f79 Mon Sep 17 00:00:00 2001 From: Jareth Whitney Date: Wed, 4 Sep 2024 14:11:50 -0700 Subject: [PATCH 7/9] feature/deseng668: Revised for accessibility, made some merge fixes. --- .../create/authoring/AuthoringBanner.tsx | 75 ++++++++++--------- .../create/authoring/AuthoringBottomNav.tsx | 5 +- .../create/authoring/AuthoringDetails.tsx | 37 +++++---- .../create/authoring/AuthoringFeedback.tsx | 59 ++++++++------- .../create/authoring/AuthoringSummary.tsx | 35 +++++---- .../engagementAuthoringUpdateAction.tsx | 24 ++---- .../admin/view/AuthoringTabElements.tsx | 14 ++-- met-web/src/routes/AuthenticatedRoutes.tsx | 1 + 8 files changed, 135 insertions(+), 115 deletions(-) diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx index f129a9da9..e94300817 100644 --- a/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx @@ -4,8 +4,8 @@ import { useOutletContext } from 'react-router-dom'; import { TextField } from 'components/common/Input'; import { AuthoringTemplateOutletContext } from './types'; import { colors } from 'styles/Theme'; -import { EyebrowText } from 'components/common/Typography'; -import { MetLabel, MetHeader3 } from 'components/common'; +import { EyebrowText as FormDescriptionText } from 'components/common/Typography'; +import { MetLabel, MetHeader3, MetLabel as MetBigLabel } from 'components/common'; import ImageUpload from 'components/imageUpload'; const ENGAGEMENT_UPLOADER_HEIGHT = '360px'; @@ -23,11 +23,18 @@ const AuthoringBanner = () => { const [viewResultsSectionSelectEnabled, setViewResultsSectionSelectEnabled] = useState(true); //Define the styles + const metBigLabelStyles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + lineHeight: 1.167, + color: '#292929', + fontWeight: '700', + }; const metHeader3Styles = { fontSize: '1.05rem', marginBottom: '0.7rem', }; - const eyebrowTextStyles = { + const formDescriptionTextStyles = { fontSize: '0.9rem', marginBottom: '1.5rem', }; @@ -77,13 +84,13 @@ const AuthoringBanner = () => {