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

dotCMS/core#21558 Edit Template Properties Modal makes changes get lost #1865

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,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 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does your vscode is not running the formatter on save?

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