Merge branch '2.0' of github.com:payloadcms/payload into 2.0

This commit is contained in:
James
2023-10-08 16:47:47 -04:00
20 changed files with 326 additions and 187 deletions

View File

@@ -12,6 +12,10 @@ keywords: point, geolocation, geospatial, geojson, 2dsphere, config, configurati
related queries.
</Banner>
<Banner type="warning">
<strong>Note:</strong> The Point field type is currently only supported in MongoDB.
</Banner>
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/point.png"
srcDark="https://payloadcms.com/images/docs/fields/point-dark.png"

View File

@@ -3,7 +3,7 @@ title: Live Preview
label: Overview
order: 10
desc: With Live Preview you can render your front-end application directly within the Admin panel. Your changes take effect as you type. No save needed.
keywords: 'live preview', 'preview', 'live', 'iframe', 'iframe preview', 'visual editing', 'design'
keywords: live preview, preview, live, iframe, iframe preview, visual editing, design
---
**With Live Preview you can render your front-end application directly within the Admin panel. As you type, your changes take effect in real-time. No need to save a draft or publish your changes.**

View File

@@ -29,13 +29,7 @@ const AccountView: React.FC = () => {
const config = useConfig()
const {
admin: {
components: {
views: { Account: CustomAccount } = {
Account: undefined,
},
} = {},
},
admin: { components: { views: { Account: CustomAccountComponent } = {} } = {} },
collections,
routes: { api },
serverURL,
@@ -133,7 +127,9 @@ const AccountView: React.FC = () => {
return (
<RenderCustomComponent
CustomComponent={CustomAccount}
CustomComponent={
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
}
DefaultComponent={DefaultAccount}
componentProps={{
action,

View File

@@ -14,13 +14,7 @@ const Dashboard: React.FC = () => {
const [filteredGlobals, setFilteredGlobals] = useState<SanitizedGlobalConfig[]>([])
const {
admin: {
components: {
views: { Dashboard: CustomDashboard } = {
Dashboard: undefined,
},
} = {},
} = {},
admin: { components: { views: { Dashboard: CustomDashboardComponent } = {} } = {} } = {},
collections,
globals,
} = useConfig()
@@ -37,7 +31,9 @@ const Dashboard: React.FC = () => {
return (
<RenderCustomComponent
CustomComponent={CustomDashboard}
CustomComponent={
typeof CustomDashboardComponent === 'function' ? CustomDashboardComponent : undefined
}
DefaultComponent={DefaultDashboard}
componentProps={{
collections: collections.filter(

View File

@@ -2,21 +2,48 @@ import React from 'react'
import { Route } from 'react-router-dom'
import type { User } from '../../../../auth'
import type { SanitizedConfig } from '../../../../exports/config'
import type { SanitizedConfig } from '../../../../config/types'
export type adminViewType = 'Account' | 'Dashboard'
const internalViews: adminViewType[] = ['Account', 'Dashboard']
export const customRoutes = (props: {
canAccessAdmin?: boolean
customRoutes?: SanitizedConfig['admin']['components']['routes']
config: SanitizedConfig
match: { url: string }
user: User | null | undefined
}) => {
const { canAccessAdmin, customRoutes, match, user } = props
const { canAccessAdmin, config, match, user } = props
const {
admin: { components: { views: customViewConfigs } = {} },
} = config
const customViews = Object.entries(customViewConfigs || {})
.filter(([viewKey, view]) => {
// Remove internal views from the list of custom views
// This way we can easily iterate over the remaining views
return Boolean(
!internalViews.includes(viewKey as any) &&
typeof view !== 'function' &&
typeof view === 'object',
)
})
?.map(([, view]) => view)
if (Array.isArray(customViews)) {
return customViews.map((CustomView) => {
// You are responsible for ensuring that your own custom route is secure
// i.e. return `Unauthorized` in your own component if the user does not have permission
if (typeof CustomView === 'function') {
return <CustomView user={user} />
}
const { Component, exact, path, sensitive, strict } = CustomView
if (Array.isArray(customRoutes)) {
return customRoutes.map(({ Component, exact, path, sensitive, strict }) => {
return (
// You are responsible for ensuring that your own custom route is secure
// i.e. return `Unauthorized` in your own component if the user does not have permission
<Route
exact={exact}
key={`${match.url}${path}`}

View File

@@ -44,12 +44,7 @@ export const Routes: React.FC = () => {
const config = useConfig()
const {
admin: {
components: { routes: customRoutesConfig } = {},
inactivityRoute: logoutInactivityRoute,
logoutRoute,
user: userSlug,
},
admin: { inactivityRoute: logoutInactivityRoute, logoutRoute, user: userSlug },
collections,
globals,
routes,
@@ -110,7 +105,7 @@ export const Routes: React.FC = () => {
</Route>
{customRoutes({
canAccessAdmin,
customRoutes: customRoutesConfig,
config,
match,
user,
})}

View File

@@ -1,7 +1,7 @@
import joi from 'joi'
import { adminViewSchema } from './shared/adminViewSchema'
import { livePreviewSchema } from './shared/componentSchema'
import { routeSchema } from './shared/routeSchema'
const component = joi.alternatives().try(joi.object().unknown(), joi.func())
@@ -53,11 +53,13 @@ export default joi.object({
Button: component,
}),
providers: joi.array().items(component),
routes: routeSchema,
views: joi.object({
Account: component,
Dashboard: component,
}),
views: joi.alternatives().try(
joi.object({
Account: joi.alternatives().try(component, adminViewSchema),
Dashboard: joi.alternatives().try(component, adminViewSchema),
}),
joi.object().pattern(joi.string(), component),
),
}),
css: joi.string(),
dateFormat: joi.string(),

View File

@@ -2,7 +2,7 @@ import joi from 'joi'
import { componentSchema } from './componentSchema'
export const routeSchema = joi.array().items(
export const adminViewSchema = joi.array().items(
joi.object().keys({
Component: componentSchema,
exact: joi.bool(),

View File

@@ -232,12 +232,25 @@ export type Endpoint = {
root?: boolean
}
export type CustomAdminView = React.ComponentType<{
export type AdminViewConfig = {
Component: AdminViewComponent
/** Whether the path should be matched exactly or as a prefix */
exact?: boolean
path: string
sensitive?: boolean
strict?: boolean
}
export type AdminViewProps = {
canAccessAdmin?: boolean
collection?: SanitizedCollectionConfig
global?: SanitizedGlobalConfig
user: User | null | undefined
}>
}
export type AdminViewComponent = React.ComponentType<AdminViewProps>
export type AdminView = AdminViewComponent | AdminViewConfig
export type EditViewConfig = {
/**
@@ -257,15 +270,6 @@ export type EditViewComponent = React.ComponentType<{
export type EditView = EditViewComponent | EditViewConfig
export type AdminRoute = {
Component: CustomAdminView
/** Whether the path should be matched exactly or as a prefix */
exact?: boolean
path: string
sensitive?: boolean
strict?: boolean
}
export type Locale = {
/**
* value of supported locale
@@ -422,15 +426,18 @@ export type Config = {
*/
providers?: React.ComponentType<{ children: React.ReactNode }>[]
/**
* Add custom routes in the admin dashboard
* Replace or modify top-level admin routes, or add new ones:
* + `Account` - `/admin/account`
* + `Dashboard` - `/admin`
* + `:path` - `/admin/:path`
*/
routes?: AdminRoute[]
/* Replace complete pages */
views?: {
/** Add custom admin views */
[key: string]: AdminView
/** Replace the account screen */
Account?: React.ComponentType<any>
Account?: AdminView
/** Replace the admin homepage */
Dashboard?: React.ComponentType<any>
Dashboard?: AdminView
}
}
/** Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. */

View File

@@ -30,7 +30,7 @@ const AfterNavLinks: React.FC = () => {
<NavLink
activeClassName="active"
style={{ textDecoration: 'none' }}
to={`${adminRoute}/custom-default-route`}
to={`${adminRoute}/custom-default-view`}
>
Default Template
</NavLink>
@@ -39,7 +39,7 @@ const AfterNavLinks: React.FC = () => {
<NavLink
activeClassName="active"
style={{ textDecoration: 'none' }}
to={`${adminRoute}/custom-minimal-route`}
to={`${adminRoute}/custom-minimal-view`}
>
Minimal Template
</NavLink>

View File

@@ -1,69 +0,0 @@
import React, { useEffect } from 'react'
import { Redirect } from 'react-router-dom'
import type { CustomAdminView } from '../../../../../packages/payload/src/config/types'
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
// As this is the demo project, we import our dependencies from the `src` directory.
import DefaultTemplate from '../../../../../packages/payload/src/admin/components/templates/Default'
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
import Meta from '../../../../../packages/payload/src/admin/components/utilities/Meta'
// In your projects, you can import as follows:
// import { DefaultTemplate } from 'payload/components/templates';
// import { Button, Eyebrow } from 'payload/components/elements';
// import { AdminView } from 'payload/config';
// import { useStepNav } from 'payload/components/hooks';
// import { useConfig, Meta } from 'payload/components/utilities';
const CustomDefaultRoute: CustomAdminView = ({ canAccessAdmin, user }) => {
const {
routes: { admin: adminRoute },
} = useConfig()
const { setStepNav } = useStepNav()
// This effect will only run one time and will allow us
// to set the step nav to display our custom route name
useEffect(() => {
setStepNav([
{
label: 'Custom Route with Default Template',
},
])
}, [setStepNav])
// If an unauthorized user tries to navigate straight to this page,
// Boot 'em out
if (!user || (user && !canAccessAdmin)) {
return <Redirect to={`${adminRoute}/unauthorized`} />
}
return (
<DefaultTemplate>
<Meta
description="Building custom routes into Payload is easy."
keywords="Custom React Components, Payload, CMS"
title="Custom Route with Default Template"
/>
<div
style={{
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1>Custom Route</h1>
<p>
Here is a custom route that was added in the Payload config. It uses the Default Template,
so the sidebar is rendered.
</p>
<Button buttonStyle="secondary" el="link" to={`${adminRoute}`}>
Go to Dashboard
</Button>
</div>
</DefaultTemplate>
)
}
export default CustomDefaultRoute

View File

@@ -0,0 +1,27 @@
import React, { Fragment } from 'react'
import type { AdminViewComponent } from '../../../../../packages/payload/src/config/types'
const CustomAccountView: AdminViewComponent = () => {
return (
<Fragment>
<div
style={{
marginTop: 'calc(var(--base) * 2)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1>Custom Account View</h1>
<p>This custom view was added through the Payload config:</p>
<ul>
<li>
<code>components.views.Account</code>
</li>
</ul>
</div>
</Fragment>
)
}
export default CustomAccountView

View File

@@ -0,0 +1,27 @@
import React, { Fragment } from 'react'
import { AdminViewComponent } from '../../../../../packages/payload/src/config/types'
const CustomDashboardView: AdminViewComponent = () => {
return (
<Fragment>
<div
style={{
marginTop: 'calc(var(--base) * 2)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1>Custom Dashboard View</h1>
<p>This custom view was added through the Payload config:</p>
<ul>
<li>
<code>components.views.Dashboard</code>
</li>
</ul>
</div>
</Fragment>
)
}
export default CustomDashboardView

View File

@@ -4,8 +4,27 @@
// In your own projects, you'd import as follows:
// @import '~payload/scss';
.custom-minimal-route {
.custom-default-view {
&__login-btn {
margin-right: base(0.5);
}
&__content {
display: flex;
flex-direction: column;
gap: base(1);
& > * {
margin: 0;
}
}
&__controls {
display: flex;
gap: calc(var(--base) / 2);
& > * {
margin: 0;
}
}
}

View File

@@ -1,21 +1,30 @@
import React, { Fragment, useEffect } from 'react'
import React, { useEffect } from 'react'
import { Redirect } from 'react-router-dom'
import type { EditViewComponent } from '../../../../../packages/payload/src/config/types'
import type { AdminViewComponent } from '../../../../../packages/payload/src/config/types'
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
// As this is the demo project, we import our dependencies from the `src` directory.
import DefaultTemplate from '../../../../../packages/payload/src/admin/components/templates/Default'
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
import Meta from '../../../../../packages/payload/src/admin/components/utilities/Meta'
const CustomDefaultView: EditViewComponent = ({
canAccessAdmin,
// collection,
// global,
user,
}) => {
// In your projects, you can import as follows:
// import { DefaultTemplate } from 'payload/components/templates';
// import { Button, Eyebrow } from 'payload/components/elements';
// import { AdminView } from 'payload/config';
// import { useStepNav } from 'payload/components/hooks';
// import { useConfig, Meta } from 'payload/components/utilities';
import './index.scss'
const baseClass = 'custom-default-view'
const CustomDefaultView: AdminViewComponent = ({ canAccessAdmin, user }) => {
const {
routes: { admin: adminRoute },
} = useConfig()
const { setStepNav } = useStepNav()
// This effect will only run one time and will allow us
@@ -24,7 +33,7 @@ const CustomDefaultView: EditViewComponent = ({
useEffect(() => {
setStepNav([
{
label: 'Custom Default View',
label: 'Custom Admin View with Default Template',
},
])
}, [setStepNav])
@@ -36,39 +45,31 @@ const CustomDefaultView: EditViewComponent = ({
}
return (
<Fragment>
<DefaultTemplate>
<Meta
description="Building custom views into Payload is easy."
keywords="Custom React Components, Payload, CMS"
title="Custom Admin View with Default Template"
/>
<div
className={`${baseClass}__content`}
style={{
marginTop: 'calc(var(--base) * 2)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1>Custom Default View</h1>
<p>This custom Default view was added through one of the following Payload configs:</p>
<ul>
<li>
<code>components.views.Edit.Default</code>
<p>
{'This allows you to override only the default edit view specifically, but '}
<b>
<em>not</em>
</b>
{
' any nested views like versions, etc. The document header will render above this component.'
}
</p>
</li>
<li>
<code>components.views.Edit.Default.Component</code>
<p>
This is the most granular override, allowing you to override only the Default
component, or any of its other properties like path and label.
</p>
</li>
</ul>
<h1>Custom Admin View</h1>
<p>
Here is a custom admin view that was added in the Payload config. It uses the Default
Template, so the sidebar is rendered.
</p>
<div className={`${baseClass}__controls`}>
<Button buttonStyle="secondary" el="link" to={`${adminRoute}`}>
Go to Dashboard
</Button>
</div>
</div>
</Fragment>
</DefaultTemplate>
)
}

View File

@@ -0,0 +1,75 @@
import React, { Fragment, useEffect } from 'react'
import { Redirect } from 'react-router-dom'
import type { EditViewComponent } from '../../../../../packages/payload/src/config/types'
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
const CustomDefaultView: EditViewComponent = ({
canAccessAdmin,
// collection,
// global,
user,
}) => {
const {
routes: { admin: adminRoute },
} = useConfig()
const { setStepNav } = useStepNav()
// This effect will only run one time and will allow us
// to set the step nav to display our custom route name
useEffect(() => {
setStepNav([
{
label: 'Custom Default View',
},
])
}, [setStepNav])
// If an unauthorized user tries to navigate straight to this page,
// Boot 'em out
if (!user || (user && !canAccessAdmin)) {
return <Redirect to={`${adminRoute}/unauthorized`} />
}
return (
<Fragment>
<div
style={{
marginTop: 'calc(var(--base) * 2)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1>Custom Default View</h1>
<p>This custom Default view was added through one of the following Payload configs:</p>
<ul>
<li>
<code>components.views.Edit.Default</code>
<p>
{'This allows you to override only the default edit view specifically, but '}
<b>
<em>not</em>
</b>
{
' any nested views like versions, etc. The document header will render above this component.'
}
</p>
</li>
<li>
<code>components.views.Edit.Default.Component</code>
<p>
This is the most granular override, allowing you to override only the Default
component, or any of its other properties like path and label.
</p>
</li>
</ul>
</div>
</Fragment>
)
}
export default CustomDefaultView

View File

@@ -0,0 +1,30 @@
// As this is the demo folder, we import Payload SCSS functions relatively.
@import '../../../../../packages/payload/src/exports/scss.scss';
// In your own projects, you'd import as follows:
// @import '~payload/scss';
.custom-minimal-view {
&__login-btn {
margin-right: base(0.5);
}
&__content {
display: flex;
flex-direction: column;
gap: base(1);
& > * {
margin: 0;
}
}
&__controls {
display: flex;
gap: calc(var(--base) / 2);
& > * {
margin: 0;
}
}
}

View File

@@ -12,25 +12,29 @@ import { useConfig } from '../../../../../packages/payload/src/admin/components/
import './index.scss'
const baseClass = 'custom-minimal-route'
const baseClass = 'custom-minimal-view'
const CustomMinimalRoute: React.FC = () => {
const CustomMinimalView: React.FC = () => {
const {
routes: { admin: adminRoute },
} = useConfig()
return (
<MinimalTemplate className={baseClass}>
<h1>Custom Route</h1>
<p>Here is a custom route that was added in the Payload config.</p>
<Button className={`${baseClass}__login-btn`} el="link" to={`${adminRoute}/login`}>
Go to Login
</Button>
<Button buttonStyle="secondary" el="link" to={`${adminRoute}`}>
Go to Dashboard
</Button>
<div className={`${baseClass}__content`}>
<h1>Custom Admin View</h1>
<p>Here is a custom admin view that was added in the Payload config.</p>
<div className={`${baseClass}__controls`}>
<Button className={`${baseClass}__login-btn`} el="link" to={`${adminRoute}/login`}>
Go to Login
</Button>
<Button buttonStyle="secondary" el="link" to={`${adminRoute}`}>
Go to Dashboard
</Button>
</div>
</div>
</MinimalTemplate>
)
}
export default CustomMinimalRoute
export default CustomMinimalView

View File

@@ -11,10 +11,10 @@ import CustomTabComponent from './components/CustomTabComponent'
import DemoUIFieldCell from './components/DemoUIField/Cell'
import DemoUIFieldField from './components/DemoUIField/Field'
import Logout from './components/Logout'
import CustomDefaultRoute from './components/routes/CustomDefault'
import CustomMinimalRoute from './components/routes/CustomMinimal'
import CustomDefaultView from './components/views/CustomDefault'
import CustomDefaultEditView from './components/views/CustomDefaultEdit'
import CustomEditView from './components/views/CustomEdit'
import CustomMinimalRoute from './components/views/CustomMinimal'
import CustomVersionsView from './components/views/CustomVersions'
import CustomView from './components/views/CustomView'
import { globalSlug, postsSlug, slugPluralLabel, slugSingularLabel } from './shared'
@@ -32,16 +32,6 @@ export default buildConfigWithDefaults({
css: path.resolve(__dirname, 'styles.scss'),
components: {
// providers: [CustomProvider, CustomProvider],
routes: [
{
path: '/custom-minimal-route',
Component: CustomMinimalRoute,
},
{
path: '/custom-default-route',
Component: CustomDefaultRoute,
},
],
afterDashboard: [AfterDashboard],
beforeLogin: [BeforeLogin],
logout: {
@@ -51,6 +41,14 @@ export default buildConfigWithDefaults({
views: {
// Dashboard: CustomDashboardView,
// Account: CustomAccountView,
CustomMinimalRoute: {
path: '/custom-minimal-view',
Component: CustomMinimalRoute,
},
CustomDefaultRoute: {
path: '/custom-default-view',
Component: CustomDefaultView,
},
},
},
},
@@ -184,7 +182,7 @@ export default buildConfigWithDefaults({
views: {
Edit: {
// This will override one specific nested view within the `/edit/:id` route, i.e. `/edit/:id/versions`
Default: CustomDefaultView,
Default: CustomDefaultEditView,
Versions: CustomVersionsView,
MyCustomView: {
path: '/custom-tab-view',
@@ -328,7 +326,7 @@ export default buildConfigWithDefaults({
components: {
views: {
Edit: {
Default: CustomDefaultView,
Default: CustomDefaultEditView,
Versions: CustomVersionsView,
MyCustomView: {
path: '/custom-tab-view',