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" ;
3
3
4
4
/**
5
5
* Merge fields deeply
@@ -20,12 +20,12 @@ export const mergeFields = ({
20
20
patchFields : Field [ ] ;
21
21
} ) => {
22
22
let fields = deepCopyObjectSimple ( baseFields ) ;
23
- const toHandleFields = patchFields ;
23
+ const toHandleFields = deepCopyObjectSimple ( patchFields ) ;
24
24
25
25
for ( let i = 0 ; i < toHandleFields . length ; i ++ ) {
26
26
const field = toHandleFields [ i ] ;
27
27
if ( fieldAffectsData ( field ) ) {
28
- const existingField = findField ( fields , field . name ) ;
28
+ const existingField = findFieldByName ( fields , field . name ) ;
29
29
if ( existingField ) {
30
30
// Check if field type matches
31
31
if ( field . type !== existingField . type ) {
@@ -44,8 +44,7 @@ export const mergeFields = ({
44
44
} ) ;
45
45
existingField . fields = [ ...result . mergedFields , ...result . restFields ] ;
46
46
} else {
47
- // Merge the field
48
- // Existing field has always priority
47
+ // Merge the field (existing field has always priority)
49
48
Object . assign ( existingField , deepMerge < Field > ( field , existingField ) ) ;
50
49
}
51
50
@@ -67,9 +66,52 @@ export const mergeFields = ({
67
66
toHandleFields . splice ( i , 1 ) ;
68
67
i -- ;
69
68
}
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
+ }
73
115
}
74
116
}
75
117
@@ -82,18 +124,18 @@ export const mergeFields = ({
82
124
* @param fields The fields list
83
125
* @param name The field name
84
126
*/
85
- const findField = ( fields : Field [ ] , name : string ) : Field | undefined => {
127
+ const findFieldByName = ( fields : Field [ ] , name : string ) : Field | undefined => {
86
128
for ( const field of fields ) {
87
129
if ( "fields" in field && ! fieldAffectsData ( field ) ) {
88
130
// 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 ) ;
90
132
if ( found ) {
91
133
return found ;
92
134
}
93
135
} else if ( field . type === "tabs" ) {
94
136
// For each tab, find the field
95
137
for ( const tab of field . tabs ) {
96
- const found = findField ( tab . fields , name ) ;
138
+ const found = findFieldByName ( tab . fields , name ) ;
97
139
if ( found ) {
98
140
return found ;
99
141
}
@@ -105,6 +147,28 @@ const findField = (fields: Field[], name: string): Field | undefined => {
105
147
return undefined ;
106
148
} ;
107
149
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
+
108
172
/**
109
173
* Get all virtual fields from a list of fields (including subfields and tabs)
110
174
*
0 commit comments