1- import { deepCopyObjectSimple , deepMerge , type Field } from "payload" ;
2- import { fieldAffectsData , fieldIsVirtual } from "payload/shared" ;
1+ import { deepCopyObjectSimple , deepMerge , NamedTab , Tab , type Field } from "payload" ;
2+ import { fieldAffectsData , fieldIsVirtual , tabHasName } from "payload/shared" ;
33
44/**
55 * Merge fields deeply
@@ -20,12 +20,12 @@ export const mergeFields = ({
2020 patchFields : Field [ ] ;
2121} ) => {
2222 let fields = deepCopyObjectSimple ( baseFields ) ;
23- const toHandleFields = patchFields ;
23+ const toHandleFields = deepCopyObjectSimple ( patchFields ) ;
2424
2525 for ( let i = 0 ; i < toHandleFields . length ; i ++ ) {
2626 const field = toHandleFields [ i ] ;
2727 if ( fieldAffectsData ( field ) ) {
28- const existingField = findField ( fields , field . name ) ;
28+ const existingField = findFieldByName ( fields , field . name ) ;
2929 if ( existingField ) {
3030 // Check if field type matches
3131 if ( field . type !== existingField . type ) {
@@ -44,8 +44,7 @@ export const mergeFields = ({
4444 } ) ;
4545 existingField . fields = [ ...result . mergedFields , ...result . restFields ] ;
4646 } else {
47- // Merge the field
48- // Existing field has always priority
47+ // Merge the field (existing field has always priority)
4948 Object . assign ( existingField , deepMerge < Field > ( field , existingField ) ) ;
5049 }
5150
@@ -67,9 +66,52 @@ export const mergeFields = ({
6766 toHandleFields . splice ( i , 1 ) ;
6867 i -- ;
6968 }
70- } else {
71- // Field type not allowed (e.g. tabs)
72- throw new Error ( `Field type '${ field . type } ' is not allowed inside '${ path } '` ) ;
69+ } else if ( field . type === "tabs" ) {
70+ // Merge tabs if tabs field exists
71+ const tabsField = fields . find ( f => f . type === "tabs" ) ;
72+ if ( tabsField ) {
73+ for ( let t = 0 ; t < field . tabs . length ; t ++ ) {
74+ const tab = field . tabs [ i ] ;
75+ const tabType = tabHasName ( tab ) ? "named" : "unnamed" ;
76+ const existingTab = findTabField (
77+ tabsField . tabs ,
78+ tabType ,
79+ // If tab is named, use the name
80+ tabType === "named"
81+ ? ( tab as NamedTab ) . name
82+ : // If tab has custom originalTabLabel, search by that
83+ tab . custom ?. originalTabLabel
84+ ? tab . custom . originalTabLabel
85+ : // Otherwise, search by label (if it's a string)
86+ typeof tab . label === "string"
87+ ? tab . label
88+ : "" ,
89+ ) ;
90+ if ( existingTab ) {
91+ // Merge the tab (existing tab has always priority)
92+ const result = mergeFields ( {
93+ path : tabType ? `${ path } .${ ( tab as NamedTab ) . name } ` : path ,
94+ baseFields : existingTab . fields ,
95+ patchFields : tab . fields ,
96+ } ) ;
97+ existingTab . fields = [ ...result . mergedFields , ...result . restFields ] ;
98+ const { fields : _ , ...restTab } = tab ;
99+ if ( tab . custom ?. originalTabLabel && tab . label ) delete existingTab . label ;
100+ Object . assign ( existingTab , deepMerge < Tab > ( restTab , existingTab ) ) ;
101+
102+ // Remove tab
103+ field . tabs . splice ( t , 1 ) ;
104+ t -- ;
105+ }
106+ }
107+
108+ // Add the rest tabs
109+ tabsField . tabs . push ( ...field . tabs ) ;
110+
111+ // Remove from toHandleFields / mark as done
112+ toHandleFields . splice ( i , 1 ) ;
113+ i -- ;
114+ }
73115 }
74116 }
75117
@@ -82,18 +124,18 @@ export const mergeFields = ({
82124 * @param fields The fields list
83125 * @param name The field name
84126 */
85- const findField = ( fields : Field [ ] , name : string ) : Field | undefined => {
127+ const findFieldByName = ( fields : Field [ ] , name : string ) : Field | undefined => {
86128 for ( const field of fields ) {
87129 if ( "fields" in field && ! fieldAffectsData ( field ) ) {
88130 // Find in subfields if field not affecting data (e.g. row)
89- const found = findField ( field . fields , name ) ;
131+ const found = findFieldByName ( field . fields , name ) ;
90132 if ( found ) {
91133 return found ;
92134 }
93135 } else if ( field . type === "tabs" ) {
94136 // For each tab, find the field
95137 for ( const tab of field . tabs ) {
96- const found = findField ( tab . fields , name ) ;
138+ const found = findFieldByName ( tab . fields , name ) ;
97139 if ( found ) {
98140 return found ;
99141 }
@@ -105,6 +147,28 @@ const findField = (fields: Field[], name: string): Field | undefined => {
105147 return undefined ;
106148} ;
107149
150+ /**
151+ * Find a tab field by name or label in a list of tabs
152+ *
153+ * @param tabs The tabs list
154+ * @param type The tab type (named or unnamed)
155+ * @param nameOrLabel The tab name or label
156+ */
157+ const findTabField = (
158+ tabs : Tab [ ] ,
159+ type : "unnamed" | "named" ,
160+ nameOrLabel : string ,
161+ ) : Tab | undefined => {
162+ for ( const tab of tabs ) {
163+ if (
164+ ( type === "unnamed" && ! tabHasName ( tab ) && tab . label === nameOrLabel ) ||
165+ ( type === "named" && tabHasName ( tab ) && tab . name === nameOrLabel )
166+ ) {
167+ return tab ;
168+ }
169+ }
170+ } ;
171+
108172/**
109173 * Get all virtual fields from a list of fields (including subfields and tabs)
110174 *
0 commit comments