diff --git a/docs/concepts/cms-integration.md b/docs/concepts/cms-integration.md index b45dde257e..a5a6026b91 100644 --- a/docs/concepts/cms-integration.md +++ b/docs/concepts/cms-integration.md @@ -87,13 +87,24 @@ CREATE src/app/cms/components/cms-inventory/cms-inventory.component.spec.ts (795 UPDATE src/app/cms/cms.module.ts (4956 bytes) ``` +## Design View + +> [!IMPORTANT] +> To use the new Design View for the PWA within the Intershop Administration Portal ICM version 11.7.0 or above is needed. + +The Intershop PWA 5.0.0 introduces experimental support for the new Design View that can be used within the Intershop Administration Portal (IAP). +Besides access to the IAP ICM 11 is required, that provides the necessary CMS Management REST API to get information about available CMS models and the CMS page tree and to edit CMS components. + +ICM 11 does not provide the basic support for a design preview (as mentioned in the next section) so the _Design View_ tab in the ICM backoffice cannot be used to preview content changes in the PWA. +The new Design View in the IAP currently only supports content editing but not content preview. + ## Design Preview In conjunction with Intershop Commerce Management (ICM) 7.10.39.1, Intershop PWA 3.3.0 introduced basic support for a design preview. This means the _Design View_ tab in the ICM backoffice can be used to preview content changes in the PWA, but without any direct editing capabilities. Direct item preview for products, categories and content pages works now as well in the context of the PWA. -The preview feature basically consists of the [`PreviewService`](../../src/app/core/services/preview/preview.service.ts) that handles the preview functionality by listening for `PreviewContextID` initialization or changes and saving it to the browser session storage. +The preview feature basically consists of the [`PreviewService`](../../src/app/core/utils/preview/preview.service.ts) that handles the preview functionality by listening for `PreviewContextID` initialization or changes and saving it to the browser session storage. The [`PreviewInterceptor`](../../src/app/core/interceptors/preview.interceptor.ts) than handles adding a currently available PreviewContextID as matrix parameter `;prectx=` to all REST requests so they can be evaluated on the ICM side returning content fitting to the set preview context. To end a preview session and to delete the saved `PreviewContextID` in the browser session storage, use the _Finish Preview_ button of the _Design View_ configuration. diff --git a/src/app/core/interceptors/preview.interceptor.ts b/src/app/core/interceptors/preview.interceptor.ts index dd81fc8e46..f4ccb4a973 100644 --- a/src/app/core/interceptors/preview.interceptor.ts +++ b/src/app/core/interceptors/preview.interceptor.ts @@ -2,7 +2,7 @@ import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/c import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { PreviewService } from 'ish-core/services/preview/preview.service'; +import { PreviewService } from 'ish-core/utils/preview/preview.service'; /** * add PreviewContextID to every request if it is available in the SessionStorage @@ -12,7 +12,7 @@ export class PreviewInterceptor implements HttpInterceptor { constructor(private previewService: PreviewService) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { - if (this.previewService.previewContextId) { + if (this.previewService.previewContextId && !this.previewService.isDesignViewMode) { return next.handle( req.clone({ url: `${req.url};prectx=${this.previewService.previewContextId}`, diff --git a/src/app/core/utils/design-view/design-view.service.spec.ts b/src/app/core/utils/design-view/design-view.service.spec.ts new file mode 100644 index 0000000000..7d164e21a5 --- /dev/null +++ b/src/app/core/utils/design-view/design-view.service.spec.ts @@ -0,0 +1,25 @@ +import { TestBed } from '@angular/core/testing'; +import { provideMockStore } from '@ngrx/store/testing'; + +import { getCurrentLocale } from 'ish-core/store/core/configuration'; + +import { DesignViewService } from './design-view.service'; + +describe('Design View Service', () => { + let designViewService: DesignViewService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [{ selector: getCurrentLocale, value: 'en_US' }], + }), + ], + }); + designViewService = TestBed.inject(DesignViewService); + }); + + it('should be created', () => { + expect(designViewService).toBeTruthy(); + }); +}); diff --git a/src/app/core/utils/design-view/design-view.service.ts b/src/app/core/utils/design-view/design-view.service.ts new file mode 100644 index 0000000000..94d313ad82 --- /dev/null +++ b/src/app/core/utils/design-view/design-view.service.ts @@ -0,0 +1,151 @@ +import { ApplicationRef, Injectable } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { Store, select } from '@ngrx/store'; +import { filter, first, fromEvent, map, switchMap } from 'rxjs'; + +import { getCurrentLocale } from 'ish-core/store/core/configuration'; +import { DomService } from 'ish-core/utils/dom/dom.service'; +import { whenTruthy } from 'ish-core/utils/operators'; + +interface DesignViewMessage { + type: + | 'dv-clientAction' + | 'dv-clientNavigation' + | 'dv-clientReady' + | 'dv-clientRefresh' + | 'dv-clientLocale' + | 'dv-clientStable'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + payload?: any; +} + +@Injectable({ providedIn: 'root' }) +export class DesignViewService { + private allowedHostMessageTypes = ['dv-clientRefresh']; + + constructor( + private router: Router, + private appRef: ApplicationRef, + private domService: DomService, + private store: Store + ) { + this.init(); + } + + /** + * Send a message to the host window. + * Send the message to any host since the PWA is not supposed to know a fixed IAP URL (we are not sending secrets). + * + * @param message The message to send to the host (including type and payload) + */ + messageToHost(message: DesignViewMessage) { + window.parent.postMessage(message, '*'); + } + + /** + * Start method that sets up Design View communication. + * Needs to be called *once* for the whole application. + */ + private init() { + if (!this.shouldInit()) { + return; + } + + this.listenToHostMessages(); + this.listenToApplication(); + + // tell the host client is ready + this.messageToHost({ type: 'dv-clientReady' }); + } + + /** + * Decides whether to init the Design View capabilities or not. + * Is used by the init method, so it will only initialize when + * (1) there is a window (i.e. the application does not run in SSR/Universal) + * (2) application does not run on top level window (i.e. it runs in the Design View iframe) + */ + private shouldInit() { + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + return typeof window !== 'undefined' && window.parent && window.parent !== window; + } + + /** + * Subscribe to messages from the host window. + * Incoming messages are filtered using `allowedHostMessageTypes` + * Should only be called *once* during initialization. + */ + private listenToHostMessages() { + fromEvent(window, 'message') + .pipe( + filter(e => e.data.hasOwnProperty('type') && this.allowedHostMessageTypes.includes(e.data.type)), + map(message => message.data) + ) + .subscribe(message => this.handleHostMessage(message)); + } + + /** + * Listen to events throughout the application and send message to host when + * (1) route has changed (`dv-clientNavigation`), + * (2) application is stable, i.e. all async tasks have been completed (`dv-clientStable`) or + * (3) content include has been reloaded (`dv-clientStable`). + * + * Should only be called *once* during initialization. + */ + private listenToApplication() { + const navigation$ = this.router.events.pipe(filter(e => e instanceof NavigationEnd)); + const stable$ = this.appRef.isStable.pipe(whenTruthy(), first()); + const navigationStable$ = navigation$.pipe(switchMap(() => stable$)); + + // send `dv-clientNavigation` event for each route change + navigation$.subscribe(e => this.messageToHost({ type: 'dv-clientNavigation', payload: { url: e.url } })); + + stable$.subscribe(() => { + this.applyHierarchyHighlighting(); + }); + + // send `dv-clientStable` event when application is stable or loading of the content included finished + navigationStable$.subscribe(() => { + this.messageToHost({ type: 'dv-clientStable' }); + this.applyHierarchyHighlighting(); + }); + + // send `dv-clientLocale` event when application is stable and the current application locale was determined + stable$ + .pipe( + switchMap(() => + this.store.pipe(select(getCurrentLocale), whenTruthy()).pipe( + first() // PWA reloads after each locale change, only one locale is active during runtime + ) + ) + ) + .subscribe(locale => this.messageToHost({ type: 'dv-clientLocale', payload: { locale } })); + } + + /** + * Handle incoming message from the host window. + * Invoked by the event listener in `listenToHostMessages()` when a new message arrives. + */ + private handleHostMessage(message: DesignViewMessage) { + switch (message.type) { + case 'dv-clientRefresh': { + location.reload(); + return; + } + } + } + + /** + * Workaround for the missing Firefox CSS support for :has to highlight + * only the last .design-view-wrapper in the .design-view-wrapper hierarchy. + * + */ + private applyHierarchyHighlighting() { + const designViewWrapper: NodeListOf = document.querySelectorAll('.design-view-wrapper'); + + designViewWrapper.forEach(element => { + if (!element.querySelector('.design-view-wrapper')) { + this.domService.addClass(element, 'last-design-view-wrapper'); + } + }); + } +} diff --git a/src/app/core/services/preview/preview.service.spec.ts b/src/app/core/utils/preview/preview.service.spec.ts similarity index 100% rename from src/app/core/services/preview/preview.service.spec.ts rename to src/app/core/utils/preview/preview.service.spec.ts diff --git a/src/app/core/services/preview/preview.service.ts b/src/app/core/utils/preview/preview.service.ts similarity index 93% rename from src/app/core/services/preview/preview.service.ts rename to src/app/core/utils/preview/preview.service.ts index 1c5206a2fb..265f217617 100644 --- a/src/app/core/services/preview/preview.service.ts +++ b/src/app/core/utils/preview/preview.service.ts @@ -1,4 +1,3 @@ -/* eslint-disable ish-custom-rules/no-intelligence-in-artifacts */ import { ApplicationRef, Injectable } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Store, select } from '@ngrx/store'; @@ -8,7 +7,7 @@ import { getICMBaseURL } from 'ish-core/store/core/configuration'; import { whenTruthy } from 'ish-core/utils/operators'; interface StorefrontEditingMessage { - type: string; + type: 'sfe-pwaready' | 'sfe-pwanavigation' | 'sfe-pwastable' | 'sfe-setcontext'; // eslint-disable-next-line @typescript-eslint/no-explicit-any payload?: any; } @@ -79,7 +78,11 @@ export class PreviewService { * (3) OR the debug mode is on (`initOnTopLevel`). */ private shouldInit() { - return typeof window !== 'undefined' && ((window.parent && window.parent !== window) || this.initOnTopLevel); + return ( + typeof window !== 'undefined' && + ((window.parent && window.parent !== window) || this.initOnTopLevel) && + !this.isDesignViewMode + ); } /** @@ -175,4 +178,9 @@ export class PreviewService { get previewContextId() { return this._previewContextId ?? (!SSR ? sessionStorage.getItem('PreviewContextID') : undefined); } + + // TODO: replace usage of previewContextId to identify Design View mode + get isDesignViewMode(): boolean { + return this.previewContextId === 'DESIGNVIEW'; + } } diff --git a/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.html b/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.html new file mode 100644 index 0000000000..d82864b0dc --- /dev/null +++ b/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.html @@ -0,0 +1,58 @@ + +
+
+ + + + +
{{ pagelet.displayName ? pagelet.displayName : '(Language missing)' }}
+ + +
+
+ + + +
{{ pagelet.slot(this.slotId).displayName }}
+ +
+ + +
{{ include.displayName }}
+ +
+
+
+ +
+
+ + + + diff --git a/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.scss b/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.scss new file mode 100644 index 0000000000..2329d8b7a5 --- /dev/null +++ b/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.scss @@ -0,0 +1,91 @@ +@import 'variables'; + +// DesignView colors +$design-view-color-pagelet: #00b8d9; +$design-view-color-slot: #399; +$design-view-color-include: #ce5399; + +.design-view-wrapper { + position: relative; + + > .design-view-wrapper-actions { + position: absolute; + top: -43px; + left: -2px; + z-index: 1000; + display: none; + align-items: center; + justify-content: flex-end; + color: $color-inverse; + + .name { + padding: 0 $space-default; + white-space: nowrap; + } + + .btn { + margin-bottom: 0; + font-size: 20px; + color: $color-inverse; + } + } + + &.pagelet.last-design-view-wrapper { + /* + * Highlight only the last .pagelet element in the hierarchy and highlight neither slot nor include. + * The class .last-design-view-wrapper is applied in TypeScript because &.pagelet:not(:has(.design-view-wrapper)) + * does not work in Firefox yet. So it cannot be done in CSS only. + */ + &:hover { + outline-width: 3px; + outline-style: solid; + outline-offset: -1px; + + > .design-view-wrapper-actions { + display: flex; + } + } + + &.slot, + &.include { + min-width: 100px; + min-height: 30px; + } + + &.pagelet { + > .design-view-wrapper-actions { + background-color: $design-view-color-pagelet; + } + + &:hover { + outline-color: $design-view-color-pagelet; + } + } + + &.slot { + > .design-view-wrapper-actions { + background-color: $design-view-color-slot; + } + + &:hover { + outline-color: $design-view-color-slot; + } + } + + &.include { + > .design-view-wrapper-actions { + background-color: $design-view-color-include; + } + + &:hover { + outline-color: $design-view-color-include; + } + } + } +} + +// apply design view specific changes to be able to edit all components +::ng-deep .carousel-inner { + // carousel slide item + overflow: inherit; +} diff --git a/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.spec.ts b/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.spec.ts new file mode 100644 index 0000000000..e1bb989897 --- /dev/null +++ b/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.spec.ts @@ -0,0 +1,130 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { anything, instance, mock, when } from 'ts-mockito'; + +import { CMSFacade } from 'ish-core/facades/cms.facade'; +import { ContentPagelet } from 'ish-core/models/content-pagelet/content-pagelet.model'; +import { + ContentPageletEntryPointView, + ContentPageletView, + createContentPageletView, +} from 'ish-core/models/content-view/content-view.model'; +import { DesignViewService } from 'ish-core/utils/design-view/design-view.service'; +import { PreviewService } from 'ish-core/utils/preview/preview.service'; + +import { ContentDesignViewWrapperComponent } from './content-design-view-wrapper.component'; + +describe('Content Design View Wrapper Component', () => { + let component: ContentDesignViewWrapperComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let cmsFacade: CMSFacade; + let designViewService: DesignViewService; + let previewService: PreviewService; + + beforeEach(async () => { + cmsFacade = mock(CMSFacade); + designViewService = mock(DesignViewService); + previewService = mock(PreviewService); + + when(cmsFacade.pagelet$(anything())).thenReturn( + of({ id: 'xyz', displayName: 'Pagelet Name xyz' } as ContentPageletView) + ); + when(previewService.isDesignViewMode).thenReturn(true); + + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ContentDesignViewWrapperComponent, MockComponent(FaIconComponent)], + providers: [ + { provide: CMSFacade, useFactory: () => instance(cmsFacade) }, + { provide: DesignViewService, useFactory: () => instance(designViewService) }, + { provide: PreviewService, useFactory: () => instance(previewService) }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentDesignViewWrapperComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should not be rendered if no input parameter is given', () => { + fixture.detectChanges(); + + expect(element).toMatchInlineSnapshot(`N/A`); + }); + + it('should be rendered if a pageletId is given', () => { + component.pageletId = 'xyz'; + fixture.detectChanges(); + + expect(component.type).toEqual('pagelet'); + expect(element).toMatchInlineSnapshot(` +
+
+
Pagelet Name xyz
+ +
+
+ `); + }); + + it('should be rendered if a slotId is given', () => { + component.slotId = 'xyz_slot_id'; + component.pagelet = createContentPageletView({ + id: 'xyz_pagelet_id', + displayName: 'Pagelet Name xyz_slot', + slots: [ + { + definitionQualifiedName: component.slotId, + displayName: 'Slot Name xyz', + }, + ], + } as ContentPagelet); + fixture.detectChanges(); + + expect(component.type).toEqual('slot'); + expect(element).toMatchInlineSnapshot(` +
+
+
Slot Name xyz
+ +
+
+ `); + }); + + it('should be rendered if an include is given', () => { + component.include = { + id: 'xyz_include_id', + displayName: 'Include Name xyz', + } as ContentPageletEntryPointView; + fixture.detectChanges(); + + expect(component.type).toEqual('include'); + expect(element).toMatchInlineSnapshot(` +
+
+
Include Name xyz
+ +
+
+ `); + }); +}); diff --git a/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.ts b/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.ts new file mode 100644 index 0000000000..6bfae1c0e7 --- /dev/null +++ b/src/app/shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.ts @@ -0,0 +1,51 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { CMSFacade } from 'ish-core/facades/cms.facade'; +import { ContentPageletEntryPointView, ContentPageletView } from 'ish-core/models/content-view/content-view.model'; +import { DesignViewService } from 'ish-core/utils/design-view/design-view.service'; +import { PreviewService } from 'ish-core/utils/preview/preview.service'; + +@Component({ + selector: 'ish-content-design-view-wrapper', + templateUrl: './content-design-view-wrapper.component.html', + styleUrls: ['./content-design-view-wrapper.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ContentDesignViewWrapperComponent implements OnInit { + // pagelet parameter + @Input() pageletId: string; + // slot parameters + @Input() slotId: string; + @Input() pagelet: ContentPageletView; + // include parameter + @Input() include: ContentPageletEntryPointView; + + pagelet$: Observable; + type: 'pagelet' | 'slot' | 'include'; + + isDesignViewMode = false; // temporary activation switch + + constructor( + private cmsFacade: CMSFacade, + private previewService: PreviewService, + private designViewService: DesignViewService + ) {} + + ngOnInit() { + if (this.pageletId) { + this.type = 'pagelet'; + this.pagelet$ = this.cmsFacade.pagelet$(this.pageletId); + } else if (this.slotId) { + this.type = 'slot'; + } else if (this.include) { + this.type = 'include'; + } + + this.isDesignViewMode = this.previewService.isDesignViewMode; + } + + triggerAction(id: string, action: string) { + this.designViewService.messageToHost({ type: 'dv-clientAction', payload: { id, action } }); + } +} diff --git a/src/app/shared/cms/components/content-include/content-include.component.html b/src/app/shared/cms/components/content-include/content-include.component.html index 29975a599d..eb29f04079 100644 --- a/src/app/shared/cms/components/content-include/content-include.component.html +++ b/src/app/shared/cms/components/content-include/content-include.component.html @@ -1,3 +1,5 @@ - + + + diff --git a/src/app/shared/cms/components/content-include/content-include.component.spec.ts b/src/app/shared/cms/components/content-include/content-include.component.spec.ts index ee5ee4e966..14c511aedc 100644 --- a/src/app/shared/cms/components/content-include/content-include.component.spec.ts +++ b/src/app/shared/cms/components/content-include/content-include.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { anything, instance, mock, when } from 'ts-mockito'; @@ -7,6 +8,7 @@ import { ContentPageletEntryPointView, createContentPageletEntryPointView, } from 'ish-core/models/content-view/content-view.model'; +import { ContentDesignViewWrapperComponent } from 'ish-shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component'; import { ContentIncludeComponent } from './content-include.component'; @@ -33,7 +35,7 @@ describe('Content Include Component', () => { when(cmsFacade.contentInclude$(anything())).thenReturn(of(include)); await TestBed.configureTestingModule({ - declarations: [ContentIncludeComponent], + declarations: [ContentIncludeComponent, MockComponent(ContentDesignViewWrapperComponent)], providers: [{ provide: CMSFacade, useValue: instance(cmsFacade) }], }).compileComponents(); }); diff --git a/src/app/shared/cms/components/content-pagelet/content-pagelet.component.html b/src/app/shared/cms/components/content-pagelet/content-pagelet.component.html index 67482eeb9f..2531cd6ce6 100644 --- a/src/app/shared/cms/components/content-pagelet/content-pagelet.component.html +++ b/src/app/shared/cms/components/content-pagelet/content-pagelet.component.html @@ -1 +1,3 @@ - + + + diff --git a/src/app/shared/cms/components/content-pagelet/content-pagelet.component.spec.ts b/src/app/shared/cms/components/content-pagelet/content-pagelet.component.spec.ts index 9f6365524c..a6f056c3e3 100644 --- a/src/app/shared/cms/components/content-pagelet/content-pagelet.component.spec.ts +++ b/src/app/shared/cms/components/content-pagelet/content-pagelet.component.spec.ts @@ -67,7 +67,7 @@ describe('Content Pagelet Component', () => { expect(() => component.ngOnChanges()).not.toThrow(); expect(() => fixture.detectChanges()).not.toThrow(); - expect(element).toMatchInlineSnapshot(`N/A`); + expect(element).toMatchInlineSnapshot(``); expect(consoleSpy).toHaveBeenCalledWith('did not find mapping for id (fq)'); }); @@ -78,6 +78,10 @@ describe('Content Pagelet Component', () => { expect(() => component.ngOnChanges()).not.toThrow(); expect(() => fixture.detectChanges()).not.toThrow(); - expect(element).toMatchInlineSnapshot(`foo`); + expect(element).toMatchInlineSnapshot(` + foo + `); }); }); diff --git a/src/app/shared/cms/components/content-slot/content-slot.component.html b/src/app/shared/cms/components/content-slot/content-slot.component.html index d7e483d732..e905af14ac 100644 --- a/src/app/shared/cms/components/content-slot/content-slot.component.html +++ b/src/app/shared/cms/components/content-slot/content-slot.component.html @@ -1,5 +1,7 @@ - + + - - - + + + + diff --git a/src/app/shared/cms/components/content-slot/content-slot.component.spec.ts b/src/app/shared/cms/components/content-slot/content-slot.component.spec.ts index 1af26f3918..41344ac299 100644 --- a/src/app/shared/cms/components/content-slot/content-slot.component.spec.ts +++ b/src/app/shared/cms/components/content-slot/content-slot.component.spec.ts @@ -1,7 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; import { ContentPagelet } from 'ish-core/models/content-pagelet/content-pagelet.model'; import { createContentPageletView } from 'ish-core/models/content-view/content-view.model'; +import { ContentDesignViewWrapperComponent } from 'ish-shared/cms/components/content-design-view-wrapper/content-design-view-wrapper.component'; import { ContentSlotComponent } from './content-slot.component'; @@ -12,7 +14,7 @@ describe('Content Slot Component', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ContentSlotComponent], + declarations: [ContentSlotComponent, MockComponent(ContentDesignViewWrapperComponent)], }).compileComponents(); }); diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c07b9e0ef0..6ba87008da 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -53,6 +53,7 @@ import { CMSStandardPageComponent } from './cms/components/cms-standard-page/cms import { CMSStaticPageComponent } from './cms/components/cms-static-page/cms-static-page.component'; import { CMSTextComponent } from './cms/components/cms-text/cms-text.component'; import { CMSVideoComponent } from './cms/components/cms-video/cms-video.component'; +import { ContentDesignViewWrapperComponent } from './cms/components/content-design-view-wrapper/content-design-view-wrapper.component'; import { ContentIncludeComponent } from './cms/components/content-include/content-include.component'; import { ContentNavigationComponent } from './cms/components/content-navigation/content-navigation.component'; import { ContentPageletComponent } from './cms/components/content-pagelet/content-pagelet.component'; @@ -260,6 +261,7 @@ const exportedComponents = [ ContentIncludeComponent, ContentNavigationComponent, ContentPageletComponent, + ContentDesignViewWrapperComponent, ContentViewcontextComponent, ErrorMessageComponent, FilterNavigationComponent, diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index 320293439b..11e7076b56 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -846,6 +846,9 @@ "customer.credentials.passwordreset.invalid_password.error.PasswordExpressionViolation": "Das Kennwort muss mindestens 7 Zeichen enthalten, bestehend aus Zahlen und Buchstaben, ohne Leerzeichen.", "customer.missing_fields.error": "Fehlende Felder", "customer.wishlist.profane_word.error": "Ihre aktuelle Eingabe kann nicht bearbeitet werden. Unpassende, beleidigende bzw. vulgäre Sprache wird nicht akzeptiert. Bitte geben Sie etwas anderes ein.", + "designview.add.link.title": "Hinzufügen", + "designview.delete.link.title": "Entfernen", + "designview.edit.link.title": "Bearbeiten", "dialog.close.text": "Schließen", "email.recommended_product.heading": "Ich habe bei {{0}} etwas für dich gefunden", "email.recommended_product.text": "Sieh mal, was ich für dich gefunden habe. {{0}}\n{{1}}", diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index 2c4b5a58a8..d350c45e57 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -846,6 +846,9 @@ "customer.credentials.passwordreset.invalid_password.error.PasswordExpressionViolation": "The password must include 7 characters minimum, containing numbers and letters, no spaces.", "customer.missing_fields.error": "Missing fields", "customer.wishlist.profane_word.error": "Your current entry cannot be processed. Language deemed inappropriate, derogatory, or profane will not be accepted. Please create a new entry.", + "designview.add.link.title": "Add", + "designview.delete.link.title": "Remove", + "designview.edit.link.title": "Edit", "dialog.close.text": "Close", "email.recommended_product.heading": "I found an item for you at {{0}}", "email.recommended_product.text": "Take a look at what I found for you. {{0}}\n{{1}}", diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json index 0359f4f1fe..00007464e8 100644 --- a/src/assets/i18n/fr_FR.json +++ b/src/assets/i18n/fr_FR.json @@ -846,6 +846,9 @@ "customer.credentials.passwordreset.invalid_password.error.PasswordExpressionViolation": "Le mot de passe doit comporter un minimum de 7 caractères, comprenant des chiffres et des lettres, sans espaces.", "customer.missing_fields.error": "Champs manquants", "customer.wishlist.profane_word.error": "Le texte saisi ne peut pas être traité. Les termes jugés inappropriés, injurieux ou vulgaires ne sont pas acceptés. Veuillez saisir un nouveau texte.", + "designview.add.link.title": "Ajouter", + "designview.delete.link.title": "Supprimer", + "designview.edit.link.title": "Modifier", "dialog.close.text": "Fermer", "email.recommended_product.heading": "J’ai trouvé un article pour vous sur {{0}}", "email.recommended_product.text": "Allez voir ce que j’ai trouvé pour vous. {{0}}\n{{1}}",