Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b54ba7b
#26696 adding a jsp to render a custom field
jdotcms Nov 16, 2023
925994c
#26696 adding a mapping to the jsp
jdotcms Nov 16, 2023
0d86323
#26696 restricting the access only for BE logged users
jdotcms Nov 16, 2023
0f35b8a
#26696 just some doc
jdotcms Nov 16, 2023
e02307c
#26696 fixing a sintax error
jdotcms Nov 20, 2023
1809f62
Added custom field to edit-content fields. Now renders existing fields
KevinDavilaDotCMS Nov 22, 2023
d4bb4e2
#26696 removing unnecessary code
jdotcms Nov 22, 2023
3ec1fc2
Merge branch 'issue-26696-legacy-custom-field' of github.com:dotCMS/c…
jdotcms Nov 22, 2023
e80e2f2
#26696 removing unnecessary code
jdotcms Nov 22, 2023
885fc09
#26696 removing unnecessary code
jdotcms Nov 22, 2023
4c1b844
Working on Custom Field
KevinDavilaDotCMS Nov 22, 2023
452ebe5
Merge branch 'issue-26696-legacy-custom-field' of https://github.com/…
KevinDavilaDotCMS Nov 22, 2023
af30881
Merged with master
KevinDavilaDotCMS Nov 22, 2023
f12757a
Added Tests on Custom Field
KevinDavilaDotCMS Nov 22, 2023
1846920
Changed import reference
KevinDavilaDotCMS Nov 22, 2023
4b20743
Added reference to contentType for edit page.
KevinDavilaDotCMS Nov 27, 2023
feaed6e
Fixed suggestions on PR. Removed comments
KevinDavilaDotCMS Nov 28, 2023
ce72aea
Added TurnOnFullscreen logic. Fixed PR suggestions
KevinDavilaDotCMS Nov 28, 2023
2fd71f3
Resolved conflicts
KevinDavilaDotCMS Nov 28, 2023
5f4806e
Added CustomField to Fields module
KevinDavilaDotCMS Nov 29, 2023
9d29387
Changed jasmine reference for jest on DotEditContentField test
KevinDavilaDotCMS Nov 29, 2023
0840d03
Unify turnOn and turnOff fullscreen events in toggleFullscreen event
KevinDavilaDotCMS Nov 30, 2023
479f886
Added new mechanism on Field Types Test. Removed nested syntax on cus…
KevinDavilaDotCMS Dec 1, 2023
296c69d
Added new logic to create DotEditContent Fields TestBeds
KevinDavilaDotCMS Dec 4, 2023
0b964e5
Changed DotIcon to DotButton
KevinDavilaDotCMS Dec 4, 2023
bf1bf0b
Changed TestBed on DotEditContentField and DotEditContentCustomField
KevinDavilaDotCMS Dec 5, 2023
fc46f9f
Merged with master. Resolved conflicted to PR
KevinDavilaDotCMS Dec 5, 2023
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
Expand Up @@ -60,6 +60,11 @@
[field]="field"
[attr.data-testId]="'field-' + field.variable" />

<dot-edit-content-custom-field
*ngSwitchCase="fieldTypes.CUSTOM_FIELD"
[field]="field"
[attr.data-testId]="'field-' + field.variable" />

<dot-block-editor
*ngSwitchCase="fieldTypes.BLOCK_EDITOR"
[formControlName]="field.variable"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { describe } from '@jest/globals';
import { mockProvider } from '@ngneat/spectator';
import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { byTestId, createComponentFactory, Spectator, mockProvider } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';

import { HttpClientTestingModule } from '@angular/common/http/testing';
import { Type } from '@angular/core';
import { FactoryProvider, Type } from '@angular/core';
import { ControlContainer, FormGroupDirective } from '@angular/forms';
import { By } from '@angular/platform-browser';

Expand All @@ -17,6 +16,7 @@ import { DotEditContentFieldComponent } from './dot-edit-content-field.component
import { DotEditContentBinaryFieldComponent } from '../../fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component';
import { DotEditContentCalendarFieldComponent } from '../../fields/dot-edit-content-calendar-field/dot-edit-content-calendar-field.component';
import { DotEditContentCheckboxFieldComponent } from '../../fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component';
import { DotEditContentCustomFieldComponent } from '../../fields/dot-edit-content-custom-field/dot-edit-content-custom-field.component';
import { DotEditContentJsonFieldComponent } from '../../fields/dot-edit-content-json-field/dot-edit-content-json-field.component';
import { DotEditContentMultiSelectFieldComponent } from '../../fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component';
import { DotEditContentRadioFieldComponent } from '../../fields/dot-edit-content-radio-field/dot-edit-content-radio-field.component';
Expand All @@ -25,6 +25,7 @@ import { DotEditContentTagFieldComponent } from '../../fields/dot-edit-content-t
import { DotEditContentTextAreaComponent } from '../../fields/dot-edit-content-text-area/dot-edit-content-text-area.component';
import { DotEditContentTextFieldComponent } from '../../fields/dot-edit-content-text-field/dot-edit-content-text-field.component';
import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum';
import { DotEditContentService } from '../../services/dot-edit-content.service';
import {
createFormGroupDirectiveMock,
FIELDS_MOCK,
Expand Down Expand Up @@ -52,7 +53,11 @@ const dotMessageServiceMock = {

// This holds the mapping between the field type and the component that should be used to render it.
// We need to hold this record here, because for some reason the references just fall to undefined.
const FIELD_TYPES_COMPONENTS: Record<FIELD_TYPES, Type<unknown>> = {
const FIELD_TYPES_COMPONENTS: Record<
FIELD_TYPES,
| Type<unknown>
| { component: Type<unknown>; providers?: FactoryProvider[]; declarations?: Type<unknown>[] }
> = {
// We had to use unknown because components have different types.
[FIELD_TYPES.TEXT]: DotEditContentTextFieldComponent,
[FIELD_TYPES.TEXTAREA]: DotEditContentTextAreaComponent,
Expand All @@ -64,9 +69,16 @@ const FIELD_TYPES_COMPONENTS: Record<FIELD_TYPES, Type<unknown>> = {
[FIELD_TYPES.TAG]: DotEditContentTagFieldComponent,
[FIELD_TYPES.CHECKBOX]: DotEditContentCheckboxFieldComponent,
[FIELD_TYPES.MULTI_SELECT]: DotEditContentMultiSelectFieldComponent,
[FIELD_TYPES.BINARY]: DotEditContentBinaryFieldComponent,
[FIELD_TYPES.BLOCK_EDITOR]: DotBlockEditorComponent,
[FIELD_TYPES.JSON]: DotEditContentJsonFieldComponent
[FIELD_TYPES.CUSTOM_FIELD]: {
component: DotEditContentCustomFieldComponent,
providers: [mockProvider(DotEditContentService)]
},
[FIELD_TYPES.BINARY]: DotEditContentBinaryFieldComponent,
[FIELD_TYPES.JSON]: {
component: DotEditContentJsonFieldComponent,
declarations: [MockComponent(DotEditContentJsonFieldComponent)]
}
Comment on lines +78 to +81
Copy link
Member

Choose a reason for hiding this comment

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

Why do you pass the component and then mock it?

Copy link
Contributor

Choose a reason for hiding this comment

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

cc: @rjvelazco you have more context of this case

};

describe('FIELD_TYPES and FIELDS_MOCK', () => {
Expand All @@ -80,19 +92,19 @@ describe('FIELD_TYPES and FIELDS_MOCK', () => {
});

describe.each([...FIELDS_MOCK])('DotEditContentFieldComponent all fields', (fieldMock) => {
const fieldTestBed = FIELD_TYPES_COMPONENTS[fieldMock.fieldType];
let spectator: Spectator<DotEditContentFieldComponent>;
const createComponent = createComponentFactory({
imports: [HttpClientTestingModule],
// Avoid mocking Monaco Editor - It's too complex to mock
declarations: [MockComponent(DotEditContentJsonFieldComponent)],
declarations: [...(fieldTestBed?.declarations || [])],
component: DotEditContentFieldComponent,
componentViewProviders: [
{
provide: ControlContainer,
useValue: createFormGroupDirectiveMock()
}
],
providers: [FormGroupDirective]
providers: [FormGroupDirective, DotEditContentService, ...(fieldTestBed?.providers || [])]
});

beforeEach(async () => {
Expand Down Expand Up @@ -133,10 +145,8 @@ describe.each([...FIELDS_MOCK])('DotEditContentFieldComponent all fields', (fiel
const field = spectator.debugElement.query(
By.css(`[data-testId="field-${fieldMock.variable}"]`)
);

expect(
field.componentInstance instanceof FIELD_TYPES_COMPONENTS[fieldMock.fieldType]
).toBeTruthy();
const FIELD_TYPE = fieldTestBed.component ? fieldTestBed.component : fieldTestBed;
expect(field.componentInstance instanceof FIELD_TYPE).toBeTruthy();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
padding-bottom: 0;
height: 100%;
overflow: auto;
position: relative;
}

.row {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EMPTY, Observable } from 'rxjs';

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { map, switchMap } from 'rxjs/operators';
Expand All @@ -19,7 +19,7 @@ import { DotEditContentService } from '../../services/dot-edit-content.service';
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [DotEditContentService]
})
export class EditContentLayoutComponent {
export class EditContentLayoutComponent implements OnInit {
private activatedRoute = inject(ActivatedRoute);

public contentType = this.activatedRoute.snapshot.params['contentType'];
Expand All @@ -33,6 +33,7 @@ export class EditContentLayoutComponent {
switchMap(({ contentType, ...contentData }) => {
if (contentType) {
this.contentType = contentType;
this.dotEditContentService.currentContentType = contentType;

return this.dotEditContentService.getContentTypeFormData(contentType).pipe(
map(({ layout, fields }) => ({
Expand All @@ -50,6 +51,12 @@ export class EditContentLayoutComponent {
.getContentTypeFormData(this.contentType)
.pipe(map(({ layout, fields }) => ({ layout, fields })));

ngOnInit() {
if (this.contentType) {
this.dotEditContentService.currentContentType = this.contentType;
}
}

/**
* Saves the contentlet with the given values.
* @param value - An object containing the key-value pairs of the contentlet to be saved.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<iframe
class="legacy-custom-field"
#iframe
[src]="src | safeUrl"
[ngClass]="{ 'legacy-custom-field--fullscreen': isFullscreen }"
[ngStyle]="!isFullscreen && { height: variables?.height, width: variables?.width }"
[title]="'Content Type ' + field.variable + ' and field ' + field.name"
(load)="onIframeLoad()"
data-testId="iframe"
frameborder="0"></iframe>

<p-button
*ngIf="isFullscreen"
(click)="isFullscreen = false"
icon="pi pi-times"
styleClass="p-button-rounded p-button-text" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@use "variables" as *;

iframe {
display: block;
width: 100%;
}

.legacy-custom-field--fullscreen {
width: 100%;
height: 100%;
z-index: 99999;
position: absolute;
top: 0;
left: 0;
}

p-button {
position: absolute;
top: $spacing-4;
right: $spacing-9;
z-index: 99999;
cursor: pointer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Spectator, createComponentFactory } from '@ngneat/spectator/jest';

import { ControlContainer, FormControl, FormGroup, FormGroupDirective } from '@angular/forms';
import { By } from '@angular/platform-browser';

import { DotEditContentCustomFieldComponent } from './dot-edit-content-custom-field.component';

import { DotEditContentService } from '../../services/dot-edit-content.service';
import { CUSTOM_FIELD_MOCK, createFormGroupDirectiveMock } from '../../utils/mocks';

describe('DotEditContentCustomFieldComponent', () => {
let spectator: Spectator<DotEditContentCustomFieldComponent>;

const FAKE_FORM_GROUP = new FormGroup({
custom: new FormControl()
});

const createComponent = createComponentFactory({
component: DotEditContentCustomFieldComponent,
detectChanges: false,
componentViewProviders: [
{ provide: ControlContainer, useValue: createFormGroupDirectiveMock(FAKE_FORM_GROUP) }
],
providers: [
FormGroupDirective,
{
provide: DotEditContentService,
useValue: {
currentContentType: 'test'
}
}
]
});

beforeEach(() => {
spectator = createComponent();
});

it('should have a valid iframe src', () => {
spectator.setInput('field', CUSTOM_FIELD_MOCK);
spectator.detectChanges();
expect(spectator.component.src).toBe(
`/html/legacy_custom_field/legacy-custom-field.jsp?variable=test&field=${CUSTOM_FIELD_MOCK.variable}`
);
});

it('should set the iframe contentWindow form property correctly on iframe load', () => {
spectator.setInput('field', CUSTOM_FIELD_MOCK);
spectator.component.onIframeLoad();
spectator.detectChanges();
expect(spectator.component.iframe.nativeElement.contentWindow['form']).toEqual(
FAKE_FORM_GROUP
);
});

it('should the component receive iframe turnOnFullscreen info', () => {
spectator.setInput('field', CUSTOM_FIELD_MOCK);
spectator.detectChanges();
const iframe = spectator.debugElement.query(By.css('[data-testId="iframe"]'));
const onMessageFromCustomField = jest.spyOn(
spectator.component,
'onMessageFromCustomField'
);

iframe.nativeElement.contentWindow.parent.dispatchEvent(
new MessageEvent('message', {
origin: 'http://localhost:3000',
data: { type: 'toggleFullscreen' }
})
);

expect(onMessageFromCustomField).toHaveBeenCalled();
expect(spectator.component.isFullscreen).toBe(true);
});

it('should the iframe get the form reference from component', () => {
spectator.setInput('field', CUSTOM_FIELD_MOCK);
spectator.component.onIframeLoad();
spectator.detectChanges();
spectator.component.form.get('custom').setValue('A text');

const iframe = spectator.debugElement.query(By.css('[data-testId="iframe"]'));
expect(iframe.nativeElement.contentWindow['form'].get('custom').value).toBe('A text');
});

it('should the component form be modified from iframe', () => {
spectator.setInput('field', CUSTOM_FIELD_MOCK);
spectator.component.onIframeLoad();
spectator.detectChanges();

const iframe = spectator.debugElement.query(By.css('[data-testId="iframe"]'));
iframe.nativeElement.contentWindow['form'].get('custom').setValue('Other text');

expect(spectator.component.form.get('custom').value).toBe('Other text');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { NgClass, NgIf, NgStyle } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
HostListener,
Input,
NgZone,
OnInit,
ViewChild,
inject
} from '@angular/core';
import { ControlContainer, FormGroupDirective } from '@angular/forms';

import { ButtonModule } from 'primeng/button';

import { DotCMSContentTypeField } from '@dotcms/dotcms-models';
import { DotIconModule, SafeUrlPipe } from '@dotcms/ui';

import { DotEditContentService } from '../../services/dot-edit-content.service';

@Component({
selector: 'dot-edit-content-custom-field',
standalone: true,
imports: [SafeUrlPipe, NgStyle, NgClass, DotIconModule, NgIf, ButtonModule],
templateUrl: './dot-edit-content-custom-field.component.html',
styleUrls: ['./dot-edit-content-custom-field.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotEditContentCustomFieldComponent implements OnInit {
@Input() field!: DotCMSContentTypeField;

@ViewChild('iframe') iframe!: ElementRef<HTMLIFrameElement>;

private controlContainer = inject(ControlContainer);
private editContentService = inject(DotEditContentService);
private zone = inject(NgZone);

private contentType = this.editContentService.currentContentType;
variables!: { [key: string]: string };
isFullscreen = false;
src!: string;

ngOnInit() {
this.src = `/html/legacy_custom_field/legacy-custom-field.jsp?variable=${this.contentType}&field=${this.field.variable}`;
this.variables = this.field.fieldVariables.reduce((result, item) => {
result[item.key] = item.value;

return result;
}, {});
Comment on lines +46 to +50
Copy link
Contributor

Choose a reason for hiding this comment

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

We are using this in multiple places. Should we create a global util?

}

/**
* Handles the message received from the custom field.
* @param event The message event containing the data.
*/
@HostListener('window:message', ['$event'])
onMessageFromCustomField(event: MessageEvent) {
if (event.data.type === 'toggleFullscreen') {
this.isFullscreen = !this.isFullscreen;
}
}

/**
* Event handler for when the iframe has finished loading.
* Sets the form property of the iframe's content window.
*/
onIframeLoad() {
const iframeWindow = this.iframe.nativeElement.contentWindow as Window;
iframeWindow['form'] = this.zone.run(() => this.form);
}

/**
* Get the form control associated with the custom field component.
* @returns The form group.
*/
get form() {
return (this.controlContainer as FormGroupDirective).form;
}
}
Loading