Skip to content

Commit 03d6c9d

Browse files
Merge branch 'feat/exo' into feat/exo-fragment-types
2 parents 9b847d2 + d7ab506 commit 03d6c9d

6 files changed

Lines changed: 143 additions & 11 deletions

File tree

lib/adapters/REST/endpoints/experience.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,14 @@ export const update: RestEndpoint<'Experience', 'update'> = (
5757
rawData: UpdateExperienceProps,
5858
headers?: RawAxiosRequestHeaders,
5959
) => {
60-
const data: SetOptional<typeof rawData, 'sys'> & { componentTypeId?: string } = copy(rawData)
60+
const data: SetOptional<typeof rawData, 'sys'> & {
61+
componentTypeId?: string
62+
templateId?: string
63+
} = copy(rawData)
6164
if (rawData.sys.componentType) {
6265
data.componentTypeId = rawData.sys.componentType.sys.id
66+
} else if (rawData.sys.template) {
67+
data.templateId = rawData.sys.template.sys.id
6368
}
6469
delete data.sys
6570
return raw.put<ExperienceProps>(http, getBaseUrl(params) + `/${params.experienceId}`, data, {

lib/common-types.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,21 @@ export interface MetadataProps {
377377
}
378378

379379
/**
380-
* Metadata shape for ExO entities (ComponentType, Experience/View, Template).
380+
* Base metadata shape for ExO entities (ComponentType, Template).
381+
* Mirrors upstream MetadataSchema: tags and concepts only.
381382
* - tags: optional (upstream MetadataSchema.tags is z.optional as of SPA-3821)
382383
* - concepts: optional taxonomy concept links
383-
* - name: optional display name for variant labeling (SPA-3939)
384384
*/
385385
export interface ExoMetadataProps {
386386
tags?: Link<'Tag'>[]
387387
concepts?: Link<'TaxonomyConcept'>[]
388+
}
389+
390+
/**
391+
* Extended metadata shape for Experience entities only.
392+
* Adds name? for variant labeling (SPA-3939), mirroring upstream ExperienceMetadata.
393+
*/
394+
export interface ExperienceMetadataProps extends ExoMetadataProps {
388395
name?: string
389396
}
390397

lib/entities/component-type.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type ComponentTypeViewport = {
2424
export type ComponentTypeContentProperty = {
2525
id: string
2626
name: string
27-
type: string
27+
type: 'String' | 'Number' | 'Boolean'
2828
required: boolean
2929
defaultValue?: unknown
3030
}
@@ -53,7 +53,6 @@ export type ComponentTypeDesignProperty = {
5353
validations?: {
5454
in?: ComponentTypeDesignPropertyValidation[]
5555
}
56-
designTokenSet: string[]
5756
}
5857

5958
// Dimension key map

lib/entities/experience.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {
22
CursorPaginatedCollectionProp,
33
CursorPaginationParams,
4-
ExoMetadataProps,
4+
ExperienceMetadataProps,
55
Link,
66
} from '../common-types'
77
import type {
@@ -35,6 +35,9 @@ export type ExperienceSys = {
3535
updatedAt: string
3636
createdBy: Link<'User'>
3737
updatedBy: Link<'User'>
38+
archivedAt?: string
39+
archivedBy?: Link<'User'>
40+
archivedVersion?: number
3841
variant?: string
3942
variantType?: string
4043
variantDimension?: string
@@ -54,7 +57,7 @@ type ExperienceCommonProps = {
5457
designProperties: Record<string, DimensionedDesignPropertyValue>
5558
dimensionKeyMap: ExperienceDimensionKeyMap
5659
contentBindings?: ExperienceContentBindings
57-
metadata?: ExoMetadataProps
60+
metadata?: ExperienceMetadataProps
5861
slots?: Record<string, Array<FragmentNode | InlineFragmentNode>>
5962
}
6063

@@ -72,10 +75,13 @@ export type ExperienceQueryOptions = CursorPaginationParams & {
7275
// Omit the payload entirely for a full publish (all locales).
7376
export type ExperienceLocalePublishPayload = { add: string[] } | { remove: string[] }
7477

75-
// Create payload — no sys, uses componentTypeId instead of sys.componentType link
76-
export type CreateExperienceProps = ExperienceCommonProps & {
77-
componentTypeId: string
78-
}
78+
// Create payload — no sys, uses either componentTypeId (component-type-backed) or
79+
// templateId (template-backed). The two fields are mutually exclusive.
80+
export type CreateExperienceProps = ExperienceCommonProps &
81+
(
82+
| { componentTypeId: string; templateId?: never }
83+
| { templateId: string; componentTypeId?: never }
84+
)
7985

8086
export type UpdateExperienceProps = ExperienceProps
8187

lib/plain/entities/experience.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export type ExperiencePlainClientAPI = {
6060
* @internal - Experimental endpoint, subject to breaking changes without notice
6161
* @example
6262
* ```javascript
63+
* // Component-type-backed experience
6364
* const experience = await client.experience.create({
6465
* spaceId: '<space_id>',
6566
* environmentId: '<environment_id>',
@@ -72,6 +73,20 @@ export type ExperiencePlainClientAPI = {
7273
* designProperties: {},
7374
* dimensionKeyMap: { designProperties: {} },
7475
* });
76+
*
77+
* // Template-backed experience
78+
* const experience = await client.experience.create({
79+
* spaceId: '<space_id>',
80+
* environmentId: '<environment_id>',
81+
* }, {
82+
* name: 'My Template Experience',
83+
* description: 'A template-backed experience',
84+
* templateId: '<template_id>',
85+
* viewports: [],
86+
* contentProperties: {},
87+
* designProperties: {},
88+
* dimensionKeyMap: { designProperties: {} },
89+
* });
7590
* ```
7691
*/
7792
create(

test/unit/adapters/REST/endpoints/experience.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,55 @@ describe('Rest Experience', { concurrent: true }, () => {
251251
})
252252
})
253253

254+
test('create sends templateId when creating a template-backed experience', async () => {
255+
const mockResponse = {
256+
sys: { id: 'new-experience-456', type: 'Experience', version: 1 },
257+
name: 'Template Experience',
258+
description: 'A template-backed experience',
259+
viewports: [],
260+
contentProperties: {},
261+
designProperties: {},
262+
dimensionKeyMap: { designProperties: {} },
263+
}
264+
265+
const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockResponse }))
266+
267+
return adapterMock
268+
.makeRequest({
269+
entityType: 'Experience',
270+
action: 'create',
271+
userAgent: 'mocked',
272+
params: {
273+
spaceId: 'space123',
274+
environmentId: 'master',
275+
},
276+
payload: {
277+
name: 'Template Experience',
278+
description: 'A template-backed experience',
279+
templateId: 'tmpl-789',
280+
viewports: [],
281+
contentProperties: {},
282+
designProperties: {},
283+
dimensionKeyMap: { designProperties: {} },
284+
},
285+
})
286+
.then((r) => {
287+
expect(r).to.eql(mockResponse)
288+
expect(httpMock.post.mock.calls[0][0]).to.eql(
289+
'/spaces/space123/environments/master/experiences',
290+
)
291+
expect(httpMock.post.mock.calls[0][1]).to.eql({
292+
name: 'Template Experience',
293+
description: 'A template-backed experience',
294+
templateId: 'tmpl-789',
295+
viewports: [],
296+
contentProperties: {},
297+
designProperties: {},
298+
dimensionKeyMap: { designProperties: {} },
299+
})
300+
})
301+
})
302+
254303
test('update calls correct URL with version header', async () => {
255304
const mockResponse = {
256305
sys: { id: 'experience123', type: 'Experience', version: 2 },
@@ -301,6 +350,57 @@ describe('Rest Experience', { concurrent: true }, () => {
301350
})
302351
})
303352

353+
test('update sends templateId when experience is template-backed', async () => {
354+
const mockResponse = {
355+
sys: { id: 'experience123', type: 'Experience', version: 2 },
356+
name: 'Updated Experience',
357+
description: 'A template-backed experience',
358+
viewports: [],
359+
contentProperties: {},
360+
designProperties: {},
361+
dimensionKeyMap: { designProperties: {} },
362+
}
363+
364+
const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockResponse }))
365+
366+
return adapterMock
367+
.makeRequest({
368+
entityType: 'Experience',
369+
action: 'update',
370+
userAgent: 'mocked',
371+
params: {
372+
spaceId: 'space123',
373+
environmentId: 'master',
374+
experienceId: 'experience123',
375+
},
376+
payload: {
377+
sys: {
378+
version: 1,
379+
template: {
380+
sys: { type: 'Link', linkType: 'Template', id: 'tmpl-456' },
381+
},
382+
},
383+
name: 'Updated Experience',
384+
description: 'A template-backed experience',
385+
viewports: [],
386+
contentProperties: {},
387+
designProperties: {},
388+
dimensionKeyMap: { designProperties: {} },
389+
},
390+
})
391+
.then((r) => {
392+
expect(r).to.eql(mockResponse)
393+
expect(httpMock.put.mock.calls[0][0]).to.eql(
394+
'/spaces/space123/environments/master/experiences/experience123',
395+
)
396+
expect(httpMock.put.mock.calls[0][2].headers['X-Contentful-Version']).to.eql(1)
397+
const body = httpMock.put.mock.calls[0][1]
398+
expect(body.templateId).to.eql('tmpl-456')
399+
expect(body.componentTypeId).to.be.undefined
400+
expect(body.sys).to.be.undefined
401+
})
402+
})
403+
304404
test('delete calls correct URL', async () => {
305405
const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: {} }))
306406

0 commit comments

Comments
 (0)