Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,51 @@ describe('DotUveActionsHandlerService – SECTION_OFFSET', () => {
});
});
});

describe('DotUveActionsHandlerService – REGISTER_STYLE_SCHEMAS', () => {
let spectator: SpectatorService<DotUveActionsHandlerService>;
let service: DotUveActionsHandlerService;

const createService = createServiceFactory({
service: DotUveActionsHandlerService,
providers: [
mockProvider(DotWorkflowActionsFireService),
mockProvider(DotMessageService),
mockProvider(MessageService),
mockProvider(DotCopyContentModalService),
{
provide: UVEStore,
useValue: buildMockStore()
}
]
});

beforeEach(() => {
jest.clearAllMocks();
spectator = createService();
service = spectator.service;
});

it('should call setStyleSchemas on the store with the received schemas', () => {
const mockSchemas = [{ variable: 'Banner', schema: { color: { type: 'color' } } }];
const setStyleSchemas = jest.fn();
const mockStore = { ...buildMockStore(), setStyleSchemas };

service.handleAction(
{
action: DotCMSUVEAction.REGISTER_STYLE_SCHEMAS,
payload: { schemas: mockSchemas }
},
{
uveStore: mockStore as unknown as InstanceType<typeof UVEStore>,
dialog: null,
inlineEditingService: null,
contentWindow: null,
host: 'http://localhost',
onCopyContent: jest.fn()
}
);

expect(setStyleSchemas).toHaveBeenCalledWith(mockSchemas);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,85 @@ describe('withEditor', () => {

expect(store.$styleSchema()).toBeUndefined();
});

it('should use schemas seeded from page load (via setPageAsset)', () => {
const mockSchema = { contentType: 'testContentType', sections: [] };

store.setPageAsset({
pageAsset: {
...MOCK_RESPONSE_HEADLESS,
page: {
...MOCK_RESPONSE_HEADLESS.page,
styleEditorSchemas: [mockSchema]
}
}
});

patchStoreState(store, {
editorActiveContentlet: {
language_id: '1',
pageContainers: [],
pageId: '123',
container: {
identifier: 'test-container-id',
uuid: 'test-container-uuid',
acceptTypes: 'test',
maxContentlets: 1
},
contentlet: {
identifier: 'test-contentlet-id',
inode: 'test-inode',
title: 'Test Contentlet',
contentType: 'testContentType'
}
}
});

expect(store.$styleSchema()).toEqual(mockSchema);
});

it('should prefer editorStyleSchemas over pageAsset schemas when both exist', () => {
// pageSchema uses a different contentType than the active contentlet.
// If editorStyleSchemas were bypassed in favour of pageAsset schemas,
// nothing would match and the result would be undefined.
const pageSchema = { contentType: 'pageOnlyType', sections: [] };
const iframeSchema = { contentType: 'iframeType', sections: [] };

store.setPageAsset({
pageAsset: {
...MOCK_RESPONSE_HEADLESS,
page: {
...MOCK_RESPONSE_HEADLESS.page,
styleEditorSchemas: [pageSchema]
}
}
});

patchStoreState(store, {
editorActiveContentlet: {
language_id: '1',
pageContainers: [],
pageId: '123',
container: {
identifier: 'test-container-id',
uuid: 'test-container-uuid',
acceptTypes: 'test',
maxContentlets: 1
},
contentlet: {
identifier: 'test-contentlet-id',
inode: 'test-inode',
title: 'Test Contentlet',
contentType: 'iframeType'
}
},
editorStyleSchemas: [iframeSchema]
});

// editorStyleSchemas is non-empty so it is used exclusively;
// the pageAsset schemas (which only have 'pageOnlyType') are ignored.
expect(store.$styleSchema()).toEqual(iframeSchema);
});
});

describe('$editorIsInDraggingState', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,12 @@ export function withEditor() {
}),
$styleSchema: computed<StyleEditorFormSchema>(() => {
const activeContentlet = store.editorActiveContentlet();
const styleSchemas = store.editorStyleSchemas();
const contentSchema = styleSchemas.find(
(schema) => schema.contentType === activeContentlet?.contentlet?.contentType
);
return contentSchema;
return store
.editorStyleSchemas()
.find(
(schema) =>
schema.contentType === activeContentlet?.contentlet?.contentType
);
}),
$isDragging: computed<boolean>(() => {
const editorState = store.editorState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ export function withPage() {
pageAsset: payload.pageAsset,
...(content !== undefined && { content })
};
patchState(store, { pageAssetResponse: nextResponse });
patchState(store, {
pageAssetResponse: nextResponse,
editorStyleSchemas: payload.pageAsset.page.styleEditorSchemas ?? []
});
},
setPageAssetResponseOptimistic: (
pageAssetResponse: PageLoadingConfigState['pageAssetResponse']
Expand Down
80 changes: 20 additions & 60 deletions core-web/libs/sdk/client/src/lib/client/page/page-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,7 @@ describe('PageClient', () => {
}) as Partial<FetchHttpClient> as FetchHttpClient
);

mockRequest.mockImplementation((url: string) => {
if (url.includes('/contenttype-schema')) {
return Promise.resolve({ entity: [] });
}

return Promise.resolve(mockGraphQLResponse);
});
mockRequest.mockResolvedValue(mockGraphQLResponse);
});

afterEach(() => {
Expand Down Expand Up @@ -127,17 +121,6 @@ describe('PageClient', () => {
body: expect.stringContaining(`... on Banner`)
});

expect(mockRequest).toHaveBeenCalledWith(
'https://demo.dotcms.com/api/v1/page/test-page-id/contenttype-schema',
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
Authorization: 'Bearer test-token',
Accept: 'application/json'
})
})
);

expect(result).toEqual({
pageAsset: {
layout: {
Expand Down Expand Up @@ -840,15 +823,15 @@ describe('PageClient', () => {
});

describe('styleEditorSchemas', () => {
it('should include styleEditorSchemas in result when endpoint returns schemas', async () => {
it('should include styleEditorSchemas in result when GQL returns schemas', async () => {
const mockSchemas = [{ variable: 'Banner', schema: { color: { type: 'color' } } }];

mockRequest.mockImplementation((url: string) => {
if (url.includes('/contenttype-schema')) {
return Promise.resolve({ entity: mockSchemas });
mockRequest.mockResolvedValue({
...mockGraphQLResponse,
data: {
...mockGraphQLResponse.data,
page: { ...mockGraphQLResponse.data.page, styleEditorSchemas: mockSchemas }
}

return Promise.resolve(mockGraphQLResponse);
});

const pageClient = new PageClient(
Expand All @@ -861,26 +844,13 @@ describe('PageClient', () => {
expect(result.styleEditorSchemas).toEqual(mockSchemas);
});

it('should omit styleEditorSchemas from result when endpoint returns empty array', async () => {
const pageClient = new PageClient(
validConfig,
requestOptions,
new FetchHttpClient()
);
const result = await pageClient.get('/graphql-page');

expect(result.styleEditorSchemas).toBeUndefined();
});

it('should omit styleEditorSchemas and log debug when schema endpoint fails', async () => {
const debugSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);

mockRequest.mockImplementation((url: string) => {
if (url.includes('/contenttype-schema')) {
return Promise.reject(new Error('Network error'));
it('should omit styleEditorSchemas from result when GQL returns null (not in EDIT_MODE)', async () => {
mockRequest.mockResolvedValue({
...mockGraphQLResponse,
data: {
...mockGraphQLResponse.data,
page: { ...mockGraphQLResponse.data.page, styleEditorSchemas: null }
}

return Promise.resolve(mockGraphQLResponse);
});

const pageClient = new PageClient(
Expand All @@ -891,20 +861,15 @@ describe('PageClient', () => {
const result = await pageClient.get('/graphql-page');

expect(result.styleEditorSchemas).toBeUndefined();
expect(debugSpy).toHaveBeenCalledWith(
'[DotCMS PageClient]: Skipping style editor schemas:',
expect.any(Error)
);
debugSpy.mockRestore();
});

it('should omit styleEditorSchemas when endpoint returns a non-array entity', async () => {
mockRequest.mockImplementation((url: string) => {
if (url.includes('/contenttype-schema')) {
return Promise.resolve({ entity: null });
it('should omit styleEditorSchemas from result when GQL returns an empty array', async () => {
mockRequest.mockResolvedValue({
...mockGraphQLResponse,
data: {
...mockGraphQLResponse.data,
page: { ...mockGraphQLResponse.data.page, styleEditorSchemas: [] }
}

return Promise.resolve(mockGraphQLResponse);
});

const pageClient = new PageClient(
Expand All @@ -917,9 +882,7 @@ describe('PageClient', () => {
expect(result.styleEditorSchemas).toBeUndefined();
});

it('should warn and return empty when pageId is missing from the page response', async () => {
const consolaWarnSpy = jest.spyOn(console, 'warn');

it('should omit styleEditorSchemas when identifier is missing from the page response', async () => {
mockRequest.mockResolvedValue({
...mockGraphQLResponse,
data: {
Expand All @@ -936,9 +899,6 @@ describe('PageClient', () => {
const result = await pageClient.get('/graphql-page');

expect(result.styleEditorSchemas).toBeUndefined();
expect(consolaWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('"identifier"')
);
});
});
});
Expand Down
19 changes: 4 additions & 15 deletions core-web/libs/sdk/client/src/lib/client/page/page-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@ import {
DotRequestOptions
} from '@dotcms/types';

import {
buildPageQuery,
buildQuery,
fetchGraphQL,
fetchStyleEditorSchemas,
mapContentResponse
} from './utils';
import { buildPageQuery, buildQuery, fetchGraphQL, mapContentResponse } from './utils';

import { graphqlToPageEntity } from '../../utils';
import { BaseApiClient } from '../base/api/base-api';
Expand Down Expand Up @@ -253,6 +247,8 @@ export class PageClient extends BaseApiClient {
? graphqlToPageEntity(response.data.page)
: null;

const styleEditorSchemas = pageResponse ? pageResponse.page.styleEditorSchemas : [];

if (!pageResponse) {
throw new DotErrorPage(
`Page '${normalizedUrl}' was not found`,
Expand All @@ -268,13 +264,6 @@ export class PageClient extends BaseApiClient {
);
}

const styleEditorSchemas = await fetchStyleEditorSchemas(
pageResponse.page.identifier,
this.config,
this.requestOptions,
this.httpClient
);

// 5. Build response — include any non-fatal errors for consumers to inspect
const contentResponse = mapContentResponse(response.data, Object.keys(content));

Expand All @@ -286,7 +275,7 @@ export class PageClient extends BaseApiClient {
variables: requestVariables
},
errors: response.errors?.length ? response.errors : undefined,
...(styleEditorSchemas.length > 0 && { styleEditorSchemas })
...(styleEditorSchemas?.length && { styleEditorSchemas })
};
} catch (error) {
if (error instanceof DotErrorPage) {
Expand Down
Loading
Loading