Skip to content

Commit

Permalink
feat(edit-ema): #26997 Add page properties functionality (#27111)
Browse files Browse the repository at this point in the history
* add page properties functionality

* solve feedback
  • Loading branch information
zJaaal authored Dec 27, 2023
1 parent 48caf15 commit 75c0eba
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
<nav class="edit-ema-nav-bar">
<ng-container *ngFor="let item of items">
<ng-container
[ngTemplateOutlet]="item.action ? button : anchor"
[ngTemplateOutletContext]="{ item: item }">
</ng-container>
</ng-container>
</nav>

<ng-template #anchor let-item="item">
<a
class="edit-ema-nav-bar__item"
*ngFor="let item of items"
[routerLink]="item.href"
(click)="item?.action()"
data-testId="nav-bar-item"
routerLinkActive="edit-ema-nav-bar__item--active"
queryParamsHandling="merge"
Expand All @@ -16,7 +23,22 @@
{{ item.label }}
</span>
</a>
</nav>
</ng-template>

<ng-template #button let-item="item">
<button
class="edit-ema-nav-bar__item edit-ema-nav-bar__item--button"
(click)="item?.action()"
data-testId="nav-bar-item">
<ng-container
[ngTemplateOutlet]="item.icon ? icon : iconURL"
[ngTemplateOutletContext]="{ item: item }">
</ng-container>
<span class="item__label" data-testId="nav-bar-item-label">
{{ item.label }}
</span>
</button>
</ng-template>

<ng-template #icon let-item="item">
<i [class]="'pi ' + item.icon" data-testId="nav-bar-item-icon"></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
}
}

.edit-ema-nav-bar__item--button {
background-color: transparent;
border: none;
}

.item__image,
use {
user-select: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('EditEmaNavigationBarComponent', () => {
describe('DOM', () => {
describe('Nav Bar', () => {
it('should have 5 items', () => {
const links = spectator.queryAll('a');
const links = spectator.queryAll(byTestId('nav-bar-item'));

expect(links.length).toBe(5);
expect(links[0].textContent.trim()).toBe('Content');
Expand All @@ -85,6 +85,12 @@ describe('EditEmaNavigationBarComponent', () => {
expect(links[4].getAttribute('ng-reflect-router-link')).toBeNull();
});

it("should be a button if action is defined on last item 'Action'", () => {
const actionLink = spectator.query('button[data-testId="nav-bar-item"]');

expect(actionLink).not.toBeNull();
});

it("should trigger mockedAction on clicking last item 'Action'", () => {
const actionLink = spectator.query(byText('Action'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,29 @@
[visible]="pageToolsVisible"
[currentPageUrlParams]="sp.seoProperties"></dot-page-tools-seo>
</ng-container>
<p-dialog
#dialog
*ngIf="dialogState$ | async as ds"
[visible]="ds.type === 'shell'"
[style]="{ height: '90vh', width: '90vw' }"
[header]="ds.header"
[draggable]="false"
[resizable]="false"
[maximizable]="true"
[modal]="true"
(visibleChange)="store.resetDialog()"
data-testId="dialog">
<iframe
#dialogIframe
[style]="{ border: 'none', display: ds.iframeLoading ? 'none' : 'block' }"
[src]="ds.iframeURL | safeUrl"
(load)="onIframeLoad()"
title="dialog"
data-testId="dialog-iframe"
width="100%"
height="100%"></iframe>
<dot-spinner
*ngIf="ds.iframeLoading"
[ngStyle]="{ position: 'absolute', top: '50%' }"
data-testId="spinner"></dot-spinner>
</p-dialog>
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, expect } from '@jest/globals';
import { SpectatorRouting, createRoutingFactory } from '@ngneat/spectator/jest';
import { SpectatorRouting, byTestId, createRoutingFactory } from '@ngneat/spectator/jest';
import { of } from 'rxjs';

import { HttpClientTestingModule } from '@angular/common/http/testing';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';

Expand All @@ -22,6 +23,7 @@ import { EditEmaStore } from './store/dot-ema.store';
import { DotActionUrlService } from '../services/dot-action-url/dot-action-url.service';
import { DotPageApiService } from '../services/dot-page-api.service';
import { DEFAULT_PERSONA, WINDOW } from '../shared/consts';
import { NG_CUSTOM_EVENTS } from '../shared/enums';

describe('DotEmaShellComponent', () => {
let spectator: SpectatorRouting<DotEmaShellComponent>;
Expand All @@ -48,7 +50,8 @@ describe('DotEmaShellComponent', () => {
return of({
page: {
title: 'hello world',
identifier: '123'
identifier: '123',
inode: '123'
},
viewAs: {
language: {
Expand Down Expand Up @@ -147,4 +150,91 @@ describe('DotEmaShellComponent', () => {
expect(navigate).toHaveBeenCalledWith(['/pages']);
});
});

describe('page properties', () => {
it('should open the dialog when triggering store.initEditAction with shell as context', () => {
spectator.detectChanges();
store.initActionEdit({
inode: '123',
title: 'hello world',
type: 'shell'
});
spectator.detectChanges();

expect(spectator.query(byTestId('dialog-iframe'))).not.toBeNull();
});

it('should trigger a navigate when saving and the url changed', () => {
const navigate = jest.spyOn(router, 'navigate');

spectator.detectChanges();
store.initActionEdit({
inode: '123',
title: 'hello world',
type: 'shell'
});
spectator.detectChanges();

const dialogIframe = spectator.debugElement.query(
By.css('[data-testId="dialog-iframe"]')
);

spectator.triggerEventHandler(dialogIframe, 'load', {}); // There's no way we can load the iframe, because we are setting a real src and will not load

dialogIframe.nativeElement.contentWindow.document.dispatchEvent(
new CustomEvent('ng-event', {
detail: {
name: NG_CUSTOM_EVENTS.SAVE_CONTENTLET,
payload: {
htmlPageReferer: '/my-awesome-page'
}
}
})
);
spectator.detectChanges();

expect(navigate).toHaveBeenCalledWith([], {
queryParams: {
url: 'my-awesome-page'
},
queryParamsHandling: 'merge'
});
});

it('should trigger a store load if the url is the same', () => {
const loadMock = jest.spyOn(store, 'load');

spectator.detectChanges();
store.initActionEdit({
inode: '123',
title: 'hello world',
type: 'shell'
});
spectator.detectChanges();

const dialogIframe = spectator.debugElement.query(
By.css('[data-testId="dialog-iframe"]')
);

spectator.triggerEventHandler(dialogIframe, 'load', {}); // There's no way we can load the iframe, because we are setting a real src and will not load

dialogIframe.nativeElement.contentWindow.document.dispatchEvent(
new CustomEvent('ng-event', {
detail: {
name: NG_CUSTOM_EVENTS.SAVE_CONTENTLET,
payload: {
htmlPageReferer: '/index'
}
}
})
);
spectator.detectChanges();

expect(loadMock).toHaveBeenCalledWith({
language_id: 1,
url: 'index',
persona_id: DEFAULT_PERSONA.identifier
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Observable, Subject } from 'rxjs';
import { Observable, Subject, fromEvent } from 'rxjs';

import { CommonModule } from '@angular/common';
import { Component, OnInit, inject, OnDestroy } from '@angular/core';
import { Component, OnInit, inject, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { ActivatedRoute, Params, Router, RouterModule } from '@angular/router';

import { ConfirmationService, MessageService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { ToastModule } from 'primeng/toast';

import { map, skip, takeUntil } from 'rxjs/operators';
Expand All @@ -17,13 +18,14 @@ import {
} from '@dotcms/data-access';
import { SiteService } from '@dotcms/dotcms-js';
import { DotPageToolUrlParams } from '@dotcms/dotcms-models';
import { SafeUrlPipe } from '@dotcms/ui';

import { EditEmaNavigationBarComponent } from './components/edit-ema-navigation-bar/edit-ema-navigation-bar.component';
import { EditEmaStore } from './store/dot-ema.store';

import { DotPageToolsSeoComponent } from '../dot-page-tools-seo/dot-page-tools-seo.component';
import { DotActionUrlService } from '../services/dot-action-url/dot-action-url.service';
import { DotPageApiService } from '../services/dot-page-api.service';
import { DotPageApiParams, DotPageApiService } from '../services/dot-page-api.service';
import { DEFAULT_LANGUAGE_ID, DEFAULT_PERSONA, DEFAULT_URL, WINDOW } from '../shared/consts';
import { NavigationBarItem } from '../shared/models';

Expand All @@ -36,7 +38,9 @@ import { NavigationBarItem } from '../shared/models';
ToastModule,
EditEmaNavigationBarComponent,
RouterModule,
DotPageToolsSeoComponent
DotPageToolsSeoComponent,
DialogModule,
SafeUrlPipe
],
providers: [
EditEmaStore,
Expand All @@ -56,21 +60,25 @@ import { NavigationBarItem } from '../shared/models';
styleUrls: ['./dot-ema-shell.component.scss']
})
export class DotEmaShellComponent implements OnInit, OnDestroy {
@ViewChild('dialogIframe') dialogIframe!: ElementRef<HTMLIFrameElement>;
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly store = inject(EditEmaStore);
private readonly siteService = inject(SiteService);
readonly store = inject(EditEmaStore);

private readonly destroy$ = new Subject<boolean>();
private queryParams: DotPageApiParams;
pageToolsVisible = false;

dialogState$ = this.store.dialogState$;

// We can internally navigate, so the PageID can change
// We need to move the logic to a function, we still need to add enterprise logic
shellProperties$: Observable<{
items: NavigationBarItem[];
seoProperties: DotPageToolUrlParams;
}> = this.store.shellProperties$.pipe(
map(({ currentUrl, pageId, host, languageId, siteId }) => ({
map(({ currentUrl, page, host, languageId, siteId }) => ({
items: [
{
icon: 'pi-file',
Expand All @@ -85,7 +93,7 @@ export class DotEmaShellComponent implements OnInit, OnDestroy {
{
icon: 'pi-sliders-h',
label: 'Rules',
href: `rules/${pageId}`
href: `rules/${page.identifier}`
},
{
iconURL: 'experiments',
Expand All @@ -102,7 +110,13 @@ export class DotEmaShellComponent implements OnInit, OnDestroy {
{
icon: 'pi-ellipsis-v',
label: 'Properties',
href: 'edit-content'
action: () => {
this.store.initActionEdit({
inode: page.inode,
title: page.title,
type: 'shell'
});
}
}
],
seoProperties: {
Expand All @@ -116,11 +130,13 @@ export class DotEmaShellComponent implements OnInit, OnDestroy {

ngOnInit(): void {
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParams: Params) => {
this.store.load({
this.queryParams = {
language_id: queryParams['language_id'] ?? DEFAULT_LANGUAGE_ID,
url: queryParams['url'] ?? DEFAULT_URL,
persona_id: queryParams['com.dotmarketing.persona.id'] ?? DEFAULT_PERSONA.identifier
});
};

this.store.load(this.queryParams);
});

// We need to skip one because it's the initial value
Expand All @@ -133,4 +149,30 @@ export class DotEmaShellComponent implements OnInit, OnDestroy {
this.destroy$.next(true);
this.destroy$.complete();
}

onIframeLoad() {
this.store.setDialogIframeLoading(false);

fromEvent(
// The events are getting sended to the document
this.dialogIframe.nativeElement.contentWindow.document,
'ng-event'
)
.pipe(takeUntil(this.destroy$))
.subscribe((event: CustomEvent) => {
if (event.detail.name === 'save-page') {
const url = event.detail.payload.htmlPageReferer.split('?')[0].replace('/', '');

this.queryParams.url !== url
? // If the url is different we need to navigate
this.router.navigate([], {
queryParams: {
url
},
queryParamsHandling: 'merge'
})
: this.store.load(this.queryParams); // If the url is the same we need to fetch the page
}
});
}
}
Loading

0 comments on commit 75c0eba

Please sign in to comment.