Skip to content

Commit 83b3190

Browse files
authored
refactor(edit-content): add Language Variables to JSON Field, refactor to use DotEditContentMonacoEditorControl (dotCMS#32110)
This commit refactors the DotEditContentJsonFieldComponent to utilize the new DotEditContentMonacoEditorControl for JSON editing. Key changes include: - Updated the HTML template to replace ngx-monaco-editor with dot-edit-content-monaco-editor-control. - Adjusted SCSS styles to accommodate the new editor control. - Modified the component's TypeScript file to remove unnecessary imports and streamline the code. - Enhanced the test suite to validate the integration of the new editor control and its properties. This change improves the maintainability and functionality of the JSON field editor. ### Changes: This pull request introduces significant updates to the JSON field editor component and its integration with the Monaco editor. The changes focus on enhancing functionality, improving test coverage, and refactoring the codebase for better maintainability. Key updates include replacing the direct usage of `ngx-monaco-editor` with a custom wrapper component, adding support for forcing specific languages in the Monaco editor, and improving the test structure for the JSON field editor. ### JSON Field Editor Enhancements: * Replaced the `ngx-monaco-editor` with a custom wrapper component, `DotEditContentMonacoEditorControlComponent`, to provide more control over the editor's behavior and customization. [[1]](diffhunk://#diff-b8aa90bf87b74a086b7d91cd7a45b06022453dba3f26a7fd751a7fd2aacb2f42L1-R17) [[2]](diffhunk://#diff-cb3c5e92c52c399050b6321834f69fe6125ce05b964f3fdd5425d7ae0e623026L1-R68) * Introduced a language variable selector (`DotLanguageVariableSelectorComponent`) for inserting language variables into the JSON editor. [[1]](diffhunk://#diff-b8aa90bf87b74a086b7d91cd7a45b06022453dba3f26a7fd751a7fd2aacb2f42L1-R17) [[2]](diffhunk://#diff-cb3c5e92c52c399050b6321834f69fe6125ce05b964f3fdd5425d7ae0e623026L1-R68) * Added support for forcing specific languages in the Monaco editor using the new `$forcedLanguage` input property. [[1]](diffhunk://#diff-c1909062ea002433d379181fe62a87bf3b509cfac047b67debda6576920f5b07R91-R96) [[2]](diffhunk://#diff-a5d0c8034761266780cde3f0981d0b7575af0f8e45fa756fabf8d14509f11b68R66-R85) ### Test and Mock Updates: * Updated test cases for the JSON field editor to validate the rendering of new components (`DotLanguageVariableSelectorComponent`, `DotEditContentMonacoEditorControlComponent`) and their interactions. * Added new test cases for the Monaco editor wrapper component to ensure the correct application of forced languages and custom properties. [[1]](diffhunk://#diff-a5d0c8034761266780cde3f0981d0b7575af0f8e45fa756fabf8d14509f11b68R66-R85) [[2]](diffhunk://#diff-a5d0c8034761266780cde3f0981d0b7575af0f8e45fa756fabf8d14509f11b68L79-R100) ### Styling and Layout Improvements: * Refactored the JSON field editor's layout to include separate containers for controls and the editor, improving the UI structure and maintainability. ### Codebase Refactoring: * Removed redundant imports and dependencies related to `ngx-monaco-editor` and replaced them with the custom wrapper component. [[1]](diffhunk://#diff-cb3c5e92c52c399050b6321834f69fe6125ce05b964f3fdd5425d7ae0e623026L1-R68) [[2]](diffhunk://#diff-7de4eab63be6786869cc64142ef14ec11998fe5d7aa9bfd3c9a38979ebbd5216L1-R84) * Added a new `Json` option to the `AvailableLanguageMonaco` enum for explicit JSON language support. These changes collectively improve the functionality, usability, and maintainability of the JSON field editor component. ### Checklist - [x] Tests - [ ] Translations - [ ] Security Implications Contemplated (add notes if applicable) ### Additional Info ** any additional useful context or info ** ### Screenshots ![CleanShot 2025-05-07 at 10 18 00@2x](https://github.com/user-attachments/assets/645c5332-1a7a-4e7e-8622-c7e3edd0a0b4)
1 parent d7f967a commit 83b3190

File tree

13 files changed

+263
-195
lines changed

13 files changed

+263
-195
lines changed

core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/resolvers/dot-template-create-edit.resolver.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { TestBed } from '@angular/core/testing';
77
import { DotTemplatesService } from '@dotcms/app/api/services/dot-templates/dot-templates.service';
88
import { DotRouterService } from '@dotcms/data-access';
99
import { DotTemplate } from '@dotcms/dotcms-models';
10-
import { MockDotRouterService } from '@dotcms/utils-testing';
10+
import { MockDotRouterService, setupResizeObserverMock } from '@dotcms/utils-testing';
1111

1212
import { DotTemplateCreateEditResolver } from './dot-template-create-edit.resolver';
1313

14+
// Setup ResizeObserver mock
15+
setupResizeObserverMock();
16+
1417
const templateMock: DotTemplate = {
1518
anonymous: false,
1619
friendlyName: 'Published template',

core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe } from '@jest/globals';
2+
import { MonacoEditorModule, MonacoEditorLoaderService } from '@materia-ui/ngx-monaco-editor';
23
import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
34
import { EditorComponent } from '@tinymce/tinymce-angular';
45
import { MockComponent } from 'ng-mocks';
@@ -7,7 +8,7 @@ import { of } from 'rxjs';
78
import { provideHttpClient } from '@angular/common/http';
89
import { provideHttpClientTesting } from '@angular/common/http/testing';
910
import { Provider, signal, Type } from '@angular/core';
10-
import { ControlContainer, FormGroupDirective } from '@angular/forms';
11+
import { ControlContainer, FormGroupDirective, ReactiveFormsModule } from '@angular/forms';
1112
import { By } from '@angular/platform-browser';
1213

1314
import { BlockEditorModule, DotBlockEditorComponent } from '@dotcms/block-editor';
@@ -18,7 +19,8 @@ import {
1819
DotMessageService,
1920
DotWorkflowActionsFireService
2021
} from '@dotcms/data-access';
21-
import { DotKeyValueComponent } from '@dotcms/ui';
22+
import { DotKeyValueComponent, DotLanguageVariableSelectorComponent } from '@dotcms/ui';
23+
import { monacoMock } from '@dotcms/utils-testing';
2224

2325
import { DotEditContentFieldComponent } from './dot-edit-content-field.component';
2426

@@ -42,6 +44,7 @@ import { DotEditContentTextFieldComponent } from '../../fields/dot-edit-content-
4244
import { DotEditContentWYSIWYGFieldComponent } from '../../fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component';
4345
import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum';
4446
import { DotEditContentService } from '../../services/dot-edit-content.service';
47+
import { DotEditContentMonacoEditorControlComponent } from '../../shared/dot-edit-content-monaco-editor-control/dot-edit-content-monaco-editor-control.component';
4548
import { DotEditContentStore } from '../../store/edit-content.store';
4649
import {
4750
BINARY_FIELD_CONTENTLET,
@@ -70,6 +73,9 @@ declare module '@tiptap/core' {
7073
}
7174
}
7275

76+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
77+
(global as any).monaco = monacoMock;
78+
7379
// This holds the mapping between the field type and the component that should be used to render it.
7480
// We need to hold this record here, because for some reason the references just fall to undefined.
7581
const FIELD_TYPES_COMPONENTS: Record<FIELD_TYPES, Type<unknown> | DotEditFieldTestBed> = {
@@ -157,7 +163,15 @@ const FIELD_TYPES_COMPONENTS: Record<FIELD_TYPES, Type<unknown> | DotEditFieldTe
157163
},
158164
[FIELD_TYPES.JSON]: {
159165
component: DotEditContentJsonFieldComponent,
160-
declarations: [MockComponent(DotEditContentJsonFieldComponent)]
166+
imports: [ReactiveFormsModule, MonacoEditorModule],
167+
providers: [
168+
mockProvider(DotMessageDisplayService),
169+
{ provide: MonacoEditorLoaderService, useValue: { isMonacoLoaded$: of(true) } }
170+
],
171+
declarations: [
172+
MockComponent(DotLanguageVariableSelectorComponent),
173+
MockComponent(DotEditContentMonacoEditorControlComponent)
174+
]
161175
},
162176
[FIELD_TYPES.KEY_VALUE]: {
163177
component: DotEditContentKeyValueComponent,
Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1-
<ngx-monaco-editor
2-
[formControlName]="contentTypeField().variable"
3-
[options]="monacoEditorOptions()"></ngx-monaco-editor>
1+
<div
2+
class="dot-json-field__container flex flex-column gap-2 h-full w-full"
3+
data-testid="json-field-container">
4+
<div
5+
class="dot-json-field__controls flex justify-content-end"
6+
data-testid="json-field-controls">
7+
<dot-language-variable-selector
8+
data-testid="json-field-language-selector"
9+
(onSelectLanguageVariable)="onSelectLanguageVariable($event)" />
10+
</div>
11+
12+
<div class="dot-json-field__editor" data-testid="json-field-editor">
13+
<dot-edit-content-monaco-editor-control
14+
#monaco
15+
data-testid="json-field-monaco-editor"
16+
[field]="$field()"
17+
[forceLanguage]="languages.Json" />
18+
</div>
19+
</div>

core-web/libs/edit-content/src/lib/fields/dot-edit-content-json-field/dot-edit-content-json-field.component.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
resize: vertical;
1010
}
1111

12-
ngx-monaco-editor {
12+
dot-edit-content-monaco-editor-control {
1313
border-radius: $border-radius-md;
14-
border: $field-border-size solid $color-palette-gray-400;
1514
display: block;
1615
height: 100%;
16+
min-height: 9.375rem;
1717
overflow: auto;
1818
width: 100%;
1919
}
Lines changed: 71 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,86 @@
1-
import { MonacoEditorComponent, MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
2-
import { Spectator } from '@ngneat/spectator';
3-
import { createComponentFactory } from '@ngneat/spectator/jest';
4-
import { MockComponent } from 'ng-mocks';
5-
6-
import {
7-
ControlContainer,
8-
FormControl,
9-
FormGroup,
10-
FormGroupDirective,
11-
FormsModule,
12-
ReactiveFormsModule
13-
} from '@angular/forms';
14-
15-
import {
16-
DEFAULT_JSON_FIELD_EDITOR_CONFIG,
17-
DotEditContentJsonFieldComponent
18-
} from './dot-edit-content-json-field.component';
19-
20-
import { createFormGroupDirectiveMock, JSON_FIELD_MOCK } from '../../utils/mocks';
1+
import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator/jest';
2+
3+
import { provideHttpClient } from '@angular/common/http';
4+
import { provideHttpClientTesting } from '@angular/common/http/testing';
5+
import { ControlContainer, FormGroupDirective } from '@angular/forms';
6+
7+
import { DotLanguageVariableSelectorComponent } from '@dotcms/ui';
8+
import { monacoMock } from '@dotcms/utils-testing';
9+
10+
import { DotEditContentJsonFieldComponent } from './dot-edit-content-json-field.component';
11+
12+
import { AvailableLanguageMonaco } from '../../models/dot-edit-content-field.constant';
13+
import { DotEditContentMonacoEditorControlComponent } from '../../shared/dot-edit-content-monaco-editor-control/dot-edit-content-monaco-editor-control.component';
14+
import { JSON_FIELD_MOCK, createFormGroupDirectiveMock } from '../../utils/mocks';
15+
16+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17+
(global as any).monaco = monacoMock;
2118

2219
describe('DotEditContentJsonFieldComponent', () => {
23-
describe('test with value', () => {
24-
let spectator: Spectator<DotEditContentJsonFieldComponent>;
25-
let controlContainer: ControlContainer;
20+
let spectator: Spectator<DotEditContentJsonFieldComponent>;
21+
let component: DotEditContentJsonFieldComponent;
2622

27-
const FAKE_FORM_GROUP = new FormGroup({
28-
json: new FormControl("{ 'test': 'test' }")
29-
});
23+
const createComponent = createComponentFactory({
24+
component: DotEditContentJsonFieldComponent,
25+
componentMocks: [
26+
DotLanguageVariableSelectorComponent,
27+
DotEditContentMonacoEditorControlComponent
28+
],
29+
componentViewProviders: [
30+
{
31+
provide: ControlContainer,
32+
useValue: createFormGroupDirectiveMock()
33+
}
34+
],
35+
providers: [FormGroupDirective, provideHttpClient(), provideHttpClientTesting()]
36+
});
37+
38+
beforeEach(() => {
39+
// Limpiar cualquier llamada anterior a los mocks
40+
jest.clearAllMocks();
3041

31-
const createComponent = createComponentFactory({
32-
component: DotEditContentJsonFieldComponent,
33-
imports: [FormsModule, ReactiveFormsModule, MonacoEditorModule],
34-
declarations: [MockComponent(MonacoEditorComponent)],
35-
componentViewProviders: [
36-
{
37-
provide: ControlContainer,
38-
useValue: createFormGroupDirectiveMock(FAKE_FORM_GROUP)
39-
}
40-
],
41-
providers: [FormGroupDirective],
42+
spectator = createComponent({
4243
detectChanges: false
4344
});
45+
spectator.setInput('field', JSON_FIELD_MOCK);
46+
spectator.detectChanges();
4447

45-
beforeEach(() => {
46-
spectator = createComponent();
47-
controlContainer = spectator.inject(ControlContainer, true);
48-
spectator.setInput('field', JSON_FIELD_MOCK);
49-
spectator.detectComponentChanges();
50-
});
48+
component = spectator.component;
49+
});
5150

52-
it('should render the Monoaco Editor with Current Value', () => {
53-
const monacoEditorComponent = spectator.query(MonacoEditorComponent);
54-
expect(monacoEditorComponent).not.toBeNull();
55-
});
51+
it('should render the component container', () => {
52+
expect(spectator.query(byTestId('json-field-container'))).toBeTruthy();
53+
});
5654

57-
it('should have the form Variable as a FormControlName', () => {
58-
const element = spectator.query('ngx-monaco-editor');
59-
expect(element.getAttribute('ng-reflect-name')).toBe(JSON_FIELD_MOCK.variable);
60-
});
55+
it('should render the language variable selector', () => {
56+
const languageVariableSelector = spectator.query(DotLanguageVariableSelectorComponent);
57+
expect(languageVariableSelector).toBeTruthy();
58+
});
6159

62-
it('should have the right editor options', () => {
63-
const monacoEditorComponent = spectator.query(MonacoEditorComponent);
64-
expect(monacoEditorComponent.options).toEqual(DEFAULT_JSON_FIELD_EDITOR_CONFIG);
65-
});
60+
it('should render the editor container', () => {
61+
expect(spectator.query(byTestId('json-field-editor'))).toBeTruthy();
62+
});
6663

67-
it('should called markForCheck when the value changes', () => {
68-
const spy = jest.spyOn(spectator.component['cd'], 'markForCheck');
64+
it('should render the monaco editor component', () => {
65+
const monacoEditor = spectator.query(DotEditContentMonacoEditorControlComponent);
66+
expect(monacoEditor).toBeTruthy();
67+
});
6968

70-
controlContainer.control.get(JSON_FIELD_MOCK.variable).setValue('{ "test": "test" }');
69+
it('should pass JSON as forced language to monaco editor', () => {
70+
const monacoEditor = spectator.query(DotEditContentMonacoEditorControlComponent);
71+
expect(monacoEditor.$forcedLanguage()).toBe(AvailableLanguageMonaco.Json);
72+
});
7173

72-
expect(spy).toHaveBeenCalled();
73-
});
74+
it('should call insertLanguageVariableInMonaco when language variable is selected', () => {
75+
// Mock the insertLanguageVariableInMonaco private method
76+
const insertLanguageVariableInMonacoMock = jest.fn();
77+
component['insertLanguageVariableInMonaco'] = insertLanguageVariableInMonacoMock;
78+
79+
// Trigger onSelectLanguageVariable with a test variable
80+
const testVariable = 'test_variable';
81+
component.onSelectLanguageVariable(testVariable);
82+
83+
// Verify the mocked method was called with the correct variable
84+
expect(insertLanguageVariableInMonacoMock).toHaveBeenCalledWith(testVariable);
7485
});
7586
});

0 commit comments

Comments
 (0)