Files
payload/packages/ui/src/providers/DocumentInfo/index.tsx
Patrik 00771b1f2a fix(ui): uploading from drawer & focal point positioning (#7117)
Fixes #7101
Fixes #7006

Drawers were sending duplicate query params. This new approach modeled after the fix in V2, ensures that each drawer has its own action url created per document and the query params will be created when that is generated.

Also fixes the following:
- incorrect focal point cropping
- generated filenames for animated image names used incorrect heights
2024-07-18 13:43:53 -04:00

572 lines
15 KiB
TypeScript

'use client'
import type {
Data,
DocumentPermissions,
DocumentPreferences,
FormState,
PaginatedDocs,
TypeWithID,
TypeWithTimestamps,
TypeWithVersion,
Where,
} from 'payload'
import { notFound } from 'next/navigation.js'
import { reduceFieldsToValues } from 'payload/shared'
import * as qs from 'qs-esm'
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import type { DocumentInfoContext, DocumentInfoProps } from './types.js'
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
import { getFormState } from '../../utilities/getFormState.js'
import { hasSavePermission as getHasSavePermission } from '../../utilities/hasSavePermission.js'
import { isEditing as getIsEditing } from '../../utilities/isEditing.js'
import { useAuth } from '../Auth/index.js'
import { useConfig } from '../Config/index.js'
import { useLocale } from '../Locale/index.js'
import { usePreferences } from '../Preferences/index.js'
import { useTranslation } from '../Translation/index.js'
import { UploadEditsProvider, useUploadEdits } from '../UploadEdits/index.js'
const Context = createContext({} as DocumentInfoContext)
export type * from './types.js'
export const useDocumentInfo = (): DocumentInfoContext => useContext(Context)
const DocumentInfo: React.FC<
{
children: React.ReactNode
} & DocumentInfoProps
> = ({ children, ...props }) => {
const {
id,
collectionSlug,
docPermissions: docPermissionsFromProps,
globalSlug,
hasPublishPermission: hasPublishPermissionFromProps,
hasSavePermission: hasSavePermissionFromProps,
initialData: initialDataFromProps,
initialState: initialStateFromProps,
onLoadError,
onSave: onSaveFromProps,
} = props
const {
admin: { dateFormat },
collections,
globals,
routes: { api },
serverURL,
} = useConfig()
const collectionConfig = collections.find((c) => c.slug === collectionSlug)
const globalConfig = globals.find((g) => g.slug === globalSlug)
const docConfig = collectionConfig || globalConfig
const { i18n } = useTranslation()
const { uploadEdits } = useUploadEdits()
const [documentTitle, setDocumentTitle] = useState(() => {
if (!initialDataFromProps) return ''
return formatDocTitle({
collectionConfig,
data: { ...initialDataFromProps, id },
dateFormat,
fallback: id?.toString(),
globalConfig,
i18n,
})
})
const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
const [data, setData] = useState<Data>(initialDataFromProps)
const [initialState, setInitialState] = useState<FormState>(initialStateFromProps)
const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null)
const [versions, setVersions] = useState<PaginatedDocs<TypeWithVersion<any>>>(null)
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(docPermissionsFromProps)
const [hasSavePermission, setHasSavePermission] = useState<boolean>(hasSavePermissionFromProps)
const [hasPublishPermission, setHasPublishPermission] = useState<boolean>(
hasPublishPermissionFromProps,
)
const isInitializing = initialState === undefined || data === undefined
const hasInitializedDocPermissions = useRef(false)
const [unpublishedVersions, setUnpublishedVersions] =
useState<PaginatedDocs<TypeWithVersion<any>>>(null)
const { getPreference, setPreference } = usePreferences()
const { permissions } = useAuth()
const { code: locale } = useLocale()
const prevLocale = useRef(locale)
const versionsConfig = docConfig?.versions
const baseURL = `${serverURL}${api}`
let slug: string
let pluralType: 'collections' | 'globals'
let preferencesKey: string
if (globalSlug) {
slug = globalSlug
pluralType = 'globals'
preferencesKey = `global-${slug}`
}
if (collectionSlug) {
slug = collectionSlug
pluralType = 'collections'
if (id) {
preferencesKey = `collection-${slug}-${id}`
}
}
const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
const operation = isEditing ? 'update' : 'create'
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions?.permission)
const getVersions = useCallback(async () => {
let versionFetchURL
let publishedFetchURL
let unpublishedVersionJSON = null
let versionJSON = null
let shouldFetch = true
const versionParams = {
depth: 0,
where: {
and: [],
},
}
const publishedVersionParams: { depth: number; locale: string; where: Where } = {
depth: 0,
locale: locale || undefined,
where: {
and: [
{
or: [
{
_status: {
equals: 'published',
},
},
{
_status: {
exists: false,
},
},
],
},
],
},
}
if (globalSlug) {
versionFetchURL = `${baseURL}/globals/${globalSlug}/versions`
publishedFetchURL = `${baseURL}/globals/${globalSlug}?${qs.stringify(publishedVersionParams)}`
}
if (collectionSlug) {
versionFetchURL = `${baseURL}/${collectionSlug}/versions`
publishedVersionParams.where.and.push({
id: {
equals: id,
},
})
publishedFetchURL = `${baseURL}/${collectionSlug}?${qs.stringify(publishedVersionParams)}`
if (!id) {
shouldFetch = false
}
versionParams.where.and.push({
parent: {
equals: id,
},
})
}
if (shouldFetch) {
let publishedJSON
if (versionsConfig?.drafts) {
publishedJSON = await fetch(publishedFetchURL, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
}).then((res) => res.json())
if (collectionSlug) {
publishedJSON = publishedJSON?.docs?.[0]
}
}
if (shouldFetchVersions) {
versionJSON = await fetch(`${versionFetchURL}?${qs.stringify(versionParams)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
}).then((res) => res.json())
if (publishedJSON?.updatedAt) {
const newerVersionParams = {
...versionParams,
where: {
...versionParams.where,
and: [
...versionParams.where.and,
{
updatedAt: {
greater_than: publishedJSON?.updatedAt,
},
},
],
},
}
// Get any newer versions available
const newerVersionRes = await fetch(
`${versionFetchURL}?${qs.stringify(newerVersionParams)}`,
{
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
},
)
if (newerVersionRes.status === 200) {
unpublishedVersionJSON = await newerVersionRes.json()
}
}
}
setPublishedDoc(publishedJSON)
setVersions(versionJSON)
setUnpublishedVersions(unpublishedVersionJSON)
}
}, [i18n, globalSlug, collectionSlug, id, baseURL, locale, versionsConfig, shouldFetchVersions])
const getDocPermissions = React.useCallback(
async (data: Data) => {
const params = {
locale: locale || undefined,
}
const newIsEditing = getIsEditing({ id: data?.id, collectionSlug, globalSlug })
if (newIsEditing) {
const docAccessURL = collectionSlug
? `/${collectionSlug}/access/${data.id}`
: globalSlug
? `/globals/${globalSlug}/access`
: null
if (docAccessURL) {
const res = await fetch(`${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
})
const json: DocumentPermissions = await res.json()
const publishedAccessJSON = await fetch(
`${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`,
{
body: JSON.stringify({
data: {
...(data || {}),
_status: 'published',
},
}),
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
method: 'POST',
},
).then((res) => res.json())
setDocPermissions(json)
setHasSavePermission(
getHasSavePermission({
collectionSlug,
docPermissions: json,
globalSlug,
isEditing: newIsEditing,
}),
)
setHasPublishPermission(publishedAccessJSON?.update?.permission)
}
} else {
// when creating new documents, there is no permissions saved for this document yet
// use the generic entity permissions instead
const newDocPermissions = collectionSlug
? permissions?.collections?.[collectionSlug]
: permissions?.globals?.[globalSlug]
setDocPermissions(newDocPermissions)
setHasSavePermission(
getHasSavePermission({
collectionSlug,
docPermissions: newDocPermissions,
globalSlug,
isEditing: newIsEditing,
}),
)
}
},
[serverURL, api, permissions, i18n.language, locale, collectionSlug, globalSlug],
)
const getDocPreferences = useCallback(() => {
return getPreference<DocumentPreferences>(preferencesKey)
}, [getPreference, preferencesKey])
const setDocFieldPreferences = useCallback<DocumentInfoContext['setDocFieldPreferences']>(
async (path, fieldPreferences) => {
const allPreferences = await getDocPreferences()
if (preferencesKey) {
try {
await setPreference(preferencesKey, {
...allPreferences,
fields: {
...(allPreferences?.fields || {}),
[path]: {
...allPreferences?.fields?.[path],
...fieldPreferences,
},
},
})
} catch (e) {
console.error(e) // eslint-disable-line no-console
}
}
},
[setPreference, preferencesKey, getDocPreferences],
)
const onSave = React.useCallback<DocumentInfoContext['onSave']>(
async (json) => {
if (typeof onSaveFromProps === 'function') {
void onSaveFromProps(json)
}
const docPreferences = await getDocPreferences()
const newData = collectionSlug ? json.doc : json.result
const newState = await getFormState({
apiRoute: api,
body: {
id,
collectionSlug,
data: newData,
docPreferences,
globalSlug,
locale,
operation,
schemaPath: collectionSlug || globalSlug,
},
serverURL,
})
setInitialState(newState)
setData(newData)
await getDocPermissions(newData)
},
[
api,
collectionSlug,
getDocPreferences,
globalSlug,
id,
operation,
locale,
onSaveFromProps,
serverURL,
getDocPermissions,
],
)
useEffect(() => {
const abortController = new AbortController()
const localeChanged = locale !== prevLocale.current
if (
initialStateFromProps === undefined ||
initialDataFromProps === undefined ||
localeChanged
) {
if (localeChanged) prevLocale.current = locale
const getInitialState = async () => {
setIsError(false)
setIsLoading(true)
try {
const result = await getFormState({
apiRoute: api,
body: {
id,
collectionSlug,
globalSlug,
locale,
operation,
schemaPath: collectionSlug || globalSlug,
},
onError: onLoadError,
serverURL,
signal: abortController.signal,
})
setData(reduceFieldsToValues(result, true))
setInitialState(result)
} catch (err) {
if (!abortController.signal.aborted) {
if (typeof onLoadError === 'function') {
void onLoadError()
}
setIsError(true)
setIsLoading(false)
}
}
setIsLoading(false)
}
void getInitialState()
}
return () => {
abortController.abort()
}
}, [
api,
operation,
collectionSlug,
serverURL,
id,
globalSlug,
locale,
onLoadError,
initialDataFromProps,
initialStateFromProps,
])
useEffect(() => {
void getVersions()
}, [getVersions])
useEffect(() => {
setDocumentTitle(
formatDocTitle({
collectionConfig,
data: { ...data, id },
dateFormat,
fallback: id?.toString(),
globalConfig,
i18n,
}),
)
}, [collectionConfig, data, dateFormat, i18n, id, globalConfig])
useEffect(() => {
const loadDocPermissions = async () => {
const docPermissions: DocumentPermissions = docPermissionsFromProps
const hasSavePermission: boolean = hasSavePermissionFromProps
const hasPublishPermission: boolean = hasPublishPermissionFromProps
if (
!docPermissions ||
hasSavePermission === undefined ||
hasSavePermission === null ||
hasPublishPermission === undefined ||
hasPublishPermission === null
) {
await getDocPermissions(data)
}
}
if (!hasInitializedDocPermissions.current && data && (collectionSlug || globalSlug)) {
hasInitializedDocPermissions.current = true
void loadDocPermissions()
}
}, [
getDocPermissions,
docPermissionsFromProps,
hasSavePermissionFromProps,
hasPublishPermissionFromProps,
setDocPermissions,
collectionSlug,
globalSlug,
data,
])
const action: string = React.useMemo(() => {
const docURL = `${baseURL}${pluralType === 'globals' ? `/globals` : ''}/${slug}${id ? `/${id}` : ''}`
const params = {
depth: 0,
'fallback-locale': 'null',
locale,
uploadEdits: uploadEdits || undefined,
}
return `${docURL}${qs.stringify(params, {
addQueryPrefix: true,
})}`
}, [baseURL, locale, pluralType, id, slug, uploadEdits])
if (isError) notFound()
const value: DocumentInfoContext = {
...props,
action,
docConfig,
docPermissions,
getDocPermissions,
getDocPreferences,
getVersions,
hasPublishPermission,
hasSavePermission,
initialData: data,
initialState,
isInitializing,
isLoading,
onSave,
publishedDoc,
setDocFieldPreferences,
setDocumentTitle,
title: documentTitle,
unpublishedVersions,
versions,
}
return <Context.Provider value={value}>{children}</Context.Provider>
}
export const DocumentInfoProvider: React.FC<
{
children: React.ReactNode
} & DocumentInfoProps
> = (props) => {
return (
<UploadEditsProvider>
<DocumentInfo {...props} />
</UploadEditsProvider>
)
}