Skip to content

Commit 2c07f2a

Browse files
committed
feat: Allow custom tabs field in users collection (#14)
1 parent 8643f01 commit 2c07f2a

File tree

2 files changed

+88
-12
lines changed

2 files changed

+88
-12
lines changed

packages/dev/src/payload/collections/users.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ const Users: CollectionConfig = {
2525
hidden: true, // Hide id field in admin panel
2626
},
2727
},
28+
{
29+
type: "tabs",
30+
tabs: [
31+
{
32+
label: () => "User Accounts", // Change tab label
33+
custom: {
34+
originalTabLabel: "Accounts",
35+
},
36+
fields: [],
37+
},
38+
],
39+
},
2840
{
2941
name: "accounts",
3042
type: "array",

packages/payload-authjs/src/utils/payload.ts

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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

Comments
 (0)