From 009f768820024fa09e99e89ef21e0a195d36c3e5 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 15 Aug 2024 17:06:54 -0700 Subject: [PATCH 1/8] DESENG-667: Add engagement config summary section to admin view --- CHANGELOG.MD | 9 + .../common/Communication/StatusIcon.tsx | 10 +- .../common/Indicators/StatusChip.tsx | 10 +- .../common/Layout/SystemMessage.tsx | 4 +- .../DateRangePickerWithCalculation.tsx | 8 +- .../EngagementCreateAction.tsx} | 2 +- .../admin/config/EngagementUpdateAction.tsx | 65 +++++ .../EngagementVisibilityControl.tsx | 10 +- .../FeedbackMethodSelector.tsx | 4 +- .../LanguageLoader.tsx} | 0 .../{create => config}/LanguageManager.tsx | 84 +++--- .../admin/{create => config}/MultiSelect.tsx | 0 .../admin/{create => config}/UserManager.tsx | 3 +- .../admin/config/wizard/ConfigWizard.tsx | 147 +++++++++++ .../wizard/CreationWizard.tsx} | 16 +- .../{create/form => config/wizard}/index.tsx | 25 +- .../engagement/admin/view/ConfigSummary.tsx | 243 ++++++++++++++++++ .../engagement/admin/view/StatusChip.tsx | 36 +++ .../engagement/admin/view/index.tsx | 87 ++++++- .../public/view/EngagementLoader.tsx | 45 +++- .../engagement/public/view/index.tsx | 1 + met-web/src/routes/AuthenticatedRoutes.tsx | 47 ++-- .../src/services/engagementService/types.ts | 1 + met-web/src/styles/Theme.ts | 32 +-- 24 files changed, 755 insertions(+), 134 deletions(-) rename met-web/src/components/engagement/admin/{create => config}/DateRangePickerWithCalculation.tsx (98%) rename met-web/src/components/engagement/admin/{create/engagementCreateAction.tsx => config/EngagementCreateAction.tsx} (95%) create mode 100644 met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx rename met-web/src/components/engagement/admin/{create => config}/EngagementVisibilityControl.tsx (97%) rename met-web/src/components/engagement/admin/{create => config}/FeedbackMethodSelector.tsx (92%) rename met-web/src/components/engagement/admin/{create/languageLoader.tsx => config/LanguageLoader.tsx} (100%) rename met-web/src/components/engagement/admin/{create => config}/LanguageManager.tsx (70%) rename met-web/src/components/engagement/admin/{create => config}/MultiSelect.tsx (100%) rename met-web/src/components/engagement/admin/{create => config}/UserManager.tsx (97%) create mode 100644 met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx rename met-web/src/components/engagement/admin/{create/index.tsx => config/wizard/CreationWizard.tsx} (69%) rename met-web/src/components/engagement/admin/{create/form => config/wizard}/index.tsx (85%) create mode 100644 met-web/src/components/engagement/admin/view/ConfigSummary.tsx create mode 100644 met-web/src/components/engagement/admin/view/StatusChip.tsx diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 6f79d05a4..79048db4f 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,12 @@ +## August 15, 2024 + +- **Feature** Add engagement configuration summary [🎟️ DESENG-667](https://citz-gdx.atlassian.net/browse/DESENG-667) + - Added a tabbed layout to the new engagement view page + - Added a new "Configuration" tab to display the engagement's configuration details + - The other tabs are blank for now, but will be filled in future tickets + - The configuration tab allows navigating back to the engagement configuration page + - Improvements to how engagements and related resources are fetched and saved + ## August 8, 2024 - **Feature** New engagement details page [🎟️ DESENG-666](https://citz-gdx.atlassian.net/browse/DESENG-666) diff --git a/met-web/src/components/common/Communication/StatusIcon.tsx b/met-web/src/components/common/Communication/StatusIcon.tsx index 1a3c5f343..68fe3d522 100644 --- a/met-web/src/components/common/Communication/StatusIcon.tsx +++ b/met-web/src/components/common/Communication/StatusIcon.tsx @@ -26,24 +26,24 @@ type IconWeight = 'solid' | 'regular' | 'light'; export const StatusIcon = ({ status, color, - weight = 'solid', + weight = 'regular', ...props }: { - status: 'success' | 'warning' | 'error' | 'info'; + status: 'success' | 'warning' | 'danger' | 'info'; color?: string; weight?: IconWeight; } & Partial) => { let iconMap = { success: faCheckCircle, warning: faExclamationTriangle, - error: faExclamationCircle, + danger: faExclamationCircle, info: faInfoCircle, }; if (weight === 'regular') { iconMap = { success: faCheckCircleRegular, warning: faExclamationTriangleRegular, - error: faExclamationCircleRegular, + danger: faExclamationCircleRegular, info: faInfoCircleRegular, }; } @@ -51,7 +51,7 @@ export const StatusIcon = ({ iconMap = { success: faCheckCircleLight, warning: faExclamationTriangleLight, - error: faExclamationCircleLight, + danger: faExclamationCircleLight, info: faInfoCircleLight, }; } diff --git a/met-web/src/components/common/Indicators/StatusChip.tsx b/met-web/src/components/common/Indicators/StatusChip.tsx index 3ae08476c..3ae31ff15 100644 --- a/met-web/src/components/common/Indicators/StatusChip.tsx +++ b/met-web/src/components/common/Indicators/StatusChip.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Chip as MuiChip, Skeleton, useTheme } from '@mui/material'; +import { ChipProps as MuiChipProps, Chip as MuiChip, Skeleton, useTheme } from '@mui/material'; import { colors } from '..'; import { SubmissionStatus } from 'constants/engagementStatus'; @@ -23,12 +23,17 @@ export const getStatusFromStatusId = (statusId: SubmissionStatus): StatusText => } }; -export const EngagementStatusChip: React.FC = ({ label: customLabel, statusId: status }) => { +export const EngagementStatusChip: React.FC> = ({ + label: customLabel, + statusId: status, + ...props +}) => { const statusText = getStatusFromStatusId(status); const theme = useTheme(); const invert = theme.palette.mode === 'dark'; return ( = ({ label: customLabel, borderColor: colors.surface.gray[100], color: colors.surface.gray[40], }, + ...props.sx, }} /> ); diff --git a/met-web/src/components/common/Layout/SystemMessage.tsx b/met-web/src/components/common/Layout/SystemMessage.tsx index 85bea6435..6b33cf220 100644 --- a/met-web/src/components/common/Layout/SystemMessage.tsx +++ b/met-web/src/components/common/Layout/SystemMessage.tsx @@ -14,7 +14,7 @@ export const SystemMessage = ({ children, ...props }: { - status: 'success' | 'warning' | 'error' | 'info'; + status: 'success' | 'warning' | 'danger' | 'info'; onDismiss?: () => void; color?: string; coloredBackground?: boolean; @@ -30,7 +30,7 @@ export const SystemMessage = ({ maxWidth: { xs: '100%', md: '700px' }, borderRadius: '8px', backgroundColor: coloredBackground ? colors.notification[status].tint : 'transparent', - color: 'type.primary', + color: 'text.primary', padding: '0.8rem 1rem', paddingLeft: { xs: '0.5rem', md: '1rem' }, border: `1px solid ${colors.notification[status].shade}`, diff --git a/met-web/src/components/engagement/admin/create/DateRangePickerWithCalculation.tsx b/met-web/src/components/engagement/admin/config/DateRangePickerWithCalculation.tsx similarity index 98% rename from met-web/src/components/engagement/admin/create/DateRangePickerWithCalculation.tsx rename to met-web/src/components/engagement/admin/config/DateRangePickerWithCalculation.tsx index 83e617355..104caea8f 100644 --- a/met-web/src/components/engagement/admin/create/DateRangePickerWithCalculation.tsx +++ b/met-web/src/components/engagement/admin/config/DateRangePickerWithCalculation.tsx @@ -44,12 +44,16 @@ export const DateRangePickerWithCalculation = () => { if (name === 'end_date') { trigger('end_date'); } - if (!value?.end_date) return; - setNumberOfDays(value.end_date.clone().add(1, 'second').diff(value.start_date, 'days')); }); return () => subscription.unsubscribe(); }, [watch]); + useEffect(() => { + if (startDate && endDate) { + setNumberOfDays(endDate.clone().add(1, 'second').diff(startDate, 'days')); + } + }, [startDate, endDate]); + const getDayStyle = (props: PickersDayProps) => { const standardStyle = { margin: 0, diff --git a/met-web/src/components/engagement/admin/create/engagementCreateAction.tsx b/met-web/src/components/engagement/admin/config/EngagementCreateAction.tsx similarity index 95% rename from met-web/src/components/engagement/admin/create/engagementCreateAction.tsx rename to met-web/src/components/engagement/admin/config/EngagementCreateAction.tsx index 8a122c7c8..bfc8a3f05 100644 --- a/met-web/src/components/engagement/admin/create/engagementCreateAction.tsx +++ b/met-web/src/components/engagement/admin/config/EngagementCreateAction.tsx @@ -26,7 +26,7 @@ export const engagementCreateAction: ActionFunction = async ({ request }) => { formData.getAll('users').forEach((user_id) => { addTeamMemberToEngagement({ user_id: user_id.toString(), engagement_id: engagement.id }); }); - return redirect(`/engagements/${engagement.id}/form`); + return redirect(`/engagements/${engagement.id}/view`); }; export default engagementCreateAction; diff --git a/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx b/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx new file mode 100644 index 000000000..a560ad13b --- /dev/null +++ b/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx @@ -0,0 +1,65 @@ +import { ENGAGEMENT_MEMBERSHIP_STATUS } from 'models/engagementTeamMember'; +import { ActionFunction, redirect } from 'react-router-dom'; +import { patchEngagement } from 'services/engagementService'; +import { patchEngagementSlug } from 'services/engagementSlugService'; +import { + addTeamMemberToEngagement, + revokeMembership, + reinstateMembership, + getTeamMembers, +} from 'services/membershipService'; + +export const engagementUpdateAction: ActionFunction = async ({ request, params }) => { + const formData = (await request.formData()) as FormData; + const engagementId = Number(params.engagementId); + await patchEngagement({ + id: engagementId, + name: formData.get('name') as string, + start_date: formData.get('start_date') as string, + end_date: formData.get('end_date') as string, + is_internal: formData.get('is_internal') === 'true', + }); + try { + await patchEngagementSlug({ + engagement_id: engagementId, + slug: formData.get('slug') as string, + }); + } catch (e) { + console.error('Error updating engagement slug', e); + } + + const currentTeamMembers = await getTeamMembers({ engagement_id: engagementId }); + const users = formData.getAll('users') as string[]; + const usersSet = new Set(users); + + try { + // Process deactivated users for reinstatement (and active users for revocation) + // Caution - headaches ahead! There is a big difference between user_id and user.external_id + for (const member of currentTeamMembers) { + const isUserInForm = usersSet.has(String(member.user.external_id)); + if (member.status !== ENGAGEMENT_MEMBERSHIP_STATUS.Active) { + if (isUserInForm) { + // If the user was previously deactivated, reinstate them + reinstateMembership(engagementId, member.user_id); + } + } else { + if (!isUserInForm) { + // If the user was previously active but is not in the form, revoke their membership + revokeMembership(engagementId, member.user_id); + } + } + // Remove all known users from the set so we can add new members in the next step + usersSet.delete(String(member.user.external_id)); + } + // Add new members that weren't in the current team members list + for (const user of usersSet) { + addTeamMemberToEngagement({ user_id: user, engagement_id: engagementId }); + } + } catch (e) { + console.error('Error updating team members', e); + } + + return redirect(`/engagements/${engagementId}/view`); +}; + +export default engagementUpdateAction; diff --git a/met-web/src/components/engagement/admin/create/EngagementVisibilityControl.tsx b/met-web/src/components/engagement/admin/config/EngagementVisibilityControl.tsx similarity index 97% rename from met-web/src/components/engagement/admin/create/EngagementVisibilityControl.tsx rename to met-web/src/components/engagement/admin/config/EngagementVisibilityControl.tsx index 2ab6ddfc4..a5850c250 100644 --- a/met-web/src/components/engagement/admin/create/EngagementVisibilityControl.tsx +++ b/met-web/src/components/engagement/admin/config/EngagementVisibilityControl.tsx @@ -21,11 +21,11 @@ const EngagementVisibilityControl = () => { const isInternal = watch('is_internal'); const formSlug = watch('slug'); const isConfirmed = watch('_visibilityConfirmed'); - const setIsConfirmed = (value: boolean) => setValue('_visibilityConfirmed', value); + const setIsConfirmed = (value: boolean) => setValue('_visibilityConfirmed', value, { shouldDirty: true }); const [isEditing, setIsEditing] = React.useState(false); const [currentSlug, setCurrentSlug] = React.useState(formSlug); - const [hasBeenEdited, setHasBeenEdited] = React.useState(false); + const [hasBeenEdited, setHasBeenEdited] = React.useState(isConfirmed); useEffect(() => { const subscription = watch((value, { name, type }) => { @@ -41,7 +41,7 @@ const EngagementVisibilityControl = () => { }) .join('') .toLowerCase(); - setValue('slug', newSlug); + setValue('slug', newSlug, { shouldDirty: true }); setCurrentSlug(newSlug); } }); @@ -125,7 +125,7 @@ const EngagementVisibilityControl = () => { onClick={() => { setIsConfirmed(true); setHasBeenEdited(true); - setValue('slug', currentSlug); + setValue('slug', currentSlug, { shouldDirty: true }); }} > Confirm @@ -178,7 +178,7 @@ const EngagementVisibilityControl = () => { disabled={!currentSlug} variant="primary" onClick={() => { - setValue('slug', currentSlug); + setValue('slug', currentSlug, { shouldDirty: true }); setHasBeenEdited(true); setIsConfirmed(true); setIsEditing(false); diff --git a/met-web/src/components/engagement/admin/create/FeedbackMethodSelector.tsx b/met-web/src/components/engagement/admin/config/FeedbackMethodSelector.tsx similarity index 92% rename from met-web/src/components/engagement/admin/create/FeedbackMethodSelector.tsx rename to met-web/src/components/engagement/admin/config/FeedbackMethodSelector.tsx index dff2c1415..9d8c3e9b4 100644 --- a/met-web/src/components/engagement/admin/create/FeedbackMethodSelector.tsx +++ b/met-web/src/components/engagement/admin/config/FeedbackMethodSelector.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Grid, Checkbox, FormControlLabel } from '@mui/material'; import { SystemMessage } from 'components/common/Layout/SystemMessage'; -import { EngagementConfigurationData } from './form'; +import { EngagementConfigurationData } from './wizard'; import { useFormContext } from 'react-hook-form'; export const FeedbackMethodSelector = () => { @@ -26,6 +26,7 @@ export const FeedbackMethodSelector = () => { checked ? [...watch('feedback_methods'), 'survey'] : watch('feedback_methods').filter((m) => m !== 'survey'), + { shouldDirty: true }, ); }} /> @@ -40,6 +41,7 @@ export const FeedbackMethodSelector = () => { checked ? [...watch('feedback_methods'), '3rd_party'] : watch('feedback_methods').filter((m) => m !== '3rd_party'), + { shouldDirty: true }, ); }} /> diff --git a/met-web/src/components/engagement/admin/create/languageLoader.tsx b/met-web/src/components/engagement/admin/config/LanguageLoader.tsx similarity index 100% rename from met-web/src/components/engagement/admin/create/languageLoader.tsx rename to met-web/src/components/engagement/admin/config/LanguageLoader.tsx diff --git a/met-web/src/components/engagement/admin/create/LanguageManager.tsx b/met-web/src/components/engagement/admin/config/LanguageManager.tsx similarity index 70% rename from met-web/src/components/engagement/admin/create/LanguageManager.tsx rename to met-web/src/components/engagement/admin/config/LanguageManager.tsx index 6bcc04dde..d334800e3 100644 --- a/met-web/src/components/engagement/admin/create/LanguageManager.tsx +++ b/met-web/src/components/engagement/admin/config/LanguageManager.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { Box, FormControlLabel, Grid, Radio, RadioGroup, TextField } from '@mui/material'; import { textInputStyles } from 'components/common/Input/TextInput'; -import { useAsyncValue } from 'react-router-dom'; +import { useAsyncValue, useFetcher } from 'react-router-dom'; import { BodyText } from 'components/common/Typography'; import { When } from 'react-if'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -13,50 +13,58 @@ import MultiSelect from './MultiSelect'; import { SystemMessage } from 'components/common/Layout/SystemMessage'; export const LanguageManager = () => { + const SINGLE_LANGUAGE = [{ code: 'en', name: 'English' }] as Language[]; + const REQUIRED_LANGUAGES = [ + { code: 'en', name: 'English' }, + { code: 'fr', name: 'French' }, + ] as Language[]; + const requiredLanguageCodes = REQUIRED_LANGUAGES.map((l) => l.code); + const engagementForm = useFormContext(); const { setValue, watch } = engagementForm; const selectedLanguages = watch('languages') as Language[]; - const [isSingleLanguage, setIsSingleLanguage] = React.useState(null); - const requiredLanguages = isSingleLanguage !== false ? ['en'] : ['en', 'fr']; - const availableLanguages = useAsyncValue() as Language[]; - const requiredLanguagesAvailable = requiredLanguages.filter((l) => - availableLanguages.map((l) => l.code).includes(l), - ); + // const [isSingleLanguage, setIsSingleLanguage] = React.useState(null); + const fetcher = useFetcher(); + const fetcherData = fetcher.data as { languages: Language[] } | undefined; + const { languages: availableLanguages } = fetcherData ?? { languages: [] }; const [searchTerm, setSearchTerm] = React.useState(''); - useEffect(() => { - // Don't do anything if language multiplicity has not been indicated - if (isSingleLanguage === null) return; + const determineSingleLanguage = (languages: Language[]) => { + if (languages.length === 0) return null; + if (languages.length === 1) return true; + return false; + }; + const isSingleLanguage = determineSingleLanguage(selectedLanguages); - // If it's english only, remove any other languages - if (isSingleLanguage) { - setValue('languages', [{ code: 'en', name: 'English' }]); - return; - } - // If the required languages are not included, add them - if (requiredLanguagesAvailable.length) { - const languagesToAdd = availableLanguages.filter( - (l) => - requiredLanguagesAvailable.includes(l.code) && - !watch('languages') - .map((l: Language) => l.code) - .includes(l.code), - ); - setValue('languages', [...watch('languages'), ...languagesToAdd]); - } - }, [watch, isSingleLanguage]); + useEffect(() => { + fetcher.load('/languages/'); + }, []); + if (!fetcherData) return null; return ( setIsSingleLanguage(e.target.value === 'true')} + onChange={(e) => { + if (e.target.value === 'single') { + setValue('languages', SINGLE_LANGUAGE, { shouldDirty: true }); + } + if (e.target.value === 'multi') { + const optionalLanguages = selectedLanguages.filter( + (l) => !requiredLanguageCodes.includes(l.code), + ); + setValue('languages', [...REQUIRED_LANGUAGES, ...optionalLanguages], { + shouldDirty: true, + shouldValidate: true, + }); + } + }} aria-label="Select Engagement's Language Type" name="languageType" - value={isSingleLanguage} + value={isSingleLanguage && (isSingleLanguage ? 'single' : 'multi')} > - } label="English Only" /> - } label="Multi-language" /> + } label="English Only" /> + } label="Multi-language" /> @@ -68,11 +76,15 @@ export const LanguageManager = () => { if (reason === 'removeOption' && language) { setValue( 'languages', - selectedLanguages.filter((l) => l.code !== language.code), + selectedLanguages.filter((l) => l.code !== language.code, { + shouldDirty: true, + }), ); } if (reason === 'selectOption' && language) { - setValue('languages', [...selectedLanguages, language]); + setValue('languages', [...selectedLanguages, language], { + shouldDirty: true, + }); } }} options={availableLanguages ?? []} @@ -98,9 +110,9 @@ export const LanguageManager = () => { return ( - + {`${option.name}`} - {requiredLanguagesAvailable.includes(option.code) && ' (Default)'} + {requiredLanguageCodes.includes(option.code) && ' (Default)'} @@ -142,7 +154,7 @@ export const LanguageManager = () => { selectedLabel={{ singular: 'Language Added', plural: 'Languages Added' }} searchPlaceholder="Select Language" getOptionDisabled={(option) => selectedLanguages.filter((l) => l.code === option.code).length > 0} - getOptionRequired={(option) => requiredLanguagesAvailable.includes(option.code)} + getOptionRequired={(option) => requiredLanguageCodes.includes(option.code)} /> diff --git a/met-web/src/components/engagement/admin/create/MultiSelect.tsx b/met-web/src/components/engagement/admin/config/MultiSelect.tsx similarity index 100% rename from met-web/src/components/engagement/admin/create/MultiSelect.tsx rename to met-web/src/components/engagement/admin/config/MultiSelect.tsx diff --git a/met-web/src/components/engagement/admin/create/UserManager.tsx b/met-web/src/components/engagement/admin/config/UserManager.tsx similarity index 97% rename from met-web/src/components/engagement/admin/create/UserManager.tsx rename to met-web/src/components/engagement/admin/config/UserManager.tsx index 53d793a6a..e47b7ed4b 100644 --- a/met-web/src/components/engagement/admin/create/UserManager.tsx +++ b/met-web/src/components/engagement/admin/config/UserManager.tsx @@ -47,7 +47,7 @@ export const UserManager = () => { const handleAddUser = (user: User) => { if (!selectedUsers.filter((u) => u.id === user.id).length) { - setValue('users', [...selectedUsers, user]); + setValue('users', [...selectedUsers, user], { shouldDirty: true }); } }; @@ -55,6 +55,7 @@ export const UserManager = () => { setValue( 'users', selectedUsers.filter((u) => u !== user), + { shouldDirty: true }, ); }; diff --git a/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx b/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx new file mode 100644 index 000000000..3929b4ab1 --- /dev/null +++ b/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx @@ -0,0 +1,147 @@ +import React, { Suspense } from 'react'; +import { ResponsiveContainer } from 'components/common/Layout'; +import { + useFetcher, + createSearchParams, + useRouteLoaderData, + Await, + useAsyncValue, + useNavigation, +} from 'react-router-dom'; +import { FormProvider, useForm } from 'react-hook-form'; +import { AutoBreadcrumbs } from 'components/common/Navigation/Breadcrumb'; +import EngagementForm, { EngagementConfigurationData } from '.'; +import { EngagementLoaderData } from 'components/engagement/public/view'; +import { Engagement } from 'models/engagement'; +import { ENGAGEMENT_MEMBERSHIP_STATUS, EngagementTeamMember } from 'models/engagementTeamMember'; +import { BodyText, Header1, Header2 } from 'components/common/Typography'; +import dayjs from 'dayjs'; +import { Language } from 'models/language'; +import { CircularProgress, Grid, Modal, Skeleton } from '@mui/material'; +import { modalStyle } from 'components/common'; + +const EngagementConfigurationWizard = () => { + const { engagement, teamMembers, slug } = useRouteLoaderData('single-engagement') as EngagementLoaderData; + return ( + + + + Example Engagement + + } + > + + {(resolvedEngagement) => {resolvedEngagement.name}} + + +
+ Edit Configuration}> + + + + +
+ ); +}; + +const ConfigForm = () => { + const [engagement, teamMembers, slug] = useAsyncValue() as [Engagement, EngagementTeamMember[], string]; + const fetcher = useFetcher(); + const navigation = useNavigation(); + + const engagementConfigForm = useForm({ + defaultValues: { + name: engagement.name, + feedback_methods: [], + start_date: dayjs(engagement.start_date), + end_date: dayjs(engagement.end_date), + _dateConfirmed: true, + languages: [{ code: 'en', name: 'English' }] as Language[], + is_internal: engagement.is_internal, + _visibilityConfirmed: true, + slug: slug, + users: teamMembers.filter((tm) => tm.status == ENGAGEMENT_MEMBERSHIP_STATUS.Active).map((tm) => tm.user), + }, + mode: 'onSubmit', + reValidateMode: 'onChange', + }); + + const onSubmit = async (data: EngagementConfigurationData) => { + fetcher.submit( + createSearchParams({ + name: data.name, + feedback_methods: data.feedback_methods, + start_date: data.start_date.format('YYYY-MM-DD'), + end_date: data.end_date.format('YYYY-MM-DD'), + languages: data.languages.map((l) => l.code), + is_internal: data.is_internal ? 'true' : 'false', + slug: data.slug, + users: data.users.map((u) => u.external_id), + }), + { + method: 'patch', + action: `/engagements/${engagement.id}/config/`, + }, + ); + }; + + const { + formState: { isSubmitting, isSubmitted }, + } = engagementConfigForm; + + return ( + + + + + + + + + + + We're just looking over your configuration. + + + + This should only take a few seconds. + + + + + + ); +}; + +export default EngagementConfigurationWizard; diff --git a/met-web/src/components/engagement/admin/create/index.tsx b/met-web/src/components/engagement/admin/config/wizard/CreationWizard.tsx similarity index 69% rename from met-web/src/components/engagement/admin/create/index.tsx rename to met-web/src/components/engagement/admin/config/wizard/CreationWizard.tsx index c84d9a2c2..92af7f1c5 100644 --- a/met-web/src/components/engagement/admin/create/index.tsx +++ b/met-web/src/components/engagement/admin/config/wizard/CreationWizard.tsx @@ -3,7 +3,10 @@ import { ResponsiveContainer } from 'components/common/Layout'; import { useFetcher, createSearchParams } from 'react-router-dom'; import { FormProvider, useForm } from 'react-hook-form'; import { AutoBreadcrumbs } from 'components/common/Navigation/Breadcrumb'; -import EngagementForm, { EngagementConfigurationData } from './form'; +import EngagementForm, { EngagementConfigurationData } from '.'; +import { Header1, Header2 } from 'components/common/Typography'; +import { SystemMessage } from 'components/common/Layout/SystemMessage'; +import { Link } from 'components/common/Navigation'; const EngagementCreationWizard = () => { const fetcher = useFetcher(); @@ -47,6 +50,17 @@ const EngagementCreationWizard = () => { return ( + New Engagement + Create a new engagement in six easy configuration steps. + + You will be able to modify the configuration of your engagement later in the case the parameters of your + engagement change. If you prefer, you can use{' '} + + the old form + + . + +
diff --git a/met-web/src/components/engagement/admin/create/form/index.tsx b/met-web/src/components/engagement/admin/config/wizard/index.tsx similarity index 85% rename from met-web/src/components/engagement/admin/create/form/index.tsx rename to met-web/src/components/engagement/admin/config/wizard/index.tsx index 9c4f4fdd6..e9ccefd4b 100644 --- a/met-web/src/components/engagement/admin/create/form/index.tsx +++ b/met-web/src/components/engagement/admin/config/wizard/index.tsx @@ -13,8 +13,6 @@ import { LanguageManager } from '../LanguageManager'; import { UserManager } from '../UserManager'; import { User } from 'models/user'; import { Language } from 'models/language'; -import { SystemMessage } from 'components/common/Layout/SystemMessage'; -import { Link } from 'components/common/Navigation'; import { FormStep } from 'components/common/Layout/FormStep'; export interface EngagementConfigurationData { @@ -43,8 +41,6 @@ const EngagementForm = ({ onSubmit: (data: EngagementConfigurationData) => void; isNewEngagement?: boolean; }) => { - const { languages } = useLoaderData() as { languages: Language[] }; - const engagementForm = useFormContext(); const { @@ -59,18 +55,7 @@ const EngagementForm = ({ return (
- New Engagement - Create a new engagement in six easy configuration steps. - - You will be able to modify the configuration of your engagement later in the case the parameters of - your engagement change. If you prefer, you can use{' '} - - the old form - - . - -
- Configure Engagement + {isNewEngagement ? 'Configure Engagement' : 'Edit Configuration'}
- }> - - - - + {isNewEngagement ? 'Create Engagement' : 'Save Changes'} - +
diff --git a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx new file mode 100644 index 000000000..46bee0a24 --- /dev/null +++ b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx @@ -0,0 +1,243 @@ +import React, { Suspense, useEffect } from 'react'; +import { Avatar, Box, Grid, IconButton, Skeleton, Tooltip } from '@mui/material'; +import { BodyText, Header2 } from '../../../common/Typography'; +import { OutlineBox } from 'components/common/Layout'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCopy } from '@fortawesome/pro-light-svg-icons'; +import { globalFocusVisible } from 'components/common'; +import { getBaseUrl } from 'helper'; +import { Await, useAsyncValue, useRouteLoaderData } from 'react-router-dom'; +import { Engagement } from 'models/engagement'; +import { EngagementStatusChip } from 'components/common/Indicators'; +import { SubmissionStatus } from 'constants/engagementStatus'; +import dayjs from 'dayjs'; +import { EngagementLoaderData } from 'components/engagement/public/view'; +import { ENGAGEMENT_MEMBERSHIP_STATUS, EngagementTeamMember } from 'models/engagementTeamMember'; +import { Button } from 'components/common/Input'; +import { faPen } from '@fortawesome/pro-regular-svg-icons'; +import { SystemMessage } from 'components/common/Layout/SystemMessage'; + +export const ConfigSummary = () => { + const siteUrl = getBaseUrl(); + const [engagement, teamMembers, slug] = useAsyncValue() as [Engagement, EngagementTeamMember[], string]; + const [tooltipOpen, setTooltipOpen] = React.useState(false); + + useEffect(() => { + if (tooltipOpen) { + const timer = setTimeout(() => { + setTooltipOpen(false); + }, 2000); + return () => { + clearTimeout(timer); + }; + } + }, [tooltipOpen]); + + return ( + + Configuration + + + + + + + Engagement URL + + + + + { + navigator.clipboard.writeText(`${siteUrl}/${slug}`); + setTooltipOpen(true); + }} + > + + + + + {siteUrl}/ + {slug} + + + + + + + + + + + Engagement Feedback Dates + + + + + + span.MuiChip-label': { padding: '4px 12px' } }} + /> + + + + {dayjs(engagement.start_date).format('MMMM D, YYYY')} + {' '} + (12:01 am) + + + + + span.MuiChip-label': { padding: '4px 12px' } }} + /> + + + + {dayjs(engagement.end_date).format('MMMM D, YYYY')} + {' '} + (11:59 pm) + + + + + + + {dayjs(engagement.end_date) + .clone() + .add(1, 'second') + .diff(dayjs(engagement.start_date), 'days')} + + + {' '} + days + + + + + + + + + + + + Language(s) Included (1) + + + + English (Default) + + + + + + + + + + Team Member(s) Added + + + }> + + + + + + + + + + + + + ); +}; + +const TeamMemberList = () => { + const teamMembers = (useAsyncValue() as EngagementTeamMember[]).filter( + (teamMember) => teamMember.status === ENGAGEMENT_MEMBERSHIP_STATUS.Active, + ); + if (!teamMembers.length) { + return ( + + No team members added + + ); + } + return ( + <> + {teamMembers.map((teamMember) => ( + + + + {teamMember.user.first_name[0]} + {teamMember.user.last_name[0]} + + + + + {teamMember.user.first_name} {teamMember.user.last_name} + + {teamMember.user.main_role} + + + ))} + + ); +}; + +const TeamMemberListSkeleton = () => { + return ( + <> + {Array.from({ length: 3 }).map((_, index) => ( + + + + + + + + + ))} + + ); +}; diff --git a/met-web/src/components/engagement/admin/view/StatusChip.tsx b/met-web/src/components/engagement/admin/view/StatusChip.tsx new file mode 100644 index 000000000..e373b7df3 --- /dev/null +++ b/met-web/src/components/engagement/admin/view/StatusChip.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Chip, Box } from '@mui/material'; +import { useAsyncValue } from 'react-router-dom'; +import { Engagement } from 'models/engagement'; +import { EngagementStatus } from 'constants/engagementStatus'; + +export const StatusChip = ({ + status, + children, +}: { + status: 'success' | 'warning' | 'danger' | 'info'; + children: React.ReactNode; +}) => { + return ( + + ); +}; + +export const AutoEngagementStatusChip = () => { + const engagement = useAsyncValue() as Engagement; + const statusName = EngagementStatus[engagement?.status_id]; + let status = 'danger' as 'success' | 'warning' | 'danger' | 'info'; + if (statusName === 'Scheduled') { + status = 'info'; + } else if (statusName === 'Published') { + status = 'success'; + } + return {engagement?.status_id}; +}; diff --git a/met-web/src/components/engagement/admin/view/index.tsx b/met-web/src/components/engagement/admin/view/index.tsx index c8cd3aac7..10f960fb3 100644 --- a/met-web/src/components/engagement/admin/view/index.tsx +++ b/met-web/src/components/engagement/admin/view/index.tsx @@ -1,16 +1,29 @@ -import React, { Suspense } from 'react'; -import { useLoaderData, Await } from 'react-router-dom'; +import React, { Suspense, useState } from 'react'; +import { useRouteLoaderData, Await } from 'react-router-dom'; import { Engagement } from 'models/engagement'; import { AutoBreadcrumbs } from 'components/common/Navigation/Breadcrumb'; import { EngagementStatus } from 'constants/engagementStatus'; -import { Theme, useMediaQuery } from '@mui/material'; +import { Tab } from '@mui/material'; +import { ResponsiveContainer } from 'components/common/Layout'; +import { ConfigSummary } from './ConfigSummary'; +import { TabContext, TabList, TabPanel } from '@mui/lab'; +import { EngagementLoaderData } from 'components/engagement/public/view'; export const AdminEngagementView = () => { - const { engagement } = useLoaderData() as { engagement: Promise }; - const isMediumScreenOrLarger: boolean = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); + const { engagement, teamMembers, slug } = useRouteLoaderData('single-engagement') as EngagementLoaderData; + + const EngagementViewTabs = { + config: 'Configuration', + author: 'Authoring', + activity: 'Activity', + results: 'Results', + publish: 'Publishing', + }; + + const [currentTab, setCurrentTab] = useState(EngagementViewTabs.config); return ( -
+ @@ -32,6 +45,66 @@ export const AdminEngagementView = () => { )} -
+ + setCurrentTab(newValue)} + aria-label="Admin Engagement View Tabs" + TabIndicatorProps={{ sx: { display: 'none' } }} + sx={{ + '& .MuiTabs-flexContainer': { + justifyContent: 'flex-start', + width: 'max-content', + }, + }} + > + {Object.entries(EngagementViewTabs).map(([key, value]) => ( + + ))} + + + + + + + + + +
); }; diff --git a/met-web/src/components/engagement/public/view/EngagementLoader.tsx b/met-web/src/components/engagement/public/view/EngagementLoader.tsx index 05f3a3a46..792fea0d0 100644 --- a/met-web/src/components/engagement/public/view/EngagementLoader.tsx +++ b/met-web/src/components/engagement/public/view/EngagementLoader.tsx @@ -8,19 +8,38 @@ import { getEngagementIdBySlug, getSlugByEngagementId } from 'services/engagemen import { getSummaryContent } from 'services/engagementSummaryService'; import { getWidgets } from 'services/widgetService'; import { getEngagementMetadata, getMetadataTaxa } from 'services/engagementMetadataService'; +import { Engagement, EngagementMetadata } from 'models/engagement'; +import { Widget } from 'models/widget'; +import { EngagementContent } from 'models/engagementContent'; +import { TaxonType } from 'components/metadataManagement/types'; +import { getTeamMembers } from 'services/membershipService'; +import { EngagementTeamMember } from 'models/engagementTeamMember'; + +export type EngagementLoaderData = { + engagement: Promise; + slug: Promise; + widgets: Promise; + content: Promise; + contentSummary: Promise; + metadata: Promise; + taxa: Promise; + customContent: Promise; + teamMembers: Promise; +}; export const engagementLoader = async ({ params }: { params: Params }) => { - let { slug } = params; - const { engagementId } = params; - if (!slug && engagementId) { - const response = await getSlugByEngagementId(Number(engagementId)); - slug = response.slug; - } - const engagement = getEngagementIdBySlug(slug ?? '').then((response) => getEngagement(response.engagement_id)); + const { slug: slugParam, engagementId } = params; + const slug = slugParam + ? Promise.resolve(slugParam) + : getSlugByEngagementId(Number(engagementId)).then((response) => response.slug); + const engagement = slugParam + ? getEngagementIdBySlug(slugParam).then((response) => getEngagement(response.engagement_id)) + : getEngagement(Number(engagementId)); const widgets = engagement.then((response) => getWidgets(Number(response.id))); const content = engagement.then((response) => getEngagementContent(response.id)); const engagementMetadata = engagement.then((response) => getEngagementMetadata(Number(response.id))); const taxaData = getMetadataTaxa(); + const teamMembers = engagement.then((response) => getTeamMembers({ engagement_id: response.id })); const metadata = engagementMetadata.then((metaResponse) => { taxaData.then((taxaResponse) => { @@ -75,5 +94,15 @@ export const engagementLoader = async ({ params }: { params: Params }) = return finishedContent; }; - return defer({ engagement, slug, widgets, content, contentSummary, metadata, taxa, customContent }); + return defer({ + engagement, + slug, + widgets, + content, + contentSummary, + metadata, + taxa, + customContent, + teamMembers, + }); }; diff --git a/met-web/src/components/engagement/public/view/index.tsx b/met-web/src/components/engagement/public/view/index.tsx index f59f39267..812d5bfc9 100644 --- a/met-web/src/components/engagement/public/view/index.tsx +++ b/met-web/src/components/engagement/public/view/index.tsx @@ -20,4 +20,5 @@ export const PublicEngagementView = () => { export default PublicEngagementView; export { engagementLoader } from './EngagementLoader'; +export type { EngagementLoaderData } from './EngagementLoader'; export { engagementListLoader } from './EngagementListLoader'; diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index 8179d4dfc..043b0f2a8 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -34,10 +34,12 @@ import { Engagement } from 'models/engagement'; import { getAllTenants, getTenant } from 'services/tenantService'; import { engagementLoader, engagementListLoader } from 'components/engagement/public/view'; import { SurveyLoader } from 'components/survey/building/SurveyLoader'; -import { languageLoader } from 'components/engagement/admin/create/languageLoader'; +import { languageLoader } from 'components/engagement/admin/config/LanguageLoader'; import { userSearchLoader } from 'components/userManagement/userSearchLoader'; -import EngagementCreationWizard from 'components/engagement/admin/create'; -import engagementCreateAction from 'components/engagement/admin/create/engagementCreateAction'; +import EngagementCreationWizard from 'components/engagement/admin/config/wizard/CreationWizard'; +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'; const AuthenticatedRoutes = () => { return ( @@ -79,7 +81,6 @@ const AuthenticatedRoutes = () => { path="wizard" handle={{ crumb: () => ({ name: 'New Engagement' }) }} element={} - loader={languageLoader} /> { id="single-engagement" errorElement={} loader={engagementLoader} + handle={{ + crumb: async (data: { engagement: Promise }) => { + return data.engagement.then((engagement) => { + return { + link: `/engagements/${engagement.id}/view`, + name: engagement.name, + }; + }); + }, + }} > }> } /> + } + action={engagementUpdateAction} + handle={{ + crumb: () => ({ + name: 'Configure', + }), + }} + /> } /> - }) => { - return data.engagement.then((engagement) => { - return { - link: `/engagements/${engagement.id}/view`, - name: engagement.name, - }; - }); - }, - }} - element={} - /> + } /> } /> } /> @@ -117,7 +124,7 @@ const AuthenticatedRoutes = () => { } /> - } /> + } loader={languageLoader} /> Date: Thu, 15 Aug 2024 17:18:16 -0700 Subject: [PATCH 2/8] Merge cleanup + lint --- .../admin/config/LanguageManager.tsx | 2 +- .../engagement/admin/config/wizard/index.tsx | 8 ++--- .../engagement/admin/view/AuthoringTab.tsx | 4 +-- .../engagement/admin/view/ConfigSummary.tsx | 4 +-- .../engagement/admin/view/StatusChip.tsx | 36 ------------------- 5 files changed, 8 insertions(+), 46 deletions(-) delete mode 100644 met-web/src/components/engagement/admin/view/StatusChip.tsx diff --git a/met-web/src/components/engagement/admin/config/LanguageManager.tsx b/met-web/src/components/engagement/admin/config/LanguageManager.tsx index d334800e3..faa558088 100644 --- a/met-web/src/components/engagement/admin/config/LanguageManager.tsx +++ b/met-web/src/components/engagement/admin/config/LanguageManager.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { Box, FormControlLabel, Grid, Radio, RadioGroup, TextField } from '@mui/material'; import { textInputStyles } from 'components/common/Input/TextInput'; -import { useAsyncValue, useFetcher } from 'react-router-dom'; +import { useFetcher } from 'react-router-dom'; import { BodyText } from 'components/common/Typography'; import { When } from 'react-if'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; diff --git a/met-web/src/components/engagement/admin/config/wizard/index.tsx b/met-web/src/components/engagement/admin/config/wizard/index.tsx index e9ccefd4b..3be10278e 100644 --- a/met-web/src/components/engagement/admin/config/wizard/index.tsx +++ b/met-web/src/components/engagement/admin/config/wizard/index.tsx @@ -1,8 +1,8 @@ -import React, { Suspense, useState } from 'react'; -import { Header1, Header2 } from 'components/common/Typography'; +import React, { useState } from 'react'; +import { Header2 } from 'components/common/Typography'; import { Button, TextField } from 'components/common/Input'; -import { Form, useLoaderData, Await } from 'react-router-dom'; -import { Box, Skeleton } from '@mui/material'; +import { Form } from 'react-router-dom'; +import { Box } from '@mui/material'; import { Dayjs } from 'dayjs'; import { Controller, useFormContext } from 'react-hook-form'; import EngagementVisibilityControl from '../EngagementVisibilityControl'; diff --git a/met-web/src/components/engagement/admin/view/AuthoringTab.tsx b/met-web/src/components/engagement/admin/view/AuthoringTab.tsx index 8f81689fa..4ada78d1c 100644 --- a/met-web/src/components/engagement/admin/view/AuthoringTab.tsx +++ b/met-web/src/components/engagement/admin/view/AuthoringTab.tsx @@ -159,7 +159,7 @@ export const AuthoringTab = () => { Authoring Page Section Authoring - + There are incomplete or missing sections of required content in your engagement. Please complete all required content in all of the languages included in your engagement. @@ -191,7 +191,7 @@ export const AuthoringTab = () => { Feedback Configuration - + There are feedback methods included in your engagement that are incomplete. Please complete configuration for all of the feedback methods included in your engagement. diff --git a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx index 46bee0a24..12031d1bf 100644 --- a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx +++ b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx @@ -6,16 +6,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCopy } from '@fortawesome/pro-light-svg-icons'; import { globalFocusVisible } from 'components/common'; import { getBaseUrl } from 'helper'; -import { Await, useAsyncValue, useRouteLoaderData } from 'react-router-dom'; +import { Await, useAsyncValue } from 'react-router-dom'; import { Engagement } from 'models/engagement'; import { EngagementStatusChip } from 'components/common/Indicators'; import { SubmissionStatus } from 'constants/engagementStatus'; import dayjs from 'dayjs'; -import { EngagementLoaderData } from 'components/engagement/public/view'; import { ENGAGEMENT_MEMBERSHIP_STATUS, EngagementTeamMember } from 'models/engagementTeamMember'; import { Button } from 'components/common/Input'; import { faPen } from '@fortawesome/pro-regular-svg-icons'; -import { SystemMessage } from 'components/common/Layout/SystemMessage'; export const ConfigSummary = () => { const siteUrl = getBaseUrl(); diff --git a/met-web/src/components/engagement/admin/view/StatusChip.tsx b/met-web/src/components/engagement/admin/view/StatusChip.tsx deleted file mode 100644 index e373b7df3..000000000 --- a/met-web/src/components/engagement/admin/view/StatusChip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Chip, Box } from '@mui/material'; -import { useAsyncValue } from 'react-router-dom'; -import { Engagement } from 'models/engagement'; -import { EngagementStatus } from 'constants/engagementStatus'; - -export const StatusChip = ({ - status, - children, -}: { - status: 'success' | 'warning' | 'danger' | 'info'; - children: React.ReactNode; -}) => { - return ( - - ); -}; - -export const AutoEngagementStatusChip = () => { - const engagement = useAsyncValue() as Engagement; - const statusName = EngagementStatus[engagement?.status_id]; - let status = 'danger' as 'success' | 'warning' | 'danger' | 'info'; - if (statusName === 'Scheduled') { - status = 'info'; - } else if (statusName === 'Published') { - status = 'success'; - } - return {engagement?.status_id}; -}; From 57fdc0c0a8ca4bd607a9706336512657b58c2e74 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 15 Aug 2024 17:31:12 -0700 Subject: [PATCH 3/8] Fix tab styles (accidentally overridden in merge) --- .../engagement/admin/view/index.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/met-web/src/components/engagement/admin/view/index.tsx b/met-web/src/components/engagement/admin/view/index.tsx index fb02c822b..c02056bba 100644 --- a/met-web/src/components/engagement/admin/view/index.tsx +++ b/met-web/src/components/engagement/admin/view/index.tsx @@ -65,23 +65,35 @@ export const AdminEngagementView = () => { key={key} value={value} label={value} + disableFocusRipple sx={{ display: 'flex', - height: '48px', - padding: '0px 24px 0px 18px', justifyContent: 'center', alignItems: 'center', + height: '48px', + padding: '4px 24px 2px 18px', + fontSize: '14px', borderRadius: '0px 16px 0px 0px', - borderBottom: '1px solid', - borderColor: 'gray.60', - backgroundColor: 'gray.10', - color: 'text.secondary', + borderBottom: '2px solid', + borderBottomColor: 'gray.60', boxShadow: '0px 1px 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: 'focus.inner', + border: '4px solid', + borderColor: 'focus.outer', + padding: '0px 20px 0px 14px', }, }} /> From 0c67821669e6ea68027955d44d3f60d1b003d475 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 15 Aug 2024 17:34:26 -0700 Subject: [PATCH 4/8] Fix SonarCloud issues --- .../engagement/admin/config/EngagementUpdateAction.tsx | 9 ++++----- .../engagement/admin/config/LanguageManager.tsx | 1 - .../components/engagement/admin/view/ConfigSummary.tsx | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx b/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx index a560ad13b..b1f6df846 100644 --- a/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx +++ b/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx @@ -42,12 +42,11 @@ export const engagementUpdateAction: ActionFunction = async ({ request, params } // If the user was previously deactivated, reinstate them reinstateMembership(engagementId, member.user_id); } - } else { - if (!isUserInForm) { - // If the user was previously active but is not in the form, revoke their membership - revokeMembership(engagementId, member.user_id); - } + } else if (!isUserInForm) { + // If the user was previously active but is not in the form, revoke their membership + revokeMembership(engagementId, member.user_id); } + // Remove all known users from the set so we can add new members in the next step usersSet.delete(String(member.user.external_id)); } diff --git a/met-web/src/components/engagement/admin/config/LanguageManager.tsx b/met-web/src/components/engagement/admin/config/LanguageManager.tsx index faa558088..74dc49f37 100644 --- a/met-web/src/components/engagement/admin/config/LanguageManager.tsx +++ b/met-web/src/components/engagement/admin/config/LanguageManager.tsx @@ -23,7 +23,6 @@ export const LanguageManager = () => { const engagementForm = useFormContext(); const { setValue, watch } = engagementForm; const selectedLanguages = watch('languages') as Language[]; - // const [isSingleLanguage, setIsSingleLanguage] = React.useState(null); const fetcher = useFetcher(); const fetcherData = fetcher.data as { languages: Language[] } | undefined; const { languages: availableLanguages } = fetcherData ?? { languages: [] }; diff --git a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx index 12031d1bf..7098d95ad 100644 --- a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx +++ b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx @@ -227,7 +227,7 @@ const TeamMemberListSkeleton = () => { return ( <> {Array.from({ length: 3 }).map((_, index) => ( - + From d3d3862c63128e1f6d0743c86cc05957e60e29ea Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 15 Aug 2024 17:42:57 -0700 Subject: [PATCH 5/8] sonarcloud pls --- met-web/src/components/engagement/admin/view/ConfigSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx index 7098d95ad..d1528bc75 100644 --- a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx +++ b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx @@ -227,7 +227,7 @@ const TeamMemberListSkeleton = () => { return ( <> {Array.from({ length: 3 }).map((_, index) => ( - + From 0112a7c21ec298cb18b6ba098e26d3d3ab603fde Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 15 Aug 2024 17:47:21 -0700 Subject: [PATCH 6/8] >:( --- .../src/components/engagement/admin/view/ConfigSummary.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx index d1528bc75..68bbdd1c8 100644 --- a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx +++ b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx @@ -226,8 +226,8 @@ const TeamMemberList = () => { const TeamMemberListSkeleton = () => { return ( <> - {Array.from({ length: 3 }).map((_, index) => ( - + {[1, 2, 3].map((value) => ( + From 758567f3048f4bd6f699130c92043e461d5413de Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 15 Aug 2024 18:11:38 -0700 Subject: [PATCH 7/8] remove custom panel styling --- met-web/src/components/engagement/admin/view/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met-web/src/components/engagement/admin/view/index.tsx b/met-web/src/components/engagement/admin/view/index.tsx index c02056bba..6455e998f 100644 --- a/met-web/src/components/engagement/admin/view/index.tsx +++ b/met-web/src/components/engagement/admin/view/index.tsx @@ -106,7 +106,7 @@ export const AdminEngagementView = () => { - + From 993d964dff473196c6a396931c356d0fc8dd9d6f Mon Sep 17 00:00:00 2001 From: NatSquared Date: Mon, 19 Aug 2024 09:54:28 -0700 Subject: [PATCH 8/8] Wrap engagement URL copy button in LiveAnnouncer for accessibility, add index route --- .../engagement/admin/view/ConfigSummary.tsx | 67 ++++++++++--------- met-web/src/routes/AuthenticatedRoutes.tsx | 1 + 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx index 68bbdd1c8..d24a50906 100644 --- a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx +++ b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx @@ -14,6 +14,7 @@ import dayjs from 'dayjs'; import { ENGAGEMENT_MEMBERSHIP_STATUS, EngagementTeamMember } from 'models/engagementTeamMember'; import { Button } from 'components/common/Input'; import { faPen } from '@fortawesome/pro-regular-svg-icons'; +import { LiveAnnouncer, LiveMessage } from 'react-aria-live'; export const ConfigSummary = () => { const siteUrl = getBaseUrl(); @@ -44,37 +45,41 @@ export const ConfigSummary = () => { - - { - navigator.clipboard.writeText(`${siteUrl}/${slug}`); - setTooltipOpen(true); - }} - > - - - - - {siteUrl}/ - {slug} - + + + + { + navigator.clipboard.writeText(`${siteUrl}/${slug}`); + setTooltipOpen(true); + }} + aria-label="Press enter to copy engagement URL to clipboard" + > + + + + + {siteUrl}/ + {slug} + + diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index 043b0f2a8..fcf94f828 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -99,6 +99,7 @@ const AuthenticatedRoutes = () => { }, }} > + } /> }> } />