apps/newsletters-ui/src/app/components/RenderingOptionsForm.tsx (296 lines of code) (raw):

import { Alert, AlertTitle, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Grid, Snackbar, Typography, } from '@mui/material'; import { Stack } from '@mui/system'; import { useEffect, useMemo, useState } from 'react'; import type { FormDataRecord, NewsletterData, RenderingOptions, } from '@newsletters-nx/newsletters-data-client'; import { dataCollectionRenderingOptionsSchema, getEmptySchemaData, newsletterDataSchema, renderingOptionsSchema, } from '@newsletters-nx/newsletters-data-client'; import { requestNewsletterEdit } from '../api-requests/request-newsletter-edit'; import { requestNotification } from '../api-requests/request-notification'; import { StateEditForm } from './StateEditForm'; import { TemplatePreviewLoader } from './TemplatePreviewLoader'; interface Props { originalItem: NewsletterData; } export const RenderingOptionsForm = ({ originalItem }: Props) => { const [renderingOptions, setRenderingOptions] = useState< FormDataRecord | undefined >( originalItem.renderingOptions ?? getEmptySchemaData(renderingOptionsSchema), ); const [showUpdateBrazeDialog, setShowUpdateBrazeDialog] = useState<boolean>(false); const [initialState, setInitialState] = useState<NewsletterData>(originalItem); const emailRenderingManagedNewsletters = [ 'afternoon-update', 'cotton-capital', 'the-guide-staying-in', 'fashion-statement', 'five-great-reads', 'morning-mail', 'soccer-with-jonathan-wilson', 'pushing-buttons', 'morning-briefing', 'green-light', 'afternoon-update', ]; const { identityName } = originalItem; const hasEmailRenderingTemplate = emailRenderingManagedNewsletters.includes(identityName); const [subset, setSubset] = useState<FormDataRecord>({ category: initialState.category, seriesTag: initialState.seriesTag, }); const [waitingForResponse, setWaitingForResponse] = useState<boolean>(false); const [errorMessage, setErrorMessage] = useState<string | undefined>(); const [confirmationMessage, setConfirmationMessage] = useState< string | undefined >(); const [couldRequireBrazeUpdate, setCouldRequireBrazeUpdate] = useState<boolean>(false); const resetValue = initialState.renderingOptions ?? getEmptySchemaData(renderingOptionsSchema); const noChangesMade = useMemo(() => { const renderingOptionsMatch = Object.keys(resetValue ?? {}).every( (key) => resetValue?.[key as keyof RenderingOptions] === renderingOptions?.[key], ); return ( renderingOptionsMatch && initialState.category === subset['category'] && initialState.seriesTag === subset['seriesTag'] ); }, [renderingOptions, resetValue, initialState, subset]); const handleSubmit = (requestBrazeUpdate: boolean) => { setShowUpdateBrazeDialog(false); if (waitingForResponse) { return; } if (couldRequireBrazeUpdate && !showUpdateBrazeDialog) { return setShowUpdateBrazeDialog(true); } const parseResult = renderingOptionsSchema.safeParse(renderingOptions); if (!parseResult.success) { setErrorMessage('Cannot submit with validation errors'); return; } void requestUpdate(requestBrazeUpdate); }; const requestUpdate = async (includeBrazeNotification: boolean) => { const renderingOptionsParseResult = renderingOptionsSchema.safeParse(renderingOptions); if (!renderingOptionsParseResult.success) { setErrorMessage('Cannot submit with validation errors'); return; } const parsedRenderingOptions = renderingOptionsParseResult.data; setWaitingForResponse(true); const response = await requestNewsletterEdit(originalItem.listId, { renderingOptions: parsedRenderingOptions, ...subset, }).catch((error: unknown) => { setErrorMessage('Failed to submit form.'); setWaitingForResponse(false); console.log(error); return undefined; }); if (!response) { setWaitingForResponse(false); return; } if (response.ok) { setInitialState(response.data); setRenderingOptions(response.data.renderingOptions); setSubset({ category: response.data.category, seriesTag: response.data.seriesTag, }); setWaitingForResponse(false); if (includeBrazeNotification) { try { const notificationSent = ( await requestNotification(initialState.identityName, 'brazeUpdate') ).ok; if (notificationSent) { const asyncStatusUpdate = ( await requestNewsletterEdit(originalItem.listId, { brazeCampaignCreationStatus: 'REQUESTED', }) ).ok; if (!asyncStatusUpdate) { setErrorMessage('Failed to update Braze campaign status'); } } else { setErrorMessage('Failed to send Braze update request'); } setConfirmationMessage( `Rendering options updated ${ notificationSent ? 'and Braze update requested' : '' }`, ); } catch (error: unknown) { console.log(error); } } else { setConfirmationMessage('Rendering options updated'); } } else { setWaitingForResponse(false); setErrorMessage(response.message); } }; const reset = () => { setRenderingOptions( initialState.renderingOptions ?? getEmptySchemaData(renderingOptionsSchema), ); setSubset({ category: initialState.category, seriesTag: initialState.seriesTag, }); }; useEffect(() => { setCouldRequireBrazeUpdate( initialState.category === 'article-based-legacy' && subset.category === 'article-based', ); }, [initialState.category, subset.category]); return ( <> <Dialog open={showUpdateBrazeDialog}> <DialogTitle>Request update to Braze campaign?</DialogTitle> <DialogContent> <DialogContentText> You have updated the category to article-based. This usually requires a change to the Braze Campaign to fetch Newsletter content from the correct API. Request this now? </DialogContentText> </DialogContent> <DialogActions> <Button onClick={() => setShowUpdateBrazeDialog(false)}> Cancel </Button> <Button onClick={() => handleSubmit(false)} variant="contained"> Save without update </Button> <Button onClick={() => handleSubmit(true)} variant="contained"> Save and Request Update </Button> </DialogActions> </Dialog> {hasEmailRenderingTemplate && ( <Alert severity="error"> This newsletter’s rendering options are managed by email rendering. To make changes to <strong>{initialState.name}</strong>, please contact the development team </Alert> )} <Typography variant="h2">{initialState.name}</Typography> <Typography variant="subtitle1">email-rendering settings</Typography> <Alert severity={initialState.seriesTag ? 'info' : 'warning'}> <AlertTitle>Series Tags</AlertTitle> {!initialState.seriesTag && ( <Typography>Please add a series tag</Typography> )} <Typography> If no valid series tag is specified, the email rendering service will show a generic template </Typography> </Alert> <Grid container columnSpacing={2}> <Grid item xs={4}> <Typography variant="h3">Category and series tag</Typography> <StateEditForm formSchema={newsletterDataSchema.pick({ category: true, seriesTag: true, })} formData={subset} setFormData={setSubset} /> {renderingOptions && ( <> <Typography variant="h3">Rendering options</Typography> <StateEditForm formSchema={dataCollectionRenderingOptionsSchema} formData={renderingOptions} setFormData={setRenderingOptions} /> </> )} </Grid> <Grid item xs={8} paddingTop={3}> <TemplatePreviewLoader newsletterData={{ ...originalItem, ...subset, renderingOptions: (renderingOptions ?? {}) as RenderingOptions, }} minHeight={1200} /> </Grid> </Grid> <Stack maxWidth={'md'} direction={'row'} spacing={2} marginBottom={2}> <Button variant="outlined" size="large" onClick={reset} disabled={waitingForResponse || noChangesMade} > reset </Button> <Button variant="contained" size="large" onClick={() => handleSubmit(false)} disabled={waitingForResponse} > update </Button> <Snackbar sx={{ position: 'static' }} open={!!errorMessage} onClose={() => { setErrorMessage(undefined); }} > <Alert onClose={() => { setErrorMessage(undefined); }} severity="error" > {errorMessage} </Alert> </Snackbar> <Snackbar sx={{ position: 'static' }} open={!!confirmationMessage} onClose={() => { setConfirmationMessage(undefined); }} > <Alert severity="info">{confirmationMessage}</Alert> </Snackbar> </Stack> </> ); };