diff --git a/core-web/apps/dotcms-ui/src/app/app-routing.module.ts b/core-web/apps/dotcms-ui/src/app/app-routing.module.ts index ec20f121bdc0..6d7e1400f0b1 100644 --- a/core-web/apps/dotcms-ui/src/app/app-routing.module.ts +++ b/core-web/apps/dotcms-ui/src/app/app-routing.module.ts @@ -139,6 +139,9 @@ const PORTLETS_ANGULAR: Route[] = [ { canActivate: [editContentGuard], path: 'content', + data: { + reuseRoute: false + }, loadChildren: () => import('@dotcms/edit-content').then((m) => m.DotEditContentRoutes) }, { diff --git a/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.ts b/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.ts index 4d77600c35f0..606b7bff62e0 100644 --- a/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.ts +++ b/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.ts @@ -35,7 +35,12 @@ export class DotLanguagesService { /** * Return languages. - * @returns Observable + * + * This method fetches the available languages from the server. If a content inode is provided, + * it includes the content inode in the request URL to filter the languages accordingly. + * + * @param {string} [contentInode] - Optional content inode to filter the languages. + * @returns {Observable} An observable emitting the list of languages. * @memberof DotLanguagesService */ get(contentInode?: string): Observable { diff --git a/core-web/libs/data-access/src/lib/dot-workflow-actions-fire/dot-workflow-actions-fire.service.ts b/core-web/libs/data-access/src/lib/dot-workflow-actions-fire/dot-workflow-actions-fire.service.ts index b6520e930f7f..d56f4dc7d6d5 100644 --- a/core-web/libs/data-access/src/lib/dot-workflow-actions-fire/dot-workflow-actions-fire.service.ts +++ b/core-web/libs/data-access/src/lib/dot-workflow-actions-fire/dot-workflow-actions-fire.service.ts @@ -22,6 +22,7 @@ interface DotActionRequestOptions { export interface DotFireActionOptions { actionId: string; inode?: string; + identifier?: string; data?: T; } diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.html index 9ef52ee107fe..f1f478912359 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.html +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.html @@ -4,6 +4,10 @@ @let actions = $store.getActions(); @let showWorkflowActions = $store.showWorkflowActions(); + +@let currentLocaleId = $store.currentLocale()?.id; +@let currentIdentifier = $store.currentIdentifier(); + @if (form) {
@@ -84,7 +88,9 @@ fireWorkflowAction({ actionId: $event.id, inode: contentlet?.inode, - contentType: contentType.variable + contentType: contentType.variable, + languageId: currentLocaleId, + identifier: currentIdentifier }) " [actions]="actions" diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts index e1e2d4a702ca..ddf63647541a 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts @@ -12,6 +12,7 @@ import { Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { MessageService } from 'primeng/api'; +import { DialogService } from 'primeng/dynamicdialog'; import { TabPanel, TabView } from 'primeng/tabview'; import { @@ -24,6 +25,7 @@ import { DotWorkflowsActionsService, DotWorkflowService } from '@dotcms/data-access'; +import { DotCMSWorkflowAction } from '@dotcms/dotcms-models'; import { DotWorkflowActionsComponent } from '@dotcms/ui'; import { DotFormatDateServiceMock, @@ -73,6 +75,7 @@ describe('DotFormComponent', () => { mockProvider(DotWorkflowService), mockProvider(MessageService), mockProvider(DotContentletService), + mockProvider(DialogService), { provide: ActivatedRoute, @@ -321,6 +324,36 @@ describe('DotFormComponent', () => { expect(store.showWorkflowActions()).toBe(false); expect(workflowActions).toBeFalsy(); }); + + it('should send the correct parameters when firing an action. ', () => { + const spy = jest.spyOn(store, 'fireWorkflowAction'); + + workflowActionsService.getWorkFlowActions.mockReturnValue( + of(MOCK_SINGLE_WORKFLOW_ACTIONS) // Single workflow actions trigger the show + ); + store.initializeExistingContent('inode'); + spectator.detectChanges(); + + const workflowActions = spectator.query(DotWorkflowActionsComponent); + + workflowActions.actionFired.emit({ id: '1' } as DotCMSWorkflowAction); + + expect(spy).toHaveBeenCalledWith({ + actionId: '1', + inode: 'cc120e84-ae80-49d8-9473-36d183d0c1c9', + identifier: null, + data: { + contentlet: { + contentType: 'TestMock', + text1: 'content text 1', + text11: 'Tab 2 input content', + text2: 'content text 2', + text3: 'default value modified', + languageId: null + } + } + }); + }); }); }); }); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.ts index bbb34de04816..d614285d6884 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.ts @@ -152,6 +152,9 @@ export class DotEditContentFormComponent implements OnInit { } constructor() { + /** + * Effect that enables or disables the form based on the loading state. + */ effect(() => { const isLoading = this.$store.isLoading(); if (isLoading) { @@ -160,6 +163,21 @@ export class DotEditContentFormComponent implements OnInit { this.form.enable(); } }); + + /** + * Effect that initializes the form and form listener when copying locale. + * + * This effect listens for changes in the `isCopyingLocale` state from the store. + * If `isCopyingLocale` is true, it initializes the form and sets up the form listener. + */ + effect(() => { + const isCopyingLocale = this.$store.isCopyingLocale(); + + if (isCopyingLocale) { + this.initializeForm(); + this.initializeFormListener(); + } + }); } /** @@ -327,14 +345,22 @@ export class DotEditContentFormComponent implements OnInit { * @param {DotCMSWorkflowAction} action * @memberof EditContentLayoutComponent */ - fireWorkflowAction({ actionId, inode, contentType }: DotWorkflowActionParams): void { + fireWorkflowAction({ + actionId, + inode, + contentType, + languageId, + identifier + }: DotWorkflowActionParams): void { this.$store.fireWorkflowAction({ actionId, inode, + identifier, data: { contentlet: { ...this.form.value, - contentType + contentType, + languageId } } }); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.html index e4a7b6ecdd45..40a0d4793130 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.html +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.html @@ -1,6 +1,6 @@ @let locales = $locales(); @let defaultLocale = $defaultLocale(); -@let contentlet = $contentlet(); +@let currentLocale = $currentLocale(); @let isLoading = $isLoading(); @if (isLoading) { @@ -9,11 +9,7 @@ @for (locale of locales; track locale.id) { + (click)="currentLocale?.id !== locale.id ? switchLocale.emit(locale) : null" + [styleClass]="getStyleClass(locale)" /> } } diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.scss b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.scss index ace9f0347c82..b8891e1b64f8 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.scss +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.scss @@ -23,6 +23,10 @@ top: -2px; right: -4px; } + + &:not(:has(.p-chip-filled)) { + cursor: pointer; + } } } } diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.spec.ts index ae0b06155c81..a96202fc0b29 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.spec.ts @@ -2,7 +2,6 @@ import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { Chip, ChipModule } from 'primeng/chip'; -import { DotCMSContentlet } from '@dotcms/angular'; import { DotLanguage } from '@dotcms/dotcms-models'; import { DotEditContentSidebarLocalesComponent } from './dot-edit-content-sidebar-locales.component'; @@ -33,14 +32,13 @@ describe('DotEditContentSidebarLocalesComponent', () => { } ] as DotLanguage[]; const defaultLocale: DotLanguage = locales[0]; - const contentlet: DotCMSContentlet = { languageId: 1 } as DotCMSContentlet; beforeEach(() => { spectator = createComponent({ props: { locales: locales, defaultLocale: defaultLocale, - contentlet: contentlet, + currentLocale: defaultLocale, isLoading: false } as unknown }); @@ -55,7 +53,7 @@ describe('DotEditContentSidebarLocalesComponent', () => { expect(chipElements[1].label).toBe('es-es'); expect(chipElements[2].label).toBe('it-it'); - expect(chipElements[0].styleClass).toBe('p-chip-sm p-chip-primary p-chip-filled default'); + expect(chipElements[0].styleClass).toBe('p-chip-sm p-chip-filled default'); expect(chipElements[1].styleClass).toBe('p-chip-sm p-chip-primary'); expect(chipElements[2].styleClass).toBe('p-chip-sm p-chip-gray p-chip-dashed'); }); @@ -67,4 +65,22 @@ describe('DotEditContentSidebarLocalesComponent', () => { const skeleton = spectator.queryAll('p-skeleton'); expect(skeleton).toExist(); }); + + it('should emit switchLocale event when a locale chip is clicked', () => { + const chipElement = spectator.query('p-chip'); + const switchLocaleSpy = jest.spyOn(spectator.component.switchLocale, 'emit'); + + spectator.click(chipElement); + + expect(switchLocaleSpy).not.toHaveBeenCalled(); + }); + + it('should not emit switchLocale event when the current locale chip is clicked', () => { + const chipElements = spectator.queryAll('p-chip'); + const switchLocaleSpy = jest.spyOn(spectator.component.switchLocale, 'emit'); + + spectator.click(chipElements[1]); + + expect(switchLocaleSpy).toHaveBeenCalledWith(locales[1]); + }); }); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.ts index 2490721026ff..ecb8ea0d6a3c 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-locales/dot-edit-content-sidebar-locales.component.ts @@ -1,10 +1,18 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { ChipModule } from 'primeng/chip'; import { SkeletonModule } from 'primeng/skeleton'; -import { DotCMSContentlet, DotLanguage } from '@dotcms/dotcms-models'; +import { DotLanguage } from '@dotcms/dotcms-models'; + +enum LOCALE_STATUS { + BASE = 'p-chip-sm', + DEFAULT = ' default', + CURRENT = ' p-chip-filled', + TRANSLATED = ' p-chip-primary', + UNTRANSLATED = ' p-chip-gray p-chip-dashed' +} /** * Component representing the locales section in the edit content sidebar. @@ -31,12 +39,41 @@ export class DotEditContentSidebarLocalesComponent { $defaultLocale = input.required({ alias: 'defaultLocale' }); /** - * Current contentlet. + * The current locale. */ - $contentlet = input.required({ alias: 'contentlet' }); + $currentLocale = input.required({ alias: 'currentLocale' }); /** * Whether the data is loading. */ $isLoading = input.required({ alias: 'isLoading' }); + + /** + * Event emitted when the locale is switched. + */ + switchLocale = output(); + + /** + * Determines the appropriate style class for a given locale. + * + * @param {DotLanguage} locale - The locale object containing its id and translation status. + * @returns {string} The computed style class based on the locale's properties. + */ + getStyleClass({ id, translated }: DotLanguage): string { + let styleClass: string = LOCALE_STATUS.BASE; + + if (id === this.$currentLocale().id) { + styleClass += LOCALE_STATUS.CURRENT; + } else if (translated) { + styleClass += LOCALE_STATUS.TRANSLATED; + } else { + styleClass += LOCALE_STATUS.UNTRANSLATED; + } + + if (id === this.$defaultLocale().id) { + styleClass += LOCALE_STATUS.DEFAULT; + } + + return styleClass; + } } diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.html new file mode 100644 index 000000000000..1ba8bf136a1d --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.html @@ -0,0 +1,29 @@ +

{{ 'edit.content.sidebar.locales.untranslated.text' | dm }}

+
+ + +
+ + +
+ + +
diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.scss b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.scss new file mode 100644 index 000000000000..cf6c477b7349 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.scss @@ -0,0 +1,14 @@ +@use "variables" as *; + +:host { + display: flex; + flex-direction: column; + gap: $spacing-1; + + .untranslated-locale__actions { + margin-top: $spacing-3; + display: flex; + gap: $spacing-2; + justify-content: flex-end; + } +} diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.spec.ts new file mode 100644 index 000000000000..8a0c484f3f73 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.spec.ts @@ -0,0 +1,65 @@ +import { Spectator, createComponentFactory, byTestId } from '@ngneat/spectator/jest'; + +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { ButtonDirective } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { RadioButtonModule } from 'primeng/radiobutton'; + +import { DotMessageService } from '@dotcms/data-access'; +import { DotMessagePipe } from '@dotcms/ui'; +import { MockDotMessageService } from '@dotcms/utils-testing'; + +import { DotEditContentSidebarUntranslatedLocaleComponent } from './dot-edit-content-sidebar-untranslated-locale.component'; + +const messageServiceMock = new MockDotMessageService({ + 'edit.content.sidebar.locales.untranslated.populate': 'Populate from Current Locale' +}); + +describe('DotEditContentSidebarUntranslatedLocaleComponent', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: DotEditContentSidebarUntranslatedLocaleComponent, + imports: [CommonModule, RadioButtonModule, FormsModule, ButtonDirective, DotMessagePipe], + providers: [ + { provide: DynamicDialogRef, useValue: { close: jest.fn() } }, + { + provide: DynamicDialogConfig, + useValue: { data: { currentLocale: { isoCode: 'en-us' } } } + }, + { + provide: DotMessageService, + useValue: messageServiceMock + } + ] + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should close dialog with selected option on continue button click', () => { + const continueButton = spectator.query(byTestId('continue-button')); + const dialogRefCloseSpy = jest.spyOn(spectator.component.dialogRef, 'close'); + + spectator.click(continueButton); + + expect(dialogRefCloseSpy).toHaveBeenCalledWith(spectator.component.selectedOption); + }); + + it('should close dialog on cancel button click', () => { + const cancelButton = spectator.query(byTestId('cancel-button')); + const dialogRefCloseSpy = jest.spyOn(spectator.component.dialogRef, 'close'); + + spectator.click(cancelButton); + + expect(dialogRefCloseSpy).toHaveBeenCalledWith(); + }); + + it('should display the correct label for the populate radio button', () => { + expect(spectator.query(byTestId('populate-label')).textContent).toContain( + 'Populate from Current Locale en-us' + ); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.ts new file mode 100644 index 000000000000..9b429a6c5f72 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/components/dot-edit-content-sidebar-untranslated-locale/dot-edit-content-sidebar-untranslated-locale.component.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { ButtonDirective } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { RadioButtonModule } from 'primeng/radiobutton'; + +import { DotMessagePipe } from '@dotcms/ui'; + +@Component({ + selector: 'dot-edit-content-sidebar-untranslated-locale', + standalone: true, + imports: [CommonModule, RadioButtonModule, DotMessagePipe, FormsModule, ButtonDirective], + templateUrl: './dot-edit-content-sidebar-untranslated-locale.component.html', + styleUrl: './dot-edit-content-sidebar-untranslated-locale.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotEditContentSidebarUntranslatedLocaleComponent { + selectedOption = 'populate'; + + dialogRef = inject(DynamicDialogRef); + config = inject(DynamicDialogConfig); +} diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/dot-edit-content-sidebar.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/dot-edit-content-sidebar.component.html index 1396750ce5f1..0b72787b41f5 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/dot-edit-content-sidebar.component.html +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-sidebar/dot-edit-content-sidebar.component.html @@ -3,10 +3,6 @@ @let isLoadingInformation = store.isLoadingInformation(); @let referencePagesCount = store.information.relatedContent(); - -@let locales = store.locales(); -@let defaultLocale = store.defaultLocale(); -@let isLoadingLocales = store.isLoadingLocales();