Adds check for unsaved changes (#599)

This commit is contained in:
Chris Anderson 2025-01-02 19:53:15 -06:00 committed by GitHub
parent dd4a8528ca
commit 33119ef1d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 12 deletions

View file

@ -4,7 +4,7 @@ import './EmailEditor.css'
import Button, { LinkButton } from '../../../ui/Button' import Button, { LinkButton } from '../../../ui/Button'
import api from '../../../api' import api from '../../../api'
import { Campaign, Resource, Template } from '../../../types' import { Campaign, Resource, Template } from '../../../types'
import { useNavigate } from 'react-router-dom' import { useBlocker, useNavigate } from 'react-router-dom'
import { localeState } from '../CampaignDetail' import { localeState } from '../CampaignDetail'
import Modal from '../../../ui/Modal' import Modal from '../../../ui/Modal'
import HtmlEditor from './HtmlEditor' import HtmlEditor from './HtmlEditor'
@ -29,6 +29,7 @@ export default function EmailEditor() {
const [template, setTemplate] = useState<Template | undefined>(templates[0]) const [template, setTemplate] = useState<Template | undefined>(templates[0])
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
const [showConfig, setShowConfig] = useState(false) const [showConfig, setShowConfig] = useState(false)
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
useEffect(() => { useEffect(() => {
api.resources.all(project.id) api.resources.all(project.id)
@ -36,6 +37,19 @@ export default function EmailEditor() {
.catch(() => setResources([])) .catch(() => setResources([]))
}, []) }, [])
const blocker = useBlocker(
({ currentLocation, nextLocation }) => hasUnsavedChanges && currentLocation.pathname !== nextLocation.pathname,
)
useEffect(() => {
if (blocker.state !== 'blocked') return
if (confirm(t('confirm_unsaved_changes'))) {
blocker.proceed()
} else {
blocker.reset()
}
}, [blocker.state])
async function handleTemplateSave({ id, type, data }: Template) { async function handleTemplateSave({ id, type, data }: Template) {
setIsSaving(true) setIsSaving(true)
try { try {
@ -46,10 +60,16 @@ export default function EmailEditor() {
setCampaign(newCampaign) setCampaign(newCampaign)
toast.success(t('template_saved')) toast.success(t('template_saved'))
} finally { } finally {
setHasUnsavedChanges(false)
setIsSaving(false) setIsSaving(false)
} }
} }
const handleTemplateChange = (change: SetStateAction<Template | undefined>) => {
setHasUnsavedChanges(true)
setTemplate(change)
}
const campaignChange = (change: SetStateAction<Campaign>) => { const campaignChange = (change: SetStateAction<Campaign>) => {
setCampaign(change) setCampaign(change)
} }
@ -94,7 +114,7 @@ export default function EmailEditor() {
<Suspense key={template.id} fallback={null}> <Suspense key={template.id} fallback={null}>
<VisualEditor <VisualEditor
template={template} template={template}
setTemplate={setTemplate} setTemplate={handleTemplateChange}
resources={resources} resources={resources}
/> />
</Suspense> </Suspense>
@ -102,7 +122,7 @@ export default function EmailEditor() {
: <HtmlEditor : <HtmlEditor
template={template} template={template}
key={template.id} key={template.id}
setTemplate={setTemplate} /> setTemplate={handleTemplateChange} />
)) ))
} }
</section> </section>

View file

@ -1,5 +1,5 @@
import { createElement, DragEventHandler, Fragment, memo, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { createElement, DragEventHandler, Fragment, memo, ReactNode, SetStateAction, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useBlocker, useNavigate } from 'react-router-dom'
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
Background, Background,
@ -493,14 +493,12 @@ export default function JourneyEditor() {
const journeyId = journey.id const journeyId = journey.id
const loadSteps = useCallback(async () => { const loadSteps = useCallback(async () => {
const steps = await api.journeys.steps.get(project.id, journeyId) const steps = await api.journeys.steps.get(project.id, journeyId)
const { edges, nodes } = stepsToNodes(steps) const { edges, nodes } = stepsToNodes(steps)
setNodes(nodes) setNodes(nodes)
setEdges(edges) setEdges(edges)
}, [project, journeyId]) }, [project, journeyId])
useEffect(() => { useEffect(() => {
@ -508,10 +506,29 @@ export default function JourneyEditor() {
}, [loadSteps]) }, [loadSteps])
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const blocker = useBlocker(
({ currentLocation, nextLocation }) => hasUnsavedChanges && currentLocation.pathname !== nextLocation.pathname,
)
useEffect(() => {
if (blocker.state !== 'blocked') return
if (confirm(t('confirm_unsaved_changes'))) {
blocker.proceed()
} else {
blocker.reset()
}
}, [blocker.state])
const handleSetNodes = (nodes: SetStateAction<Array<Node<any, string | undefined>>>) => {
setHasUnsavedChanges(true)
setNodes(nodes)
}
const saveSteps = useCallback(async () => { const saveSteps = useCallback(async () => {
setSaving(true) setSaving(true)
try { try {
const stepMap = await api.journeys.steps.set(project.id, journey.id, nodesToSteps(nodes, edges)) const stepMap = await api.journeys.steps.set(project.id, journey.id, nodesToSteps(nodes, edges))
@ -524,6 +541,7 @@ export default function JourneyEditor() {
} catch (error: any) { } catch (error: any) {
toast.error(`Unable to save: ${error}`) toast.error(`Unable to save: ${error}`)
} finally { } finally {
setHasUnsavedChanges(false)
setSaving(false) setSaving(false)
} }
}, [project, journey, nodes, edges]) }, [project, journey, nodes, edges])
@ -580,7 +598,7 @@ export default function JourneyEditor() {
}, },
} }
setNodes(nds => nds.concat(newStep)) handleSetNodes(nds => nds.concat(newStep))
}, [setNodes, flowInstance, project, journey]) }, [setNodes, flowInstance, project, journey])
@ -658,7 +676,7 @@ export default function JourneyEditor() {
label={t('name')} label={t('name')}
name="name" name="name"
value={editNode.data.name ?? ''} value={editNode.data.name ?? ''}
onChange={name => setNodes(nds => nds.map(n => n.id === editNode.id ? { ...n, data: { ...n.data, name } } : n))} onChange={name => handleSetNodes(nds => nds.map(n => n.id === editNode.id ? { ...n, data: { ...n.data, name } } : n))}
/> />
{ {
type.hasDataKey && ( type.hasDataKey && (
@ -667,14 +685,14 @@ export default function JourneyEditor() {
subtitle={t('data_key_description')} subtitle={t('data_key_description')}
name="data_key" name="data_key"
value={editNode.data.data_key} value={editNode.data.data_key}
onChange={data_key => setNodes(nds => nds.map(n => n.id === editNode.id ? { ...n, data: { ...n.data, data_key } } : n))} onChange={data_key => handleSetNodes(nds => nds.map(n => n.id === editNode.id ? { ...n, data: { ...n.data, data_key } } : n))}
/> />
) )
} }
{ {
type.Edit && createElement(type.Edit, { type.Edit && createElement(type.Edit, {
value: editNode.data.data ?? {}, value: editNode.data.data ?? {},
onChange: data => setNodes(nds => nds.map(n => n.id === editNode.id onChange: data => handleSetNodes(nds => nds.map(n => n.id === editNode.id
? { ? {
...editNode, ...editNode,
data: { data: {