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 api from '../../../api'
import { Campaign, Resource, Template } from '../../../types'
import { useNavigate } from 'react-router-dom'
import { useBlocker, useNavigate } from 'react-router-dom'
import { localeState } from '../CampaignDetail'
import Modal from '../../../ui/Modal'
import HtmlEditor from './HtmlEditor'
@ -29,6 +29,7 @@ export default function EmailEditor() {
const [template, setTemplate] = useState<Template | undefined>(templates[0])
const [isSaving, setIsSaving] = useState(false)
const [showConfig, setShowConfig] = useState(false)
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
useEffect(() => {
api.resources.all(project.id)
@ -36,6 +37,19 @@ export default function EmailEditor() {
.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) {
setIsSaving(true)
try {
@ -46,10 +60,16 @@ export default function EmailEditor() {
setCampaign(newCampaign)
toast.success(t('template_saved'))
} finally {
setHasUnsavedChanges(false)
setIsSaving(false)
}
}
const handleTemplateChange = (change: SetStateAction<Template | undefined>) => {
setHasUnsavedChanges(true)
setTemplate(change)
}
const campaignChange = (change: SetStateAction<Campaign>) => {
setCampaign(change)
}
@ -94,7 +114,7 @@ export default function EmailEditor() {
<Suspense key={template.id} fallback={null}>
<VisualEditor
template={template}
setTemplate={setTemplate}
setTemplate={handleTemplateChange}
resources={resources}
/>
</Suspense>
@ -102,7 +122,7 @@ export default function EmailEditor() {
: <HtmlEditor
template={template}
key={template.id}
setTemplate={setTemplate} />
setTemplate={handleTemplateChange} />
))
}
</section>

View file

@ -1,5 +1,5 @@
import { createElement, DragEventHandler, Fragment, memo, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { createElement, DragEventHandler, Fragment, memo, ReactNode, SetStateAction, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { useBlocker, useNavigate } from 'react-router-dom'
import ReactFlow, {
addEdge,
Background,
@ -493,14 +493,12 @@ export default function JourneyEditor() {
const journeyId = journey.id
const loadSteps = useCallback(async () => {
const steps = await api.journeys.steps.get(project.id, journeyId)
const { edges, nodes } = stepsToNodes(steps)
setNodes(nodes)
setEdges(edges)
}, [project, journeyId])
useEffect(() => {
@ -508,10 +506,29 @@ export default function JourneyEditor() {
}, [loadSteps])
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 () => {
setSaving(true)
try {
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) {
toast.error(`Unable to save: ${error}`)
} finally {
setHasUnsavedChanges(false)
setSaving(false)
}
}, [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])
@ -658,7 +676,7 @@ export default function JourneyEditor() {
label={t('name')}
name="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 && (
@ -667,14 +685,14 @@ export default function JourneyEditor() {
subtitle={t('data_key_description')}
name="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, {
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,
data: {