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.
This commit is contained in:
@@ -23,6 +23,8 @@ import { reduceFieldsToValuesWithValidation } from '../../utilities/reduceFields
|
|||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'autosave'
|
const baseClass = 'autosave'
|
||||||
|
// The minimum time the saving state should be shown
|
||||||
|
const minimumAnimationTime = 1000
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
collection?: ClientCollectionConfig
|
collection?: ClientCollectionConfig
|
||||||
@@ -80,14 +82,19 @@ export const Autosave: React.FC<Props> = ({
|
|||||||
// Store locale in ref so the autosave func
|
// Store locale in ref so the autosave func
|
||||||
// can always retrieve the most to date locale
|
// can always retrieve the most to date locale
|
||||||
localeRef.current = locale
|
localeRef.current = locale
|
||||||
console.log(modifiedRef.current, modified)
|
|
||||||
// When debounced fields change, autosave
|
// When debounced fields change, autosave
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
let autosaveTimeout = undefined
|
let autosaveTimeout = undefined
|
||||||
|
// We need to log the time in order to figure out if we need to trigger the state off later
|
||||||
|
let startTimestamp = undefined
|
||||||
|
let endTimestamp = undefined
|
||||||
|
|
||||||
const autosave = () => {
|
const autosave = () => {
|
||||||
if (modified) {
|
if (modified) {
|
||||||
|
startTimestamp = new Date().getTime()
|
||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
||||||
let url: string
|
let url: string
|
||||||
@@ -107,92 +114,104 @@ export const Autosave: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
autosaveTimeout = setTimeout(async () => {
|
if (modifiedRef.current) {
|
||||||
if (modifiedRef.current) {
|
const { data, valid } = {
|
||||||
const { data, valid } = {
|
...reduceFieldsToValuesWithValidation(fieldRef.current, true),
|
||||||
...reduceFieldsToValuesWithValidation(fieldRef.current, true),
|
|
||||||
}
|
|
||||||
data._status = 'draft'
|
|
||||||
const skipSubmission =
|
|
||||||
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
|
|
||||||
|
|
||||||
if (!skipSubmission) {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Accept-Language': i18n.language,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
method,
|
|
||||||
signal: abortController.signal,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
const newDate = new Date()
|
|
||||||
setLastSaved(newDate.getTime())
|
|
||||||
setModified(false)
|
|
||||||
reportUpdate({
|
|
||||||
id,
|
|
||||||
entitySlug,
|
|
||||||
updatedAt: newDate.toISOString(),
|
|
||||||
})
|
|
||||||
void getVersions()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
versionsConfig?.drafts &&
|
|
||||||
versionsConfig?.drafts?.validate &&
|
|
||||||
res.status === 400
|
|
||||||
) {
|
|
||||||
const json = await res.json()
|
|
||||||
if (Array.isArray(json.errors)) {
|
|
||||||
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
|
|
||||||
([fieldErrs, nonFieldErrs], err) => {
|
|
||||||
const newFieldErrs = []
|
|
||||||
const newNonFieldErrs = []
|
|
||||||
|
|
||||||
if (err?.message) {
|
|
||||||
newNonFieldErrs.push(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(err?.data)) {
|
|
||||||
err.data.forEach((dataError) => {
|
|
||||||
if (dataError?.field) {
|
|
||||||
newFieldErrs.push(dataError)
|
|
||||||
} else {
|
|
||||||
newNonFieldErrs.push(dataError)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
[...fieldErrs, ...newFieldErrs],
|
|
||||||
[...nonFieldErrs, ...newNonFieldErrs],
|
|
||||||
]
|
|
||||||
},
|
|
||||||
[[], []],
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatchFields({
|
|
||||||
type: 'ADD_SERVER_ERRORS',
|
|
||||||
errors: fieldErrors,
|
|
||||||
})
|
|
||||||
|
|
||||||
nonFieldErrors.forEach((err) => {
|
|
||||||
toast.error(err.message || i18n.t('error:unknown'))
|
|
||||||
})
|
|
||||||
|
|
||||||
setSubmitted(true)
|
|
||||||
setSaving(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
data._status = 'draft'
|
||||||
|
const skipSubmission =
|
||||||
|
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
|
||||||
|
|
||||||
setSaving(false)
|
if (!skipSubmission) {
|
||||||
}, 1000)
|
void fetch(url, {
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': i18n.language,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method,
|
||||||
|
signal: abortController.signal,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const newDate = new Date()
|
||||||
|
// We need to log the time in order to figure out if we need to trigger the state off later
|
||||||
|
endTimestamp = newDate.getTime()
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
setLastSaved(newDate.getTime())
|
||||||
|
|
||||||
|
reportUpdate({
|
||||||
|
id,
|
||||||
|
entitySlug,
|
||||||
|
updatedAt: newDate.toISOString(),
|
||||||
|
})
|
||||||
|
setModified(false)
|
||||||
|
void getVersions()
|
||||||
|
} else {
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
if (versionsConfig?.drafts && versionsConfig?.drafts?.validate && json.errors) {
|
||||||
|
if (Array.isArray(json.errors)) {
|
||||||
|
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
|
||||||
|
([fieldErrs, nonFieldErrs], err) => {
|
||||||
|
const newFieldErrs = []
|
||||||
|
const newNonFieldErrs = []
|
||||||
|
|
||||||
|
if (err?.message) {
|
||||||
|
newNonFieldErrs.push(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(err?.data)) {
|
||||||
|
err.data.forEach((dataError) => {
|
||||||
|
if (dataError?.field) {
|
||||||
|
newFieldErrs.push(dataError)
|
||||||
|
} else {
|
||||||
|
newNonFieldErrs.push(dataError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
[...fieldErrs, ...newFieldErrs],
|
||||||
|
[...nonFieldErrs, ...newNonFieldErrs],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[[], []],
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatchFields({
|
||||||
|
type: 'ADD_SERVER_ERRORS',
|
||||||
|
errors: fieldErrors,
|
||||||
|
})
|
||||||
|
|
||||||
|
nonFieldErrors.forEach((err) => {
|
||||||
|
toast.error(err.message || i18n.t('error:unknown'))
|
||||||
|
})
|
||||||
|
|
||||||
|
setSubmitted(true)
|
||||||
|
setSaving(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// If request was faster than minimum animation time, animate the difference
|
||||||
|
if (endTimestamp - startTimestamp < minimumAnimationTime) {
|
||||||
|
autosaveTimeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
setSaving(false)
|
||||||
|
},
|
||||||
|
minimumAnimationTime - (endTimestamp - startTimestamp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +219,7 @@ export const Autosave: React.FC<Props> = ({
|
|||||||
void autosave()
|
void autosave()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(autosaveTimeout)
|
if (autosaveTimeout) clearTimeout(autosaveTimeout)
|
||||||
if (abortController.signal) abortController.abort()
|
if (abortController.signal) abortController.abort()
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -234,7 +253,7 @@ export const Autosave: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
{saving && t('general:saving')}
|
{saving && t('general:saving')}
|
||||||
{!saving && lastSaved && (
|
{!saving && Boolean(lastSaved) && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{t('version:lastSavedAgo', {
|
{t('version:lastSavedAgo', {
|
||||||
distance: formatTimeToNow({ date: lastSaved, i18n }),
|
distance: formatTimeToNow({ date: lastSaved, i18n }),
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
HeadingFeature,
|
HeadingFeature,
|
||||||
HorizontalRuleFeature,
|
HorizontalRuleFeature,
|
||||||
InlineToolbarFeature,
|
InlineToolbarFeature,
|
||||||
lexicalEditor } from '@payloadcms/richtext-lexical'
|
lexicalEditor,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
import { authenticated } from '../../access/authenticated'
|
import { authenticated } from '../../access/authenticated'
|
||||||
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
|
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export class NextRESTClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async PATCH(path: ValidPath, options: FileArg & RequestInit & RequestOptions): Promise<Response> {
|
async PATCH(path: ValidPath, options: FileArg & RequestInit & RequestOptions): Promise<Response> {
|
||||||
const { url, slug, params } = this.generateRequestParts(path)
|
const { slug, params, url } = this.generateRequestParts(path)
|
||||||
const { query, ...rest } = options
|
const { query, ...rest } = options
|
||||||
const queryParams = generateQueryString(query, params)
|
const queryParams = generateQueryString(query, params)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user