Skip to content

Commit d17382f

Browse files
authored
Merge branch 'main' into 33073-rxjs-from-v6-to-v7
2 parents 8ca4c7e + 895f386 commit d17382f

15 files changed

Lines changed: 372 additions & 17 deletions

File tree

core-web/libs/dotcms-models/src/lib/shared-models.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export const enum FeaturedFlags {
3232
FEATURE_FLAG_UVE_PREVIEW_MODE = 'FEATURE_FLAG_UVE_PREVIEW_MODE',
3333
FEATURE_FLAG_UVE_TOGGLE_LOCK = 'FEATURE_FLAG_UVE_TOGGLE_LOCK',
3434
FEATURE_FLAG_UVE_STYLE_EDITOR = 'FEATURE_FLAG_UVE_STYLE_EDITOR',
35-
FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES = 'FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES'
35+
FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES = 'FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES',
36+
FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION = 'FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION'
3637
}
3738

3839
export const enum DotConfigurationVariables {

core-web/libs/dotcms-webcomponents/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"build": {
3232
"executor": "nx:run-commands",
3333
"options": {
34-
"command": "npx stencil build --config libs/dotcms-webcomponents/stencil.config.ts --prod"
34+
"command": "yarn stencil build --config libs/dotcms-webcomponents/stencil.config.ts --prod"
3535
}
3636
},
3737
"serve": {

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import { ActionPayload, ContentTypeDragPayload } from '../shared/models';
117117
import { UVEStore } from '../store/dot-uve.store';
118118
import * as uveUtils from '../utils';
119119
import { TEMPORAL_DRAG_ITEM } from '../utils';
120+
import { SDK_EDITOR_SCRIPT_SOURCE } from '../utils/ema-legacy-script-injection';
120121

121122
global.URL.createObjectURL = jest.fn(
122123
() => 'blob:http://localhost:3000/12345678-1234-1234-1234-123456789012'
@@ -3483,6 +3484,77 @@ describe('EditEmaEditorComponent', () => {
34833484
});
34843485
});
34853486

3487+
describe('legacy script injection (FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION)', () => {
3488+
let componentStore: InstanceType<typeof UVEStore>;
3489+
3490+
beforeEach(() => {
3491+
componentStore = (
3492+
spectator.component as unknown as {
3493+
uveStore: InstanceType<typeof UVEStore>;
3494+
}
3495+
).uveStore;
3496+
3497+
jest.spyOn(dotPageApiService, 'get').mockReturnValue(of(MOCK_RESPONSE_VTL));
3498+
store.loadPageAsset({ url: 'index', clientHost: null });
3499+
});
3500+
3501+
it('should inject the UVE script when the flag is enabled', () => {
3502+
patchState(componentStore, {
3503+
flags: {
3504+
...componentStore.flags(),
3505+
[FeaturedFlags.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION]: true
3506+
}
3507+
});
3508+
3509+
const iframe = spectator.query(byTestId('iframe')) as HTMLIFrameElement;
3510+
const spyWrite = jest.spyOn(iframe.contentDocument, 'write');
3511+
3512+
iframe.dispatchEvent(new Event('load'));
3513+
spectator.detectChanges();
3514+
3515+
expect(spyWrite).toHaveBeenCalledWith(
3516+
expect.stringContaining(
3517+
`<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`
3518+
)
3519+
);
3520+
});
3521+
3522+
it('should NOT inject the UVE script when the flag is disabled', () => {
3523+
patchState(componentStore, {
3524+
flags: {
3525+
...componentStore.flags(),
3526+
[FeaturedFlags.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION]: false
3527+
}
3528+
});
3529+
3530+
const iframe = spectator.query(byTestId('iframe')) as HTMLIFrameElement;
3531+
const spyWrite = jest.spyOn(iframe.contentDocument, 'write');
3532+
3533+
iframe.dispatchEvent(new Event('load'));
3534+
spectator.detectChanges();
3535+
3536+
expect(spyWrite).toHaveBeenCalledWith(
3537+
expect.not.stringContaining(
3538+
`<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`
3539+
)
3540+
);
3541+
});
3542+
3543+
it('should NOT inject the UVE script when the flag is not set', () => {
3544+
const iframe = spectator.query(byTestId('iframe')) as HTMLIFrameElement;
3545+
const spyWrite = jest.spyOn(iframe.contentDocument, 'write');
3546+
3547+
iframe.dispatchEvent(new Event('load'));
3548+
spectator.detectChanges();
3549+
3550+
expect(spyWrite).toHaveBeenCalledWith(
3551+
expect.not.stringContaining(
3552+
`<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`
3553+
)
3554+
);
3555+
});
3556+
});
3557+
34863558
describe('when pageRender is undefined', () => {
34873559
beforeEach(() => {
34883560
jest.spyOn(dotPageApiService, 'get').mockReturnValue(

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ import {
115115
insertContentletInContainer,
116116
shouldNavigate
117117
} from '../utils';
118+
import { addEditorPageScript } from '../utils/ema-legacy-script-injection';
118119

119120
// Message keys constants
120121
const MESSAGE_KEY = {
@@ -565,18 +566,30 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy, AfterViewInit
565566
}
566567

567568
/**
568-
* Inject the editor page styles to the VTL content
569+
* Inject the editor page styles (and optionally the legacy UVE script)
570+
* into the VTL content before writing it to the iframe.
571+
*
572+
* When `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION` is enabled, the
573+
* `dot-uve.js` script tag is also injected. This restores the pre-PR
574+
* #34995 behavior for EMA customers whose headless frontends do not
575+
* consume the backend-rendered output where the script now lives.
569576
*
570577
* @private
571-
* @param {string} html
572-
* @return {*} {string}
573-
* @memberof EditEmaEditorComponent
578+
* @param html - Raw rendered HTML from the Page API.
579+
* @returns Transformed HTML ready for `document.write()`.
580+
* @see {@link addEditorPageScript} for the legacy script injection logic.
574581
*/
575582
private injectCodeToVTL(html: string): string {
576583
const url = this.uveStore.pageAPIResponse()?.page?.pageURI ?? '';
577584
const origin = this.window.location.origin;
578585
const fileWithBase = injectBaseTag({ html, url, origin });
579-
return this.addCustomStyles(fileWithBase);
586+
const fileWithStyles = this.addCustomStyles(fileWithBase);
587+
588+
if (this.uveStore.$isEmaLegacyScriptInjectionEnabled()) {
589+
return addEditorPageScript(fileWithStyles);
590+
}
591+
592+
return fileWithStyles;
580593
}
581594

582595
ngOnDestroy(): void {
@@ -747,9 +760,11 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy, AfterViewInit
747760
/**
748761
* Sets up the iframe content for traditional (VTL) pages.
749762
*
750-
* NOTE: The `dot-uve.js` editor script is intentionally NOT injected here.
751-
* It is now included by the backend directly in `entity.page.rendered`
752-
* (see PR #34927). Do not re-add script injection on the frontend.
763+
* The `dot-uve.js` editor script is normally included by the backend
764+
* directly in `entity.page.rendered` (see PR #34927). However, when
765+
* `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION` is enabled, the script
766+
* is also injected from the frontend to support EMA customers whose
767+
* headless setup does not consume the backend-rendered output.
753768
*
754769
* @memberof EditEmaEditorComponent
755770
*/

core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export const DEFAULT_PERSONA: DotCMSViewAsPersona = {
7676
export const UVE_FEATURE_FLAGS = [
7777
FeaturedFlags.FEATURE_FLAG_UVE_TOGGLE_LOCK,
7878
FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR,
79-
FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES
79+
FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES,
80+
FeaturedFlags.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION
8081
];
8182

8283
export const DEFAULT_DEVICE: DotDeviceListItem = {

core-web/libs/portlets/edit-ema/portlet/src/lib/shared/mocks.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,8 @@ export const dotPropertiesServiceMock = {
10441044
[FeaturedFlags.FEATURE_FLAG_UVE_PREVIEW_MODE]: false,
10451045
[FeaturedFlags.FEATURE_FLAG_UVE_TOGGLE_LOCK]: false,
10461046
[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR]: false,
1047-
[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES]: false
1047+
[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES]: false,
1048+
[FeaturedFlags.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION]: false
10481049
})
10491050
};
10501051

core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/withPageContext.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface PageContextComputed {
1616
$isPageLocked: Signal<boolean>;
1717
$isLockFeatureEnabled: Signal<boolean>;
1818
$isStyleEditorEnabled: Signal<boolean>;
19+
$isEmaLegacyScriptInjectionEnabled: Signal<boolean>;
1920
$hasAccessToEditMode: Signal<boolean>;
2021
$languageId: Signal<number>;
2122
$isPreviewMode: Signal<boolean>;
@@ -60,6 +61,9 @@ export function withPageContext() {
6061

6162
return flags().FEATURE_FLAG_UVE_STYLE_EDITOR;
6263
});
64+
const $isEmaLegacyScriptInjectionEnabled = computed(
65+
() => flags().FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION === true
66+
);
6367
const $isPageLocked = computed(() => {
6468
return computeIsPageLocked(page(), currentUser());
6569
});
@@ -79,6 +83,7 @@ export function withPageContext() {
7983
$isPageLocked,
8084
$isLockFeatureEnabled,
8185
$isStyleEditorEnabled,
86+
$isEmaLegacyScriptInjectionEnabled,
8287
$hasAccessToEditMode,
8388
$languageId: computed(() => viewAs()?.language?.id || 1),
8489
$pageURI: computed(() => page()?.pageURI ?? ''),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { addEditorPageScript, SDK_EDITOR_SCRIPT_SOURCE } from './ema-legacy-script-injection';
2+
3+
describe('ema-legacy-script-injection', () => {
4+
describe('SDK_EDITOR_SCRIPT_SOURCE', () => {
5+
it('should point to the UVE editor script path', () => {
6+
expect(SDK_EDITOR_SCRIPT_SOURCE).toBe('/ext/uve/dot-uve.js');
7+
});
8+
});
9+
10+
describe('addEditorPageScript', () => {
11+
const expectedScript = `<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`;
12+
13+
it('should insert the script tag before </body> when body tag exists', () => {
14+
const html = '<html><head></head><body><p>Hello</p></body></html>';
15+
const result = addEditorPageScript(html);
16+
17+
expect(result).toContain(expectedScript + '</body>');
18+
expect(result).toBe(
19+
`<html><head></head><body><p>Hello</p>${expectedScript}</body></html>`
20+
);
21+
});
22+
23+
it('should append the script tag at the end when no body tag exists', () => {
24+
const html = '<div>Advanced template content</div>';
25+
const result = addEditorPageScript(html);
26+
27+
expect(result).toBe(html + expectedScript);
28+
});
29+
30+
it('should handle empty string input', () => {
31+
const result = addEditorPageScript('');
32+
expect(result).toBe(expectedScript);
33+
});
34+
35+
it('should handle undefined input (defaults to empty string)', () => {
36+
const result = addEditorPageScript();
37+
expect(result).toBe(expectedScript);
38+
});
39+
});
40+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @module ema-legacy-script-injection
3+
*
4+
* @deprecated This module is a temporary workaround and will be removed once
5+
* EMA (Edit Mode Anywhere) headless editing is fully sunset.
6+
*
7+
* ## Why this exists
8+
*
9+
* PR #34927 moved the `dot-uve.js` editor script injection from the Angular
10+
* frontend to the backend, embedding it directly in `entity.page.rendered`.
11+
* PR #34995 then removed the frontend injection code.
12+
*
13+
* This broke EMA-based customers (e.g. Freshdesk #36029) because EMA runs
14+
* headless — the iframe loads the customer's own frontend (Next.js, etc.)
15+
* and does **not** consume the backend-rendered HTML that now contains the
16+
* script. As a result, the UVE editor controls stopped appearing in edit mode.
17+
*
18+
* ## How it works
19+
*
20+
* When the backend feature flag `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION` is
21+
* set to `true`, the Angular editor re-injects the `dot-uve.js` script tag
22+
* into VTL page content before writing it to the iframe — restoring the
23+
* pre-PR #34995 behavior.
24+
*
25+
* ## Removal plan
26+
*
27+
* Once all EMA customers have migrated to the Universal Visual Editor (UVE),
28+
* delete this file and remove:
29+
* - `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION` from `FeaturedFlags` enum
30+
* - The flag entry from `UVE_FEATURE_FLAGS` in `consts.ts`
31+
* - `$isEmaLegacyScriptInjectionEnabled` from `withPageContext.ts`
32+
* - The conditional call in `EditEmaEditorComponent.injectCodeToVTL`
33+
*
34+
* @see {@link https://github.com/dotCMS/core/pull/34927 PR #34927 — backend script injection}
35+
* @see {@link https://github.com/dotCMS/core/pull/34995 PR #34995 — frontend injection removal}
36+
*/
37+
38+
/**
39+
* Path to the UVE editor script served by the dotCMS backend.
40+
*
41+
* @deprecated Gated behind `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION`.
42+
*/
43+
export const SDK_EDITOR_SCRIPT_SOURCE = '/ext/uve/dot-uve.js';
44+
45+
/**
46+
* Injects the `dot-uve.js` editor script tag into the rendered HTML content.
47+
*
48+
* If a `</body>` tag exists, the script is inserted just before it.
49+
* For advanced templates that may lack a `<body>`, the script is appended
50+
* at the end of the content.
51+
*
52+
* @deprecated Gated behind `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION`.
53+
* @param rendered - The HTML string to inject the script into.
54+
* @returns The HTML string with the editor script tag added.
55+
*/
56+
export function addEditorPageScript(rendered = ''): string {
57+
const scriptString = `<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`;
58+
const bodyExists = rendered.includes('</body>');
59+
60+
if (!bodyExists) {
61+
return rendered + scriptString;
62+
}
63+
64+
return rendered.replace('</body>', scriptString + '</body>');
65+
}

dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public interface FeatureFlagName {
4747

4848
String FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES = "FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES";
4949

50+
String FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION = "FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION";
51+
5052
/**
5153
* Controls the active ES → OpenSearch migration phase (integer ordinal 0–3).
5254
*

0 commit comments

Comments
 (0)