feat: supports live preview config inheritance (#3456)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user