diff --git a/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx b/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx index bf741dc4a3..19d18093f2 100644 --- a/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx @@ -111,13 +111,17 @@ const Content: React.FC = ({ if (isError) return null const isEditing = Boolean(id) + const apiURL = id ? `${serverURL}${api}/${collectionSlug}/${id}?locale=${locale}` : null + const action = `${serverURL}${api}/${collectionSlug}${ id ? `/${id}` : '' }?locale=${locale}&fallback-locale=null` + const hasSavePermission = (isEditing && docPermissions?.update?.permission) || (!isEditing && (docPermissions as CollectionPermission)?.create?.permission) + const isLoading = !internalState || !docPermissions || isLoadingDocument return ( @@ -154,6 +158,7 @@ const Content: React.FC = ({ data, disableActions: true, disableLeaveWithoutSaving: true, + disableRoutes: true, hasSavePermission, internalState, isEditing, diff --git a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/Tab/index.scss b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/Tab/index.scss new file mode 100644 index 0000000000..41dc9cd8cf --- /dev/null +++ b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/Tab/index.scss @@ -0,0 +1,70 @@ +@import '../../../../../scss/styles.scss'; + +.doc-tab { + @extend %h5; + text-decoration: none; + position: relative; + display: flex; + justify-content: center; + align-items: center; + padding: calc(var(--base) / 2) calc(var(--base)); + + &:focus:not(:focus-visible) { + opacity: 1; + } + + &::before { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + border-radius: 2px; + background-color: var(--theme-elevation-50); + opacity: 0; + } + + &:hover { + &::before { + opacity: 1; + } + + .doc-tab__count { + background-color: var(--theme-elevation-150); + } + } + + &--active { + &::before { + opacity: 1; + background-color: var(--theme-elevation-100); + } + + .doc-tab { + &__count { + background-color: var(--theme-elevation-250); + } + } + + &:hover { + .doc-tab { + &__count { + background-color: var(--theme-elevation-250); + } + } + } + } + + &__label { + display: flex; + position: relative; + align-items: center; + gap: 4px; + } + + &__count { + padding: 0px 6px; + background-color: var(--theme-elevation-100); + border-radius: 1px; + } +} diff --git a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/Tab/index.tsx b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/Tab/index.tsx new file mode 100644 index 0000000000..eb29ed1f3d --- /dev/null +++ b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/Tab/index.tsx @@ -0,0 +1,79 @@ +import React, { Fragment } from 'react' +import { useTranslation } from 'react-i18next' +import { Link, useLocation, useRouteMatch } from 'react-router-dom' + +import type { DocumentTabConfig } from '../types' +import type { DocumentTabProps } from '../types' + +import { useConfig } from '../../../../utilities/Config' +import { useDocumentInfo } from '../../../../utilities/DocumentInfo' +import './index.scss' + +const baseClass = 'doc-tab' + +export const DocumentTab: React.FC = (props) => { + const { + id, + apiURL, + collection, + condition, + global, + href: tabHref, + isActive: checkIsActive, + label, + newTab, + pillLabel, + } = props + + const { t } = useTranslation('general') + const location = useLocation() + const { routes } = useConfig() + + const { versions } = useDocumentInfo() + const match = useRouteMatch() + + let href = `${match.url}${typeof tabHref === 'string' ? tabHref : ''}` + + if (typeof tabHref === 'function') { + href = tabHref({ + id, + apiURL, + collection, + global, + match, + routes, + }) + } + + const isActive = + typeof checkIsActive === 'function' + ? checkIsActive({ href, location, match }) + : typeof checkIsActive === 'boolean' + ? checkIsActive + : location.pathname.startsWith(href) + + if (!condition || (condition && condition({ collection, global }))) { + const labelToRender = typeof label === 'function' ? label({ t }) : label + const pillToRender = typeof pillLabel === 'function' ? pillLabel({ versions }) : pillLabel + + return ( + + + {labelToRender} + {pillToRender && ( + +   + {pillToRender} + + )} + + + ) + } + + return null +} diff --git a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/getCustomViews.ts b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/getCustomViews.ts new file mode 100644 index 0000000000..908e9ad07e --- /dev/null +++ b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/getCustomViews.ts @@ -0,0 +1,53 @@ +import type { CollectionEditViewConfig } from '../../../../../collections/config/types' +import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../../exports/types' +import type { GlobalEditViewConfig } from '../../../../../globals/config/types' + +import { defaultGlobalViews } from '../../../views/Global/Routes/CustomComponent' +import { defaultCollectionViews } from '../../../views/collections/Edit/Routes/CustomComponent' + +export const getCustomViews = (args: { + collection: SanitizedCollectionConfig + global: SanitizedGlobalConfig +}): (CollectionEditViewConfig | GlobalEditViewConfig)[] => { + const { collection, global } = args + + let customViews: (CollectionEditViewConfig | GlobalEditViewConfig)[] + + if (collection) { + const collectionViewsConfig = + typeof collection?.admin?.components?.views?.Edit === 'object' && + typeof collection?.admin?.components?.views?.Edit !== 'function' + ? collection?.admin?.components?.views?.Edit + : undefined + + const defaultViewKeys = Object.keys(defaultCollectionViews) + + customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => { + if (defaultViewKeys.includes(key)) { + return prev + } + + return [...prev, { ...view, key }] + }, []) + } + + if (global) { + const globalViewsConfig = + typeof global?.admin?.components?.views?.Edit === 'object' && + typeof global?.admin?.components?.views?.Edit !== 'function' + ? global?.admin?.components?.views?.Edit + : undefined + + const defaultViewKeys = Object.keys(defaultGlobalViews) + + customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => { + if (defaultViewKeys.includes(key)) { + return prev + } + + return [...prev, { ...view, key }] + }, []) + } + + return customViews +} diff --git a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/index.scss b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/index.scss index 3e77f69cf0..8007b911d1 100644 --- a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/index.scss +++ b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/index.scss @@ -13,73 +13,6 @@ display: flex; } - &__tab-link { - @extend %h5; - text-decoration: none; - position: relative; - display: flex; - justify-content: center; - align-items: center; - padding: calc(var(--base) / 2) calc(var(--base)); - - &:focus:not(:focus-visible) { - opacity: 1; - } - - &::before { - content: ''; - display: block; - position: absolute; - width: 100%; - height: 100%; - border-radius: 2px; - background-color: var(--theme-elevation-50); - opacity: 0; - } - - &:hover { - &::before { - opacity: 1; - } - - .doc-tabs__count { - background-color: var(--theme-elevation-150); - } - } - } - - &__tab--active { - .doc-tabs__tab-link { - &::before { - opacity: 1; - background-color: var(--theme-elevation-100); - } - } - - .doc-tabs__count { - background-color: var(--theme-elevation-250); - } - - &:hover { - .doc-tabs__count { - background-color: var(--theme-elevation-250); - } - } - } - - &__tab-label { - position: relative; - display: flex; - align-items: center; - gap: 4px; - } - - &__count { - padding: 0px 6px; - background-color: var(--theme-elevation-100); - border-radius: 1px; - } - @include mid-break { width: 100%; padding: 0; diff --git a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/index.tsx b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/index.tsx index 349aee8a39..5cab12d165 100644 --- a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/index.tsx +++ b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/index.tsx @@ -1,90 +1,42 @@ -import React, { Fragment } from 'react' -import { useTranslation } from 'react-i18next' -import { Link, useLocation, useRouteMatch } from 'react-router-dom' +import React from 'react' -import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../../exports/types' +import type { DocumentTabProps } from './types' -import { useConfig } from '../../../utilities/Config' -import { useDocumentInfo } from '../../../utilities/DocumentInfo' +import { DocumentTab } from './Tab' +import { getCustomViews } from './getCustomViews' import './index.scss' import { tabs } from './tabs' const baseClass = 'doc-tabs' -export const DocumentTabs: React.FC<{ - apiURL?: string - collection?: SanitizedCollectionConfig - global?: SanitizedGlobalConfig - id: string - isEditing?: boolean -}> = (props) => { - const { id, apiURL, collection, global, isEditing } = props - const match = useRouteMatch() - const { t } = useTranslation('general') - const location = useLocation() - const { routes } = useConfig() - - const { versions } = useDocumentInfo() +export const DocumentTabs: React.FC = (props) => { + const { collection, global, isEditing } = props + const customViews = getCustomViews({ collection, global }) // Don't show tabs when creating new documents if ((collection && isEditing) || global) { return (
    - {tabs?.map((tab) => { - const { - condition, - href: tabHref, - isActive: checkIsActive, - label, - newTab, - pillLabel, - } = tab - - const href = tabHref({ - id, - apiURL, - collection, - global, - match, - routes, - }) - - const isActive = - typeof checkIsActive === 'function' ? checkIsActive({ href, location, match }) : false - - if (!condition || (condition && condition({ collection, global }))) { - const labelToRender = typeof label === 'function' ? label({ t }) : label - const pillToRender = - typeof pillLabel === 'function' ? pillLabel({ versions }) : pillLabel + {tabs?.map((Tab, index) => { + return ( +
  • + +
  • + ) + })} + {customViews?.map((CustomView, index) => { + const { Tab, path } = CustomView + if (typeof Tab === 'function') { return ( -
  • - - - {labelToRender} - {pillToRender && ( - -   - {pillToRender} - - )} - - +
  • +
  • ) } - return null + return })}
diff --git a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/tabs.ts b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/tabs.ts index bef660ffcf..06f64af78d 100644 --- a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/tabs.ts +++ b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/tabs.ts @@ -1,57 +1,17 @@ -import type { useLocation, useRouteMatch } from 'react-router-dom' +import type { DocumentTabConfig } from './types' -import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../../exports/types' -import type { useConfig } from '../../../utilities/Config' -import type { useDocumentInfo } from '../../../utilities/DocumentInfo' - -export type FilterType = (args: { - collection: SanitizedCollectionConfig - global: SanitizedGlobalConfig -}) => boolean - -type TabType = { - condition?: FilterType - href?: (args: { - apiURL: string - collection: SanitizedCollectionConfig - global: SanitizedGlobalConfig - id?: string - match: ReturnType - routes: ReturnType['routes'] - }) => string - isActive?: (args: { - href: string - location: ReturnType - match: ReturnType - }) => boolean - label: ((args: { t: (key: string) => string }) => string) | string - newTab?: boolean - pillLabel?: - | ((args: { versions: ReturnType['versions'] }) => string) - | string -} - -export const tabs: TabType[] = [ +export const tabs: DocumentTabConfig[] = [ // Default { - href: ({ match }) => `${match.url}`, + href: '', isActive: ({ href, location }) => location.pathname === href || location.pathname === `${href}/create`, label: ({ t }) => t('edit'), }, - // Live Preview - // { - // condition: ({ collection, global }) => - // Boolean(collection?.admin?.livePreview || global?.admin?.livePreview), - // href: ({ match }) => `${match.url}/preview`, - // isActive: ({ href, location }) => location.pathname === href, - // label: ({ t }) => t('livePreview'), - // }, // Versions { condition: ({ collection, global }) => Boolean(collection?.versions || global?.versions), - href: ({ match }) => `${match.url}/versions`, - isActive: ({ href, location }) => location.pathname.startsWith(href), + href: '/versions', label: ({ t }) => t('version:versions'), pillLabel: ({ versions }) => typeof versions?.totalDocs === 'number' && versions?.totalDocs > 0 @@ -63,7 +23,6 @@ export const tabs: TabType[] = [ condition: ({ collection, global }) => !collection?.admin?.hideAPIURL || !global?.admin?.hideAPIURL, href: ({ apiURL }) => apiURL, - isActive: ({ href, location }) => location.pathname.startsWith(href), label: 'API', newTab: true, }, diff --git a/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/types.ts b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/types.ts new file mode 100644 index 0000000000..934a08ad4e --- /dev/null +++ b/packages/payload/src/admin/components/elements/DocumentHeader/Tabs/types.ts @@ -0,0 +1,52 @@ +import type { useLocation, useRouteMatch } from 'react-router-dom' + +import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../../exports/types' +import type { useConfig } from '../../../utilities/Config' +import type { useDocumentInfo } from '../../../utilities/DocumentInfo' + +export type DocumentTabProps = { + apiURL?: string + collection?: SanitizedCollectionConfig + global?: SanitizedGlobalConfig + id: string + isEditing?: boolean +} + +export type DocumentTabCondition = (args: { + collection: SanitizedCollectionConfig + global: SanitizedGlobalConfig +}) => boolean + +export type DocumentTabConfig = { + condition?: DocumentTabCondition + href: + | ((args: { + apiURL: string + collection: SanitizedCollectionConfig + global: SanitizedGlobalConfig + id?: string + match: ReturnType + routes: ReturnType['routes'] + }) => string) + | string + isActive?: + | ((args: { + href: string + location: ReturnType + match: ReturnType + }) => boolean) + | boolean + label: ((args: { t: (key: string) => string }) => string) | string + newTab?: boolean + pillLabel?: + | ((args: { versions: ReturnType['versions'] }) => string) + | string +} + +export type DocumentTab = + | DocumentTabConfig + | React.ComponentType< + DocumentTabProps & { + path: string + } + > diff --git a/packages/payload/src/admin/components/views/Global/Default.tsx b/packages/payload/src/admin/components/views/Global/Default.tsx index b5a90d6494..7243255031 100644 --- a/packages/payload/src/admin/components/views/Global/Default.tsx +++ b/packages/payload/src/admin/components/views/Global/Default.tsx @@ -9,12 +9,23 @@ import { FormLoadingOverlayToggle } from '../../elements/Loading' import Form from '../../forms/Form' import { OperationContext } from '../../utilities/OperationProvider' import { GlobalRoutes } from './Routes' +import { CustomGlobalComponent } from './Routes/CustomComponent' import './index.scss' const baseClass = 'global-edit' const DefaultGlobalView: React.FC = (props) => { - const { action, apiURL, data, global, initialState, isLoading, onSave, permissions } = props + const { + action, + apiURL, + data, + disableRoutes, + global, + initialState, + isLoading, + onSave, + permissions, + } = props const { i18n } = useTranslation('general') @@ -41,7 +52,11 @@ const DefaultGlobalView: React.FC = (props) => { {!isLoading && ( - + {disableRoutes ? ( + + ) : ( + + )} )} diff --git a/packages/payload/src/admin/components/views/Global/Routes/CustomComponent.tsx b/packages/payload/src/admin/components/views/Global/Routes/CustomComponent.tsx index bcc454361a..bcef37fb1d 100644 --- a/packages/payload/src/admin/components/views/Global/Routes/CustomComponent.tsx +++ b/packages/payload/src/admin/components/views/Global/Routes/CustomComponent.tsx @@ -15,7 +15,7 @@ export type globalViewType = | 'Version' | 'Versions' -const defaultViews: { +export const defaultGlobalViews: { [key in globalViewType]: React.ComponentType } = { API: null, @@ -48,7 +48,7 @@ export const CustomGlobalComponent = ( typeof Edit?.[view] === 'object' && typeof Edit[view].Component === 'function' ? Edit[view].Component - : defaultViews[view] + : defaultGlobalViews[view] if (Component) { return diff --git a/packages/payload/src/admin/components/views/Global/Routes/custom.tsx b/packages/payload/src/admin/components/views/Global/Routes/custom.tsx index 2aaf72a1aa..0f1e1c3e60 100644 --- a/packages/payload/src/admin/components/views/Global/Routes/custom.tsx +++ b/packages/payload/src/admin/components/views/Global/Routes/custom.tsx @@ -52,11 +52,7 @@ export const globalCustomRoutes = (props: { if (global) { routesToReturn.push( - + {permissions?.read?.permission ? ( ) : ( diff --git a/packages/payload/src/admin/components/views/Global/Routes/index.tsx b/packages/payload/src/admin/components/views/Global/Routes/index.tsx index 9c1937d2ef..8852ea3d8f 100644 --- a/packages/payload/src/admin/components/views/Global/Routes/index.tsx +++ b/packages/payload/src/admin/components/views/Global/Routes/index.tsx @@ -4,6 +4,7 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom' import { useAuth } from '../../../utilities/Auth' import { useConfig } from '../../../utilities/Config' +import NotFound from '../../NotFound' import { type Props } from '../types' import { CustomGlobalComponent } from './CustomComponent' import { globalCustomRoutes } from './custom' @@ -52,9 +53,12 @@ export const GlobalRoutes: React.FC = (props) => { permissions, user, })} - + + + + ) } diff --git a/packages/payload/src/admin/components/views/Global/types.ts b/packages/payload/src/admin/components/views/Global/types.ts index f1e6d58597..0c6a5cd303 100644 --- a/packages/payload/src/admin/components/views/Global/types.ts +++ b/packages/payload/src/admin/components/views/Global/types.ts @@ -12,6 +12,7 @@ export type Props = { apiURL: string autosaveEnabled: boolean data: Document + disableRoutes?: boolean global: SanitizedGlobalConfig initialState: Fields isLoading: boolean diff --git a/packages/payload/src/admin/components/views/NotFound/index.scss b/packages/payload/src/admin/components/views/NotFound/index.scss new file mode 100644 index 0000000000..b173990494 --- /dev/null +++ b/packages/payload/src/admin/components/views/NotFound/index.scss @@ -0,0 +1,36 @@ +@import '../../../scss/styles.scss'; + +.not-found { + margin-top: var(--base); + + & > * { + &:first-child { + margin-top: 0; + } + &:last-child { + margin-bottom: 0; + } + } + + &__button { + margin: 0; + } + + &--margin-top-large { + margin-top: calc(var(--base) * 2); + } + + @include mid-break { + &--margin-top-large { + margin-top: var(--base); + } + } + + @include small-break { + margin-top: calc(var(--base) / 2); + + &--margin-top-large { + margin-top: calc(var(--base) / 2); + } + } +} diff --git a/packages/payload/src/admin/components/views/NotFound/index.tsx b/packages/payload/src/admin/components/views/NotFound/index.tsx index 3c0c459d6f..72d524f7a0 100644 --- a/packages/payload/src/admin/components/views/NotFound/index.tsx +++ b/packages/payload/src/admin/components/views/NotFound/index.tsx @@ -6,14 +6,21 @@ import { Gutter } from '../../elements/Gutter' import { useStepNav } from '../../elements/StepNav' import { useConfig } from '../../utilities/Config' import Meta from '../../utilities/Meta' +import './index.scss' const baseClass = 'not-found' -const NotFound: React.FC = () => { +const NotFound: React.FC<{ + marginTop?: 'large' +}> = (props) => { + const { marginTop } = props + const { setStepNav } = useStepNav() + const { routes: { admin }, } = useConfig() + const { t } = useTranslation('general') useEffect(() => { @@ -25,7 +32,11 @@ const NotFound: React.FC = () => { }, [setStepNav, t]) return ( -
+
{

{t('nothingFound')}

{t('sorryNotFound')}

-
diff --git a/packages/payload/src/admin/components/views/Version/Version.tsx b/packages/payload/src/admin/components/views/Version/Version.tsx index 1081468134..b4bf6efb27 100644 --- a/packages/payload/src/admin/components/views/Version/Version.tsx +++ b/packages/payload/src/admin/components/views/Version/Version.tsx @@ -18,6 +18,7 @@ import { useConfig } from '../../utilities/Config' import { useDocumentInfo } from '../../utilities/DocumentInfo' import { useLocale } from '../../utilities/Locale' import Meta from '../../utilities/Meta' +import NotFound from '../NotFound' import CompareVersion from './Compare' import RenderFieldsToDiff from './RenderFieldsToDiff' import fieldComponents from './RenderFieldsToDiff/fields' @@ -84,7 +85,7 @@ const VersionView: React.FC = ({ collection, global }) => { ? originalDocFetchURL : `${compareBaseURL}/${compareValue.value}` - const [{ data: doc, isLoading: isLoadingData }] = usePayloadAPI(versionFetchURL, { + const [{ data: doc, isError, isLoading: isLoadingData }] = usePayloadAPI(versionFetchURL, { initialParams: { depth: 1, locale: '*' }, }) const [{ data: publishedDoc }] = usePayloadAPI(originalDocFetchURL, { @@ -192,6 +193,10 @@ const VersionView: React.FC = ({ collection, global }) => { const canUpdate = docPermissions?.update?.permission + if (isError) { + return + } + return (
diff --git a/packages/payload/src/admin/components/views/Version/index.scss b/packages/payload/src/admin/components/views/Version/index.scss index 686491e5e6..b817728e7e 100644 --- a/packages/payload/src/admin/components/views/Version/index.scss +++ b/packages/payload/src/admin/components/views/Version/index.scss @@ -38,10 +38,6 @@ } @include mid-break { - &__wrap { - padding-top: 0; - } - &__intro, &__header { display: block; @@ -49,8 +45,6 @@ &__controls { display: block; - margin: 0 base(-0.5) base(2); - > * { margin-bottom: base(0.5); } @@ -60,4 +54,10 @@ margin: base(0.5) 0 0 0; } } + + @include small-break { + &__wrap { + padding-top: calc(var(--base) / 2); + } + } } diff --git a/packages/payload/src/admin/components/views/collections/Edit/Default.tsx b/packages/payload/src/admin/components/views/collections/Edit/Default.tsx index 4fcf5b66b6..deb90e46da 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Default.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Default.tsx @@ -10,6 +10,7 @@ import Form from '../../../forms/Form' import { useAuth } from '../../../utilities/Auth' import { OperationContext } from '../../../utilities/OperationProvider' import { CollectionRoutes } from './Routes' +import { CustomCollectionComponent } from './Routes/CustomComponent' import './index.scss' const baseClass = 'collection-edit' @@ -25,6 +26,7 @@ const DefaultEditView: React.FC = (props) => { collection, customHeader, data, + disableRoutes, hasSavePermission, internalState, isEditing, @@ -87,7 +89,11 @@ const DefaultEditView: React.FC = (props) => { id={id} isEditing={isEditing} /> - + {disableRoutes ? ( + + ) : ( + + )} )} diff --git a/packages/payload/src/admin/components/views/collections/Edit/Routes/CustomComponent.tsx b/packages/payload/src/admin/components/views/collections/Edit/Routes/CustomComponent.tsx index ed7221af80..f80a2e8334 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Routes/CustomComponent.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Routes/CustomComponent.tsx @@ -15,7 +15,7 @@ export type collectionViewType = | 'Version' | 'Versions' -const defaultViews: { +export const defaultCollectionViews: { [key in collectionViewType]: React.ComponentType } = { API: null, @@ -48,7 +48,7 @@ export const CustomCollectionComponent = ( typeof Edit?.[view] === 'object' && typeof Edit[view].Component === 'function' ? Edit[view].Component - : defaultViews[view] + : defaultCollectionViews[view] if (Component) { return diff --git a/packages/payload/src/admin/components/views/collections/Edit/Routes/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/Routes/index.tsx index 2f4c745873..167e5f901d 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Routes/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Routes/index.tsx @@ -4,6 +4,7 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom' import { useAuth } from '../../../../utilities/Auth' import { useConfig } from '../../../../utilities/Config' +import NotFound from '../../../NotFound' import { type Props } from '../types' import { CustomCollectionComponent } from './CustomComponent' import { collectionCustomRoutes } from './custom' @@ -52,9 +53,16 @@ export const CollectionRoutes: React.FC = (props) => { permissions, user, })} - + + + + ) } diff --git a/packages/payload/src/admin/components/views/collections/Edit/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/index.tsx index c766e9a7d7..8afb0cbbe6 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/index.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Redirect, useHistory, useRouteMatch } from 'react-router-dom' +import { useHistory, useRouteMatch } from 'react-router-dom' import type { CollectionPermission } from '../../../../../auth' import type { Fields } from '../../../forms/Form/types' @@ -14,6 +14,7 @@ import { useDocumentInfo } from '../../../utilities/DocumentInfo' import { EditDepthContext } from '../../../utilities/EditDepth' import { useLocale } from '../../../utilities/Locale' import RenderCustomComponent from '../../../utilities/RenderCustomComponent' +import NotFound from '../../NotFound' import DefaultEdit from './Default' import formatFields from './formatFields' @@ -106,7 +107,7 @@ const EditView: React.FC = (props) => { }, [history, redirect]) if (isError) { - return + return } const apiURL = `${serverURL}${api}/${collectionSlug}/${id}?locale=${locale}${ diff --git a/packages/payload/src/admin/components/views/collections/Edit/types.ts b/packages/payload/src/admin/components/views/collections/Edit/types.ts index 3aff257635..3bda0a2ec7 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/types.ts +++ b/packages/payload/src/admin/components/views/collections/Edit/types.ts @@ -18,6 +18,7 @@ export type Props = IndexProps & { data: Document disableActions?: boolean disableLeaveWithoutSaving?: boolean + disableRoutes?: boolean hasSavePermission: boolean id?: string internalState?: Fields diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 1ef092434e..91525a5686 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -4,6 +4,7 @@ import type { GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType } from ' import type { Config as GeneratedTypes } from 'payload/generated-types' import type { DeepRequired } from 'ts-essentials' +import type { DocumentTab } from '../../admin/components/elements/DocumentHeader/Tabs/types' import type { CustomPreviewButtonProps, CustomPublishButtonProps, @@ -164,27 +165,17 @@ type BeforeDuplicateArgs = { export type BeforeDuplicate = (args: BeforeDuplicateArgs) => Promise | T -export type CollectionEditView = - | { - /** - * The component to render for this view - * + Replaces the default component - */ - Component: React.ComponentType - /** - * The label rendered in the admin UI for this view - * + Example: `default` is `Edit` - */ - label: string - /** - * The URL path to the nested collection edit views - * + Example: `/admin/collections/:collection/:id/:path` - * + The `:path` is the value of this property - * + Note: the default collection view uses no path - */ - path?: string - } - | React.ComponentType +export type CollectionEditViewConfig = { + /** + * The component to render for this view + * + Replaces the default component + */ + Component: React.ComponentType + Tab: DocumentTab + path: string +} + +export type CollectionEditView = CollectionEditViewConfig | React.ComponentType export type CollectionAdminOptions = { /** diff --git a/packages/payload/src/config/shared/componentSchema.ts b/packages/payload/src/config/shared/componentSchema.ts index b48fb5e249..d6946d01e5 100644 --- a/packages/payload/src/config/shared/componentSchema.ts +++ b/packages/payload/src/config/shared/componentSchema.ts @@ -2,8 +2,17 @@ import joi from 'joi' export const componentSchema = joi.alternatives().try(joi.object().unknown(), joi.func()) -export const customViewSchema = { - Component: componentSchema, - label: joi.string(), - path: joi.string(), +export const documentTabSchema = { + condition: joi.func(), + href: joi.alternatives().try(joi.string(), joi.func()).required(), + isActive: joi.alternatives().try(joi.func(), joi.boolean()), + label: joi.alternatives().try(joi.string(), joi.func()).required(), + newTab: joi.boolean(), + pillLabel: joi.alternatives().try(joi.string(), joi.func()), } + +export const customViewSchema = joi.object({ + Component: componentSchema.required(), + Tab: joi.alternatives().try(documentTabSchema, componentSchema), + path: joi.string().required(), +}) diff --git a/packages/payload/src/globals/config/types.ts b/packages/payload/src/globals/config/types.ts index e1e435a177..a85c3f51ee 100644 --- a/packages/payload/src/globals/config/types.ts +++ b/packages/payload/src/globals/config/types.ts @@ -2,6 +2,7 @@ import type { GraphQLNonNull, GraphQLObjectType } from 'graphql' import type React from 'react' import type { DeepRequired } from 'ts-essentials' +import type { DocumentTab } from '../../admin/components/elements/DocumentHeader/Tabs/types' import type { CustomPreviewButtonProps, CustomPublishButtonProps, @@ -38,27 +39,17 @@ export type AfterReadHook = (args: { req: PayloadRequest }) => any -export type GlobalEditView = - | { - /** - * The component to render for this view - * + Replaces the default component - */ - Component: React.ComponentType - /** - * The label rendered in the admin UI for this view - * + Example: `default` is `Edit` - */ - label: string - /** - * The URL path to the nested global edit views - * + Example: `/admin/globals/:slug/:path` - * + The `:path` is the value of this property - * + Note: the default global view uses no path - */ - path?: string - } - | React.ComponentType +export type GlobalEditViewConfig = { + /** + * The component to render for this view + * + Replaces the default component + */ + Component: React.ComponentType + Tab?: DocumentTab + path: string +} + +export type GlobalEditView = GlobalEditViewConfig | React.ComponentType export type GlobalAdminOptions = { /** diff --git a/test/admin/components/CustomTabComponent/index.scss b/test/admin/components/CustomTabComponent/index.scss new file mode 100644 index 0000000000..5418e91d41 --- /dev/null +++ b/test/admin/components/CustomTabComponent/index.scss @@ -0,0 +1,10 @@ +.custom-doc-tab { + text-decoration: none; + background-color: var(--theme-elevation-200); + padding: 0 6px; + border-radius: 2px; + + &:hover { + background-color: var(--theme-elevation-300); + } +} diff --git a/test/admin/components/CustomTabComponent/index.tsx b/test/admin/components/CustomTabComponent/index.tsx new file mode 100644 index 0000000000..1d1bc4879c --- /dev/null +++ b/test/admin/components/CustomTabComponent/index.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { Link, useRouteMatch } from 'react-router-dom' + +import type { DocumentTabProps } from '../../../../packages/payload/src/admin/components/elements/DocumentHeader/Tabs/types' + +import './index.scss' + +const CustomTabComponent: React.FC = (props) => { + const { path } = props + const match = useRouteMatch() + + return ( + + Custom Tab Component + + ) +} + +export default CustomTabComponent diff --git a/test/admin/config.ts b/test/admin/config.ts index cf8b0687cf..fc10bb7e1c 100644 --- a/test/admin/config.ts +++ b/test/admin/config.ts @@ -7,6 +7,7 @@ import { devUser } from '../credentials' import AfterDashboard from './components/AfterDashboard' import AfterNavLinks from './components/AfterNavLinks' import BeforeLogin from './components/BeforeLogin' +import CustomTabComponent from './components/CustomTabComponent' import DemoUIFieldCell from './components/DemoUIField/Cell' import DemoUIFieldField from './components/DemoUIField/Field' import Logout from './components/Logout' @@ -164,9 +165,17 @@ export default buildConfigWithDefaults({ Default: CustomDefaultView, Versions: CustomVersionsView, MyCustomView: { - path: '/custom', + path: '/custom-tab-view', Component: CustomView, - label: 'Custom', + Tab: { + label: 'Custom', + href: '/custom-tab-view', + }, + }, + MyCustomViewWithCustomTab: { + path: '/custom-tab-component', + Component: CustomView, + Tab: CustomTabComponent, }, }, }, @@ -293,9 +302,17 @@ export default buildConfigWithDefaults({ Default: CustomDefaultView, Versions: CustomVersionsView, MyCustomView: { - path: '/custom', + path: '/custom-tab-view', Component: CustomView, - label: 'Custom', + Tab: { + label: 'Custom', + href: '/custom-tab-view', + }, + }, + MyCustomViewWithCustomTab: { + path: '/custom-tab-component', + Component: CustomView, + Tab: CustomTabComponent, }, }, }, diff --git a/test/admin/e2e.spec.ts b/test/admin/e2e.spec.ts index 838499af3c..f8cd2a9075 100644 --- a/test/admin/e2e.spec.ts +++ b/test/admin/e2e.spec.ts @@ -43,14 +43,14 @@ describe('admin', () => { }) describe('Nav', () => { - test('should nav to collection - sidebar', async () => { + test('should nav to collection - main menu', async () => { await page.goto(url.admin) await openMainMenu(page) await page.locator(`#nav-${slug}`).click() expect(page.url()).toContain(url.list) }) - test('should nav to a global - sidebar', async () => { + test('should nav to a global - main menu', async () => { await page.goto(url.admin) await openMainMenu(page) await page.locator(`#nav-global-${globalSlug}`).click()