feat!: custom views are now public by default and fixed some issues with notFound page (#8820)
This PR aims to fix a few issues with the notFound page and custom views so it matches v2 behaviour: - Non authorised users should always be redirected to the login page regardless if not found or valid URL - Previously notFound would render for non users too potentially exposing valid but protected routes and creating a confusing workflow as the UI was being rendered as well - Custom views are now public by default - in our `admin` test suite, the `/admin/public-custom-view` is accessible to non users but `/admin/public-custom-view/protected-nested-view` is not unless the checkbox is true in the Settings global, there's e2e coverage for this - Fixes https://github.com/payloadcms/payload/issues/8716
This commit is contained in:
@@ -93,7 +93,7 @@ For more granular control, pass a configuration object instead. Payload exposes
|
|||||||
| **`path`** \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
|
| **`path`** \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
|
||||||
| **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
| **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
||||||
| **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
| **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
||||||
| **`sensitive`** | When true, will match if the path is case sensitive.
|
| **`sensitive`** | When true, will match if the path is case sensitive.|
|
||||||
| **`meta`** | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
|
| **`meta`** | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
|
||||||
|
|
||||||
_\* An asterisk denotes that a property is required._
|
_\* An asterisk denotes that a property is required._
|
||||||
@@ -133,6 +133,12 @@ The above example shows how to add a new [Root View](#root-views), but the patte
|
|||||||
route.
|
route.
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
||||||
|
<Banner type="warning">
|
||||||
|
<strong>Custom views are public</strong>
|
||||||
|
<br />
|
||||||
|
Custom views are public by default. If your view requires a user to be logged in or to have certain access rights, you should handle that within your view component yourself.
|
||||||
|
</Banner>
|
||||||
|
|
||||||
## Collection Views
|
## Collection Views
|
||||||
|
|
||||||
Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views.
|
Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getPayloadHMR } from '../getPayloadHMR.js'
|
|||||||
import { initReq } from '../initReq.js'
|
import { initReq } from '../initReq.js'
|
||||||
import { getRouteInfo } from './handleAdminPage.js'
|
import { getRouteInfo } from './handleAdminPage.js'
|
||||||
import { handleAuthRedirect } from './handleAuthRedirect.js'
|
import { handleAuthRedirect } from './handleAuthRedirect.js'
|
||||||
|
import { isCustomAdminView } from './isCustomAdminView.js'
|
||||||
import { isPublicAdminRoute } from './shared.js'
|
import { isPublicAdminRoute } from './shared.js'
|
||||||
|
|
||||||
export const initPage = async ({
|
export const initPage = async ({
|
||||||
@@ -133,7 +134,8 @@ export const initPage = async ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!permissions.canAccessAdmin &&
|
!permissions.canAccessAdmin &&
|
||||||
!isPublicAdminRoute({ adminRoute, config: payload.config, route })
|
!isPublicAdminRoute({ adminRoute, config: payload.config, route }) &&
|
||||||
|
!isCustomAdminView({ adminRoute, config: payload.config, route })
|
||||||
) {
|
) {
|
||||||
redirectTo = handleAuthRedirect({
|
redirectTo = handleAuthRedirect({
|
||||||
config: payload.config,
|
config: payload.config,
|
||||||
|
|||||||
35
packages/next/src/utilities/initPage/isCustomAdminView.ts
Normal file
35
packages/next/src/utilities/initPage/isCustomAdminView.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { AdminViewConfig, PayloadRequest, SanitizedConfig } from 'payload'
|
||||||
|
|
||||||
|
import { getRouteWithoutAdmin } from './shared.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of views marked with 'public: true' in the config
|
||||||
|
*/
|
||||||
|
export const isCustomAdminView = ({
|
||||||
|
adminRoute,
|
||||||
|
config,
|
||||||
|
route,
|
||||||
|
}: {
|
||||||
|
adminRoute: string
|
||||||
|
config: SanitizedConfig
|
||||||
|
route: string
|
||||||
|
}): boolean => {
|
||||||
|
if (config.admin?.components?.views) {
|
||||||
|
const isPublicAdminRoute = Object.entries(config.admin.components.views).some(([_, view]) => {
|
||||||
|
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
|
||||||
|
|
||||||
|
if (view.exact) {
|
||||||
|
if (routeWithoutAdmin === view.path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (routeWithoutAdmin.startsWith(view.path)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return isPublicAdminRoute
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -35,9 +35,10 @@ export const isPublicAdminRoute = ({
|
|||||||
config: SanitizedConfig
|
config: SanitizedConfig
|
||||||
route: string
|
route: string
|
||||||
}): boolean => {
|
}): boolean => {
|
||||||
return publicAdminRoutes.some((routeSegment) => {
|
const isPublicAdminRoute = publicAdminRoutes.some((routeSegment) => {
|
||||||
const segment = config.admin?.routes?.[routeSegment] || routeSegment
|
const segment = config.admin?.routes?.[routeSegment] || routeSegment
|
||||||
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
|
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
|
||||||
|
|
||||||
if (routeWithoutAdmin.startsWith(segment)) {
|
if (routeWithoutAdmin.startsWith(segment)) {
|
||||||
return true
|
return true
|
||||||
} else if (routeWithoutAdmin.includes('/verify/')) {
|
} else if (routeWithoutAdmin.includes('/verify/')) {
|
||||||
@@ -46,6 +47,8 @@ export const isPublicAdminRoute = ({
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return isPublicAdminRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRouteWithoutAdmin = ({
|
export const getRouteWithoutAdmin = ({
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ export const NotFoundPage = async ({
|
|||||||
|
|
||||||
const params = await paramsPromise
|
const params = await paramsPromise
|
||||||
|
|
||||||
|
if (!initPageResult.req.user || !initPageResult.permissions.canAccessAdmin) {
|
||||||
|
return <NotFoundClient />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultTemplate
|
<DefaultTemplate
|
||||||
i18n={initPageResult.req.i18n}
|
i18n={initPageResult.req.i18n}
|
||||||
|
|||||||
@@ -66,24 +66,29 @@ export const RootPage = async ({
|
|||||||
|
|
||||||
let dbHasUser = false
|
let dbHasUser = false
|
||||||
|
|
||||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const initPageResult = await initPage(initPageOptions)
|
const initPageResult = await initPage(initPageOptions)
|
||||||
|
|
||||||
|
dbHasUser = await initPageResult?.req.payload.db
|
||||||
|
.findOne({
|
||||||
|
collection: userSlug,
|
||||||
|
req: initPageResult?.req,
|
||||||
|
})
|
||||||
|
?.then((doc) => !!doc)
|
||||||
|
|
||||||
|
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||||
|
if (initPageResult?.req?.user) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
if (dbHasUser) {
|
||||||
|
redirect(adminRoute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof initPageResult?.redirectTo === 'string') {
|
if (typeof initPageResult?.redirectTo === 'string') {
|
||||||
redirect(initPageResult.redirectTo)
|
redirect(initPageResult.redirectTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initPageResult) {
|
if (initPageResult) {
|
||||||
dbHasUser = await initPageResult?.req.payload.db
|
|
||||||
.findOne({
|
|
||||||
collection: userSlug,
|
|
||||||
req: initPageResult?.req,
|
|
||||||
})
|
|
||||||
?.then((doc) => !!doc)
|
|
||||||
|
|
||||||
const createFirstUserRoute = formatAdminURL({ adminRoute, path: _createFirstUserRoute })
|
const createFirstUserRoute = formatAdminURL({ adminRoute, path: _createFirstUserRoute })
|
||||||
|
|
||||||
const collectionConfig = config.collections.find(({ slug }) => slug === userSlug)
|
const collectionConfig = config.collections.find(({ slug }) => slug === userSlug)
|
||||||
@@ -102,6 +107,10 @@ export const RootPage = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!DefaultView?.Component && !DefaultView?.payloadComponent && !dbHasUser) {
|
||||||
|
redirect(adminRoute)
|
||||||
|
}
|
||||||
|
|
||||||
const createMappedView = getCreateMappedComponent({
|
const createMappedView = getCreateMappedComponent({
|
||||||
importMap,
|
importMap,
|
||||||
serverProps: {
|
serverProps: {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export type ClientConfig = {
|
|||||||
Logo: MappedComponent
|
Logo: MappedComponent
|
||||||
}
|
}
|
||||||
LogoutButton?: MappedComponent
|
LogoutButton?: MappedComponent
|
||||||
}
|
} & Pick<SanitizedConfig['admin']['components'], 'views'>
|
||||||
dependencies?: Record<string, MappedComponent>
|
dependencies?: Record<string, MappedComponent>
|
||||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||||
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
|
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
|
||||||
@@ -64,6 +64,6 @@ export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperti
|
|||||||
'email',
|
'email',
|
||||||
'custom',
|
'custom',
|
||||||
'graphQL',
|
'graphQL',
|
||||||
'logger'
|
'logger',
|
||||||
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
|
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
|
||||||
]
|
]
|
||||||
|
|||||||
69
test/admin/components/views/CustomProtectedView/index.tsx
Normal file
69
test/admin/components/views/CustomProtectedView/index.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { AdminViewProps } from 'payload'
|
||||||
|
|
||||||
|
import { Button } from '@payloadcms/ui'
|
||||||
|
import LinkImport from 'next/link.js'
|
||||||
|
import { notFound, redirect } from 'next/navigation.js'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { customNestedViewTitle, customViewPath } from '../../../shared.js'
|
||||||
|
import { settingsGlobalSlug } from '../../../slugs.js'
|
||||||
|
|
||||||
|
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||||
|
|
||||||
|
export const CustomProtectedView: React.FC<AdminViewProps> = async ({ initPageResult }) => {
|
||||||
|
const {
|
||||||
|
req: {
|
||||||
|
payload: {
|
||||||
|
config: {
|
||||||
|
routes: { admin: adminRoute },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
},
|
||||||
|
req,
|
||||||
|
} = initPageResult
|
||||||
|
|
||||||
|
const settings = await req.payload.findGlobal({
|
||||||
|
slug: settingsGlobalSlug,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!settings?.canAccessProtected) {
|
||||||
|
if (user) {
|
||||||
|
redirect(`${adminRoute}/unauthorized`)
|
||||||
|
} else {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 'calc(var(--base) * 2)',
|
||||||
|
paddingLeft: 'var(--gutter-h)',
|
||||||
|
paddingRight: 'var(--gutter-h)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 id="custom-view-title">{customNestedViewTitle}</h1>
|
||||||
|
<p>This custom view was added through the Payload config:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>components.views[key].Component</code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className="custom-view__controls">
|
||||||
|
<Button buttonStyle="secondary" el="link" Link={Link} to={`${adminRoute}`}>
|
||||||
|
Go to Dashboard
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
buttonStyle="secondary"
|
||||||
|
el="link"
|
||||||
|
Link={Link}
|
||||||
|
to={`${adminRoute}/${customViewPath}`}
|
||||||
|
>
|
||||||
|
Go to Custom View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import { GlobalGroup1A } from './globals/Group1A.js'
|
|||||||
import { GlobalGroup1B } from './globals/Group1B.js'
|
import { GlobalGroup1B } from './globals/Group1B.js'
|
||||||
import { GlobalHidden } from './globals/Hidden.js'
|
import { GlobalHidden } from './globals/Hidden.js'
|
||||||
import { GlobalNoApiView } from './globals/NoApiView.js'
|
import { GlobalNoApiView } from './globals/NoApiView.js'
|
||||||
|
import { Settings } from './globals/Settings.js'
|
||||||
import { seed } from './seed.js'
|
import { seed } from './seed.js'
|
||||||
import {
|
import {
|
||||||
customAdminRoutes,
|
customAdminRoutes,
|
||||||
@@ -33,7 +34,11 @@ import {
|
|||||||
customParamViewPath,
|
customParamViewPath,
|
||||||
customRootViewMetaTitle,
|
customRootViewMetaTitle,
|
||||||
customViewPath,
|
customViewPath,
|
||||||
|
protectedCustomNestedViewPath,
|
||||||
|
publicCustomViewPath,
|
||||||
} from './shared.js'
|
} from './shared.js'
|
||||||
|
import { settingsGlobalSlug } from './slugs.js'
|
||||||
|
|
||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
admin: {
|
admin: {
|
||||||
importMap: {
|
importMap: {
|
||||||
@@ -80,6 +85,17 @@ export default buildConfigWithDefaults({
|
|||||||
path: customViewPath,
|
path: customViewPath,
|
||||||
strict: true,
|
strict: true,
|
||||||
},
|
},
|
||||||
|
ProtectedCustomNestedView: {
|
||||||
|
Component: '/components/views/CustomProtectedView/index.js#CustomProtectedView',
|
||||||
|
exact: true,
|
||||||
|
path: protectedCustomNestedViewPath,
|
||||||
|
},
|
||||||
|
PublicCustomView: {
|
||||||
|
Component: '/components/views/CustomView/index.js#CustomView',
|
||||||
|
exact: true,
|
||||||
|
path: publicCustomViewPath,
|
||||||
|
strict: true,
|
||||||
|
},
|
||||||
CustomViewWithParam: {
|
CustomViewWithParam: {
|
||||||
Component: '/components/views/CustomViewWithParam/index.js#CustomViewWithParam',
|
Component: '/components/views/CustomViewWithParam/index.js#CustomViewWithParam',
|
||||||
path: customParamViewPath,
|
path: customParamViewPath,
|
||||||
@@ -144,6 +160,7 @@ export default buildConfigWithDefaults({
|
|||||||
CustomGlobalViews2,
|
CustomGlobalViews2,
|
||||||
GlobalGroup1A,
|
GlobalGroup1A,
|
||||||
GlobalGroup1B,
|
GlobalGroup1B,
|
||||||
|
Settings,
|
||||||
],
|
],
|
||||||
i18n: {
|
i18n: {
|
||||||
translations: {
|
translations: {
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import {
|
|||||||
customViewMetaTitle,
|
customViewMetaTitle,
|
||||||
customViewPath,
|
customViewPath,
|
||||||
customViewTitle,
|
customViewTitle,
|
||||||
|
protectedCustomNestedViewPath,
|
||||||
|
publicCustomViewPath,
|
||||||
slugPluralLabel,
|
slugPluralLabel,
|
||||||
} from '../../shared.js'
|
} from '../../shared.js'
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +52,7 @@ import {
|
|||||||
noApiViewCollectionSlug,
|
noApiViewCollectionSlug,
|
||||||
noApiViewGlobalSlug,
|
noApiViewGlobalSlug,
|
||||||
postsCollectionSlug,
|
postsCollectionSlug,
|
||||||
|
settingsGlobalSlug,
|
||||||
} from '../../slugs.js'
|
} from '../../slugs.js'
|
||||||
|
|
||||||
const { beforeAll, beforeEach, describe } = test
|
const { beforeAll, beforeEach, describe } = test
|
||||||
@@ -494,6 +497,30 @@ describe('admin1', () => {
|
|||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('root — should render public custom view', async () => {
|
||||||
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${publicCustomViewPath}`)
|
||||||
|
await page.waitForURL(`**${adminRoutes.routes.admin}${publicCustomViewPath}`)
|
||||||
|
await expect(page.locator('h1#custom-view-title')).toContainText(customViewTitle)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('root — should render protected nested custom view', async () => {
|
||||||
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${protectedCustomNestedViewPath}`)
|
||||||
|
await page.waitForURL(`**${adminRoutes.routes.admin}/unauthorized`)
|
||||||
|
await expect(page.locator('.unauthorized')).toBeVisible()
|
||||||
|
|
||||||
|
await page.goto(globalURL.global(settingsGlobalSlug))
|
||||||
|
|
||||||
|
const checkbox = page.locator('#field-canAccessProtected')
|
||||||
|
|
||||||
|
await checkbox.check()
|
||||||
|
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${protectedCustomNestedViewPath}`)
|
||||||
|
await page.waitForURL(`**${adminRoutes.routes.admin}${protectedCustomNestedViewPath}`)
|
||||||
|
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
||||||
|
})
|
||||||
|
|
||||||
test('collection - should render custom tab view', async () => {
|
test('collection - should render custom tab view', async () => {
|
||||||
await page.goto(customViewsURL.create)
|
await page.goto(customViewsURL.create)
|
||||||
await page.locator('#field-title').fill('Test')
|
await page.locator('#field-title').fill('Test')
|
||||||
|
|||||||
13
test/admin/globals/Settings.ts
Normal file
13
test/admin/globals/Settings.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { GlobalConfig } from 'payload'
|
||||||
|
|
||||||
|
import { settingsGlobalSlug } from '../slugs.js'
|
||||||
|
|
||||||
|
export const Settings: GlobalConfig = {
|
||||||
|
slug: settingsGlobalSlug,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'canAccessProtected',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ export interface Config {
|
|||||||
'custom-global-views-two': CustomGlobalViewsTwo;
|
'custom-global-views-two': CustomGlobalViewsTwo;
|
||||||
'group-globals-one': GroupGlobalsOne;
|
'group-globals-one': GroupGlobalsOne;
|
||||||
'group-globals-two': GroupGlobalsTwo;
|
'group-globals-two': GroupGlobalsTwo;
|
||||||
|
settings: Setting;
|
||||||
};
|
};
|
||||||
locale: 'es' | 'en';
|
locale: 'es' | 'en';
|
||||||
user: User & {
|
user: User & {
|
||||||
@@ -341,7 +342,6 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'disable-duplicate';
|
relationTo: 'disable-duplicate';
|
||||||
value: string | DisableDuplicate;
|
value: string | DisableDuplicate;
|
||||||
} | null);
|
} | null);
|
||||||
editedAt?: string | null;
|
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
@@ -455,6 +455,16 @@ export interface GroupGlobalsTwo {
|
|||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "settings".
|
||||||
|
*/
|
||||||
|
export interface Setting {
|
||||||
|
id: string;
|
||||||
|
canAccessProtected?: boolean | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "auth".
|
* via the `definition` "auth".
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ export const slugPluralLabel = 'Posts'
|
|||||||
|
|
||||||
export const customViewPath = '/custom-view'
|
export const customViewPath = '/custom-view'
|
||||||
|
|
||||||
|
export const customNestedViewPath = `${customViewPath}/nested-view`
|
||||||
|
|
||||||
|
export const publicCustomViewPath = '/public-custom-view'
|
||||||
|
|
||||||
|
export const protectedCustomNestedViewPath = `${publicCustomViewPath}/protected-nested-view`
|
||||||
|
|
||||||
export const customParamViewPathBase = '/custom-param'
|
export const customParamViewPathBase = '/custom-param'
|
||||||
|
|
||||||
export const customParamViewPath = `${customParamViewPathBase}/:id`
|
export const customParamViewPath = `${customParamViewPathBase}/:id`
|
||||||
@@ -14,8 +20,6 @@ export const customViewTitle = 'Custom View'
|
|||||||
|
|
||||||
export const customParamViewTitle = 'Custom Param View'
|
export const customParamViewTitle = 'Custom Param View'
|
||||||
|
|
||||||
export const customNestedViewPath = `${customViewPath}/nested-view`
|
|
||||||
|
|
||||||
export const customNestedViewTitle = 'Custom Nested View'
|
export const customNestedViewTitle = 'Custom Nested View'
|
||||||
|
|
||||||
export const customEditLabel = 'Custom Edit Label'
|
export const customEditLabel = 'Custom Edit Label'
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export const globalSlug = 'global'
|
|||||||
export const group1GlobalSlug = 'group-globals-one'
|
export const group1GlobalSlug = 'group-globals-one'
|
||||||
export const group2GlobalSlug = 'group-globals-two'
|
export const group2GlobalSlug = 'group-globals-two'
|
||||||
export const hiddenGlobalSlug = 'hidden-global'
|
export const hiddenGlobalSlug = 'hidden-global'
|
||||||
|
|
||||||
|
export const settingsGlobalSlug = 'settings'
|
||||||
export const noApiViewGlobalSlug = 'global-no-api-view'
|
export const noApiViewGlobalSlug = 'global-no-api-view'
|
||||||
export const globalSlugs = [
|
export const globalSlugs = [
|
||||||
customGlobalViews1GlobalSlug,
|
customGlobalViews1GlobalSlug,
|
||||||
|
|||||||
Reference in New Issue
Block a user