Skip to content

Commit

Permalink
feat: show Block Editor Side on Traditional (#30585)
Browse files Browse the repository at this point in the history
This pull request includes multiple changes to enhance the block editor
functionality and improve the workflow actions in the `core-web`
project. The most important changes include the addition of a new block
editor sidebar component, updates to the workflow actions service, and
modifications to the event handling and test cases.

### Enhancements to Block Editor Functionality:

* Added a new `DotBlockEditorSidebarComponent` with its HTML, SCSS, and
TypeScript files to handle the block editor sidebar functionality. This
component includes methods for saving editor changes, closing the
sidebar, and handling events.
[[1]](diffhunk://#diff-5490d402dcc3eb8dca39a3c27a2a45b3338bea2a103206240dbc0134f6a9b4ecR1-R35)
[[2]](diffhunk://#diff-bd12ae9c296a163014328299a9345bd409b8f203772fd8dc954502841871f874R1-R43)
[[3]](diffhunk://#diff-1513562a5281a737d81d7b060a1f2663f57aed0934c633f947cddf3af61d56bbR1-R170)
* Updated `edit-ema-editor.component.html` to include the new block
editor sidebar component and handle the `onSaved` event to reload the
store.

### Improvements to Workflow Actions Service:

* Modified `dot-workflow-actions-fire.service.ts` to use URLSearchParams
for appending query parameters and refactored the handling of `inode`
and `indexPolicy`.
* Updated test cases in `dot-workflow-actions-fire.service.spec.ts` to
reflect changes in the workflow actions fire service and ensure proper
handling of the `indexPolicy` parameter.
[[1]](diffhunk://#diff-edfb8c91327f7780ddeeaa0d3fd01655756ebdc77197088c6973460e0c38ddfaL185-R185)
[[2]](diffhunk://#diff-edfb8c91327f7780ddeeaa0d3fd01655756ebdc77197088c6973460e0c38ddfaL198-R197)
[[3]](diffhunk://#diff-edfb8c91327f7780ddeeaa0d3fd01655756ebdc77197088c6973460e0c38ddfaL218-R217)
[[4]](diffhunk://#diff-edfb8c91327f7780ddeeaa0d3fd01655756ebdc77197088c6973460e0c38ddfaL236-R234)

### Event Handling and Test Cases:

* Updated `dot-events.service.ts` to cast the filtered observable as
`Observable<DotEvent<T>>`.
* Added new test cases for the `DotBlockEditorSidebarComponent` to
ensure correct behavior of the sidebar, block editor inputs, save
functionality, and error handling.
* Updated `edit-ema-editor.component.spec.ts` to include the new block
editor sidebar component and handle the inline edit block editor event.
[[1]](diffhunk://#diff-34ddc5fbacaf04b962f2037385ed284310d5faf35ba409d5705b2caadd5d796aR31)
[[2]](diffhunk://#diff-34ddc5fbacaf04b962f2037385ed284310d5faf35ba409d5705b2caadd5d796aR82-R85)
[[3]](diffhunk://#diff-34ddc5fbacaf04b962f2037385ed284310d5faf35ba409d5705b2caadd5d796aL111-R119)

### Minor Changes:

* Added a method to inject the inline block editor in
`dot-edit-content-html.service.ts`.

### Video


https://github.com/user-attachments/assets/e6d59b8a-119a-4f93-87ae-44148541b86d

### Handle Error



https://github.com/user-attachments/assets/8c606cce-f562-4fca-a9eb-50c6082eb383

---------

Co-authored-by: Jalinson Diaz <[email protected]>
  • Loading branch information
rjvelazco and zJaaal authored Nov 12, 2024
1 parent 786c18d commit 184c09f
Show file tree
Hide file tree
Showing 22 changed files with 803 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ export class DotEditContentHtmlService {
}
}

// Inject Block Editor
private injectInlineBlockEditor(): void {
const doc = this.getEditPageDocument();
const editBlockEditorNodes = doc.querySelectorAll('[data-block-editor-content]');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class DotEventsService {
// TODO: need to make this method to support multiple events
return this.subject
.asObservable()
.pipe(filter((res: DotEvent<T>) => res.name === eventName));
.pipe(filter((res) => res.name === eventName)) as Observable<DotEvent<T>>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,7 @@ describe('DotWorkflowActionsFireService', () => {
const requestBody = {
contentlet: {
contentType: 'persona',
name: 'test',
indexPolicy: 'WAIT_FOR'
name: 'test'
}
};

Expand All @@ -195,7 +194,7 @@ describe('DotWorkflowActionsFireService', () => {
});

const req = spectator.expectOne(
'/api/v1/workflow/actions/default/fire/PUBLISH',
'/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR',
HttpMethod.PUT
);

Expand All @@ -215,8 +214,7 @@ describe('DotWorkflowActionsFireService', () => {
const requestBody = {
contentlet: {
contentType: 'persona',
name: 'test',
indexPolicy: 'WAIT_FOR'
name: 'test'
},
individualPermissions: { READ: ['123'], WRITE: ['456'] }
};
Expand All @@ -233,7 +231,7 @@ describe('DotWorkflowActionsFireService', () => {
});

const req = spectator.expectOne(
'/api/v1/workflow/actions/default/fire/PUBLISH',
'/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR',
HttpMethod.PUT
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,21 @@ export class DotWorkflowActionsFireService {
const bodyRequest = individualPermissions
? { contentlet, individualPermissions }
: { contentlet };
const params = new URLSearchParams({});

if (data['inode']) {
url += `?inode=${data['inode']}`;
// It's not best approach but this legacy code
if (contentlet['inode']) {
params.append('inode', contentlet['inode']);
delete contentlet['inode'];
}

if (contentlet['indexPolicy']) {
params.append('indexPolicy', contentlet['indexPolicy']);
delete contentlet['indexPolicy'];
}

if (params.toString()) {
url = `${url}?${params.toString()}`;
}

if (formData) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<p-sidebar
[blockScroll]="true"
[closeOnEscape]="false"
[dismissible]="false"
[showCloseIcon]="false"
[visible]="!!contentlet()"
data-testId="sidebar"
position="right">
<div class="container" (keydown.escape)="$event.stopPropagation()" data-testId="dot-container">
@if (contentlet(); as contentlet) {
<dot-block-editor
[field]="contentlet.field"
[value]="contentlet.content"
[languageId]="contentlet.languageId"
(valueChange)="value.set($event)"
data-testId="dot-block-editor" />
}
<footer>
<button
pButton
(click)="close()"
[label]="'Cancel' | dm"
[disabled]="loading()"
class="p-button-outlined"
data-testId="cancel-btn"></button>
<button
pButton
(click)="saveEditorChanges()"
[disabled]="!value() || loading()"
[label]="'Save' | dm"
[loading]="loading()"
data-testId="save-btn"></button>
</footer>
</div>
</p-sidebar>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@use "variables" as *;

:host {
.container {
display: flex;
flex-direction: column;
justify-content: space-between;
}

dot-block-editor {
display: flex;
flex-direction: column;
height: calc(100% - 4rem);
}
}

::ng-deep {
p-sidebar {
.p-sidebar-right {
width: 50%;
max-width: 1040px;
}

.p-sidebar-content {
.container {
height: 100%;
}
}
}
dot-block-editor .editor-wrapper {
height: 100% !important;
}
}

footer {
display: flex;
justify-content: flex-end;
gap: $spacing-3;

.p-button-secondary {
margin-right: $spacing-3;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { byTestId, Spectator } from '@ngneat/spectator';
import { createComponentFactory } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { of, throwError } from 'rxjs';

import { Sidebar } from 'primeng/sidebar';

import { BlockEditorModule, DotBlockEditorComponent } from '@dotcms/block-editor';
import {
DotAlertConfirmService,
DotContentTypeService,
DotMessageService,
DotWorkflowActionsFireService
} from '@dotcms/data-access';
import { DotCMSContentType } from '@dotcms/dotcms-models';
import {
dotcmsContentTypeBasicMock,
MockDotMessageService,
mockResponseView
} from '@dotcms/utils-testing';

import { DotBlockEditorSidebarComponent } from './dot-block-editor-sidebar.component';

const BLOCK_EDITOR_FIELD = {
clazz: 'com.dotcms.contenttype.model.field.ImmutableStoryBlockField',
contentTypeId: '799f176a-d32e-4844-a07c-1b5fcd107578',
dataType: 'LONG_TEXT',
fieldType: 'Story-Block',
fieldTypeLabel: 'Block Editor',
fixed: false,
iDate: 1649791703000,
id: '71fe962eb681c5ffd6cd1623e5fc575a',
indexed: false,
listed: false,
hint: 'A helper text',
modDate: 1699364930000,
name: 'Blog Content',
readOnly: false,
required: false,
searchable: false,
sortOrder: 13,
unique: false,
variable: 'testName',
fieldVariables: [
{
clazz: 'com.dotcms.contenttype.model.field.ImmutableFieldVariable',
fieldId: '71fe962eb681c5ffd6cd1623e5fc575a',
id: 'b19e1d5d-47ad-40d7-b2bf-ccd0a5a86590',
key: 'allowedBlocks',
value: 'heading1'
},
{
clazz: 'com.dotcms.contenttype.model.field.ImmutableFieldVariable',
fieldId: '71fe962eb681c5ffd6cd1623e5fc575a',
id: 'b19e1d5d-47ad-40d7-b2bf-ccd0a5a86590',
key: 'allowedContentTypes',
value: 'Activity'
},
{
clazz: 'com.dotcms.contenttype.model.field.ImmutableFieldVariable',
fieldId: '71fe962eb681c5ffd6cd1623e5fc575a',
id: 'b19e1d5d-47ad-40d7-b2bf-ccd0a5a86590',
key: 'styles',
value: 'height:50%'
}
]
};

const messageServiceMock = new MockDotMessageService({
'editpage.inline.error': 'An error occurred',
error: 'Error'
});

const EVENT_DATA = {
fieldName: 'testName',
contentType: 'Blog',
language: '2',
inode: 'testInode',
blockEditorContent: '{"field":"field value"}'
};

const contentTypeMock: DotCMSContentType = {
...dotcmsContentTypeBasicMock,
fields: [BLOCK_EDITOR_FIELD]
};

describe('DotBlockEditorSidebarComponent', () => {
let spectator: Spectator<DotBlockEditorSidebarComponent>;
let dotContentTypeService: DotContentTypeService;
let dotAlertConfirmService: DotAlertConfirmService;
let dotWorkflowActionsFireService: DotWorkflowActionsFireService;

const createComponent = createComponentFactory({
component: DotBlockEditorSidebarComponent,
imports: [BlockEditorModule],
declarations: [MockComponent(DotBlockEditorComponent)],
providers: [
DotAlertConfirmService,
{ provide: DotMessageService, useValue: messageServiceMock },
{
provide: DotWorkflowActionsFireService,
useValue: {
saveContentlet: jest.fn()
}
},
{
provide: DotContentTypeService,
useValue: {
getContentType: jest.fn().mockReturnValue(of(contentTypeMock))
}
}
]
});

beforeEach(() => {
spectator = createComponent();
dotContentTypeService = spectator.inject(DotContentTypeService, true);
dotAlertConfirmService = spectator.inject(DotAlertConfirmService, true);
dotWorkflowActionsFireService = spectator.inject(DotWorkflowActionsFireService, true);
spectator.component.open(EVENT_DATA);
spectator.detectChanges();
});

it('should set sidebar with correct inputs', () => {
const sidebar = spectator.query(Sidebar);
expect(sidebar.position).toBe('right');
expect(sidebar.blockScroll).toBe(true);
expect(sidebar.dismissible).toBe(false);
expect(sidebar.showCloseIcon).toBe(false);
expect(sidebar.closeOnEscape).toBe(false);
expect(sidebar.visible).toBe(true);
});

it('should set inputs to the block editor', () => {
const blockEditor = spectator.query(DotBlockEditorComponent);

expect(blockEditor.field).toEqual(BLOCK_EDITOR_FIELD);
expect(blockEditor.languageId).toBe(parseInt(EVENT_DATA.language));
expect(blockEditor.value).toEqual(JSON.parse(EVENT_DATA.blockEditorContent));
expect(dotContentTypeService.getContentType).toHaveBeenCalledWith('Blog');
});

it('should save changes in the editor', () => {
const spyWorkflowService = jest.spyOn(dotWorkflowActionsFireService, 'saveContentlet');
const blockEditor = spectator.query(DotBlockEditorComponent);

const newValue = { data: 'test value 1' };
blockEditor.valueChange.emit(newValue);

spectator.detectChanges();

const saveBtn = spectator.query(byTestId('save-btn')) as HTMLButtonElement;

saveBtn.click();
spectator.detectChanges();

expect(dotContentTypeService.getContentType).toHaveBeenCalledWith('Blog');
expect(spyWorkflowService).toHaveBeenCalledWith({ testName: JSON.stringify(newValue) });
});

it('should close the sidebar', () => {
const cancelBtn = spectator.query(byTestId('cancel-btn')) as HTMLButtonElement;

cancelBtn.click();
spectator.detectChanges();

const sidebar = spectator.query(Sidebar);

expect(sidebar.visible).toBe(false);
});

it('should display a toast on saving error', () => {
const error404 = mockResponseView(404, '', null, {
error: { message: 'An error occurred' }
});
const dotAletConfirmServiceSpy = jest.spyOn(dotAlertConfirmService, 'alert');
const spyWorkflowService = jest
.spyOn(dotWorkflowActionsFireService, 'saveContentlet')
.mockReturnValue(throwError(error404));

const blockEditor = spectator.query(DotBlockEditorComponent);
const newValue = { data: 'test value 1' };
blockEditor.valueChange.emit(newValue);

spectator.detectChanges();

const saveBtn = spectator.query(byTestId('save-btn')) as HTMLButtonElement;
saveBtn.click();
spectator.detectChanges();

expect(spyWorkflowService).toHaveBeenCalled();
expect(dotAletConfirmServiceSpy).toHaveBeenCalled();
});

it('should call event.stopPropagation on escape keydown', () => {
const event = new KeyboardEvent('keydown', { key: 'Escape' });
jest.spyOn(event, 'stopPropagation');

const container = spectator.query('[data-testId="dot-container"]');
container.dispatchEvent(event);

expect(event.stopPropagation).toHaveBeenCalled();
});

afterEach(() => jest.clearAllMocks());
});
Loading

0 comments on commit 184c09f

Please sign in to comment.