feat: supports live preview config inheritance (#3456)

This commit is contained in:
Jacob Fletcher
2023-10-06 18:20:13 -04:00
committed by GitHub
parent 88c84ad8d6
commit b6de427f04
16 changed files with 131 additions and 94 deletions

View File

@@ -27,11 +27,13 @@ export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (prop
const { t } = useTranslation('general')
const location = useLocation()
const { routes } = useConfig()
const { versions } = useDocumentInfo()
const config = useConfig()
const documentInfo = useDocumentInfo()
const match = useRouteMatch()
const { routes } = config
const { versions } = documentInfo
let href = `${match.url}${typeof tabHref === 'string' ? tabHref : ''}`
if (typeof tabHref === 'function') {
@@ -52,7 +54,7 @@ export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (prop
? checkIsActive
: location.pathname.startsWith(href)
if (!condition || (condition && condition({ collection, global }))) {
if (!condition || (condition && condition({ collection, config, documentInfo, global }))) {
const labelToRender = typeof label === 'function' ? label({ t }) : label
const pillToRender = typeof pillLabel === 'function' ? pillLabel({ versions }) : pillLabel

View File

@@ -10,8 +10,22 @@ export const tabs: DocumentTabConfig[] = [
},
// Live Preview
{
condition: ({ collection, global }) =>
Boolean(collection?.admin?.livePreview || global?.admin?.livePreview),
condition: ({ collection, config, global }) => {
if (collection) {
return Boolean(
config?.admin?.livePreview?.collections?.includes(collection.slug) ||
collection?.admin?.livePreview,
)
}
if (global) {
return Boolean(
config?.admin?.livePreview?.globals?.includes(global.slug) || global?.admin?.livePreview,
)
}
return false
},
href: ({ match }) => `${match.url}/preview`,
isActive: ({ href, location }) => location.pathname === href,
label: ({ t }) => t('livePreview'),

View File

@@ -1,8 +1,10 @@
import type { useLocation, useRouteMatch } from 'react-router-dom'
import type { Config } from '../../../../../exports/config'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../../exports/types'
import type { useConfig } from '../../../utilities/Config'
import type { useDocumentInfo } from '../../../utilities/DocumentInfo'
import type { ContextType } from '../../../utilities/DocumentInfo/types'
export type DocumentTabProps = {
apiURL?: string
@@ -14,6 +16,8 @@ export type DocumentTabProps = {
export type DocumentTabCondition = (args: {
collection: SanitizedCollectionConfig
config: Config
documentInfo: ContextType
global: SanitizedGlobalConfig
}) => boolean

View File

@@ -14,8 +14,8 @@ export const globalCustomRoutes = (props: {
match: match<{
[key: string]: string | undefined
}>
permissions: GlobalPermission
user: User
permissions: GlobalPermission | null
user: User | null | undefined
}): React.ReactElement[] => {
const { global, match, permissions, user } = props

View File

@@ -19,11 +19,15 @@ export const GlobalRoutes: React.FC<GlobalEditViewProps> = (props) => {
const match = useRouteMatch()
const {
admin: { livePreview },
routes: { admin: adminRoute },
} = useConfig()
const { user } = useAuth()
const livePreviewEnabled =
livePreview?.globals?.some((c) => c === global.slug) || global?.admin?.livePreview
return (
<Switch>
<Route
@@ -51,13 +55,15 @@ export const GlobalRoutes: React.FC<GlobalEditViewProps> = (props) => {
<Unauthorized />
)}
</Route>
<Route
exact
key={`${global.slug}-live-preview`}
path={`${adminRoute}/globals/${global.slug}/preview`}
>
<CustomGlobalComponent view="LivePreview" {...props} />
</Route>
{livePreviewEnabled && (
<Route
exact
key={`${global.slug}-live-preview`}
path={`${adminRoute}/globals/${global.slug}/preview`}
>
<CustomGlobalComponent view="LivePreview" {...props} />
</Route>
)}
{globalCustomRoutes({
global,
match,

View File

@@ -115,13 +115,15 @@ const Preview: React.FC<
export const LivePreview: React.FC<
EditViewProps & {
livePreviewConfig?: LivePreviewConfig
popupState: ReturnType<typeof usePopupWindow>
url?: string
}
> = (props) => {
let url
const { livePreviewConfig, url } = props
let breakpoints: LivePreviewConfig['breakpoints'] = [
const breakpoints: LivePreviewConfig['breakpoints'] = [
...(livePreviewConfig?.breakpoints || []),
{
name: 'responsive',
height: '100%',
@@ -130,16 +132,6 @@ export const LivePreview: React.FC<
},
]
if ('collection' in props) {
url = props?.collection.admin.livePreview.url
breakpoints = breakpoints.concat(props?.collection.admin.livePreview.breakpoints)
}
if ('global' in props) {
url = props?.global.admin.livePreview.url
breakpoints = breakpoints.concat(props?.global.admin.livePreview.breakpoints)
}
return (
<LivePreviewProvider {...props} breakpoints={breakpoints} url={url}>
<Preview {...props} />

View File

@@ -12,6 +12,7 @@ import RenderFields from '../../forms/RenderFields'
import { filterFields } from '../../forms/RenderFields/filterFields'
import { fieldTypes } from '../../forms/field-types'
import { LeaveWithoutSaving } from '../../modals/LeaveWithoutSaving'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale'
import Meta from '../../utilities/Meta'
@@ -24,27 +25,34 @@ const baseClass = 'live-preview'
export const LivePreviewView: React.FC<EditViewProps> = (props) => {
const { i18n, t } = useTranslation('general')
const config = useConfig()
const documentInfo = useDocumentInfo()
const locale = useLocale()
let urlFromConfig: LivePreviewConfig['url']
let livePreviewConfig: LivePreviewConfig = config?.admin?.livePreview
if ('collection' in props) {
urlFromConfig = props?.collection.admin.livePreview.url
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.collection.admin.livePreview || {}),
}
}
if ('global' in props) {
urlFromConfig = props?.global.admin.livePreview.url
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.global.admin.livePreview || {}),
}
}
const url =
typeof urlFromConfig === 'function'
? urlFromConfig({
typeof livePreviewConfig?.url === 'function'
? livePreviewConfig?.url({
data: props?.data,
documentInfo,
locale,
})
: urlFromConfig
: livePreviewConfig?.url
const popupState = usePopupWindow({
eventType: 'livePreview',
@@ -134,7 +142,12 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
)}
</Gutter>
</div>
<LivePreview {...props} popupState={popupState} url={url} />
<LivePreview
{...props}
livePreviewConfig={livePreviewConfig}
popupState={popupState}
url={url}
/>
</div>
</Fragment>
)

View File

@@ -19,11 +19,15 @@ export const CollectionRoutes: React.FC<CollectionEditViewProps> = (props) => {
const match = useRouteMatch()
const {
admin: { livePreview },
routes: { admin: adminRoute },
} = useConfig()
const { user } = useAuth()
const livePreviewEnabled =
livePreview?.collections?.some((c) => c === collection.slug) || collection?.admin?.livePreview
return (
<Switch>
<Route
@@ -55,13 +59,15 @@ export const CollectionRoutes: React.FC<CollectionEditViewProps> = (props) => {
<Unauthorized />
)}
</Route>
<Route
exact
key={`${collection.slug}-live-preview`}
path={`${adminRoute}/collections/${collection.slug}/:id/preview`}
>
<CustomCollectionComponent view="LivePreview" {...props} />
</Route>
{livePreviewEnabled && (
<Route
exact
key={`${collection.slug}-live-preview`}
path={`${adminRoute}/collections/${collection.slug}/:id/preview`}
>
<CustomCollectionComponent view="LivePreview" {...props} />
</Route>
)}
{collectionCustomRoutes({
collection,
match,

View File

@@ -1,7 +1,11 @@
import joi from 'joi'
import { endpointsSchema } from '../../config/schema'
import { componentSchema, customViewSchema } from '../../config/shared/componentSchema'
import {
componentSchema,
customViewSchema,
livePreviewSchema,
} from '../../config/shared/componentSchema'
const strategyBaseSchema = joi.object().keys({
logout: joi.boolean(),
@@ -59,17 +63,7 @@ const collectionSchema = joi.object().keys({
beforeDuplicate: joi.func(),
}),
listSearchableFields: joi.array().items(joi.string()),
livePreview: joi.object({
breakpoints: joi.array().items(
joi.object({
name: joi.string(),
height: joi.alternatives().try(joi.number(), joi.string()),
label: joi.string(),
width: joi.alternatives().try(joi.number(), joi.string()),
}),
),
url: joi.alternatives().try(joi.string(), joi.func()),
}),
livePreview: joi.object(livePreviewSchema),
pagination: joi.object({
defaultLimit: joi.number(),
limits: joi.array().items(joi.number()),

View File

@@ -1,5 +1,6 @@
import joi from 'joi'
import { livePreviewSchema } from './shared/componentSchema'
import { routeSchema } from './shared/routeSchema'
const component = joi.alternatives().try(joi.object().unknown(), joi.func())
@@ -63,6 +64,11 @@ export default joi.object({
disable: joi.bool(),
inactivityRoute: joi.string(),
indexHTML: joi.string(),
livePreview: joi.object({
...livePreviewSchema,
collections: joi.array().items(joi.string()),
globals: joi.array().items(joi.string()),
}),
logoutRoute: joi.string(),
meta: joi.object().keys({
favicon: joi.string(),

View File

@@ -16,3 +16,15 @@ export const customViewSchema = joi.object({
Tab: joi.alternatives().try(documentTabSchema, componentSchema),
path: joi.string().required(),
})
export const livePreviewSchema = {
breakpoints: joi.array().items(
joi.object({
name: joi.string(),
height: joi.alternatives().try(joi.number(), joi.string()),
label: joi.string(),
width: joi.alternatives().try(joi.number(), joi.string()),
}),
),
url: joi.alternatives().try(joi.string(), joi.func()),
}

View File

@@ -439,6 +439,10 @@ export type Config = {
inactivityRoute?: string
/** Replace the entirety of the index.html file used by the Admin panel. Reference the base index.html file to ensure your replacement has the appropriate HTML elements. */
indexHTML?: string
livePreview?: LivePreviewConfig & {
collections?: string[]
globals?: string[]
}
/** The route for the logout page. */
logoutRoute?: string
/** Base meta data to use for the Admin panel. Included properties are titleSuffix, ogImage, and favicon. */

View File

@@ -1,7 +1,11 @@
import joi from 'joi'
import { endpointsSchema } from '../../config/schema'
import { componentSchema, customViewSchema } from '../../config/shared/componentSchema'
import {
componentSchema,
customViewSchema,
livePreviewSchema,
} from '../../config/shared/componentSchema'
const globalSchema = joi
.object()
@@ -41,17 +45,7 @@ const globalSchema = joi
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
hidden: joi.alternatives().try(joi.boolean(), joi.func()),
hideAPIURL: joi.boolean(),
livePreview: joi.object({
breakpoints: joi.array().items(
joi.object({
name: joi.string(),
height: joi.alternatives().try(joi.number(), joi.string()),
label: joi.string(),
width: joi.alternatives().try(joi.number(), joi.string()),
}),
),
url: joi.alternatives().try(joi.string(), joi.func()),
}),
livePreview: joi.object(livePreviewSchema),
preview: joi.func(),
}),
custom: joi.object().pattern(joi.string(), joi.any()),

View File

@@ -17,23 +17,6 @@ export const Pages: CollectionConfig = {
delete: () => true,
},
admin: {
livePreview: {
url: ({ data }) => `http://localhost:3001/${data?.slug}`,
breakpoints: [
{
label: 'Mobile',
name: 'mobile',
width: 375,
height: 667,
},
// {
// label: 'Desktop',
// name: 'desktop',
// width: 1440,
// height: 900,
// },
],
},
useAsTitle: 'title',
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
},

View File

@@ -17,17 +17,6 @@ export const Posts: CollectionConfig = {
delete: () => true,
},
admin: {
livePreview: {
url: ({ data, documentInfo }) => `http://localhost:3001/${documentInfo.slug}/${data?.slug}`,
breakpoints: [
{
label: 'Mobile',
name: 'mobile',
width: 375,
height: 667,
},
],
},
useAsTitle: 'title',
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
},

View File

@@ -19,7 +19,25 @@ import { postsPage } from './seed/posts-page'
export const pagesSlug = 'pages'
export default buildConfigWithDefaults({
admin: {},
admin: {
livePreview: {
// You can also define this per collection or per global
// The Live Preview config is inherited from the top down
url: ({ data, documentInfo }) =>
`http://localhost:3001${
documentInfo.slug !== 'pages' ? `/${documentInfo.slug}` : ''
}/${data?.slug}`,
breakpoints: [
{
label: 'Mobile',
name: 'mobile',
width: 375,
height: 667,
},
],
collections: ['pages', 'posts'],
},
},
cors: ['http://localhost:3001'],
csrf: ['http://localhost:3001'],
collections: [