Skip to content

Commit 014ee1a

Browse files
authored
feat(ui): change autosave logic to send updates as soon as possible, improving live preview speed (#7201)
Now has a minimum animation time for the autosave but it fires off the send events sooner to improve the live preview timing.
1 parent cf6da01 commit 014ee1a

File tree

3 files changed

+102
-82
lines changed

3 files changed

+102
-82
lines changed

packages/ui/src/elements/Autosave/index.tsx

Lines changed: 99 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { reduceFieldsToValuesWithValidation } from '../../utilities/reduceFields
2323
import './index.scss'
2424

2525
const baseClass = 'autosave'
26+
// The minimum time the saving state should be shown
27+
const minimumAnimationTime = 1000
2628

2729
export type Props = {
2830
collection?: ClientCollectionConfig
@@ -80,14 +82,19 @@ export const Autosave: React.FC<Props> = ({
8082
// Store locale in ref so the autosave func
8183
// can always retrieve the most to date locale
8284
localeRef.current = locale
83-
console.log(modifiedRef.current, modified)
85+
8486
// When debounced fields change, autosave
8587
useEffect(() => {
8688
const abortController = new AbortController()
8789
let autosaveTimeout = undefined
90+
// We need to log the time in order to figure out if we need to trigger the state off later
91+
let startTimestamp = undefined
92+
let endTimestamp = undefined
8893

8994
const autosave = () => {
9095
if (modified) {
96+
startTimestamp = new Date().getTime()
97+
9198
setSaving(true)
9299

93100
let url: string
@@ -107,100 +114,112 @@ export const Autosave: React.FC<Props> = ({
107114
}
108115

109116
if (url) {
110-
autosaveTimeout = setTimeout(async () => {
111-
if (modifiedRef.current) {
112-
const { data, valid } = {
113-
...reduceFieldsToValuesWithValidation(fieldRef.current, true),
114-
}
115-
data._status = 'draft'
116-
const skipSubmission =
117-
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
118-
119-
if (!skipSubmission) {
120-
const res = await fetch(url, {
121-
body: JSON.stringify(data),
122-
credentials: 'include',
123-
headers: {
124-
'Accept-Language': i18n.language,
125-
'Content-Type': 'application/json',
126-
},
127-
method,
128-
signal: abortController.signal,
129-
})
117+
if (modifiedRef.current) {
118+
const { data, valid } = {
119+
...reduceFieldsToValuesWithValidation(fieldRef.current, true),
120+
}
121+
data._status = 'draft'
122+
const skipSubmission =
123+
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
130124

131-
if (res.status === 200) {
125+
if (!skipSubmission) {
126+
void fetch(url, {
127+
body: JSON.stringify(data),
128+
credentials: 'include',
129+
headers: {
130+
'Accept-Language': i18n.language,
131+
'Content-Type': 'application/json',
132+
},
133+
method,
134+
signal: abortController.signal,
135+
})
136+
.then((res) => {
132137
const newDate = new Date()
133-
setLastSaved(newDate.getTime())
134-
setModified(false)
135-
reportUpdate({
136-
id,
137-
entitySlug,
138-
updatedAt: newDate.toISOString(),
139-
})
140-
void getVersions()
141-
}
142-
143-
if (
144-
versionsConfig?.drafts &&
145-
versionsConfig?.drafts?.validate &&
146-
res.status === 400
147-
) {
148-
const json = await res.json()
149-
if (Array.isArray(json.errors)) {
150-
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
151-
([fieldErrs, nonFieldErrs], err) => {
152-
const newFieldErrs = []
153-
const newNonFieldErrs = []
154-
155-
if (err?.message) {
156-
newNonFieldErrs.push(err)
157-
}
158-
159-
if (Array.isArray(err?.data)) {
160-
err.data.forEach((dataError) => {
161-
if (dataError?.field) {
162-
newFieldErrs.push(dataError)
163-
} else {
164-
newNonFieldErrs.push(dataError)
165-
}
166-
})
167-
}
168-
169-
return [
170-
[...fieldErrs, ...newFieldErrs],
171-
[...nonFieldErrs, ...newNonFieldErrs],
172-
]
173-
},
174-
[[], []],
175-
)
138+
// We need to log the time in order to figure out if we need to trigger the state off later
139+
endTimestamp = newDate.getTime()
176140

177-
dispatchFields({
178-
type: 'ADD_SERVER_ERRORS',
179-
errors: fieldErrors,
180-
})
141+
if (res.status === 200) {
142+
setLastSaved(newDate.getTime())
181143

182-
nonFieldErrors.forEach((err) => {
183-
toast.error(err.message || i18n.t('error:unknown'))
144+
reportUpdate({
145+
id,
146+
entitySlug,
147+
updatedAt: newDate.toISOString(),
184148
})
149+
setModified(false)
150+
void getVersions()
151+
} else {
152+
return res.json()
153+
}
154+
})
155+
.then((json) => {
156+
if (versionsConfig?.drafts && versionsConfig?.drafts?.validate && json.errors) {
157+
if (Array.isArray(json.errors)) {
158+
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
159+
([fieldErrs, nonFieldErrs], err) => {
160+
const newFieldErrs = []
161+
const newNonFieldErrs = []
162+
163+
if (err?.message) {
164+
newNonFieldErrs.push(err)
165+
}
185166

186-
setSubmitted(true)
167+
if (Array.isArray(err?.data)) {
168+
err.data.forEach((dataError) => {
169+
if (dataError?.field) {
170+
newFieldErrs.push(dataError)
171+
} else {
172+
newNonFieldErrs.push(dataError)
173+
}
174+
})
175+
}
176+
177+
return [
178+
[...fieldErrs, ...newFieldErrs],
179+
[...nonFieldErrs, ...newNonFieldErrs],
180+
]
181+
},
182+
[[], []],
183+
)
184+
185+
dispatchFields({
186+
type: 'ADD_SERVER_ERRORS',
187+
errors: fieldErrors,
188+
})
189+
190+
nonFieldErrors.forEach((err) => {
191+
toast.error(err.message || i18n.t('error:unknown'))
192+
})
193+
194+
setSubmitted(true)
195+
setSaving(false)
196+
return
197+
}
198+
}
199+
})
200+
.then(() => {
201+
// If request was faster than minimum animation time, animate the difference
202+
if (endTimestamp - startTimestamp < minimumAnimationTime) {
203+
autosaveTimeout = setTimeout(
204+
() => {
205+
setSaving(false)
206+
},
207+
minimumAnimationTime - (endTimestamp - startTimestamp),
208+
)
209+
} else {
187210
setSaving(false)
188-
return
189211
}
190-
}
191-
}
212+
})
192213
}
193-
194-
setSaving(false)
195-
}, 1000)
214+
}
196215
}
197216
}
198217
}
199218

200219
void autosave()
201220

202221
return () => {
203-
clearTimeout(autosaveTimeout)
222+
if (autosaveTimeout) clearTimeout(autosaveTimeout)
204223
if (abortController.signal) abortController.abort()
205224
setSaving(false)
206225
}
@@ -234,7 +253,7 @@ export const Autosave: React.FC<Props> = ({
234253
return (
235254
<div className={baseClass}>
236255
{saving && t('general:saving')}
237-
{!saving && lastSaved && (
256+
{!saving && Boolean(lastSaved) && (
238257
<React.Fragment>
239258
{t('version:lastSavedAgo', {
240259
distance: formatTimeToNow({ date: lastSaved, i18n }),

templates/website/src/payload/collections/Posts/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
HeadingFeature,
77
HorizontalRuleFeature,
88
InlineToolbarFeature,
9-
lexicalEditor } from '@payloadcms/richtext-lexical'
9+
lexicalEditor,
10+
} from '@payloadcms/richtext-lexical'
1011

1112
import { authenticated } from '../../access/authenticated'
1213
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'

test/helpers/NextRESTClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export class NextRESTClient {
148148
}
149149

150150
async PATCH(path: ValidPath, options: FileArg & RequestInit & RequestOptions): Promise<Response> {
151-
const { url, slug, params } = this.generateRequestParts(path)
151+
const { slug, params, url } = this.generateRequestParts(path)
152152
const { query, ...rest } = options
153153
const queryParams = generateQueryString(query, params)
154154

0 commit comments

Comments
 (0)