Skip to content

Commit c786d9e

Browse files
committed
CMS-46358 Add property group normalization and testing
1 parent 807c4e0 commit c786d9e

File tree

3 files changed

+257
-1
lines changed

3 files changed

+257
-1
lines changed

docs/3-modelling.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,38 @@ export default buildConfig({
5151
});
5252
```
5353

54+
### Optional: Define Property Groups
55+
56+
Property groups help organize your content type properties in the CMS editor. You can define custom property groups in your config file:
57+
58+
```js
59+
import { buildConfig } from '@optimizely/cms-sdk';
60+
61+
export default buildConfig({
62+
components: ['./src/components/**/*.tsx'],
63+
propertyGroups: [
64+
{
65+
key: 'seo',
66+
displayName: 'SEO',
67+
sortOrder: 1,
68+
},
69+
{
70+
key: 'meta',
71+
displayName: 'Metadata',
72+
sortOrder: 2,
73+
},
74+
],
75+
});
76+
```
77+
78+
Each property group has:
79+
80+
- `key` (required): A unique identifier for the group
81+
- `displayName` (optional): The name shown in the CMS editor. If not provided, it's auto-generated from the key
82+
- `sortOrder` (optional): Controls the display order. If not provided, it's auto-assigned based on array position
83+
84+
You can then reference these groups in your content type properties using the `group` field.
85+
5486
## Step 2. Sync content types to the CMS
5587

5688
Run the following command:

packages/optimizely-cms-cli/src/service/utils.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ export async function readFromPath(configPath: string, section: string) {
185185
* - Validates that each property group has a non-empty key
186186
* - Auto-generates displayName from key (capitalized) if missing
187187
* - Auto-assigns sortOrder based on array position (index + 1) if missing
188+
* - Deduplicates property groups by key, keeping the last occurrence
188189
* @param propertyGroups - The property groups array from the config
189190
* @returns Validated and normalized property groups array
190191
* @throws Error if validation fails (empty or missing key)
@@ -196,7 +197,7 @@ export function normalizePropertyGroups(
196197
throw new Error('propertyGroups must be an array');
197198
}
198199

199-
return propertyGroups.map((group, index) => {
200+
const normalizedGroups = propertyGroups.map((group, index) => {
200201
// Validate key is present and not empty
201202
if (
202203
!group.key ||
@@ -226,6 +227,31 @@ export function normalizePropertyGroups(
226227
sortOrder,
227228
};
228229
});
230+
231+
// Deduplicate by key, keeping the last occurrence
232+
const groupMap = new Map<string, PropertyGroupType>();
233+
const duplicates = new Set<string>();
234+
235+
for (const group of normalizedGroups) {
236+
if (groupMap.has(group.key)) {
237+
duplicates.add(group.key);
238+
}
239+
groupMap.set(group.key, group);
240+
}
241+
242+
// Warn about duplicates
243+
if (duplicates.size > 0) {
244+
console.warn(
245+
chalk.yellow(
246+
`Warning: Duplicate property group keys found: ${Array.from(
247+
duplicates
248+
).join(', ')}. Keeping the last occurrence of each.`
249+
)
250+
);
251+
}
252+
253+
// Return deduplicated array in the order they were last seen
254+
return Array.from(groupMap.values());
229255
}
230256

231257
/**
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { normalizePropertyGroups } from '../service/utils.js';
3+
4+
describe('normalizePropertyGroups', () => {
5+
let consoleWarnSpy: any;
6+
7+
beforeEach(() => {
8+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
9+
});
10+
11+
afterEach(() => {
12+
consoleWarnSpy.mockRestore();
13+
});
14+
15+
it('should handle duplicate keys by keeping the last occurrence', () => {
16+
const input = [
17+
{
18+
key: 'seo',
19+
displayName: 'SEO',
20+
sortOrder: 1,
21+
},
22+
{
23+
key: 'seo',
24+
displayName: 'SEO Updated',
25+
sortOrder: 5,
26+
},
27+
{
28+
key: 'meta',
29+
displayName: 'Meta',
30+
sortOrder: 2,
31+
},
32+
];
33+
34+
const result = normalizePropertyGroups(input);
35+
36+
expect(result).toHaveLength(2);
37+
expect(result[0]).toEqual({
38+
key: 'seo',
39+
displayName: 'SEO Updated',
40+
sortOrder: 5,
41+
});
42+
expect(result[1]).toEqual({
43+
key: 'meta',
44+
displayName: 'Meta',
45+
sortOrder: 2,
46+
});
47+
expect(consoleWarnSpy).toHaveBeenCalledWith(
48+
expect.stringContaining('Duplicate property group keys found: seo')
49+
);
50+
});
51+
52+
it('should handle multiple duplicates correctly', () => {
53+
const input = [
54+
{
55+
key: 'seo',
56+
displayName: 'SEO 1',
57+
sortOrder: 1,
58+
},
59+
{
60+
key: 'meta',
61+
displayName: 'Meta',
62+
sortOrder: 2,
63+
},
64+
{
65+
key: 'seo',
66+
displayName: 'SEO 2',
67+
sortOrder: 3,
68+
},
69+
{
70+
key: 'layout',
71+
displayName: 'Layout',
72+
sortOrder: 4,
73+
},
74+
{
75+
key: 'seo',
76+
displayName: 'SEO Final',
77+
sortOrder: 5,
78+
},
79+
];
80+
81+
const result = normalizePropertyGroups(input);
82+
83+
expect(result).toHaveLength(3);
84+
expect(result.find((g) => g.key === 'seo')).toEqual({
85+
key: 'seo',
86+
displayName: 'SEO Final',
87+
sortOrder: 5,
88+
});
89+
expect(consoleWarnSpy).toHaveBeenCalledWith(
90+
expect.stringContaining('Duplicate property group keys found: seo')
91+
);
92+
});
93+
94+
it('should auto-generate displayName from key if missing', () => {
95+
const input = [
96+
{
97+
key: 'seo',
98+
sortOrder: 1,
99+
},
100+
];
101+
102+
const result = normalizePropertyGroups(input);
103+
104+
expect(result[0]).toEqual({
105+
key: 'seo',
106+
displayName: 'Seo',
107+
sortOrder: 1,
108+
});
109+
});
110+
111+
it('should auto-assign sortOrder based on array position if missing', () => {
112+
const input = [
113+
{
114+
key: 'first',
115+
displayName: 'First',
116+
},
117+
{
118+
key: 'second',
119+
displayName: 'Second',
120+
},
121+
{
122+
key: 'third',
123+
displayName: 'Third',
124+
},
125+
];
126+
127+
const result = normalizePropertyGroups(input);
128+
129+
expect(result[0].sortOrder).toBe(1);
130+
expect(result[1].sortOrder).toBe(2);
131+
expect(result[2].sortOrder).toBe(3);
132+
});
133+
134+
it('should throw error for empty key', () => {
135+
const input = [
136+
{
137+
key: '',
138+
displayName: 'Empty',
139+
sortOrder: 1,
140+
},
141+
];
142+
143+
expect(() => normalizePropertyGroups(input)).toThrow(
144+
'Error in property groups: Property group at index 0 has an empty or missing "key" field'
145+
);
146+
});
147+
148+
it('should throw error for missing key', () => {
149+
const input = [
150+
{
151+
displayName: 'No Key',
152+
sortOrder: 1,
153+
},
154+
];
155+
156+
expect(() => normalizePropertyGroups(input)).toThrow(
157+
'Error in property groups: Property group at index 0 has an empty or missing "key" field'
158+
);
159+
});
160+
161+
it('should throw error if propertyGroups is not an array', () => {
162+
expect(() => normalizePropertyGroups({} as any)).toThrow(
163+
'propertyGroups must be an array'
164+
);
165+
});
166+
167+
it('should handle empty array', () => {
168+
const result = normalizePropertyGroups([]);
169+
expect(result).toEqual([]);
170+
});
171+
172+
it('should preserve order for non-duplicate keys', () => {
173+
const input = [
174+
{
175+
key: 'first',
176+
displayName: 'First',
177+
sortOrder: 1,
178+
},
179+
{
180+
key: 'second',
181+
displayName: 'Second',
182+
sortOrder: 2,
183+
},
184+
{
185+
key: 'third',
186+
displayName: 'Third',
187+
sortOrder: 3,
188+
},
189+
];
190+
191+
const result = normalizePropertyGroups(input);
192+
193+
expect(result[0].key).toBe('first');
194+
expect(result[1].key).toBe('second');
195+
expect(result[2].key).toBe('third');
196+
expect(consoleWarnSpy).not.toHaveBeenCalled();
197+
});
198+
});

0 commit comments

Comments
 (0)