fix(next): establishes pattern for preview urls (#5581)

This commit is contained in:
Jacob Fletcher
2024-04-01 17:30:49 -04:00
committed by GitHub
parent 037ed3cd54
commit 799370f753
15 changed files with 234 additions and 130 deletions

View File

@@ -0,0 +1,45 @@
import httpStatus from 'http-status'
import { findByIDOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
import { routeError } from '../routeError.js'
export const preview: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const result = await findByIDOperation({
id,
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
req,
})
let previewURL: string
const generatePreviewURL = req.payload.config.collections.find(
(config) => config.slug === collection.config.slug,
)?.admin?.preview
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
})
} catch (err) {
routeError({
collection,
err,
req,
})
}
}
return Response.json(previewURL, {
status: httpStatus.OK,
})
}

View File

@@ -0,0 +1,44 @@
import httpStatus from 'http-status'
import { findOneOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
import type { GlobalRouteHandler } from '../types.js'
import { routeError } from '../routeError.js'
export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const result = await findOneOperation({
slug: globalConfig.slug,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
globalConfig,
req,
})
let previewURL: string
const generatePreviewURL = req.payload.config.globals.find(
(config) => config.slug === globalConfig.slug,
)?.admin?.preview
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
})
} catch (err) {
routeError({
err,
req,
})
}
}
return Response.json(previewURL, {
status: httpStatus.OK,
})
}

View File

@@ -34,6 +34,7 @@ import { find } from './collections/find.js'
import { findByID } from './collections/findByID.js' import { findByID } from './collections/findByID.js'
import { findVersionByID } from './collections/findVersionByID.js' import { findVersionByID } from './collections/findVersionByID.js'
import { findVersions } from './collections/findVersions.js' import { findVersions } from './collections/findVersions.js'
import { preview as previewCollection } from './collections/preview.js'
import { restoreVersion } from './collections/restoreVersion.js' import { restoreVersion } from './collections/restoreVersion.js'
import { update } from './collections/update.js' import { update } from './collections/update.js'
import { updateByID } from './collections/updateByID.js' import { updateByID } from './collections/updateByID.js'
@@ -42,6 +43,7 @@ import { docAccess as docAccessGlobal } from './globals/docAccess.js'
import { findOne } from './globals/findOne.js' import { findOne } from './globals/findOne.js'
import { findVersionByID as findVersionByIdGlobal } from './globals/findVersionByID.js' import { findVersionByID as findVersionByIdGlobal } from './globals/findVersionByID.js'
import { findVersions as findVersionsGlobal } from './globals/findVersions.js' import { findVersions as findVersionsGlobal } from './globals/findVersions.js'
import { preview as previewGlobal } from './globals/preview.js'
import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js' import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js'
import { update as updateGlobal } from './globals/update.js' import { update as updateGlobal } from './globals/update.js'
import { routeError } from './routeError.js' import { routeError } from './routeError.js'
@@ -60,6 +62,7 @@ const endpoints = {
getFile, getFile,
init, init,
me, me,
preview: previewCollection,
versions: findVersions, versions: findVersions,
}, },
PATCH: { PATCH: {
@@ -88,6 +91,7 @@ const endpoints = {
'doc-versions': findVersionsGlobal, 'doc-versions': findVersionsGlobal,
'doc-versions-by-id': findVersionByIdGlobal, 'doc-versions-by-id': findVersionByIdGlobal,
findOne, findOne,
preview: previewGlobal,
}, },
POST: { POST: {
'doc-access': docAccessGlobal, 'doc-access': docAccessGlobal,
@@ -171,6 +175,7 @@ export const GET =
endpoints: req.payload.config.endpoints, endpoints: req.payload.config.endpoints,
request, request,
}) })
if (disableEndpoints) return disableEndpoints if (disableEndpoints) return disableEndpoints
collection = req.payload.collections?.[slug1] collection = req.payload.collections?.[slug1]
@@ -212,10 +217,16 @@ export const GET =
if (slug2 === 'file') { if (slug2 === 'file') {
// /:collection/file/:filename // /:collection/file/:filename
res = await endpoints.collection.GET.getFile({ collection, filename: slug3, req }) res = await endpoints.collection.GET.getFile({ collection, filename: slug3, req })
} else if (slug3 in endpoints.collection.GET) {
// /:collection/:id/preview
res = await (endpoints.collection.GET[slug3] as CollectionRouteHandlerWithID)({
id: slug2,
collection,
req,
})
} else if (`doc-${slug2}-by-id` in endpoints.collection.GET) { } else if (`doc-${slug2}-by-id` in endpoints.collection.GET) {
// /:collection/access/:id // /:collection/access/:id
// /:collection/versions/:id // /:collection/versions/:id
res = await ( res = await (
endpoints.collection.GET[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID endpoints.collection.GET[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
)({ id: slug3, collection, req }) )({ id: slug3, collection, req })
@@ -229,6 +240,7 @@ export const GET =
endpoints: globalConfig.endpoints, endpoints: globalConfig.endpoints,
request, request,
}) })
if (disableEndpoints) return disableEndpoints if (disableEndpoints) return disableEndpoints
const customEndpointResponse = await handleCustomEndpoints({ const customEndpointResponse = await handleCustomEndpoints({
@@ -236,6 +248,7 @@ export const GET =
entitySlug: `${slug1}/${slug2}`, entitySlug: `${slug1}/${slug2}`,
payloadRequest: req, payloadRequest: req,
}) })
if (customEndpointResponse) return customEndpointResponse if (customEndpointResponse) return customEndpointResponse
switch (slug.length) { switch (slug.length) {
@@ -244,9 +257,16 @@ export const GET =
res = await endpoints.global.GET.findOne({ globalConfig, req }) res = await endpoints.global.GET.findOne({ globalConfig, req })
break break
case 3: case 3:
if (`doc-${slug3}` in endpoints.global.GET) { if (slug3 in endpoints.global.GET) {
// /globals/:slug/preview
res = await (endpoints.global.GET[slug3] as GlobalRouteHandler)({
globalConfig,
req,
})
} else if (`doc-${slug3}` in endpoints.global.GET) {
// /globals/:slug/access // /globals/:slug/access
// /globals/:slug/versions // /globals/:slug/versions
// /globals/:slug/preview
res = await (endpoints.global.GET?.[`doc-${slug3}`] as GlobalRouteHandler)({ res = await (endpoints.global.GET?.[`doc-${slug3}`] as GlobalRouteHandler)({
globalConfig, globalConfig,
req, req,

View File

@@ -1,11 +1 @@
export type CustomPreviewButtonProps = React.ComponentType< export type CustomPreviewButton = React.ComponentType
DefaultPreviewButtonProps & {
DefaultButton: React.ComponentType<DefaultPreviewButtonProps>
}
>
export type DefaultPreviewButtonProps = {
disabled: boolean
label: string
preview: () => void
}

View File

@@ -1 +1 @@
export type CustomPublishButtonProps = React.ComponentType export type CustomPublishButton = React.ComponentType

View File

@@ -1 +1 @@
export type CustomSaveButtonProps = React.ComponentType export type CustomSaveButton = React.ComponentType

View File

@@ -1 +1 @@
export type CustomSaveDraftButtonProps = React.ComponentType export type CustomSaveDraftButton = React.ComponentType

View File

@@ -2,11 +2,10 @@ export type { RichTextAdapter, RichTextFieldProps } from './RichText.js'
export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js' export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js'
export type { ConditionalDateProps } from './elements/DatePicker.js' export type { ConditionalDateProps } from './elements/DatePicker.js'
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js' export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
export type { DefaultPreviewButtonProps } from './elements/PreviewButton.js' export type { CustomPreviewButton } from './elements/PreviewButton.js'
export type { CustomPreviewButtonProps } from './elements/PreviewButton.js' export type { CustomPublishButton } from './elements/PublishButton.js'
export type { CustomPublishButtonProps } from './elements/PublishButton.js' export type { CustomSaveButton } from './elements/SaveButton.js'
export type { CustomSaveButtonProps } from './elements/SaveButton.js' export type { CustomSaveDraftButton } from './elements/SaveDraftButton.js'
export type { CustomSaveDraftButtonProps } from './elements/SaveDraftButton.js'
export type { export type {
DocumentTab, DocumentTab,
DocumentTabComponent, DocumentTabComponent,

View File

@@ -2,10 +2,10 @@ import type { GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType } from '
import type { DeepRequired } from 'ts-essentials' import type { DeepRequired } from 'ts-essentials'
import type { import type {
CustomPreviewButtonProps, CustomPreviewButton,
CustomPublishButtonProps, CustomPublishButton,
CustomSaveButtonProps, CustomSaveButton,
CustomSaveDraftButtonProps, CustomSaveDraftButton,
} from '../../admin/types.js' } from '../../admin/types.js'
import type { Auth, ClientUser, IncomingAuthType } from '../../auth/types.js' import type { Auth, ClientUser, IncomingAuthType } from '../../auth/types.js'
import type { import type {
@@ -211,23 +211,23 @@ export type CollectionAdminOptions = {
/** /**
* Replaces the "Preview" button * Replaces the "Preview" button
*/ */
PreviewButton?: CustomPreviewButtonProps PreviewButton?: CustomPreviewButton
/** /**
* Replaces the "Publish" button * Replaces the "Publish" button
* + drafts must be enabled * + drafts must be enabled
*/ */
PublishButton?: CustomPublishButtonProps PublishButton?: CustomPublishButton
/** /**
* Replaces the "Save" button * Replaces the "Save" button
* + drafts must be disabled * + drafts must be disabled
*/ */
SaveButton?: CustomSaveButtonProps SaveButton?: CustomSaveButton
/** /**
* Replaces the "Save Draft" button * Replaces the "Save Draft" button
* + drafts must be enabled * + drafts must be enabled
* + autosave must be disabled * + autosave must be disabled
*/ */
SaveDraftButton?: CustomSaveDraftButtonProps SaveDraftButton?: CustomSaveDraftButton
} }
views?: { views?: {
/** /**

View File

@@ -2,10 +2,10 @@ import type { GraphQLNonNull, GraphQLObjectType } from 'graphql'
import type { DeepRequired } from 'ts-essentials' import type { DeepRequired } from 'ts-essentials'
import type { import type {
CustomPreviewButtonProps, CustomPreviewButton,
CustomPublishButtonProps, CustomPublishButton,
CustomSaveButtonProps, CustomSaveButton,
CustomSaveDraftButtonProps, CustomSaveDraftButton,
} from '../../admin/types.js' } from '../../admin/types.js'
import type { User } from '../../auth/types.js' import type { User } from '../../auth/types.js'
import type { import type {
@@ -79,23 +79,23 @@ export type GlobalAdminOptions = {
/** /**
* Replaces the "Preview" button * Replaces the "Preview" button
*/ */
PreviewButton?: CustomPreviewButtonProps PreviewButton?: CustomPreviewButton
/** /**
* Replaces the "Publish" button * Replaces the "Publish" button
* + drafts must be enabled * + drafts must be enabled
*/ */
PublishButton?: CustomPublishButtonProps PublishButton?: CustomPublishButton
/** /**
* Replaces the "Save" button * Replaces the "Save" button
* + drafts must be disabled * + drafts must be disabled
*/ */
SaveButton?: CustomSaveButtonProps SaveButton?: CustomSaveButton
/** /**
* Replaces the "Save Draft" button * Replaces the "Save Draft" button
* + drafts must be enabled * + drafts must be enabled
* + autosave must be disabled * + autosave must be disabled
*/ */
SaveDraftButton?: CustomSaveDraftButtonProps SaveDraftButton?: CustomSaveDraftButton
} }
views?: { views?: {
/** /**

View File

@@ -154,15 +154,9 @@ export const DocumentControls: React.FC<{
</div> </div>
<div className={`${baseClass}__controls-wrapper`}> <div className={`${baseClass}__controls-wrapper`}>
<div className={`${baseClass}__controls`}> <div className={`${baseClass}__controls`}>
{/* {(collectionConfig?.admin?.preview || globalConfig?.admin?.preview) && ( {componentMap?.isPreviewEnabled && (
<PreviewButton <PreviewButton CustomComponent={componentMap.PreviewButton} />
CustomComponent={ )}
collectionConfig?.admin?.components?.edit?.PreviewButton ||
globalConfig?.admin?.components?.elements?.PreviewButton
}
generatePreviewURL={collectionConfig?.admin?.preview || globalConfig?.admin?.preview}
/>
)} */}
{hasSavePermission && ( {hasSavePermission && (
<React.Fragment> <React.Fragment>
{collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts ? ( {collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts ? (

View File

@@ -1,31 +1,24 @@
'use client' 'use client'
import type { GeneratePreviewURL } from 'payload/config' import React from 'react'
import type { CustomPreviewButtonProps, DefaultPreviewButtonProps } from 'payload/types'
import React, { useCallback, useRef, useState } from 'react'
import { toast } from 'react-toastify'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useLocale } from '../../providers/Locale/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { Button } from '../Button/index.js' import { Button } from '../Button/index.js'
import { usePreviewURL } from './usePreviewURL.js'
const baseClass = 'preview-btn' const baseClass = 'preview-btn'
const DefaultPreviewButton: React.FC<DefaultPreviewButtonProps> = ({ const DefaultPreviewButton: React.FC = () => {
disabled, const { generatePreviewURL, label } = usePreviewURL()
label,
preview,
}) => {
return ( return (
<Button <Button
buttonStyle="secondary" buttonStyle="secondary"
className={baseClass} className={baseClass}
disabled={disabled} // disabled={disabled}
onClick={preview} onClick={() =>
generatePreviewURL({
openPreviewWindow: true,
})
}
size="small" size="small"
> >
{label} {label}
@@ -33,66 +26,12 @@ const DefaultPreviewButton: React.FC<DefaultPreviewButtonProps> = ({
) )
} }
export type PreviewButtonProps = { type Props = {
CustomComponent?: CustomPreviewButtonProps CustomComponent?: React.ReactNode
generatePreviewURL?: GeneratePreviewURL
} }
export const PreviewButton: React.FC<PreviewButtonProps> = ({ export const PreviewButton: React.FC<Props> = ({ CustomComponent }) => {
CustomComponent, if (CustomComponent) return CustomComponent
generatePreviewURL,
}) => {
const { id, collectionSlug, globalSlug } = useDocumentInfo()
const [isLoading, setIsLoading] = useState(false) return <DefaultPreviewButton />
const { code: locale } = useLocale()
const { token } = useAuth()
const {
routes: { api },
serverURL,
} = useConfig()
const { t } = useTranslation()
const isGeneratingPreviewURL = useRef(false)
// we need to regenerate the preview URL every time the button is clicked
// to do this we need to fetch the document data fresh from the API
// this will ensure the latest data is used when generating the preview URL
const preview = useCallback(async () => {
if (!generatePreviewURL || isGeneratingPreviewURL.current) return
isGeneratingPreviewURL.current = true
try {
setIsLoading(true)
let url = `${serverURL}${api}`
if (collectionSlug) url = `${url}/${collectionSlug}/${id}`
if (globalSlug) url = `${url}/globals/${globalSlug}`
const data = await fetch(`${url}?draft=true&locale=${locale}&fallback-locale=null`).then(
(res) => res.json(),
)
const previewURL = await generatePreviewURL(data, { locale, token })
if (!previewURL) throw new Error()
setIsLoading(false)
isGeneratingPreviewURL.current = false
window.open(previewURL, '_blank')
} catch (err) {
setIsLoading(false)
isGeneratingPreviewURL.current = false
toast.error(t('error:previewing'))
}
}, [serverURL, api, collectionSlug, globalSlug, id, generatePreviewURL, locale, token, t])
return (
<RenderCustomComponent
CustomComponent={CustomComponent}
DefaultComponent={DefaultPreviewButton}
componentProps={{
DefaultButton: DefaultPreviewButton,
disabled: isLoading || !generatePreviewURL,
label: isLoading ? t('general:loading') : t('version:preview'),
preview,
}}
/>
)
} }

View File

@@ -0,0 +1,70 @@
'use client'
import { useCallback, useRef, useState } from 'react'
import { toast } from 'react-toastify'
import { useConfig } from '../../providers/Config/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useLocale } from '../../providers/Locale/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
export const usePreviewURL = (): {
generatePreviewURL: ({ openPreviewWindow }: { openPreviewWindow?: boolean }) => void
isLoading: boolean
label: string
previewURL: string
} => {
const { id, collectionSlug, globalSlug } = useDocumentInfo()
const [isLoading, setIsLoading] = useState(false)
const [previewURL, setPreviewURL] = useState('')
const { code: locale } = useLocale()
const {
routes: { api },
serverURL,
} = useConfig()
const { t } = useTranslation()
const isGeneratingPreviewURL = useRef(false)
// we need to regenerate the preview URL every time the button is clicked
// to do this we need to fetch the document data fresh from the API
// this will ensure the latest data is used when generating the preview URL
const generatePreviewURL = useCallback(
async ({ openPreviewWindow = false }) => {
if (isGeneratingPreviewURL.current) return
isGeneratingPreviewURL.current = true
try {
setIsLoading(true)
let url = `${serverURL}${api}`
if (collectionSlug) url = `${url}/${collectionSlug}/${id}/preview`
if (globalSlug) url = `${url}/globals/${globalSlug}/preview`
const res = await fetch(`${url}${locale ? `?locale=${locale}` : ''}`)
if (!res.ok) throw new Error()
const newPreviewURL = await res.json()
if (!newPreviewURL) throw new Error()
setPreviewURL(newPreviewURL)
setIsLoading(false)
isGeneratingPreviewURL.current = false
if (openPreviewWindow) window.open(newPreviewURL, '_blank')
} catch (err) {
setIsLoading(false)
isGeneratingPreviewURL.current = false
toast.error(t('error:previewing'))
}
},
[serverURL, api, collectionSlug, globalSlug, id, locale, t],
)
return {
generatePreviewURL,
isLoading,
label: isLoading ? t('general:loading') : t('version:preview'),
previewURL,
}
}

View File

@@ -70,8 +70,8 @@ export const buildComponentMap = (args: {
const SaveDraftButtonComponent = collectionConfig?.admin?.components?.edit?.SaveDraftButton const SaveDraftButtonComponent = collectionConfig?.admin?.components?.edit?.SaveDraftButton
const SaveDraftButton = SaveDraftButtonComponent ? <SaveDraftButtonComponent /> : undefined const SaveDraftButton = SaveDraftButtonComponent ? <SaveDraftButtonComponent /> : undefined
/* const PreviewButtonComponent = collectionConfig?.admin?.components?.edit?.PreviewButton const PreviewButtonComponent = collectionConfig?.admin?.components?.edit?.PreviewButton
const PreviewButton = PreviewButtonComponent ? <PreviewButtonComponent /> : undefined */ const PreviewButton = PreviewButtonComponent ? <PreviewButtonComponent /> : undefined
const PublishButtonComponent = collectionConfig?.admin?.components?.edit?.PublishButton const PublishButtonComponent = collectionConfig?.admin?.components?.edit?.PublishButton
const PublishButton = PublishButtonComponent ? <PublishButtonComponent /> : undefined const PublishButton = PublishButtonComponent ? <PublishButtonComponent /> : undefined
@@ -111,7 +111,7 @@ export const buildComponentMap = (args: {
BeforeListTable, BeforeListTable,
Edit: <Edit collectionSlug={collectionConfig.slug} />, Edit: <Edit collectionSlug={collectionConfig.slug} />,
List: <List collectionSlug={collectionConfig.slug} />, List: <List collectionSlug={collectionConfig.slug} />,
/* PreviewButton, */ PreviewButton,
PublishButton, PublishButton,
SaveButton, SaveButton,
SaveDraftButton, SaveDraftButton,
@@ -123,6 +123,7 @@ export const buildComponentMap = (args: {
fieldSchema: fields, fieldSchema: fields,
readOnly: readOnlyOverride, readOnly: readOnlyOverride,
}), }),
isPreviewEnabled: !!collectionConfig?.admin?.preview,
} }
return { return {
@@ -143,8 +144,8 @@ export const buildComponentMap = (args: {
const SaveDraftButton = globalConfig?.admin?.components?.elements?.SaveDraftButton const SaveDraftButton = globalConfig?.admin?.components?.elements?.SaveDraftButton
const SaveDraftButtonComponent = SaveDraftButton ? <SaveDraftButton /> : undefined const SaveDraftButtonComponent = SaveDraftButton ? <SaveDraftButton /> : undefined
/* const PreviewButton = globalConfig?.admin?.components?.elements?.PreviewButton const PreviewButton = globalConfig?.admin?.components?.elements?.PreviewButton
const PreviewButtonComponent = PreviewButton ? <PreviewButton /> : undefined */ const PreviewButtonComponent = PreviewButton ? <PreviewButton /> : undefined
const PublishButton = globalConfig?.admin?.components?.elements?.PublishButton const PublishButton = globalConfig?.admin?.components?.elements?.PublishButton
const PublishButtonComponent = PublishButton ? <PublishButton /> : undefined const PublishButtonComponent = PublishButton ? <PublishButton /> : undefined
@@ -164,7 +165,7 @@ export const buildComponentMap = (args: {
const componentMap: GlobalComponentMap = { const componentMap: GlobalComponentMap = {
Edit: <Edit globalSlug={globalConfig.slug} />, Edit: <Edit globalSlug={globalConfig.slug} />,
/* PreviewButton: PreviewButtonComponent, */ PreviewButton: PreviewButtonComponent,
PublishButton: PublishButtonComponent, PublishButton: PublishButtonComponent,
SaveButton: SaveButtonComponent, SaveButton: SaveButtonComponent,
SaveDraftButton: SaveDraftButtonComponent, SaveDraftButton: SaveDraftButtonComponent,
@@ -176,6 +177,7 @@ export const buildComponentMap = (args: {
fieldSchema: fields, fieldSchema: fields,
readOnly: readOnlyOverride, readOnly: readOnlyOverride,
}), }),
isPreviewEnabled: !!globalConfig?.admin?.preview,
} }
return { return {

View File

@@ -103,12 +103,13 @@ export type GlobalComponentMap = ConfigComponentMapBase
export type ConfigComponentMapBase = { export type ConfigComponentMapBase = {
Edit: React.ReactNode Edit: React.ReactNode
/* PreviewButton: React.ReactNode */ PreviewButton: React.ReactNode
PublishButton: React.ReactNode PublishButton: React.ReactNode
SaveButton: React.ReactNode SaveButton: React.ReactNode
SaveDraftButton: React.ReactNode SaveDraftButton: React.ReactNode
actionsMap: ActionMap actionsMap: ActionMap
fieldMap: FieldMap fieldMap: FieldMap
isPreviewEnabled: boolean
} }
export type ComponentMap = { export type ComponentMap = {