Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
390 lines
12 KiB
Plaintext
390 lines
12 KiB
Plaintext
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider, LivePreviewProvider } from '@payloadcms/ui';
|
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
|
|
import { handleLivePreview, handlePreview } from '@payloadcms/ui/rsc';
|
|
import { isEditing as getIsEditing } from '@payloadcms/ui/shared';
|
|
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState';
|
|
import { notFound, redirect } from 'next/navigation.js';
|
|
import { isolateObjectProperty, logError } from 'payload';
|
|
import { formatAdminURL, hasAutosaveEnabled, hasDraftsEnabled } from 'payload/shared';
|
|
import React from 'react';
|
|
import { DocumentHeader } from '../../elements/DocumentHeader/index.js';
|
|
import { getPreferences } from '../../utilities/getPreferences.js';
|
|
import { NotFoundView } from '../NotFound/index.js';
|
|
import { getDocPreferences } from './getDocPreferences.js';
|
|
import { getDocumentData } from './getDocumentData.js';
|
|
import { getDocumentPermissions } from './getDocumentPermissions.js';
|
|
import { getDocumentView } from './getDocumentView.js';
|
|
import { getIsLocked } from './getIsLocked.js';
|
|
import { getMetaBySegment } from './getMetaBySegment.js';
|
|
import { getVersions } from './getVersions.js';
|
|
import { renderDocumentSlots } from './renderDocumentSlots.js';
|
|
export const generateMetadata = async args => getMetaBySegment(args);
|
|
/**
|
|
* This function is responsible for rendering
|
|
* an Edit Document view on the server for both:
|
|
* - default document edit views
|
|
* - on-demand edit views within drawers
|
|
*/
|
|
export const renderDocument = async ({
|
|
disableActions,
|
|
documentSubViewType,
|
|
drawerSlug,
|
|
importMap,
|
|
initialData,
|
|
initPageResult,
|
|
overrideEntityVisibility,
|
|
params,
|
|
redirectAfterCreate,
|
|
redirectAfterDelete,
|
|
redirectAfterDuplicate,
|
|
redirectAfterRestore,
|
|
searchParams,
|
|
versions,
|
|
viewType
|
|
}) => {
|
|
const {
|
|
collectionConfig,
|
|
docID: idFromArgs,
|
|
globalConfig,
|
|
locale,
|
|
permissions,
|
|
req,
|
|
req: {
|
|
i18n,
|
|
payload,
|
|
payload: {
|
|
config,
|
|
config: {
|
|
routes: {
|
|
admin: adminRoute,
|
|
api: apiRoute
|
|
}
|
|
}
|
|
},
|
|
user
|
|
},
|
|
visibleEntities
|
|
} = initPageResult;
|
|
const segments = Array.isArray(params?.segments) ? params.segments : [];
|
|
const collectionSlug = collectionConfig?.slug || undefined;
|
|
const globalSlug = globalConfig?.slug || undefined;
|
|
let isEditing = getIsEditing({
|
|
id: idFromArgs,
|
|
collectionSlug,
|
|
globalSlug
|
|
});
|
|
// Fetch the doc required for the view
|
|
let doc = !idFromArgs && !globalSlug ? initialData || null : await getDocumentData({
|
|
id: idFromArgs,
|
|
collectionSlug,
|
|
globalSlug,
|
|
locale,
|
|
payload,
|
|
req,
|
|
segments,
|
|
user
|
|
});
|
|
if (isEditing && !doc) {
|
|
// If it's a collection document that doesn't exist, redirect to collection list
|
|
if (collectionSlug) {
|
|
const redirectURL = formatAdminURL({
|
|
adminRoute,
|
|
path: `/collections/${collectionSlug}?notFound=${encodeURIComponent(idFromArgs)}`
|
|
});
|
|
redirect(redirectURL);
|
|
} else {
|
|
// For globals or other cases, keep the 404 behavior
|
|
throw new Error('not-found');
|
|
}
|
|
}
|
|
const isTrashedDoc = Boolean(doc && 'deletedAt' in doc && typeof doc?.deletedAt === 'string');
|
|
// CRITICAL FIX FOR TRANSACTION RACE CONDITION:
|
|
// When running parallel operations with Promise.all, if they share the same req object
|
|
// and one operation calls initTransaction() which MUTATES req.transactionID, that mutation
|
|
// is visible to all parallel operations. This causes:
|
|
// 1. Operation A (e.g., getDocumentPermissions → docAccessOperation) calls initTransaction()
|
|
// which sets req.transactionID = Promise, then resolves it to a UUID
|
|
// 2. Operation B (e.g., getIsLocked) running in parallel receives the SAME req with the mutated transactionID
|
|
// 3. Operation A (does not even know that Operation B even exists and is stil using the transactionID) commits/ends its transaction
|
|
// 4. Operation B tries to use the now-expired session → MongoExpiredSessionError!
|
|
//
|
|
// Solution: Use isolateObjectProperty to create a Proxy that isolates the 'transactionID' property.
|
|
// This allows each operation to have its own transactionID without affecting the parent req.
|
|
// If parent req already has a transaction, preserve it (don't isolate), since this
|
|
// issue only arises when one of the operations calls initTransaction() themselves -
|
|
// because then, that operation will also try to commit/end the transaction itself.
|
|
// If the transactionID is already set, the parallel operations will not try to
|
|
// commit/end the transaction themselves, so we don't need to isolate the
|
|
// transactionID property.
|
|
const reqForPermissions = req.transactionID ? req : isolateObjectProperty(req, 'transactionID');
|
|
const reqForLockCheck = req.transactionID ? req : isolateObjectProperty(req, 'transactionID');
|
|
const [docPreferences, {
|
|
docPermissions,
|
|
hasPublishPermission,
|
|
hasSavePermission
|
|
}, {
|
|
currentEditor,
|
|
isLocked,
|
|
lastUpdateTime
|
|
}, entityPreferences] = await Promise.all([
|
|
// Get document preferences
|
|
getDocPreferences({
|
|
id: idFromArgs,
|
|
collectionSlug,
|
|
globalSlug,
|
|
payload,
|
|
user
|
|
}),
|
|
// Get permissions - isolated transactionID prevents cross-contamination
|
|
getDocumentPermissions({
|
|
id: idFromArgs,
|
|
collectionConfig,
|
|
data: doc,
|
|
globalConfig,
|
|
req: reqForPermissions
|
|
}),
|
|
// Fetch document lock state - isolated transactionID prevents cross-contamination
|
|
getIsLocked({
|
|
id: idFromArgs,
|
|
collectionConfig,
|
|
globalConfig,
|
|
isEditing,
|
|
req: reqForLockCheck
|
|
}),
|
|
// get entity preferences
|
|
getPreferences(collectionSlug ? `collection-${collectionSlug}` : `global-${globalSlug}`, payload, req.user.id, req.user.collection)]);
|
|
const operation = collectionSlug && idFromArgs || globalSlug ? 'update' : 'create';
|
|
const [{
|
|
hasPublishedDoc,
|
|
mostRecentVersionIsAutosaved,
|
|
unpublishedVersionCount,
|
|
versionCount
|
|
}, {
|
|
state: formState
|
|
}] = await Promise.all([getVersions({
|
|
id: idFromArgs,
|
|
collectionConfig,
|
|
doc,
|
|
docPermissions,
|
|
globalConfig,
|
|
locale: locale?.code,
|
|
payload,
|
|
user
|
|
}), buildFormState({
|
|
id: idFromArgs,
|
|
collectionSlug,
|
|
data: doc,
|
|
docPermissions,
|
|
docPreferences,
|
|
fallbackLocale: false,
|
|
globalSlug,
|
|
locale: locale?.code,
|
|
operation,
|
|
readOnly: isTrashedDoc || isLocked,
|
|
renderAllFields: true,
|
|
req,
|
|
schemaPath: collectionSlug || globalSlug,
|
|
skipValidation: true
|
|
})]);
|
|
const documentViewServerProps = {
|
|
doc,
|
|
hasPublishedDoc,
|
|
i18n,
|
|
initPageResult,
|
|
locale,
|
|
params,
|
|
payload,
|
|
permissions,
|
|
routeSegments: segments,
|
|
searchParams,
|
|
user,
|
|
versions
|
|
};
|
|
if (!overrideEntityVisibility && (collectionSlug && !visibleEntities?.collections?.find(visibleSlug => visibleSlug === collectionSlug) || globalSlug && !visibleEntities?.globals?.find(visibleSlug => visibleSlug === globalSlug))) {
|
|
throw new Error('not-found');
|
|
}
|
|
const formattedParams = new URLSearchParams();
|
|
if (hasDraftsEnabled(collectionConfig || globalConfig)) {
|
|
formattedParams.append('draft', 'true');
|
|
}
|
|
if (locale?.code) {
|
|
formattedParams.append('locale', locale.code);
|
|
}
|
|
const apiQueryParams = `?${formattedParams.toString()}`;
|
|
const apiURL = formatAdminURL({
|
|
apiRoute,
|
|
path: collectionSlug ? `/${collectionSlug}/${idFromArgs}${apiQueryParams}` : globalSlug ? `/${globalSlug}${apiQueryParams}` : ''
|
|
});
|
|
let View = null;
|
|
let showHeader = true;
|
|
const RootViewOverride = collectionConfig?.admin?.components?.views?.edit?.root && 'Component' in collectionConfig.admin.components.views.edit.root ? collectionConfig?.admin?.components?.views?.edit?.root?.Component : globalConfig?.admin?.components?.views?.edit?.root && 'Component' in globalConfig.admin.components.views.edit.root ? globalConfig?.admin?.components?.views?.edit?.root?.Component : null;
|
|
if (RootViewOverride) {
|
|
View = RootViewOverride;
|
|
showHeader = false;
|
|
} else {
|
|
({
|
|
View
|
|
} = getDocumentView({
|
|
collectionConfig,
|
|
config,
|
|
docPermissions,
|
|
globalConfig,
|
|
routeSegments: segments
|
|
}));
|
|
}
|
|
if (!View) {
|
|
View = NotFoundView;
|
|
}
|
|
/**
|
|
* Handle case where autoSave is enabled and the document is being created
|
|
* => create document and redirect
|
|
*/
|
|
const shouldAutosave = hasSavePermission && hasAutosaveEnabled(collectionConfig || globalConfig);
|
|
const validateDraftData = collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate;
|
|
let id = idFromArgs;
|
|
if (shouldAutosave && !validateDraftData && !idFromArgs && collectionSlug) {
|
|
doc = await payload.create({
|
|
collection: collectionSlug,
|
|
data: initialData || {},
|
|
depth: 0,
|
|
draft: true,
|
|
fallbackLocale: false,
|
|
locale: locale?.code,
|
|
req,
|
|
user
|
|
});
|
|
if (doc?.id) {
|
|
id = doc.id;
|
|
isEditing = getIsEditing({
|
|
id: doc.id,
|
|
collectionSlug,
|
|
globalSlug
|
|
});
|
|
if (!drawerSlug && redirectAfterCreate !== false) {
|
|
const redirectURL = formatAdminURL({
|
|
adminRoute,
|
|
path: `/collections/${collectionSlug}/${doc.id}`
|
|
});
|
|
redirect(redirectURL);
|
|
}
|
|
} else {
|
|
throw new Error('not-found');
|
|
}
|
|
}
|
|
const documentSlots = renderDocumentSlots({
|
|
id,
|
|
collectionConfig,
|
|
globalConfig,
|
|
hasSavePermission,
|
|
locale,
|
|
permissions,
|
|
req
|
|
});
|
|
// Extract Description from documentSlots to pass to DocumentHeader
|
|
const {
|
|
Description
|
|
} = documentSlots;
|
|
const clientProps = {
|
|
formState,
|
|
...documentSlots,
|
|
documentSubViewType,
|
|
viewType
|
|
};
|
|
const {
|
|
isLivePreviewEnabled,
|
|
livePreviewConfig,
|
|
livePreviewURL
|
|
} = await handleLivePreview({
|
|
collectionSlug,
|
|
config,
|
|
data: doc,
|
|
globalSlug,
|
|
operation,
|
|
req
|
|
});
|
|
const {
|
|
isPreviewEnabled,
|
|
previewURL
|
|
} = await handlePreview({
|
|
collectionSlug,
|
|
config,
|
|
data: doc,
|
|
globalSlug,
|
|
operation,
|
|
req
|
|
});
|
|
return {
|
|
data: doc,
|
|
Document: /*#__PURE__*/_jsx(DocumentInfoProvider, {
|
|
apiURL: apiURL,
|
|
collectionSlug: collectionConfig?.slug,
|
|
currentEditor: currentEditor,
|
|
disableActions: disableActions ?? false,
|
|
docPermissions: docPermissions,
|
|
globalSlug: globalConfig?.slug,
|
|
hasPublishedDoc: hasPublishedDoc,
|
|
hasPublishPermission: hasPublishPermission,
|
|
hasSavePermission: hasSavePermission,
|
|
id: id,
|
|
initialData: doc,
|
|
initialState: formState,
|
|
isEditing: isEditing,
|
|
isLocked: isLocked,
|
|
isTrashed: isTrashedDoc,
|
|
lastUpdateTime: lastUpdateTime,
|
|
mostRecentVersionIsAutosaved: mostRecentVersionIsAutosaved,
|
|
redirectAfterCreate: redirectAfterCreate,
|
|
redirectAfterDelete: redirectAfterDelete,
|
|
redirectAfterDuplicate: redirectAfterDuplicate,
|
|
redirectAfterRestore: redirectAfterRestore,
|
|
unpublishedVersionCount: unpublishedVersionCount,
|
|
versionCount: versionCount,
|
|
children: /*#__PURE__*/_jsxs(LivePreviewProvider, {
|
|
breakpoints: livePreviewConfig?.breakpoints,
|
|
isLivePreviewEnabled: isLivePreviewEnabled && operation !== 'create',
|
|
isLivePreviewing: Boolean(entityPreferences?.value?.editViewType === 'live-preview' && livePreviewURL),
|
|
isPreviewEnabled: Boolean(isPreviewEnabled),
|
|
previewURL: previewURL,
|
|
typeofLivePreviewURL: typeof livePreviewConfig?.url,
|
|
url: livePreviewURL,
|
|
children: [showHeader && !drawerSlug && /*#__PURE__*/_jsx(DocumentHeader, {
|
|
AfterHeader: Description,
|
|
collectionConfig: collectionConfig,
|
|
globalConfig: globalConfig,
|
|
permissions: permissions,
|
|
req: req
|
|
}), /*#__PURE__*/_jsx(HydrateAuthProvider, {
|
|
permissions: permissions
|
|
}), /*#__PURE__*/_jsx(EditDepthProvider, {
|
|
children: RenderServerComponent({
|
|
clientProps,
|
|
Component: View,
|
|
importMap,
|
|
serverProps: documentViewServerProps
|
|
})
|
|
})]
|
|
})
|
|
}, locale?.code)
|
|
};
|
|
};
|
|
export async function DocumentView(props) {
|
|
try {
|
|
const {
|
|
Document: RenderedDocument
|
|
} = await renderDocument(props);
|
|
return RenderedDocument;
|
|
} catch (error) {
|
|
if (error?.message === 'NEXT_REDIRECT') {
|
|
throw error;
|
|
}
|
|
logError({
|
|
err: error,
|
|
payload: props.initPageResult.req.payload
|
|
});
|
|
if (error.message === 'not-found') {
|
|
notFound();
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=index.js.map |