chore: adjusts endpoint for buildFormState

This commit is contained in:
Jarrod Flesch
2024-02-20 23:13:48 -05:00
26 changed files with 386 additions and 201 deletions

View File

@@ -8,4 +8,5 @@ export default ({ params, searchParams }) =>
collectionSlug: params.collection, collectionSlug: params.collection,
searchParams, searchParams,
config, config,
route: `/${params.collection + '/' + params.segments?.join('/')}`,
}) })

View File

@@ -3,4 +3,4 @@
import { Dashboard } from '@payloadcms/next/pages/Dashboard' import { Dashboard } from '@payloadcms/next/pages/Dashboard'
import config from 'payload-config' import config from 'payload-config'
export default async () => Dashboard({ config }) export default async ({ searchParams }) => Dashboard({ config, searchParams })

View File

@@ -22,6 +22,8 @@ export const Account = async ({
const { config, payload, permissions, user, i18n, locale } = await initPage({ const { config, payload, permissions, user, i18n, locale } = await initPage({
config: configPromise, config: configPromise,
redirectUnauthenticatedUser: true, redirectUnauthenticatedUser: true,
searchParams,
route: `/account`,
}) })
const { const {

View File

@@ -15,15 +15,19 @@ export const CollectionList = async ({
collectionSlug, collectionSlug,
config: configPromise, config: configPromise,
searchParams, searchParams,
route,
}: { }: {
collectionSlug: string collectionSlug: string
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined } searchParams: { [key: string]: string | string[] | undefined }
route
}) => { }) => {
const { config, payload, permissions, user, collectionConfig } = await initPage({ const { config, payload, permissions, user, collectionConfig } = await initPage({
config: configPromise, config: configPromise,
redirectUnauthenticatedUser: true, redirectUnauthenticatedUser: true,
collectionSlug, collectionSlug,
route,
searchParams,
}) })
let listPreferences: ListPreferences let listPreferences: ListPreferences

View File

@@ -7,12 +7,16 @@ import { DefaultDashboard } from './Default'
export const Dashboard = async ({ export const Dashboard = async ({
config: configPromise, config: configPromise,
searchParams,
}: { }: {
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined }
}) => { }) => {
const { config, user, permissions } = await initPage({ const { config, user, permissions } = await initPage({
config: configPromise, config: configPromise,
redirectUnauthenticatedUser: true, redirectUnauthenticatedUser: true,
route: '',
searchParams,
}) })
const CustomDashboardComponent = config.admin.components?.views?.Dashboard const CustomDashboardComponent = config.admin.components?.views?.Dashboard

View File

@@ -43,12 +43,16 @@ export const Document = async ({
const isEditing = Boolean(globalSlug || (collectionSlug && !!id)) const isEditing = Boolean(globalSlug || (collectionSlug && !!id))
const route = `/${collectionSlug || globalSlug + '/' + params.segments.join('/')}`
const { config, payload, permissions, user, collectionConfig, globalConfig, locale, i18n } = const { config, payload, permissions, user, collectionConfig, globalConfig, locale, i18n } =
await initPage({ await initPage({
config: configPromise, config: configPromise,
redirectUnauthenticatedUser: true, redirectUnauthenticatedUser: true,
collectionSlug, collectionSlug,
globalSlug, globalSlug,
searchParams,
route,
}) })
if (!collectionConfig && !globalConfig) { if (!collectionConfig && !globalConfig) {
@@ -149,7 +153,7 @@ export const Document = async ({
limit: 1, limit: 1,
})) as any as { docs: { value: DocumentPreferences }[] } })) as any as { docs: { value: DocumentPreferences }[] }
const formState = await buildStateFromSchema({ const initialState = await buildStateFromSchema({
id, id,
data: data || {}, data: data || {},
fieldSchema: formatFields(fields, isEditing), fieldSchema: formatFields(fields, isEditing),
@@ -176,13 +180,11 @@ export const Document = async ({
globalSlug, globalSlug,
data, data,
hasSavePermission, hasSavePermission,
formState, initialState,
isEditing, isEditing,
docPermissions, docPermissions,
docPreferences,
updatedAt: data?.updatedAt?.toString(), updatedAt: data?.updatedAt?.toString(),
user, user,
locale,
payload, payload,
config, config,
searchParams, searchParams,

View File

@@ -35,7 +35,7 @@ export const Login: React.FC<{
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined } searchParams: { [key: string]: string | string[] | undefined }
}> = async ({ config: configPromise, searchParams }) => { }> = async ({ config: configPromise, searchParams }) => {
const { config, user } = await initPage({ config: configPromise }) const { config, user } = await initPage({ config: configPromise, searchParams, route: '/login' })
const { const {
admin: { components: { afterLogin, beforeLogin } = {}, user: userSlug }, admin: { components: { afterLogin, beforeLogin } = {}, user: userSlug },

View File

@@ -0,0 +1,67 @@
import httpStatus from 'http-status'
import {
BuildFormStateArgs,
FieldSchemaMap,
buildFieldSchemaMap,
buildStateFromSchema,
reduceFieldsToValues,
} from '@payloadcms/ui'
import { Field, PayloadRequest, SanitizedConfig } from 'payload/types'
let cached = global._payload_fieldSchemaMap
if (!cached) {
// eslint-disable-next-line no-multi-assign
cached = global._payload_fieldSchemaMap = null
}
export const getFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
if (cached) {
return cached
}
cached = buildFieldSchemaMap(config)
return cached
}
export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
const { data: reqData, user, t, locale } = req
// TODO: run ADMIN access control for user
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
const { id, operation, docPreferences, formState, schemaPath } = reqData as BuildFormStateArgs
const schemaPathSegments = schemaPath.split('.')
let fieldSchema: Field[]
if (schemaPathSegments.length === 1) {
if (req.payload.collections[schemaPath]) {
fieldSchema = req.payload.collections[schemaPath].config.fields
} else if (req.payload.globals[schemaPath]) {
fieldSchema = req.payload.globals[schemaPath].config.fields
}
} else if (fieldSchemaMap.has(schemaPath)) {
fieldSchema = fieldSchemaMap.get(schemaPath)
}
const data = reduceFieldsToValues(formState, true)
const result = await buildStateFromSchema({
id,
data,
fieldSchema,
locale,
operation,
preferences: docPreferences,
t,
user,
})
return Response.json(result, {
status: httpStatus.OK,
})
}

View File

@@ -12,6 +12,7 @@ import {
} from './types' } from './types'
import { RouteError } from './RouteError' import { RouteError } from './RouteError'
import { buildFormState } from './buildFormState'
import { endpointsAreDisabled } from './checkEndpoints' import { endpointsAreDisabled } from './checkEndpoints'
import { me } from './auth/me' import { me } from './auth/me'
@@ -50,6 +51,9 @@ const endpoints = {
GET: { GET: {
access, access,
}, },
POST: {
'form-state': buildFormState,
},
}, },
collection: { collection: {
GET: { GET: {
@@ -295,6 +299,7 @@ export const POST =
config, config,
params: { collection: slug1 }, params: { collection: slug1 },
}) })
collection = req.payload.collections?.[slug1] collection = req.payload.collections?.[slug1]
const disableEndpoints = endpointsAreDisabled({ const disableEndpoints = endpointsAreDisabled({
@@ -315,6 +320,7 @@ export const POST =
payloadRequest: req, payloadRequest: req,
endpoints: collection.config.endpoints, endpoints: collection.config.endpoints,
}) })
if (customEndpointResponse) return customEndpointResponse if (customEndpointResponse) return customEndpointResponse
switch (slug.length) { switch (slug.length) {
@@ -324,6 +330,7 @@ export const POST =
break break
case 2: case 2:
if (slug2 in endpoints.collection.POST) { if (slug2 in endpoints.collection.POST) {
// /:collection/form-state
// /:collection/login // /:collection/login
// /:collection/logout // /:collection/logout
// /:collection/unlock // /:collection/unlock

View File

@@ -1,4 +1,5 @@
import { headers as getHeaders } from 'next/headers' import { headers as getHeaders } from 'next/headers'
import qs from 'qs'
import { auth } from './auth' import { auth } from './auth'
@@ -22,12 +23,16 @@ export const initPage = async ({
collectionSlug, collectionSlug,
globalSlug, globalSlug,
localeParam, localeParam,
searchParams,
route,
}: { }: {
config: SanitizedConfig | Promise<SanitizedConfig> config: SanitizedConfig | Promise<SanitizedConfig>
redirectUnauthenticatedUser?: boolean redirectUnauthenticatedUser?: boolean
collectionSlug?: string collectionSlug?: string
globalSlug?: string globalSlug?: string
localeParam?: string localeParam?: string
searchParams?: { [key: string]: string | string[] | undefined }
route?: string
}): Promise<{ }): Promise<{
payload: Awaited<ReturnType<typeof getPayload>> payload: Awaited<ReturnType<typeof getPayload>>
permissions: Awaited<ReturnType<typeof auth>>['permissions'] permissions: Awaited<ReturnType<typeof auth>>['permissions']
@@ -46,15 +51,18 @@ export const initPage = async ({
config: configPromise, config: configPromise,
cookies, cookies,
}) })
const language = getRequestLanguage({ cookies, headers }) const language = getRequestLanguage({ cookies, headers })
const config = await configPromise const config = await configPromise
const { localization, routes, collections, globals } = config const { localization, routes, collections, globals } = config
if (redirectUnauthenticatedUser && !user) { if (redirectUnauthenticatedUser && !user && route !== '/login') {
// `redirect(`${payload.config.routes.admin}/unauthorized`)` is not built yet const stringifiedSearchParams = Object.keys(searchParams ?? {}).length
redirect(`${routes.admin}/login`) ? `?${qs.stringify(searchParams)}`
: ''
redirect(`${routes.admin}/login?redirect=${routes.admin + route + stringifiedSearchParams}`)
} }
const payload = await getPayload({ const payload = await getPayload({

View File

@@ -25,6 +25,9 @@ import IDLabel from '../IDLabel'
import type { EditViewProps } from '../../views/types' import type { EditViewProps } from '../../views/types'
import { DefaultEditView } from '../../views/Edit' import { DefaultEditView } from '../../views/Edit'
import { Gutter } from '../Gutter' import { Gutter } from '../Gutter'
import { LoadingOverlay } from '../Loading'
import { getFormState } from '../../views/Edit/getFormState'
import { useFieldPath } from '../../forms/FieldPathProvider'
const Content: React.FC<DocumentDrawerProps> = ({ collectionSlug, Header, drawerSlug, onSave }) => { const Content: React.FC<DocumentDrawerProps> = ({ collectionSlug, Header, drawerSlug, onSave }) => {
const config = useConfig() const config = useConfig()
@@ -37,7 +40,7 @@ const Content: React.FC<DocumentDrawerProps> = ({ collectionSlug, Header, drawer
const { closeModal, modalState, toggleModal } = useModal() const { closeModal, modalState, toggleModal } = useModal()
const locale = useLocale() const locale = useLocale()
const { user } = useAuth() const { user } = useAuth()
const [internalState, setInternalState] = useState<FormState>() const [initialState, setInitialState] = useState<FormState>()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const hasInitializedState = useRef(false) const hasInitializedState = useRef(false)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
@@ -48,6 +51,7 @@ const Content: React.FC<DocumentDrawerProps> = ({ collectionSlug, Header, drawer
const { admin: { components: { views: { Edit } = {} } = {} } = {}, fields: fieldsFromConfig } = const { admin: { components: { views: { Edit } = {} } = {} } = {}, fields: fieldsFromConfig } =
collectionConfig collectionConfig
const { schemaPath } = useFieldPath()
const { id, docPermissions } = useDocumentInfo() const { id, docPermissions } = useDocumentInfo()
// The component definition could come from multiple places in the config // The component definition could come from multiple places in the config
@@ -105,7 +109,33 @@ const Content: React.FC<DocumentDrawerProps> = ({ collectionSlug, Header, drawer
(isEditing && docPermissions?.update?.permission) || (isEditing && docPermissions?.update?.permission) ||
(!isEditing && (docPermissions as CollectionPermission)?.create?.permission) (!isEditing && (docPermissions as CollectionPermission)?.create?.permission)
const isLoading = !internalState || !docPermissions || isLoadingDocument useEffect(() => {
if (!hasInitializedState.current && data) {
const getInitialState = async () => {
const result = await getFormState({
serverURL,
apiRoute: api,
body: {
id,
operation: isEditing ? 'update' : 'create',
formState: data,
docPreferences: null, // TODO: get this
schemaPath,
},
})
setInitialState(result)
}
getInitialState()
}
}, [])
const isLoading = !initialState || !docPermissions || isLoadingDocument
if (isLoading) {
return <LoadingOverlay />
}
const componentProps: EditViewProps = { const componentProps: EditViewProps = {
id, id,
@@ -144,11 +174,10 @@ const Content: React.FC<DocumentDrawerProps> = ({ collectionSlug, Header, drawer
onSave, onSave,
collectionSlug: collectionConfig.slug, collectionSlug: collectionConfig.slug,
docPermissions: docPermissions as CollectionPermission, docPermissions: docPermissions as CollectionPermission,
docPreferences: null, docPreferences: null, // TODO: get this
user, user,
updatedAt: data?.updatedAt, updatedAt: data?.updatedAt,
locale, initialState,
initializeFormState: true,
} }
return ( return (

View File

@@ -28,3 +28,4 @@ export { default as buildInitialState } from '../forms/Form'
export { default as FieldDescription } from '../forms/FieldDescription' export { default as FieldDescription } from '../forms/FieldDescription'
export { default as useField } from '../forms/useField' export { default as useField } from '../forms/useField'
export { default as Error } from '../forms/Error' export { default as Error } from '../forms/Error'
export type { BuildFormStateArgs } from '../forms/utilities/buildStateFromSchema'

View File

@@ -5,3 +5,5 @@ export type { EntityToGroup, Group } from '../utilities/groupNavItems'
export { EntityType, groupNavItems } from '../utilities/groupNavItems' export { EntityType, groupNavItems } from '../utilities/groupNavItems'
export { withMergedProps } from '../utilities/withMergedProps' export { withMergedProps } from '../utilities/withMergedProps'
export type { FieldMap, MappedField } from '../utilities/buildComponentMap/types' export type { FieldMap, MappedField } from '../utilities/buildComponentMap/types'
export { buildFieldSchemaMap } from '../utilities/buildFieldSchemaMap'
export type { FieldSchemaMap } from '../utilities/buildFieldSchemaMap/types'

View File

@@ -16,7 +16,6 @@ export const FieldPathProvider: React.FC<{
children: React.ReactNode children: React.ReactNode
}> = (props) => { }> = (props) => {
const { children, path, schemaPath } = props const { children, path, schemaPath } = props
return ( return (
<FieldPathContext.Provider <FieldPathContext.Provider
value={{ value={{

View File

@@ -39,7 +39,6 @@ import getSiblingDataFunc from './getSiblingData'
import initContextState from './initContextState' import initContextState from './initContextState'
import reduceFieldsToValues from './reduceFieldsToValues' import reduceFieldsToValues from './reduceFieldsToValues'
import useDebounce from '../../hooks/useDebounce' import useDebounce from '../../hooks/useDebounce'
import { FieldPathProvider } from '../FieldPathProvider'
const baseClass = 'form' const baseClass = 'form'
@@ -514,7 +513,7 @@ const Form: React.FC<Props> = (props) => {
<ProcessingContext.Provider value={processing}> <ProcessingContext.Provider value={processing}>
<ModifiedContext.Provider value={modified}> <ModifiedContext.Provider value={modified}>
<FormFieldsContext.Provider value={fieldsReducer}> <FormFieldsContext.Provider value={fieldsReducer}>
<FieldPathProvider path="">{children}</FieldPathProvider> {children}
</FormFieldsContext.Provider> </FormFieldsContext.Provider>
</ModifiedContext.Provider> </ModifiedContext.Provider>
</ProcessingContext.Provider> </ProcessingContext.Provider>

View File

@@ -39,6 +39,7 @@ type BlockFieldProps = UseDraggableSortableReturn & {
path: string path: string
labels: Labels labels: Labels
permissions: FieldPermissions permissions: FieldPermissions
schemaPath: string
} }
export const BlockRow: React.FC<BlockFieldProps> = ({ export const BlockRow: React.FC<BlockFieldProps> = ({
@@ -54,6 +55,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
listeners, listeners,
moveRow, moveRow,
path: parentPath, path: parentPath,
schemaPath,
permissions, permissions,
readOnly, readOnly,
removeRow, removeRow,
@@ -132,7 +134,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
onToggle={(collapsed) => setCollapse(row.id, collapsed)} onToggle={(collapsed) => setCollapse(row.id, collapsed)}
> >
<HiddenInput name={`${path}.id`} value={row.id} /> <HiddenInput name={`${path}.id`} value={row.id} />
<FieldPathProvider path={path} schemaPath={`${parentPath}.${block.slug}`}> <FieldPathProvider path={path} schemaPath={`${schemaPath}.${block.slug}`}>
<RenderFields <RenderFields
className={`${baseClass}__fields`} className={`${baseClass}__fields`}
fieldMap={block.subfields} fieldMap={block.subfields}

View File

@@ -96,6 +96,7 @@ const BlocksField: React.FC<Props> = (props) => {
valid, valid,
value, value,
path, path,
schemaPath,
} = useField<number>({ } = useField<number>({
hasRows: true, hasRows: true,
path: pathFromProps || name, path: pathFromProps || name,
@@ -248,6 +249,7 @@ const BlocksField: React.FC<Props> = (props) => {
labels={labels} labels={labels}
moveRow={moveRow} moveRow={moveRow}
path={path} path={path}
schemaPath={schemaPath}
permissions={permissions} permissions={permissions}
readOnly={readOnly} readOnly={readOnly}
removeRow={removeRow} removeRow={removeRow}

View File

@@ -1,7 +1,7 @@
import type { TFunction } from '@payloadcms/translations' import type { TFunction } from '@payloadcms/translations'
import type { User } from 'payload/auth' import type { User } from 'payload/auth'
import type { Field as FieldSchema, Data } from 'payload/types' import type { Field as FieldSchema, Data, DocumentPreferences } from 'payload/types'
import type { FormState } from '../../Form/types' import type { FormState } from '../../Form/types'
import { iterateFields } from './iterateFields' import { iterateFields } from './iterateFields'
@@ -21,6 +21,14 @@ type Args = {
user?: User | null user?: User | null
} }
export type BuildFormStateArgs = {
id?: string | number
operation?: 'create' | 'update'
docPreferences: DocumentPreferences
formState?: FormState
schemaPath: string
}
const buildStateFromSchema = async (args: Args): Promise<FormState> => { const buildStateFromSchema = async (args: Args): Promise<FormState> => {
const { id, data: fullData = {}, fieldSchema, locale, operation, preferences, t, user } = args const { id, data: fullData = {}, fieldSchema, locale, operation, preferences, t, user } = args

View File

@@ -11,7 +11,7 @@ import type { AuthContext } from './types'
import { requests } from '../../utilities/api' import { requests } from '../../utilities/api'
import useDebounce from '../../hooks/useDebounce' import useDebounce from '../../hooks/useDebounce'
import { useConfig } from '../Config' import { useConfig } from '../Config'
import { usePathname, useRouter } from 'next/navigation' import { usePathname, useRouter, useSearchParams } from 'next/navigation'
// import { useLocale } from '../Locale' // import { useLocale } from '../Locale'
const Context = createContext({} as AuthContext) const Context = createContext({} as AuthContext)
@@ -19,6 +19,7 @@ const Context = createContext({} as AuthContext)
const maxTimeoutTime = 2147483647 const maxTimeoutTime = 2147483647
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const searchParams = useSearchParams()
const [user, setUser] = useState<User | null>() const [user, setUser] = useState<User | null>()
const [tokenInMemory, setTokenInMemory] = useState<string>() const [tokenInMemory, setTokenInMemory] = useState<string>()
const [tokenExpiration, setTokenExpiration] = useState<number>() const [tokenExpiration, setTokenExpiration] = useState<number>()
@@ -209,6 +210,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (autoLoginJson?.token) { if (autoLoginJson?.token) {
setTokenAndExpiration(autoLoginJson) setTokenAndExpiration(autoLoginJson)
} }
push(searchParams.get('redirect') || admin)
} else { } else {
setUser(null) setUser(null)
revokeTokenAndExpire() revokeTokenAndExpire()

View File

@@ -0,0 +1,25 @@
import type { SanitizedConfig } from 'payload/types'
import { FieldSchemaMap } from './types'
import { traverseFields } from './traverseFields'
export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
const result: FieldSchemaMap = new Map()
config.collections.forEach((collection) => {
traverseFields({
schemaPath: collection.slug,
fields: collection.fields,
schemaMap: result,
})
})
config.globals.forEach((global) => {
traverseFields({
schemaPath: global.slug,
fields: global.fields,
schemaMap: result,
})
})
return result
}

View File

@@ -0,0 +1,53 @@
import { Field, tabHasName } from 'payload/types'
import { FieldSchemaMap } from './types'
type Args = {
fields: Field[]
schemaMap: FieldSchemaMap
schemaPath: string
}
export const traverseFields = ({ fields, schemaMap, schemaPath }: Args) => {
fields.map((field) => {
switch (field.type) {
case 'group':
case 'array':
traverseFields({
fields: field.fields,
schemaMap,
schemaPath: `${schemaPath}.${field.name}`,
})
break
case 'collapsible':
case 'row':
traverseFields({
fields: field.fields,
schemaMap,
schemaPath,
})
break
case 'blocks':
field.blocks.map((block) => {
traverseFields({
fields: block.fields,
schemaMap,
schemaPath: `${schemaPath}.${field.name}.${block.slug}`,
})
})
break
case 'tabs':
field.tabs.map((tab) => {
const tabSchemaPath = tabHasName(tab) ? `${schemaPath}.${tab.name}` : schemaPath
traverseFields({
fields: tab.fields,
schemaMap,
schemaPath: tabSchemaPath,
})
})
break
}
})
}

View File

@@ -0,0 +1,3 @@
import { Field } from 'payload/types'
export type FieldSchemaMap = Map<string, Field[]>

View File

@@ -1,63 +0,0 @@
'use server'
import { getPayload } from 'payload'
import { FormState } from '../../forms/Form/types'
import configPromise from 'payload-config'
import buildStateFromSchema from '../../forms/utilities/buildStateFromSchema'
import { reduceFieldsToValues } from '../..'
import { DocumentPreferences } from 'payload/types'
import { Locale } from 'payload/config'
import { User } from 'payload/auth'
import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
export const getFormStateFromServer = async (
args: {
collectionSlug: string
docPreferences: DocumentPreferences
locale: Locale
id?: string
operation: 'create' | 'update'
user: User
language: string
},
{
formState,
}: {
formState: FormState
},
) => {
const { collectionSlug, docPreferences, locale, id, operation, user, language } = args
const payload = await getPayload({
config: configPromise,
})
const collectionConfig = payload.collections[collectionSlug]?.config
if (!collectionConfig) {
throw new Error(`Collection with slug "${collectionSlug}" not found`)
}
const data = reduceFieldsToValues(formState, true)
const { t } = await initI18n({
translations,
language: language,
config: payload.config.i18n,
context: 'api',
})
const result = await buildStateFromSchema({
id,
data,
fieldSchema: collectionConfig.fields,
locale: locale.code,
operation,
preferences: docPreferences,
t,
user,
})
return result
}

View File

@@ -0,0 +1,26 @@
import { SanitizedConfig } from 'payload/types'
import { FormState } from '../../forms/Form/types'
import { BuildFormStateArgs } from '../..'
export const getFormState = async (args: {
serverURL: SanitizedConfig['serverURL']
apiRoute: SanitizedConfig['routes']['api']
body: BuildFormStateArgs
}): Promise<FormState> => {
const { serverURL, apiRoute, body } = args
const res = await fetch(`${serverURL}${apiRoute}/form-state`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (res.ok) {
const json = (await res.json()) as FormState
return json
}
return body?.formState
}

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment, useCallback } from 'react'
import { FormLoadingOverlayToggle } from '../../elements/Loading' import { FormLoadingOverlayToggle } from '../../elements/Loading'
import Form from '../../forms/Form' import Form from '../../forms/Form'
@@ -9,18 +9,19 @@ import { OperationProvider } from '../../providers/OperationProvider'
import { DocumentControls } from '../../elements/DocumentControls' import { DocumentControls } from '../../elements/DocumentControls'
import { DocumentFields } from '../../elements/DocumentFields' import { DocumentFields } from '../../elements/DocumentFields'
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving' import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving'
// import Meta from '../../../../utilities/Meta'
import Auth from './Auth' import Auth from './Auth'
import { SetStepNav } from './SetStepNav' import { SetStepNav } from './SetStepNav'
import { EditViewProps } from '../types' import { EditViewProps } from '../types'
import { getFormStateFromServer } from './action'
import { Upload } from './Upload' import { Upload } from './Upload'
import { useConfig } from '../../providers/Config' import { useConfig } from '../../providers/Config'
import { useTranslation } from '../../providers/Translation'
import { useComponentMap } from '../../providers/ComponentMapProvider' import { useComponentMap } from '../../providers/ComponentMapProvider'
import { SetDocumentTitle } from './SetDocumentTitle' import { SetDocumentTitle } from './SetDocumentTitle'
import { Props as FormProps, FormState } from '../../forms/Form/types'
import './index.scss' import './index.scss'
import { BuildFormStateArgs } from '../..'
import { getFormState } from './getFormState'
import { FieldPathProvider } from '../../forms/FieldPathProvider'
const baseClass = 'collection-edit' const baseClass = 'collection-edit'
@@ -32,20 +33,22 @@ export const DefaultEditView: React.FC<EditViewProps> = (props) => {
AfterDocument, AfterDocument,
AfterFields, AfterFields,
data, data,
formState: initialStateFromProps, initialState,
initializeFormState,
// isLoading, // isLoading,
onSave: onSaveFromProps, onSave: onSaveFromProps,
docPreferences,
docPermissions, docPermissions,
docPreferences,
user, user,
locale,
} = props } = props
const config = useConfig() const config = useConfig()
const { collections, globals } = config const {
serverURL,
collections,
globals,
routes: { api: apiRoute },
} = config
const { i18n } = useTranslation()
const { getFieldMap } = useComponentMap() const { getFieldMap } = useComponentMap()
const collectionConfig = const collectionConfig =
@@ -55,6 +58,8 @@ export const DefaultEditView: React.FC<EditViewProps> = (props) => {
const globalConfig = const globalConfig =
'globalSlug' in props && globals.find((global) => global.slug === props.globalSlug) 'globalSlug' in props && globals.find((global) => global.slug === props.globalSlug)
const [schemaPath] = React.useState(collectionConfig?.slug || globalConfig?.slug)
const fieldMap = getFieldMap({ const fieldMap = getFieldMap({
collectionSlug: collectionConfig?.slug, collectionSlug: collectionConfig?.slug,
globalSlug: globalConfig?.slug, globalSlug: globalConfig?.slug,
@@ -64,22 +69,6 @@ export const DefaultEditView: React.FC<EditViewProps> = (props) => {
const isEditing = 'isEditing' in props ? props.isEditing : undefined const isEditing = 'isEditing' in props ? props.isEditing : undefined
const operation = isEditing ? 'update' : 'create' const operation = isEditing ? 'update' : 'create'
const [initialState] = useState(() => {
if (initializeFormState) {
const initializedState = getFormStateFromServer.bind(null, {
collectionSlug: collectionConfig?.slug,
id: id || undefined,
locale,
language: i18n.language,
operation,
docPreferences,
user,
})({ formState: {} })
return initializedState
} else return initialStateFromProps
})
const auth = collectionConfig ? collectionConfig.auth : undefined const auth = collectionConfig ? collectionConfig.auth : undefined
const upload = collectionConfig ? collectionConfig.upload : undefined const upload = collectionConfig ? collectionConfig.upload : undefined
const hasSavePermission = 'hasSavePermission' in props ? props.hasSavePermission : undefined const hasSavePermission = 'hasSavePermission' in props ? props.hasSavePermission : undefined
@@ -137,41 +126,56 @@ export const DefaultEditView: React.FC<EditViewProps> = (props) => {
// setViewActions(defaultActions) // setViewActions(defaultActions)
// }, [id, location.pathname, collectionConfig?.admin?.components?.views?.Edit, setViewActions]) // }, [id, location.pathname, collectionConfig?.admin?.components?.views?.Edit, setViewActions])
const rebuildFormState = getFormStateFromServer.bind(null, { const onChange: FormProps['onChange'][0] = useCallback(
collectionSlug: collectionConfig?.slug, async ({ formState: prevFormState }) =>
id: id || undefined, getFormState({
locale, serverURL,
language: i18n.language, apiRoute,
operation, body: {
docPreferences, id,
user, operation,
}) formState: prevFormState,
docPreferences,
schemaPath,
},
}),
[
serverURL,
apiRoute,
collectionConfig,
globalConfig,
id,
operation,
docPreferences,
schemaPath,
],
)
return ( return (
<main className={classes}> <main className={classes}>
<OperationProvider operation={operation}> <FieldPathProvider path="" schemaPath={schemaPath}>
<Form <OperationProvider operation={operation}>
action={action} <Form
className={`${baseClass}__form`} action={action}
disabled={!hasSavePermission} className={`${baseClass}__form`}
initialState={initialState} disabled={!hasSavePermission}
method={id ? 'PATCH' : 'POST'} initialState={initialState}
beforeSubmit={[rebuildFormState]} method={id ? 'PATCH' : 'POST'}
onChange={[rebuildFormState]} onChange={[onChange]}
onSuccess={onSave} onSuccess={onSave}
> >
<FormLoadingOverlayToggle <FormLoadingOverlayToggle
action={operation} action={operation}
// formIsLoading={isLoading} // formIsLoading={isLoading}
// loadingSuffix={getTranslation(collectionConfig.labels.singular, i18n)} // loadingSuffix={getTranslation(collectionConfig.labels.singular, i18n)}
name={`collection-edit--${ name={`collection-edit--${
typeof collectionConfig?.labels?.singular === 'string' typeof collectionConfig?.labels?.singular === 'string'
? collectionConfig.labels.singular ? collectionConfig.labels.singular
: 'document' : 'document'
}`} }`}
type="withoutNav" type="withoutNav"
/> />
{/* <Meta {/* <Meta
description={`${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation( description={`${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation(
collection.labels.singular, collection.labels.singular,
i18n, i18n,
@@ -182,61 +186,62 @@ export const DefaultEditView: React.FC<EditViewProps> = (props) => {
i18n, i18n,
)}`} )}`}
/> */} /> */}
{BeforeDocument} {BeforeDocument}
{preventLeaveWithoutSaving && <LeaveWithoutSaving />} {preventLeaveWithoutSaving && <LeaveWithoutSaving />}
<SetStepNav <SetStepNav
collectionSlug={collectionConfig?.slug} collectionSlug={collectionConfig?.slug}
globalSlug={globalConfig?.slug} globalSlug={globalConfig?.slug}
useAsTitle={collectionConfig?.admin?.useAsTitle} useAsTitle={collectionConfig?.admin?.useAsTitle}
id={id} id={id}
isEditing={isEditing || false} isEditing={isEditing || false}
pluralLabel={collectionConfig?.labels?.plural} pluralLabel={collectionConfig?.labels?.plural}
/> />
<SetDocumentTitle <SetDocumentTitle
config={config} config={config}
collectionConfig={collectionConfig} collectionConfig={collectionConfig}
globalConfig={globalConfig} globalConfig={globalConfig}
/> />
<DocumentControls <DocumentControls
apiURL={apiURL} apiURL={apiURL}
slug={collectionConfig?.slug} slug={collectionConfig?.slug}
data={data} data={data}
disableActions={disableActions} disableActions={disableActions}
hasSavePermission={hasSavePermission} hasSavePermission={hasSavePermission}
id={id} id={id}
isEditing={isEditing} isEditing={isEditing}
permissions={docPermissions} permissions={docPermissions}
/> />
<DocumentFields <DocumentFields
BeforeFields={ BeforeFields={
<Fragment> <Fragment>
{auth && ( {auth && (
<Auth <Auth
className={`${baseClass}__auth`} className={`${baseClass}__auth`}
collectionSlug={collectionConfig.slug} collectionSlug={collectionConfig.slug}
email={data?.email} email={data?.email}
operation={operation} operation={operation}
readOnly={!hasSavePermission} readOnly={!hasSavePermission}
requirePassword={!isEditing} requirePassword={!isEditing}
useAPIKey={auth.useAPIKey} useAPIKey={auth.useAPIKey}
verify={auth.verify} verify={auth.verify}
/> />
)} )}
{upload && ( {upload && (
<Upload <Upload
uploadConfig={upload} uploadConfig={upload}
collectionSlug={collectionConfig.slug} collectionSlug={collectionConfig.slug}
initialState={initialState} initialState={initialState}
/> />
)} )}
</Fragment> </Fragment>
} }
fieldMap={fieldMap} fieldMap={fieldMap}
AfterFields={AfterFields} AfterFields={AfterFields}
/> />
{AfterDocument} {AfterDocument}
</Form> </Form>
</OperationProvider> </OperationProvider>
</FieldPathProvider>
</main> </main>
) )
} }

View File

@@ -1,7 +1,6 @@
import type { CollectionPermission, GlobalPermission, Permissions, User } from 'payload/auth' import type { CollectionPermission, GlobalPermission, Permissions, User } from 'payload/auth'
import type { Document, DocumentPreferences, Payload, SanitizedConfig } from 'payload/types' import type { Document, DocumentPreferences, Payload, SanitizedConfig } from 'payload/types'
import type { FormState } from '../forms/Form/types' import type { FormState } from '../forms/Form/types'
import type { Locale } from 'payload/config'
import { I18n } from '@payloadcms/translations' import { I18n } from '@payloadcms/translations'
export type EditViewProps = ( export type EditViewProps = (
@@ -22,15 +21,13 @@ export type EditViewProps = (
action?: string action?: string
apiURL: string apiURL: string
canAccessAdmin?: boolean canAccessAdmin?: boolean
data: Document
docPreferences: DocumentPreferences docPreferences: DocumentPreferences
data: Document
// isLoading: boolean // isLoading: boolean
onSave?: (json: any) => void onSave?: (json: any) => void
updatedAt: string updatedAt: string
user: User | null | undefined user: User | null | undefined
locale: Locale initialState?: FormState
formState?: FormState
initializeFormState?: boolean
BeforeDocument?: React.ReactNode BeforeDocument?: React.ReactNode
AfterDocument?: React.ReactNode AfterDocument?: React.ReactNode
AfterFields?: React.ReactNode AfterFields?: React.ReactNode