feat: added support for conditional tabs (#8720)
Adds support for conditional tabs.
You can now add a `condition` function like other fields to each
individual tab's admin config like so:
```ts
{
name: 'contentTab',
admin: {
condition: (data) => Boolean(data?.enableTab)
}
}
```
This will toggle the individual tab's visibility in the document listing
### Example
https://github.com/user-attachments/assets/45cf9cfd-eaed-4dfe-8a32-1992385fd05c
This is an updated PR from
https://github.com/payloadcms/payload/pull/8406 thanks to @willviles
---------
Co-authored-by: Will Viles <will@willviles.com>
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { FieldWithSubFields, TabsField } from 'payload'
|
||||
import type { FieldWithSubFields, Tab, TabsField } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
|
||||
|
||||
@@ -17,7 +17,7 @@ export const recursivelyBuildNestedPaths = ({ field, nestedFieldName2, parentNam
|
||||
if (field.type === 'tabs') {
|
||||
// if the tab has a name, treat it as a group
|
||||
// otherwise, treat it as a row
|
||||
return field.tabs.reduce((tabSchema, tab: any) => {
|
||||
return (field.tabs as Tab[]).reduce((tabSchema, tab: any) => {
|
||||
tabSchema.push(
|
||||
...recursivelyBuildNestedPaths({
|
||||
field: {
|
||||
|
||||
@@ -25,8 +25,11 @@ import type {
|
||||
} from '../types.js'
|
||||
|
||||
export type ClientTab =
|
||||
| ({ fields: ClientField[]; readonly path?: string } & Omit<NamedTab, 'fields'>)
|
||||
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
|
||||
| ({ fields: ClientField[]; passesCondition?: boolean; readonly path?: string } & Omit<
|
||||
NamedTab,
|
||||
'fields'
|
||||
>)
|
||||
| ({ fields: ClientField[]; passesCondition?: boolean } & Omit<UnnamedTab, 'fields'>)
|
||||
|
||||
type TabsFieldBaseClientProps = FieldPaths
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type {
|
||||
CollectionConfig,
|
||||
@@ -288,6 +289,16 @@ export const sanitizeFields = async ({
|
||||
tab.label = toWords(tab.name)
|
||||
}
|
||||
|
||||
if (
|
||||
'admin' in tab &&
|
||||
tab.admin?.condition &&
|
||||
typeof tab.admin.condition === 'function' &&
|
||||
!tab.id
|
||||
) {
|
||||
// Always attach a UUID to tabs with a condition so there's no conflicts even if there are duplicate nested names
|
||||
tab.id = tabHasName(tab) ? `${tab.name}_${uuid()}` : uuid()
|
||||
}
|
||||
|
||||
tab.fields = await sanitizeFields({
|
||||
config,
|
||||
existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames,
|
||||
|
||||
@@ -788,6 +788,7 @@ type TabBase = {
|
||||
*/
|
||||
description?: LabelFunction | StaticDescription
|
||||
fields: Field[]
|
||||
id?: string
|
||||
interfaceName?: string
|
||||
saveToJWT?: boolean | string
|
||||
} & Omit<FieldBase, 'required' | 'validate'>
|
||||
@@ -819,11 +820,11 @@ export type UnnamedTab = {
|
||||
} & Omit<TabBase, 'name' | 'virtual'>
|
||||
|
||||
export type Tab = NamedTab | UnnamedTab
|
||||
|
||||
export type TabsField = {
|
||||
admin?: Omit<Admin, 'description'>
|
||||
tabs: Tab[]
|
||||
type: 'tabs'
|
||||
} & {
|
||||
tabs: Tab[]
|
||||
} & Omit<FieldBase, 'admin' | 'localized' | 'name' | 'saveToJWT' | 'virtual'>
|
||||
|
||||
export type TabsFieldClient = {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--active {
|
||||
opacity: 1 !important;
|
||||
|
||||
|
||||
@@ -14,13 +14,20 @@ import './index.scss'
|
||||
const baseClass = 'tabs-field__tab-button'
|
||||
|
||||
type TabProps = {
|
||||
readonly hidden?: boolean
|
||||
readonly isActive?: boolean
|
||||
readonly parentPath: string
|
||||
readonly setIsActive: () => void
|
||||
readonly tab: ClientTab
|
||||
}
|
||||
|
||||
export const TabComponent: React.FC<TabProps> = ({ isActive, parentPath, setIsActive, tab }) => {
|
||||
export const TabComponent: React.FC<TabProps> = ({
|
||||
hidden,
|
||||
isActive,
|
||||
parentPath,
|
||||
setIsActive,
|
||||
tab,
|
||||
}) => {
|
||||
const { i18n } = useTranslation()
|
||||
const [errorCount, setErrorCount] = useState(undefined)
|
||||
|
||||
@@ -40,6 +47,7 @@ export const TabComponent: React.FC<TabProps> = ({ isActive, parentPath, setIsAc
|
||||
baseClass,
|
||||
fieldHasErrors && `${baseClass}--has-error`,
|
||||
isActive && `${baseClass}--active`,
|
||||
hidden && `${baseClass}--hidden`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
margin-left: calc(var(--gutter-h) * -1);
|
||||
margin-right: calc(var(--gutter-h) * -1);
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__content-wrap {
|
||||
padding-left: var(--gutter-h);
|
||||
padding-right: var(--gutter-h);
|
||||
@@ -50,6 +54,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__tab--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-bottom: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { useCollapsible } from '../../elements/Collapsible/provider.js'
|
||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||
import { useFormFields } from '../../forms/Form/index.js'
|
||||
import { RenderFields } from '../../forms/RenderFields/index.js'
|
||||
import { useField } from '../../forms/useField/index.js'
|
||||
import { withCondition } from '../../forms/withCondition/index.js'
|
||||
@@ -22,9 +23,9 @@ import { usePreferences } from '../../providers/Preferences/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { FieldDescription } from '../FieldDescription/index.js'
|
||||
import { fieldBaseClass } from '../shared/index.js'
|
||||
import './index.scss'
|
||||
import { TabsProvider } from './provider.js'
|
||||
import { TabComponent } from './Tab/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'tabs-field'
|
||||
|
||||
@@ -60,45 +61,52 @@ const TabsFieldComponent: TabsFieldClientComponent = (props) => {
|
||||
const { preferencesKey } = useDocumentInfo()
|
||||
const { i18n } = useTranslation()
|
||||
const { isWithinCollapsible } = useCollapsible()
|
||||
const [activeTabIndex, setActiveTabIndex] = useState<number>(0)
|
||||
|
||||
const tabStates = useFormFields(([fields]) => {
|
||||
return tabs.map((tab, index) => {
|
||||
const id = tab?.id
|
||||
|
||||
return {
|
||||
index,
|
||||
passesCondition: fields?.[id]?.passesCondition ?? true,
|
||||
tab,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const [activeTabIndex, setActiveTabIndex] = useState<number>(
|
||||
() => tabStates.filter(({ passesCondition }) => passesCondition)?.[0]?.index ?? 0,
|
||||
)
|
||||
|
||||
const tabsPrefKey = `tabs-${indexPath}`
|
||||
const [activeTabPath, setActiveTabPath] = useState<string>(() =>
|
||||
generateTabPath({ activeTabConfig: tabs[activeTabIndex], path: parentPath }),
|
||||
)
|
||||
|
||||
const activePathChildrenPath = tabHasName(tabs[activeTabIndex]) ? activeTabPath : parentPath
|
||||
|
||||
const [activeTabSchemaPath, setActiveTabSchemaPath] = useState<string>(() =>
|
||||
generateTabPath({ activeTabConfig: tabs[0], path: parentSchemaPath }),
|
||||
)
|
||||
|
||||
const activePathChildrenPath = tabHasName(tabs[activeTabIndex]) ? activeTabPath : parentPath
|
||||
const activeTabInfo = tabStates[activeTabIndex]
|
||||
const activeTabConfig = activeTabInfo?.tab
|
||||
const activePathSchemaChildrenPath = tabHasName(tabs[activeTabIndex])
|
||||
? activeTabSchemaPath
|
||||
: parentSchemaPath
|
||||
|
||||
useEffect(() => {
|
||||
if (preferencesKey) {
|
||||
const getInitialPref = async () => {
|
||||
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
|
||||
const initialIndex = path
|
||||
? existingPreferences?.fields?.[path]?.tabIndex
|
||||
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
|
||||
const activeTabDescription = activeTabConfig.admin?.description ?? activeTabConfig.description
|
||||
|
||||
const newIndex = initialIndex || 0
|
||||
setActiveTabIndex(newIndex)
|
||||
const activeTabStaticDescription =
|
||||
typeof activeTabDescription === 'function'
|
||||
? activeTabDescription({ t: i18n.t })
|
||||
: activeTabDescription
|
||||
|
||||
setActiveTabPath(generateTabPath({ activeTabConfig: tabs[newIndex], path: parentPath }))
|
||||
setActiveTabSchemaPath(
|
||||
generateTabPath({ activeTabConfig: tabs[newIndex], path: parentSchemaPath }),
|
||||
)
|
||||
}
|
||||
void getInitialPref()
|
||||
}
|
||||
}, [path, getPreference, preferencesKey, tabsPrefKey, tabs, parentPath, parentSchemaPath])
|
||||
const hasVisibleTabs = tabStates.some(({ passesCondition }) => passesCondition)
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
async (incomingTabIndex: number): Promise<void> => {
|
||||
setActiveTabIndex(incomingTabIndex)
|
||||
|
||||
setActiveTabPath(
|
||||
generateTabPath({ activeTabConfig: tabs[incomingTabIndex], path: parentPath }),
|
||||
)
|
||||
@@ -145,14 +153,34 @@ const TabsFieldComponent: TabsFieldClientComponent = (props) => {
|
||||
],
|
||||
)
|
||||
|
||||
const activeTabConfig = tabs[activeTabIndex]
|
||||
useEffect(() => {
|
||||
if (preferencesKey) {
|
||||
const getInitialPref = async () => {
|
||||
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
|
||||
const initialIndex = path
|
||||
? existingPreferences?.fields?.[path]?.tabIndex
|
||||
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
|
||||
|
||||
const activeTabDescription = activeTabConfig.admin?.description ?? activeTabConfig.description
|
||||
const newIndex = initialIndex || 0
|
||||
setActiveTabIndex(newIndex)
|
||||
|
||||
const activeTabStaticDescription =
|
||||
typeof activeTabDescription === 'function'
|
||||
? activeTabDescription({ t: i18n.t })
|
||||
: activeTabDescription
|
||||
setActiveTabPath(generateTabPath({ activeTabConfig: tabs[newIndex], path: parentPath }))
|
||||
setActiveTabSchemaPath(
|
||||
generateTabPath({ activeTabConfig: tabs[newIndex], path: parentSchemaPath }),
|
||||
)
|
||||
}
|
||||
void getInitialPref()
|
||||
}
|
||||
}, [path, getPreference, preferencesKey, tabsPrefKey, tabs, parentPath, parentSchemaPath])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabInfo?.passesCondition === false) {
|
||||
const nextTab = tabStates.find(({ passesCondition }) => passesCondition)
|
||||
if (nextTab) {
|
||||
void handleTabChange(nextTab.index)
|
||||
}
|
||||
}
|
||||
}, [activeTabInfo, tabStates, handleTabChange])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -161,6 +189,7 @@ const TabsFieldComponent: TabsFieldClientComponent = (props) => {
|
||||
className,
|
||||
baseClass,
|
||||
isWithinCollapsible && `${baseClass}--within-collapsible`,
|
||||
!hasVisibleTabs && `${baseClass}--hidden`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
@@ -168,31 +197,31 @@ const TabsFieldComponent: TabsFieldClientComponent = (props) => {
|
||||
<TabsProvider>
|
||||
<div className={`${baseClass}__tabs-wrap`}>
|
||||
<div className={`${baseClass}__tabs`}>
|
||||
{tabs.map((tab, tabIndex) => {
|
||||
return (
|
||||
{tabStates.map(({ index, passesCondition, tab }) => (
|
||||
<TabComponent
|
||||
isActive={activeTabIndex === tabIndex}
|
||||
key={tabIndex}
|
||||
hidden={!passesCondition}
|
||||
isActive={activeTabIndex === index}
|
||||
key={index}
|
||||
parentPath={path}
|
||||
setIsActive={() => {
|
||||
void handleTabChange(tabIndex)
|
||||
void handleTabChange(index)
|
||||
}}
|
||||
tab={tab}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__content-wrap`}>
|
||||
{activeTabConfig && (
|
||||
<ActiveTabContent
|
||||
<TabContent
|
||||
description={activeTabStaticDescription}
|
||||
fields={activeTabConfig.fields}
|
||||
forceRender={forceRender}
|
||||
hidden={false}
|
||||
parentIndexPath={
|
||||
tabHasName(activeTabConfig)
|
||||
? ''
|
||||
: `${indexPath ? indexPath + '-' : ''}` + String(activeTabIndex)
|
||||
: `${indexPath ? indexPath + '-' : ''}` + String(activeTabInfo.index)
|
||||
}
|
||||
parentPath={activePathChildrenPath}
|
||||
parentSchemaPath={activePathSchemaChildrenPath}
|
||||
@@ -218,21 +247,23 @@ const TabsFieldComponent: TabsFieldClientComponent = (props) => {
|
||||
export const TabsField = withCondition(TabsFieldComponent)
|
||||
|
||||
type ActiveTabProps = {
|
||||
description: StaticDescription
|
||||
fields: ClientField[]
|
||||
forceRender?: boolean
|
||||
label?: string
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
path: string
|
||||
permissions: SanitizedFieldPermissions
|
||||
readOnly: boolean
|
||||
readonly description: StaticDescription
|
||||
readonly fields: ClientField[]
|
||||
readonly forceRender?: boolean
|
||||
readonly hidden: boolean
|
||||
readonly label?: string
|
||||
readonly parentIndexPath: string
|
||||
readonly parentPath: string
|
||||
readonly parentSchemaPath: string
|
||||
readonly path: string
|
||||
readonly permissions: SanitizedFieldPermissions
|
||||
readonly readOnly: boolean
|
||||
}
|
||||
function ActiveTabContent({
|
||||
function TabContent({
|
||||
description,
|
||||
fields,
|
||||
forceRender,
|
||||
hidden,
|
||||
label,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
@@ -253,6 +284,7 @@ function ActiveTabContent({
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
hidden && `${baseClass}__tab--hidden`,
|
||||
`${baseClass}__tab`,
|
||||
label && `${baseClass}__tabConfigLabel-${toKebabCase(getTranslation(label, i18n))}`,
|
||||
]
|
||||
|
||||
@@ -750,6 +750,26 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
childPermissions = parentPermissions
|
||||
}
|
||||
|
||||
const pathSegments = path ? path.split('.') : []
|
||||
|
||||
// If passesCondition is false then this should always result to false
|
||||
// If the tab has no admin.condition provided then fallback to passesCondition and let that decide the result
|
||||
let tabPassesCondition = passesCondition
|
||||
|
||||
if (passesCondition && typeof tab.admin?.condition === 'function') {
|
||||
tabPassesCondition = tab.admin.condition(fullData, data, {
|
||||
blockData,
|
||||
path: pathSegments,
|
||||
user: req.user,
|
||||
})
|
||||
}
|
||||
|
||||
if (tab?.id) {
|
||||
state[tab.id] = {
|
||||
passesCondition: tabPassesCondition,
|
||||
}
|
||||
}
|
||||
|
||||
return iterateFields({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
@@ -767,7 +787,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
omitParents,
|
||||
operation,
|
||||
parentIndexPath: isNamedTab ? '' : tabIndexPath,
|
||||
parentPassesCondition: passesCondition,
|
||||
parentPassesCondition: tabPassesCondition,
|
||||
parentPath: isNamedTab ? tabPath : parentPath,
|
||||
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
|
||||
permissions: childPermissions,
|
||||
|
||||
@@ -126,12 +126,69 @@ describe('Tabs', () => {
|
||||
|
||||
test('should render array data within named tabs', async () => {
|
||||
await navigateToDoc(page, url)
|
||||
await switchTab(page, '.tabs-field__tab-button:nth-child(5)')
|
||||
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Name")')
|
||||
await expect(page.locator('#field-tab__array__0__text')).toHaveValue(
|
||||
"Hello, I'm the first row, in a named tab",
|
||||
)
|
||||
})
|
||||
|
||||
test('should render conditional tab when checkbox is toggled', async () => {
|
||||
await navigateToDoc(page, url)
|
||||
|
||||
const conditionalTabSelector = '.tabs-field__tab-button:text-is("Conditional Tab")'
|
||||
const button = page.locator(conditionalTabSelector)
|
||||
await expect(
|
||||
async () => await expect(page.locator(conditionalTabSelector)).toHaveClass(/--hidden/),
|
||||
).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
const checkboxSelector = `input#field-conditionalTabVisible`
|
||||
await page.locator(checkboxSelector).check()
|
||||
await expect(page.locator(checkboxSelector)).toBeChecked()
|
||||
|
||||
await expect(
|
||||
async () => await expect(page.locator(conditionalTabSelector)).not.toHaveClass(/--hidden/),
|
||||
).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
await switchTab(page, conditionalTabSelector)
|
||||
|
||||
await expect(
|
||||
page.locator('label[for="field-conditionalTab__conditionalTabField"]'),
|
||||
).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('should hide nested conditional tab when checkbox is toggled', async () => {
|
||||
await navigateToDoc(page, url)
|
||||
|
||||
// Show the conditional tab
|
||||
const conditionalTabSelector = '.tabs-field__tab-button:text-is("Conditional Tab")'
|
||||
const checkboxSelector = `input#field-conditionalTabVisible`
|
||||
await page.locator(checkboxSelector).check()
|
||||
await switchTab(page, conditionalTabSelector)
|
||||
|
||||
// Now assert on the nested conditional tab
|
||||
const nestedConditionalTabSelector = '.tabs-field__tab-button:text-is("Nested Conditional Tab")'
|
||||
|
||||
await expect(
|
||||
async () =>
|
||||
await expect(page.locator(nestedConditionalTabSelector)).not.toHaveClass(/--hidden/),
|
||||
).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
const nestedCheckboxSelector = `input#field-conditionalTab__nestedConditionalTabVisible`
|
||||
await page.locator(nestedCheckboxSelector).uncheck()
|
||||
|
||||
await expect(
|
||||
async () => await expect(page.locator(nestedConditionalTabSelector)).toHaveClass(/--hidden/),
|
||||
).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
})
|
||||
|
||||
test('should save preferences for tab order', async () => {
|
||||
await page.goto(url.list)
|
||||
|
||||
@@ -139,7 +196,7 @@ describe('Tabs', () => {
|
||||
const href = await firstItem.getAttribute('href')
|
||||
await firstItem.click()
|
||||
|
||||
const regex = new RegExp(href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
const regex = new RegExp(href!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
|
||||
await page.waitForURL(regex)
|
||||
|
||||
|
||||
@@ -21,9 +21,103 @@ const TabsFields: CollectionConfig = {
|
||||
'This should not collapse despite there being many tabs pushing the main fields open.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'conditionalTabVisible',
|
||||
type: 'checkbox',
|
||||
label: 'Toggle Conditional Tab',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description:
|
||||
'When active, the conditional tab should be visible. When inactive, it should be hidden.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'conditionalTab',
|
||||
label: 'Conditional Tab',
|
||||
description: 'This tab should only be visible when the conditional field is checked.',
|
||||
fields: [
|
||||
{
|
||||
name: 'conditionalTabField',
|
||||
type: 'text',
|
||||
label: 'Conditional Tab Field',
|
||||
defaultValue:
|
||||
'This field should only be visible when the conditional tab is visible.',
|
||||
},
|
||||
{
|
||||
name: 'nestedConditionalTabVisible',
|
||||
type: 'checkbox',
|
||||
label: 'Toggle Nested Conditional Tab',
|
||||
defaultValue: true,
|
||||
admin: {
|
||||
description:
|
||||
'When active, the nested conditional tab should be visible. When inactive, it should be hidden.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'conditionalTabGroup',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'conditionalTabGroupTitle',
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
// duplicate name as above, should not conflict with tab IDs in form-state, if it does then tests will fail
|
||||
name: 'conditionalTab',
|
||||
label: 'Duplicate conditional tab',
|
||||
fields: [],
|
||||
admin: {
|
||||
condition: ({ conditionalTab }) =>
|
||||
!!conditionalTab?.nestedConditionalTabVisible,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
|
||||
tabs: [
|
||||
{
|
||||
label: 'Nested Unconditional Tab',
|
||||
description: 'Description for a nested unconditional tab',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedUnconditionalTabInput',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Nested Conditional Tab',
|
||||
description: 'Here is a description for a nested conditional tab',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedConditionalTabInput',
|
||||
type: 'textarea',
|
||||
defaultValue:
|
||||
'This field should only be visible when the nested conditional tab is visible.',
|
||||
},
|
||||
],
|
||||
admin: {
|
||||
condition: ({ conditionalTab }) =>
|
||||
!!conditionalTab?.nestedConditionalTabVisible,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
admin: {
|
||||
condition: ({ conditionalTabVisible }) => !!conditionalTabVisible,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Tab with Array',
|
||||
description: 'This tab has an array.',
|
||||
|
||||
@@ -1831,6 +1831,19 @@ export interface TabsField {
|
||||
* This should not collapse despite there being many tabs pushing the main fields open.
|
||||
*/
|
||||
sidebarField?: string | null;
|
||||
/**
|
||||
* When active, the conditional tab should be visible. When inactive, it should be hidden.
|
||||
*/
|
||||
conditionalTabVisible?: boolean | null;
|
||||
conditionalTab?: {
|
||||
conditionalTabField?: string | null;
|
||||
/**
|
||||
* When active, the nested conditional tab should be visible. When inactive, it should be hidden.
|
||||
*/
|
||||
nestedConditionalTabVisible?: boolean | null;
|
||||
nestedUnconditionalTabInput?: string | null;
|
||||
nestedConditionalTabInput?: string | null;
|
||||
};
|
||||
array: {
|
||||
text: string;
|
||||
id?: string | null;
|
||||
@@ -3452,6 +3465,15 @@ export interface TabsFields2Select<T extends boolean = true> {
|
||||
*/
|
||||
export interface TabsFieldsSelect<T extends boolean = true> {
|
||||
sidebarField?: T;
|
||||
conditionalTabVisible?: T;
|
||||
conditionalTab?:
|
||||
| T
|
||||
| {
|
||||
conditionalTabField?: T;
|
||||
nestedConditionalTabVisible?: T;
|
||||
nestedUnconditionalTabInput?: T;
|
||||
nestedConditionalTabInput?: T;
|
||||
};
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user