Skip to content

Commit 16b248d

Browse files
Issue 35647 translate content on page not working (#36231)
## Summary Fixes #35647 When a user selected a language that had no page version yet, the UVE editor showed a confirmation dialog asking to create a new translation. Clicking **"No"** previously navigated the user back to the previous language. This made it impossible to view or translate the content that already exists on the page in that language without first creating a full page version. ### What changed **`edit-ema-editor.component.ts` — stay on selected language when declining to create a translation** The `reject` handler in `createNewTranslation` previously called `#goBackToCurrentLanguage()` to navigate back to the previous language. It now does nothing — the page is already loaded in the target language when the dialog appears, so dismissing the dialog naturally keeps the user there. Content already on the page is shown in the selected language where versions exist, with fallback to the default language where they don't. The page is read-only (no editing) since there is no page version in that language. **`withPageApi.ts` — sync mode to Preview when page has no translation** When a page loads in an untranslated language, `pageParams.mode` is forced to `UVE_MODE.PREVIEW` so the store's mode matches the read-only state the iframe is already in. **`withEditor.ts` — expose `$isMissingTranslation` signal** Made the internal `isMissingTranslation` computed public so the mode selector can react to it. **`dot-editor-mode-selector.component.ts` — restrict the mode dropdown** - **Draft hidden**: always hidden when `$isMissingTranslation = true` — there is nothing to draft-edit. - **Published hidden**: also hidden — the fallback content shown is not the published state of that language. - **`$syncSelectedMode` effect**: replaced one-time `ngOnInit` initialization with a reactive `effect` so the dropdown updates whenever the mode or available options change (e.g. when switching languages). ### Behavior after fix | Action | Before | After | |---|---|---| | Select untranslated language | Dialog appears → clicking "No" returns to previous language | Dialog appears → clicking "No" stays on selected language (read-only) | https://github.com/user-attachments/assets/deb2a91d-b2be-4fc7-8dec-50bd5a5af195 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 527c553 commit 16b248d

10 files changed

Lines changed: 215 additions & 166 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,7 @@ describe('DotEmaShellComponent', () => {
642642

643643
const userParams = {
644644
...baseParams,
645+
mode: UVE_MODE.PREVIEW, // language 2 has translated: false, store forces mode to PREVIEW
645646
personaId: 'SomeCoolDude'
646647
};
647648

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/components/dot-editor-mode-selector/dot-editor-mode-selector.component.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('DotEditorModeSelectorComponent', () => {
1515
let store: {
1616
editorHasAccessToEditMode: ReturnType<typeof signal<boolean>>;
1717
$lockFeatureEnabled: ReturnType<typeof signal<boolean>>;
18+
$isMissingTranslation: ReturnType<typeof signal<boolean>>;
1819
pageParams: ReturnType<typeof signal<{ mode: UVE_MODE }>>;
1920
viewClearDeviceAndSocialMedia: jest.Mock;
2021
pageLoad: jest.Mock;
@@ -59,6 +60,7 @@ describe('DotEditorModeSelectorComponent', () => {
5960
store = {
6061
editorHasAccessToEditMode: signal(true),
6162
$lockFeatureEnabled: signal(false),
63+
$isMissingTranslation: signal(false),
6264
pageParams: signal({ mode: UVE_MODE.EDIT }),
6365
viewClearDeviceAndSocialMedia: jest.fn(),
6466
pageLoad: jest.fn(),
@@ -204,6 +206,37 @@ describe('DotEditorModeSelectorComponent', () => {
204206
});
205207
});
206208

209+
describe('missing translation state', () => {
210+
beforeEach(() => {
211+
store.$isMissingTranslation.set(true);
212+
store.pageParams.set({ mode: UVE_MODE.PREVIEW });
213+
spectator.detectChanges();
214+
});
215+
216+
it('should show only Preview when page has no translation', () => {
217+
openSelectOverlay();
218+
219+
expect(document.querySelector(`[data-testId="${UVE_MODE.EDIT}-menu-item"]`)).toBeNull();
220+
expect(
221+
document.querySelector(`[data-testId="${UVE_MODE.PREVIEW}-menu-item"]`)
222+
).toBeTruthy();
223+
expect(document.querySelector(`[data-testId="${UVE_MODE.LIVE}-menu-item"]`)).toBeNull();
224+
});
225+
226+
it('should hide Draft even when lock feature is enabled', () => {
227+
store.$lockFeatureEnabled.set(true);
228+
spectator.detectChanges();
229+
230+
openSelectOverlay();
231+
232+
expect(document.querySelector(`[data-testId="${UVE_MODE.EDIT}-menu-item"]`)).toBeNull();
233+
});
234+
235+
it('should sync selected mode to Preview', () => {
236+
expect(spectator.component.selectedModeModel()?.id).toBe(UVE_MODE.PREVIEW);
237+
});
238+
});
239+
207240
describe('mode guard effect (legacy behavior)', () => {
208241
it('should switch to PREVIEW when in EDIT without edit permission and lock feature is disabled', () => {
209242
store.$lockFeatureEnabled.set(false);

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/components/dot-editor-mode-selector/dot-editor-mode-selector.component.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
effect,
66
inject,
77
model,
8-
OnInit,
98
untracked
109
} from '@angular/core';
1110
import { FormsModule } from '@angular/forms';
@@ -29,16 +28,24 @@ interface EditorModeOption {
2928
templateUrl: './dot-editor-mode-selector.component.html',
3029
changeDetection: ChangeDetectionStrategy.OnPush
3130
})
32-
export class DotEditorModeSelectorComponent implements OnInit {
31+
export class DotEditorModeSelectorComponent {
3332
readonly #store = inject(UVEStore);
3433
readonly #dotMessageService = inject(DotMessageService);
3534
/**
3635
* Determines whether to show the "Draft" mode option in the mode selector.
3736
*
38-
* With feature flag enabled: Always show draft mode (user can toggle lock)
39-
* Without feature flag: Only show if user can edit the page
37+
* Never shown when the page has no version in the selected language —
38+
* there is nothing to edit.
39+
* With feature flag enabled: shown otherwise (user can toggle lock)
40+
* Without feature flag: only shown if user can edit the page
4041
*/
4142
readonly $shouldShowDraftMode = computed(() => {
43+
// Missing translation means there is nothing to draft-edit.
44+
// Always hide, regardless of lock feature flag.
45+
if (this.#store.$isMissingTranslation()) {
46+
return false;
47+
}
48+
4249
const isLockFeatureEnabled = this.#store.$lockFeatureEnabled();
4350

4451
// With new lock feature, draft mode is always available
@@ -68,18 +75,35 @@ export class DotEditorModeSelectorComponent implements OnInit {
6875
id: UVE_MODE.PREVIEW
6976
});
7077

71-
menu.push({
72-
label: this.#dotMessageService.get('uve.editor.mode.published'),
73-
description: this.#dotMessageService.get('uve.editor.mode.published.description'),
74-
id: UVE_MODE.LIVE
75-
});
78+
// Published shows the live version of the page. Not relevant when the page
79+
// has no version in the selected language — the fallback content shown is
80+
// not the published state of this language.
81+
if (!this.#store.$isMissingTranslation()) {
82+
menu.push({
83+
label: this.#dotMessageService.get('uve.editor.mode.published'),
84+
description: this.#dotMessageService.get('uve.editor.mode.published.description'),
85+
id: UVE_MODE.LIVE
86+
});
87+
}
7688

7789
return menu;
7890
});
7991

8092
readonly $currentMode = computed(() => this.#store.pageParams().mode);
8193
readonly selectedModeModel = model<EditorModeOption | null>(null);
8294

95+
/**
96+
* Keeps the PrimeNG select model in sync with the store's current mode.
97+
* Runs on init and whenever pageParams.mode or the available menu items change
98+
* (e.g. when isMissingTranslation flips and Draft/Published are removed).
99+
*/
100+
readonly $syncSelectedMode = effect(() => {
101+
const currentMode = this.$currentMode();
102+
const menuItems = this.$menuItems();
103+
const match = menuItems.find((item) => item.id === currentMode);
104+
untracked(() => this.selectedModeModel.set(match ?? null));
105+
});
106+
83107
/**
84108
* TODO: This should be in the shell or in the store
85109
* A main effect should not be hidden in a component
@@ -118,12 +142,6 @@ export class DotEditorModeSelectorComponent implements OnInit {
118142
}
119143
};
120144

121-
ngOnInit() {
122-
const currentMode = this.$currentMode();
123-
const match = this.$menuItems().find((item) => item.id === currentMode);
124-
this.selectedModeModel.set(match ?? null);
125-
}
126-
127145
onModeChange(mode: UVE_MODE) {
128146
if (mode === this.$currentMode()) return;
129147

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

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ const baseUVEState = {
216216
$urlContentMap: urlContentMapSignal, // Mutable for tests
217217
$lockOptions: toggleLockOptionsSignal, // Mutable for tests
218218
$lockFeatureEnabled: $lockFeatureEnabledSignal,
219+
$isMissingTranslation: signal(false),
219220
pageReload: jest.fn(),
220221
editorPaletteOpen: signal(true),
221222
editorCanEditContent: signal(true),
@@ -798,60 +799,17 @@ describe('DotUveToolbarComponent', () => {
798799
expect(spyLoadPageAsset).toHaveBeenCalledWith({ language_id: '1' });
799800
});
800801

801-
it('should call confirmationService.confirm when selected language has no translation', () => {
802-
const spyConfirmationService = jest.spyOn(confirmationService, 'confirm');
803-
const languageWithoutTranslation = MOCK_PAGE_LANGUAGES[1]; // Spanish, id 2, translated: false
804-
805-
spectator.triggerEventHandler(
806-
StubDotLanguageSelectorComponent,
807-
'onLanguageChange',
808-
languageWithoutTranslation
809-
);
810-
spectator.detectChanges();
811-
812-
expect(spyConfirmationService).toHaveBeenCalled();
813-
});
814-
815-
it('should emit translatePage with page and newLanguage when user confirms new translation', () => {
816-
const translatePageSpy = jest.spyOn(spectator.component.translatePage, 'emit');
817-
const languageWithoutTranslation = MOCK_PAGE_LANGUAGES[1]; // Spanish, id 2, translated: false
818-
819-
spectator.triggerEventHandler(
820-
StubDotLanguageSelectorComponent,
821-
'onLanguageChange',
822-
languageWithoutTranslation
823-
);
824-
spectator.detectChanges();
825-
826-
const acceptCallback = (confirmationService.confirm as jest.Mock).mock.calls[0][0]
827-
.accept;
828-
acceptCallback();
829-
830-
const expectedPage = pageSnapshotSignal().page;
831-
expect(translatePageSpy).toHaveBeenCalledWith({
832-
page: expectedPage,
833-
newLanguage: 2
834-
});
835-
});
836-
837-
it('should reset language selector to current language when user rejects new translation', () => {
802+
it('should call pageLoad with language_id when selected language has no translation', () => {
803+
const spyLoadPageAsset = jest.spyOn(baseUVEState, 'pageLoad');
838804
const languageWithoutTranslation = MOCK_PAGE_LANGUAGES[1]; // Spanish, id 2, translated: false
839-
const currentLanguage = baseUVEState.pageLanguage();
840-
const languageSelector = spectator.query(StubDotLanguageSelectorComponent);
841-
const valueSetSpy = jest.spyOn(languageSelector.value, 'set');
842805

843806
spectator.triggerEventHandler(
844807
StubDotLanguageSelectorComponent,
845808
'onLanguageChange',
846809
languageWithoutTranslation
847810
);
848-
spectator.detectChanges();
849-
850-
const rejectCallback = (confirmationService.confirm as jest.Mock).mock.calls[0][0]
851-
.reject;
852-
rejectCallback();
853811

854-
expect(valueSetSpy).toHaveBeenCalledWith(currentLanguage);
812+
expect(spyLoadPageAsset).toHaveBeenCalledWith({ language_id: '2' });
855813
});
856814
});
857815

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.ts

Lines changed: 7 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { map } from 'rxjs/operators';
2525

2626
import { DotDevicesService, DotMessageService, DotPersonalizeService } from '@dotcms/data-access';
2727
import { DotDeviceListItem, DotExperimentStatus, DotLanguage } from '@dotcms/dotcms-models';
28-
import { DotCMSPage, DotCMSURLContentMap, DotCMSViewAsPersona, UVE_MODE } from '@dotcms/types';
28+
import { DotCMSURLContentMap, DotCMSViewAsPersona, UVE_MODE } from '@dotcms/types';
2929
import { DotLanguageSelectorComponent, DotMessagePipe } from '@dotcms/ui';
3030

3131
import { DotEditorModeSelectorComponent } from './components/dot-editor-mode-selector/dot-editor-mode-selector.component';
@@ -77,7 +77,6 @@ export class DotUveToolbarComponent {
7777
$personaSelector = viewChild<EditEmaPersonaSelectorComponent>('personaSelector');
7878
$languageSelector = viewChild<DotLanguageSelectorComponent>('languageSelector');
7979

80-
translatePage = output<{ page: DotCMSPage; newLanguage: number }>();
8180
editUrlContentMap = output<DotCMSURLContentMap>();
8281
deviceSelectorChange = output<DeviceSelectorChange>();
8382

@@ -285,28 +284,15 @@ export class DotUveToolbarComponent {
285284
/**
286285
* Handle the language selection
287286
*
288-
* @param {number} language
289-
* @memberof DotEmaComponent
287+
* @param {DotLanguage} language
288+
* @memberof DotUveToolbarComponent
290289
*/
291290
onLanguageSelected(language: DotLanguage) {
292291
const language_id = language.id.toString();
293-
const languages = this.#store.pageLanguages();
294-
295-
// pageLanguages has the translated flag; fall back to the selector's language object
296-
// when this language has never been created for this page (not in pageLanguages yet)
297-
const currentLanguage = languages.find((lang) => lang.id === language.id) ?? language;
298-
const languageHasTranslation = currentLanguage.translated;
299-
300-
if (!languageHasTranslation) {
301-
// Show confirmation dialog to create a new translation
302-
const page = this.#store.pageAsset()?.page;
303-
if (page) {
304-
this.createNewTranslation(currentLanguage, page);
305-
}
306-
307-
return;
308-
}
309-
292+
// Always load the page in the requested language.
293+
// If the page has no version in that language, the editor's
294+
// $translatePageEffect will detect translated=false after the load
295+
// and show the "create translation?" dialog at that point.
310296
this.#store.pageLoad({ language_id });
311297
}
312298

@@ -396,39 +382,6 @@ export class DotUveToolbarComponent {
396382
});
397383
}
398384

399-
/*
400-
* Asks the user for confirmation to create a new translation for a given language.
401-
*
402-
* @param {DotLanguage} language - The language to create a new translation for.
403-
* @private
404-
*
405-
* @return {void}
406-
*/
407-
private createNewTranslation(language: DotLanguage, page: DotCMSPage): void {
408-
this.#confirmationService.confirm({
409-
header: this.#dotMessageService.get(
410-
'editpage.language-change-missing-lang-populate.confirm.header'
411-
),
412-
message: this.#dotMessageService.get(
413-
'editpage.language-change-missing-lang-populate.confirm.message',
414-
language.language
415-
),
416-
rejectIcon: 'hidden',
417-
acceptIcon: 'hidden',
418-
key: 'shell-confirm-dialog',
419-
accept: () => {
420-
this.translatePage.emit({
421-
page: page,
422-
newLanguage: language.id
423-
});
424-
},
425-
reject: () => {
426-
// If is rejected, bring back the current language on selector
427-
this.$languageSelector()?.value.set(this.#store.pageLanguage());
428-
}
429-
});
430-
}
431-
432385
/**
433386
* Gets the minimum allowed date for the calendar component.
434387
* Sets hours/minutes/seconds/milliseconds to 0 to avoid collisions with preview date

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
@let dropzone = $dropzone();
44

55
<dot-uve-toolbar
6-
(translatePage)="translatePage($event)"
76
(editUrlContentMap)="editContentMap($event)"
87
(deviceSelectorChange)="handleDeviceSelectorChange($event)" />
98

@@ -271,13 +270,11 @@
271270
</div>
272271
}
273272

274-
@if ($showDialogs()) {
275-
<dot-edit-ema-dialog
276-
(action)="onCustomEvent($event)"
277-
(reloadFromDialog)="reloadPage()"
278-
#dialog
279-
data-testId="ema-dialog" />
280-
}
273+
<dot-edit-ema-dialog
274+
(action)="onCustomEvent($event)"
275+
(reloadFromDialog)="reloadPage()"
276+
#dialog
277+
data-testId="ema-dialog" />
281278
<p-confirmDialog
282279
[style]="{
283280
width: '400px'

0 commit comments

Comments
 (0)