Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(edit-ema): #26997 Add page properties functionality #27111

Merged
merged 3 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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