Skip to content
This repository has been archived by the owner on Dec 16, 2024. It is now read-only.

Commit

Permalink
dotCMS/core#21558 Edit Template Properties Modal makes changes get lo…
Browse files Browse the repository at this point in the history
…st (#1865)

* fix: Must supply a value for form control with name: 'title'

* remove dot-template store service from Dot Template Advanced Component

* fix: Edit Template Properties Modal makes changes get lost on advance template

* fix: Edit Template Properties Modal makes changes get lost on design template

* update template design

* clean up

* feedback

* clean up

* fix dot-template store tests

* fix tests on dot-template-create-edit

* fix dot-template-builder tests

* fix dot-template tests

* fix DotEditLayoutComponent

* clean up

* rename saveDraft to updateTemplate

* add docs

* feedback

* feedback

* clean up

* feedback

* clean up

* fix doc
  • Loading branch information
rjvelazco authored Feb 7, 2022
1 parent b138533 commit 01e78ad
Show file tree
Hide file tree
Showing 16 changed files with 740 additions and 248 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!--Advance template support was removed on commit https://github.com/dotCMS/core-web/pull/589-->
<dot-edit-layout-designer
(save)="onSave($event)"
(updateTemplate)="nextUpdateTemplate($event)"
[layout]="pageState.layout"
[title]="pageState.page.title"
[theme]="pageState.template.theme"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ActivatedRoute } from '@angular/router';
import { By } from '@angular/platform-browser';
import { Component, DebugElement, Input } from '@angular/core';
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';

import { of, throwError } from 'rxjs';
import { HttpCode, ResponseView } from '@dotcms/dotcms-js';
Expand Down Expand Up @@ -181,6 +181,28 @@ describe('DotEditLayoutComponent', () => {
expect(component.pageState).toEqual(new DotPageRender(mockDotRenderedPage()));
});

it('should save the layout after 10000', fakeAsync(() => {
const res: DotPageRender = new DotPageRender(mockDotRenderedPage());
spyOn(dotPageLayoutService, 'save').and.returnValue(of(res));

layoutDesignerDe.triggerEventHandler('updateTemplate', fakeLayout);

tick(10000);
expect(dotGlobalMessageService.loading).toHaveBeenCalledWith('Saving');
expect(dotGlobalMessageService.success).toHaveBeenCalledWith('Saved');
expect(dotGlobalMessageService.error).not.toHaveBeenCalled();

expect(dotPageLayoutService.save).toHaveBeenCalledWith('123', {
...fakeLayout,
title: null
});
expect(dotTemplateContainersCacheService.set).toHaveBeenCalledWith({
'/default/': processedContainers[0].container,
'/banner/': processedContainers[1].container
});
expect(component.pageState).toEqual(new DotPageRender(mockDotRenderedPage()));
}));

it('should handle error when save fail', (done) => {
spyOn(dotPageLayoutService, 'save').and.returnValue(
throwError(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { pluck, filter, take } from 'rxjs/operators';
import { Component, HostBinding, OnInit } from '@angular/core';
import { pluck, filter, take, debounceTime, switchMap, takeUntil } from 'rxjs/operators';
import { Component, HostBinding, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DotPageRenderState } from '../../shared/models/dot-rendered-page-state.model';
import { DotRouterService } from '@services/dot-router/dot-router.service';
Expand All @@ -14,16 +14,20 @@ import { DotLayout } from '@models/dot-edit-layout-designer';
import { DotHttpErrorManagerService } from '@services/dot-http-error-manager/dot-http-error-manager.service';
import { DotEditLayoutService } from '@services/dot-edit-layout/dot-edit-layout.service';
import { HttpErrorResponse } from '@angular/common/http';
import { Subject } from 'rxjs';

@Component({
selector: 'dot-edit-layout',
templateUrl: './dot-edit-layout.component.html',
styleUrls: ['./dot-edit-layout.component.scss']
})
export class DotEditLayoutComponent implements OnInit {
export class DotEditLayoutComponent implements OnInit, OnDestroy {
pageState: DotPageRender | DotPageRenderState;
apiLink: string;

updateTemplate = new Subject<DotLayout>();
destroy$: Subject<boolean> = new Subject<boolean>();

@HostBinding('style.minWidth') width = '100%';

constructor(
Expand All @@ -50,9 +54,15 @@ export class DotEditLayoutComponent implements OnInit {
this.templateContainersCacheService.set(mappedContainers);
});

this.saveTemplateDebounce();
this.apiLink = `api/v1/page/render${this.pageState.page.pageURI}?language_id=${this.pageState.page.languageId}`;
}

ngOnDestroy() {
this.destroy$.next(true);
this.destroy$.complete();
}

/**
* Handle cancel in layout event
*
Expand All @@ -76,31 +86,101 @@ export class DotEditLayoutComponent implements OnInit {
);

this.dotPageLayoutService
.save(this.pageState.page.identifier, {
...value,
// To save a layout and no a template the title should be null
title: null
})
// To save a layout and no a template the title should be null
.save(this.pageState.page.identifier, { ...value, title: null })
.pipe(take(1))
.subscribe(
(updatedPage: DotPageRender) => {
const mappedContainers = this.getRemappedContainers(updatedPage.containers);
this.templateContainersCacheService.set(mappedContainers);
(updatedPage: DotPageRender) => this.handleSuccessSaveTemplate(updatedPage),
(err: ResponseView) => this.handleErrorSaveTemplate(err),
() => this.canRouteBeDesativated(true)
);
}

/**
* Handle next template value;
*
* @param {DotLayout} value
* @memberof DotEditLayoutComponent
*/
nextUpdateTemplate(value: DotLayout) {
this.canRouteBeDesativated(false);
this.updateTemplate.next(value);
}

this.dotGlobalMessageService.success(
this.dotMessageService.get('dot.common.message.saved')
/**
* Save template changes after 10 seconds
*
* @private
* @memberof DotEditLayoutComponent
*/
private saveTemplateDebounce() {
// The reason why we are using a Subject [updateTemplate] here is
// because we can not just simply add a debounceTime to the HTTP Request
// we need to reset the time everytime the observable is called.
// More Information Here:
// - https://stackoverflow.com/questions/35991867/angular-2-using-observable-debounce-with-http-get
// - https://blog.bitsrc.io/3-ways-to-debounce-http-requests-in-angular-c407eb165ada
this.updateTemplate
.pipe(
takeUntil(this.destroy$),
debounceTime(10000),
switchMap((layout: DotLayout) => {
this.dotGlobalMessageService.loading(
this.dotMessageService.get('dot.common.message.saving')
);
this.pageState = updatedPage;
},
(err: ResponseView) => {
this.dotGlobalMessageService.error(err.response.statusText);
this.dotHttpErrorManagerService.handle( new HttpErrorResponse(err.response) ).subscribe(() => {
this.dotEditLayoutService.changeDesactivateState(true);

return this.dotPageLayoutService.save(this.pageState.page.identifier, {
...layout,
title: null
});
}
})
)
.subscribe(
(updatedPage: DotPageRender) => this.handleSuccessSaveTemplate(updatedPage),
(err: ResponseView) => this.handleErrorSaveTemplate(err),
// On Complete
() => this.canRouteBeDesativated(true)
);
}

/**
*
* Handle Success on Save template
* @param {DotPageRender} updatedPage
* @memberof DotEditLayoutComponent
*/
private handleSuccessSaveTemplate(updatedPage: DotPageRender) {
const mappedContainers = this.getRemappedContainers(updatedPage.containers);
this.templateContainersCacheService.set(mappedContainers);

this.dotGlobalMessageService.success(
this.dotMessageService.get('dot.common.message.saved')
);
this.pageState = updatedPage;
}

/**
*
* Handle Error on Save template
* @param {ResponseView} err
* @memberof DotEditLayoutComponent
*/
private handleErrorSaveTemplate(err: ResponseView) {
this.dotGlobalMessageService.error(err.response.statusText);
this.dotHttpErrorManagerService.handle(new HttpErrorResponse(err.response)).subscribe();
}

/**
* Let the user leave the route only when changes have been saved.
*
* @private
* @param {boolean} value
* @memberof DotEditLayoutComponent
*/
private canRouteBeDesativated(value: boolean): void {
this.dotEditLayoutService.changeDesactivateState(value);
}

private getRemappedContainers(containers: {
[key: string]: {
container: DotContainer;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<dot-portlet-base>
<dot-portlet-toolbar [actions]="actions$ | async">
<dot-portlet-toolbar *ngIf="actions" [actions]="actions">
<dot-global-message right></dot-global-message>
</dot-portlet-toolbar>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@ import {
ReactiveFormsModule
} from '@angular/forms';

import { of } from 'rxjs';

import { DotMessageService } from '@services/dot-message/dot-messages.service';
import { MockDotMessageService } from '@tests/dot-message-service.mock';
import { DotTemplateStore, EMPTY_TEMPLATE_ADVANCED } from '../store/dot-template.store';
import { DotTemplateAdvancedComponent } from './dot-template-advanced.component';

@Component({
Expand Down Expand Up @@ -118,21 +115,7 @@ describe('DotTemplateAdvancedComponent', () => {
});

beforeEach(() => {
const storeMock = jasmine.createSpyObj(
'DotTemplateStore',
['createTemplate', 'goToTemplateList'],
{
vm$: of({
original: {
...EMPTY_TEMPLATE_ADVANCED,
body: '<h1>Hello</h1>'
}
}),
didTemplateChanged$: of(false)
}
);

TestBed.overrideProvider(DotTemplateStore, { useValue: storeMock });
fixture = TestBed.createComponent(DotTemplateAdvancedComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
Expand Down Expand Up @@ -186,6 +169,14 @@ describe('DotTemplateAdvancedComponent', () => {
});

describe('events', () => {

it('should emit updateTemplate event when the form changes', () => {
const updateTemplate = spyOn(component.updateTemplate, 'emit');
component.form.get('body').setValue('<body></body>');

expect<any>(updateTemplate).toHaveBeenCalledWith({ body: '<body></body>' });
});

it('should have form and fields', () => {
spyOn(Date, 'now').and.returnValue(1111111);
const container = de.query(By.css('dot-container-selector'));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, OnDestroy, OnInit, Output, Input, SimpleChanges, OnChanges } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

import { Observable, Subject } from 'rxjs';
import { filter, map, take, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { DotContainer } from '@shared/models/container/dot-container.model';
import { DotTemplateItem, DotTemplateStore } from '../store/dot-template.store';
import { DotTemplateItem } from '../store/dot-template.store';
import { DotPortletToolbarActions } from '@models/dot-portlet-toolbar.model/dot-portlet-toolbar-actions.model';
import { DotMessageService } from '@services/dot-message/dot-messages.service';

Expand All @@ -14,47 +14,39 @@ import { DotMessageService } from '@services/dot-message/dot-messages.service';
templateUrl: './dot-template-advanced.component.html',
styleUrls: ['./dot-template-advanced.scss']
})
export class DotTemplateAdvancedComponent implements OnInit, OnDestroy {
export class DotTemplateAdvancedComponent implements OnInit, OnDestroy, OnChanges {
@Output() updateTemplate = new EventEmitter<DotTemplateItem>();
@Output() save = new EventEmitter<DotTemplateItem>();
@Output() cancel = new EventEmitter();

@Input() body: string;
@Input() didTemplateChanged: boolean;

// `any` because the type of the editor in the ngx-monaco-editor package is not typed
editor: any;
form: FormGroup;
actions$: Observable<DotPortletToolbarActions>;
actions: DotPortletToolbarActions;
private destroy$: Subject<boolean> = new Subject<boolean>();

constructor(
private store: DotTemplateStore,
private fb: FormBuilder,
private dotMessageService: DotMessageService
) {}

ngOnInit(): void {
this.store.vm$.pipe(take(1)).subscribe(({ original }) => {
if (original.type === 'advanced') {
this.form = this.fb.group({
title: original.title,
body: original.body,
identifier: original.identifier,
friendlyName: original.friendlyName
});
}
});
this.form = this.fb.group({ body: this.body });

this.form
.get('body')
.valueChanges.pipe(
takeUntil(this.destroy$),
filter((body: string) => body !== undefined)
)
.subscribe((body: string) => {
this.store.updateBody(body);
});

this.actions$ = this.store.didTemplateChanged$.pipe(
map((templateChange: boolean) => this.getActions(!templateChange))
);
.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe(() => this.updateTemplate.emit(this.form.value));

this.actions = this.getActions(!this.didTemplateChanged);
}

ngOnChanges(changes: SimpleChanges){
if( changes.didTemplateChanged ) {
this.actions = this.getActions(!changes.didTemplateChanged.currentValue);
}
}

ngOnDestroy(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
>
<ng-container *ngIf="item.type === 'advanced'; else elseBlock">
<dot-template-advanced
[didTemplateChanged]="didTemplateChanged"
[body]="item.body"
(updateTemplate)="updateTemplate.emit($event)"
(save)="save.emit($event)"
(cancel)="cancel.emit()"
></dot-template-advanced>
Expand All @@ -13,6 +16,7 @@
<dot-edit-layout-designer
[theme]="item.theme"
[layout]="item.layout"
(updateTemplate)="updateTemplate.emit($event)"
(save)="save.emit($event)"
></dot-edit-layout-designer>
</ng-template>
Expand Down
Loading

0 comments on commit 01e78ad

Please sign in to comment.