Skip to content

Commit 0850429

Browse files
authored
Merge pull request #99 from pegasystems/mod/tor/US-587580_2
Added richText control support
2 parents c08a9eb + 8fdc2e2 commit 0850429

File tree

13 files changed

+498
-1
lines changed

13 files changed

+498
-1
lines changed

angular.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,17 @@
7777
"glob": "*.*",
7878
"input": "./packages/angular-sdk-components/src/assets/icons/",
7979
"output": "./constellation/assets/icons"
80+
},
81+
{
82+
"glob": "**/*",
83+
"input": "./node_modules/tinymce",
84+
"output": "./tinymce"
8085
}
8186
],
8287
"styles": ["packages/angular-sdk-components/src/styles.scss"],
83-
"scripts": [],
88+
"scripts": [
89+
"./node_modules/tinymce/tinymce.min.js"
90+
],
8491
"customWebpackConfig": {
8592
"path": "./extra-webpack.config.js"
8693
},

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"@mdi/svg": "^5.0.45",
7474
"@pega/auth": "^0.1.6",
7575
"@pega/constellationjs": "SDK-8.23.0",
76+
"@tinymce/tinymce-angular": "^7.0.0",
7677
"core-js": "^2.6.12",
7778
"dayjs": "^1.10.5",
7879
"downloadjs": "^1.4.7",
@@ -122,6 +123,7 @@
122123
"postcss": "^8.4.23",
123124
"replace-in-file": "^6.3.5",
124125
"shx": "^0.3.4",
126+
"tinymce": "^6.8.2",
125127
"ts-node": "~8.6.2",
126128
"typescript": "4.9.5",
127129
"webpack": "^5.81.0"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
const { test, expect } = require('@playwright/test');
2+
3+
const config = require('../../../config');
4+
const common = require('../../../common');
5+
6+
// These values represent the data values used for the conditions and are initialised in pyDefault DT
7+
const isDisabled = true;
8+
const isVisible = true;
9+
10+
test.beforeEach(async ({ page }) => {
11+
await page.setViewportSize({ width: 1920, height: 1080 });
12+
await page.goto(config.config.baseUrl, { waitUntil: 'networkidle' });
13+
});
14+
15+
test.describe('E2E test', () => {
16+
let attributes;
17+
18+
test('should login, create case and run the RichText tests', async ({ page }) => {
19+
await common.login(config.config.apps.digv2.user.username, config.config.apps.digv2.user.password, page);
20+
21+
/** Testing announcement banner presence */
22+
const announcementBanner = page.locator('h2:has-text("Announcements")');
23+
await expect(announcementBanner).toBeVisible();
24+
25+
/** Testing worklist presence */
26+
const worklist = page.locator('div[id="worklist"]:has-text("My Worklist")');
27+
await expect(worklist).toBeVisible();
28+
29+
/** Click on the Create Case button */
30+
const createCase = page.locator('mat-list-item[id="create-case-button"]');
31+
await createCase.click();
32+
33+
/** Creating a Form Field case-type */
34+
const formFieldCase = page.locator('mat-list-item[id="case-list-item"] > span:has-text("Form Field")');
35+
await formFieldCase.click();
36+
37+
/** Selecting RichText from the Category dropdown */
38+
const selectedCategory = page.locator('mat-select[data-test-id="76729937a5eb6b0fd88c42581161facd"]');
39+
await selectedCategory.click();
40+
await page.getByRole('option', { name: 'RichText' }).click();
41+
42+
/** Selecting Required from the Sub Category dropdown */
43+
let selectedSubCategory = page.locator('mat-select[data-test-id="9463d5f18a8924b3200b56efaad63bda"]');
44+
await selectedSubCategory.click();
45+
await page.getByRole('option', { name: 'Required' }).click();
46+
47+
/** Required tests */
48+
const requiredRichTextContainer = page.locator('div[data-test-id="98a97d9fe6d092900021587f62ab8637"]');
49+
const requiredRichTextLabel = requiredRichTextContainer.locator('label');
50+
expect(await requiredRichTextLabel.innerText()).toEqual('RichText Required');
51+
await page.locator('button:has-text("submit")').click();
52+
let canNotBeBlankMsg = await requiredRichTextContainer.locator('p:has-text("Cannot be blank")');
53+
expect(canNotBeBlankMsg).toBeVisible();
54+
55+
const notRequiredRichTextContainer = page.locator('div[data-test-id="913fcb2ea3513d1f0dd357aa1766757f"]');
56+
const notRequiredRichTextLabel = notRequiredRichTextContainer.locator('label');
57+
expect(await notRequiredRichTextLabel.innerText()).toEqual('RichText Not Required');
58+
await page.locator('button:has-text("submit")').click();
59+
canNotBeBlankMsg = await notRequiredRichTextContainer.locator('p:has-text("Cannot be blank")');
60+
expect(canNotBeBlankMsg).not.toBeVisible();
61+
62+
/** Selecting Disable from the Sub Category dropdown */
63+
selectedSubCategory = page.locator('mat-select[data-test-id="9463d5f18a8924b3200b56efaad63bda"]');
64+
await selectedSubCategory.click();
65+
await page.getByRole('option', { name: 'Disable' }).click();
66+
67+
/** Disable tests */
68+
// Always Disabled RichText
69+
const alwaysDisabledRichTextContainer = page.locator('div[data-test-id="f8a6fa176e492f0b2c3a2ecce916a1cc"]');
70+
const alwaysDisabledRichTextLabel = alwaysDisabledRichTextContainer.locator('label');
71+
expect(await alwaysDisabledRichTextLabel.innerText()).toEqual('RichText Disabled Always');
72+
const alwaysDisabledRichTextBox = alwaysDisabledRichTextContainer.locator('div[role="application"]');
73+
attributes = await common.getAttributes(alwaysDisabledRichTextBox);
74+
await expect(attributes.includes('aria-disabled')).toBeTruthy();
75+
76+
// Conditionally Disabled RichText
77+
const conditionallyDisabledRichTextContainer = page.locator('div[data-test-id="a1f1fed886e4277998358560643d5b80"]');
78+
const conditionallyDisabledRichTextLabel = conditionallyDisabledRichTextContainer.locator('label');
79+
expect(await conditionallyDisabledRichTextLabel.innerText()).toEqual('RichText Disabled Condition');
80+
const conditionallyDisabledRichTextBox = conditionallyDisabledRichTextContainer.locator('div[role="application"]');
81+
attributes = await common.getAttributes(conditionallyDisabledRichTextBox);
82+
if (isDisabled) {
83+
await expect(attributes.includes('aria-disabled')).toBeTruthy();
84+
} else {
85+
await expect(attributes.includes('aria-disabled')).toBeFalsy();
86+
}
87+
88+
// Never Disabled RichText
89+
const neverDisabledRichTextContainer = page.locator('div[data-test-id="0706d1c3117909bba5dc3b11282c84c1"]');
90+
const neverDisabledRichTextLabel = neverDisabledRichTextContainer.locator('label');
91+
expect(await neverDisabledRichTextLabel.innerText()).toEqual('RichText Disabled Never');
92+
const neverDisabledRichTextBox = neverDisabledRichTextContainer.locator('div[role="application"]');
93+
const disabledValue = await neverDisabledRichTextBox.getAttribute('aria-disabled');
94+
await expect(disabledValue).toBe('false');
95+
96+
/** Selecting Update from the Sub Category dropdown */
97+
selectedSubCategory = page.locator('mat-select[data-test-id="9463d5f18a8924b3200b56efaad63bda"]');
98+
await selectedSubCategory.click();
99+
await page.getByRole('option', { name: 'Update' }).click();
100+
101+
/** Update tests */
102+
const readOnlyRichTextContainer = page.locator('div[data-test-id="2698790fe2608356645f7f37e47d4017"]');
103+
const readOnlyRichTextLabel = readOnlyRichTextContainer.locator('label');
104+
expect(await readOnlyRichTextLabel.innerText()).toEqual('RichText ReadOnly');
105+
const readOnlyRTEDiv = readOnlyRichTextContainer.locator('div[class="readonly-richtext-editor"]');
106+
expect(readOnlyRTEDiv).toBeVisible();
107+
108+
const editableRichTextContainer = page.locator('div[data-test-id="c5f3892e688f607040637162ef2d61e2"]');
109+
const editableRichTextLabel = editableRichTextContainer.locator('label');
110+
expect(await editableRichTextLabel.innerText()).toEqual('RichText Editable');
111+
const editableRichTextDiv = editableRichTextContainer.locator('div[role="application"]');
112+
expect(editableRichTextDiv).toBeVisible();
113+
114+
/** Selecting Visibility from the Sub Category dropdown */
115+
selectedSubCategory = page.locator('mat-select[data-test-id="9463d5f18a8924b3200b56efaad63bda"]');
116+
await selectedSubCategory.click();
117+
await page.getByRole('option', { name: 'Visibility' }).click();
118+
119+
/** Visibility tests */
120+
await expect(page.locator('div[data-test-id="4094af7d82d8e88494423b891852cfb3"]')).toBeVisible();
121+
122+
const neverVisibleRichText = await page.locator('div[data-test-id="be4eec910ae6fd21f9ff706a3d64dc58"]');
123+
await expect(neverVisibleRichText).not.toBeVisible();
124+
125+
const conditionallyVisibleRichText = await page.locator('div[data-test-id="c50c684046dd7f7ca04fd9e35ed0ec92"]');
126+
127+
if (isVisible) {
128+
await expect(conditionallyVisibleRichText).toBeVisible();
129+
} else {
130+
await expect(conditionallyVisibleRichText).not.toBeVisible();
131+
}
132+
}, 10000);
133+
});
134+
135+
test.afterEach(async ({ page }) => {
136+
await page.close();
137+
});

projects/angular-sdk-library/src/lib/_bridge/helpers/sdk-pega-component-map.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ import { MaterialDetailsComponent } from '../../_components/designSystemExtensio
9999
import { DashboardFilterComponent } from '../../_components/infra/dashboard-filter/dashboard-filter.component';
100100
import { ListUtilityComponent } from '../../_components/widget/list-utility/list-utility.component';
101101
import { MaterialUtilityComponent } from '../../_components/designSystemExtension/material-utility/material-utility.component';
102+
import { RichTextComponent } from '../../_components/field/rich-text/rich-text.component';
103+
import { RichTextEditorComponent } from '../../_components/designSystemExtension/rich-text-editor/rich-text-editor.component';
102104
import { ListViewActionButtonsComponent } from '../../_components/field/list-view-action-buttons/list-view-action-buttons.component';
103105

104106
// pegaSdkComponentMap is the JSON object where we'll store the components that are
@@ -187,6 +189,8 @@ const pegaSdkComponentMap = {
187189
reference: ReferenceComponent,
188190
RadioButtons: RadioButtonsComponent,
189191
Region: RegionComponent,
192+
RichText: RichTextComponent,
193+
RichTextEditor: RichTextEditorComponent,
190194
RootContainer: RootContainerComponent,
191195
ScalarList: ScalarListComponent,
192196
SemanticLink: SemanticLinkComponent,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<div [attr.data-test-id]="testId">
2+
<label [ngClass]="{ 'label-required': required === true }" class="rich-text-label">{{ label }}</label>
3+
<div class="rich-text-editor" *ngIf="!readonly">
4+
<editor
5+
[formControl]="richText"
6+
[attr.disabled]="disabled"
7+
[initialValue]="value"
8+
[init]="{
9+
base_url: '/tinymce',
10+
suffix: '.min',
11+
menubar: false,
12+
placeholder,
13+
statusbar: false,
14+
min_height: 130,
15+
plugins: ['lists', 'advlist', 'autolink', 'image', 'link', 'autoresize'],
16+
autoresize_bottom_margin: 0,
17+
toolbar: disabled ? false : 'blocks | bold italic strikethrough | bullist numlist outdent indent | link image',
18+
toolbar_location: 'bottom',
19+
content_style: 'body { font-family:Helvetica, Arial,sans-serif; font-size:14px }',
20+
branding: false,
21+
paste_data_images: true,
22+
file_picker_types: 'image',
23+
file_picker_callback: filePickerCallback
24+
}"
25+
(onBlur)="blur()"
26+
(onChange)="change($event)"
27+
></editor>
28+
<p *ngIf="richText.invalid" [ngClass]="'text-editor-error'">{{ info }}</p>
29+
</div>
30+
<div *ngIf="readonly">
31+
<div class="readonly-richtext-editor" style="margin: 10px 5px" [innerHTML]="value || '--'"></div>
32+
</div>
33+
</div>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@import '../../../../../_shared/styles.scss';
2+
.rich-text-label {
3+
margin: 5px;
4+
font-size: 16px;
5+
}
6+
7+
.rich-text-editor {
8+
margin: 10px 0px;
9+
}
10+
11+
.label-required::after {
12+
display: inline;
13+
content: " *";
14+
vertical-align: top;
15+
color: $app-neutral-dark-color;
16+
}
17+
18+
.text-editor-error {
19+
color: $app-error-light-color;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2+
3+
import { RichTextEditorComponent } from './rich-text-editor.component';
4+
5+
describe('RichTextComponent', () => {
6+
let component: RichTextEditorComponent;
7+
let fixture: ComponentFixture<RichTextEditorComponent>;
8+
9+
beforeEach(waitForAsync(() => {
10+
TestBed.configureTestingModule({
11+
declarations: [ RichTextEditorComponent ]
12+
})
13+
.compileComponents();
14+
}));
15+
16+
beforeEach(() => {
17+
fixture = TestBed.createComponent(RichTextEditorComponent);
18+
component = fixture.componentInstance;
19+
fixture.detectChanges();
20+
});
21+
22+
it('should create', () => {
23+
expect(component).toBeTruthy();
24+
});
25+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
4+
import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
5+
6+
declare let tinymce: any;
7+
8+
@Component({
9+
selector: 'app-rich-text-editor',
10+
templateUrl: './rich-text-editor.component.html',
11+
styleUrls: ['./rich-text-editor.component.scss'],
12+
standalone: true,
13+
imports: [CommonModule, EditorModule, ReactiveFormsModule],
14+
providers: [{ provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' }]
15+
})
16+
export class RichTextEditorComponent implements OnInit {
17+
@Input() placeholder;
18+
@Input() disabled;
19+
@Input() readonly;
20+
@Input() value;
21+
@Input() label;
22+
@Input() required;
23+
@Input() info;
24+
@Input() error;
25+
@Input() testId;
26+
27+
@Output() onBlur: EventEmitter<any> = new EventEmitter();
28+
@Output() onChange: EventEmitter<any> = new EventEmitter();
29+
30+
richText = new FormControl();
31+
32+
ngOnInit(): void {}
33+
34+
ngOnChanges() {
35+
if (this.required) {
36+
this.richText.addValidators(Validators.required);
37+
}
38+
39+
if (this.disabled) {
40+
this.richText.disable();
41+
} else {
42+
this.richText.enable();
43+
}
44+
45+
if (this.value) {
46+
this.richText.setValue(this.value);
47+
}
48+
}
49+
50+
filePickerCallback = (cb) => {
51+
const input = document.createElement('input');
52+
input.setAttribute('type', 'file');
53+
input.setAttribute('accept', 'image/*');
54+
55+
input.addEventListener('change', (e: any) => {
56+
const file = e.target.files[0];
57+
58+
const reader: any = new FileReader();
59+
reader.addEventListener('load', () => {
60+
/*
61+
Note: Now we need to register the blob in TinyMCEs image blob
62+
registry. In the next release this part hopefully won't be
63+
necessary, as we are looking to handle it internally.
64+
*/
65+
const blobId = `blobid${new Date().getTime()}`;
66+
console.log('editorRef', tinymce.activeEditor);
67+
const blobCache = tinymce.activeEditor.editorUpload.blobCache;
68+
const base64 = reader.result.split(',')[1];
69+
const blobInfo = blobCache.create(blobId, file, base64);
70+
blobCache.add(blobInfo);
71+
72+
/* call the callback and populate the Title field with the file name */
73+
cb(blobInfo.blobUri(), { title: file.name });
74+
});
75+
reader.readAsDataURL(file);
76+
});
77+
78+
input.click();
79+
};
80+
81+
blur() {
82+
if (tinymce.activeEditor) {
83+
const editorValue = tinymce.activeEditor.getContent({ format: 'html' });
84+
this.onBlur.emit(editorValue);
85+
}
86+
}
87+
88+
change(event) {
89+
this.onChange.emit(event);
90+
}
91+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<div *ngIf="displayMode$; else noDisplayMode">
2+
<component-mapper *ngIf="bVisible$ !== false" name="FieldValueList" [props]="{ label$, value$, displayMode$ }"></component-mapper>
3+
</div>
4+
<ng-template #noDisplayMode>
5+
<div *ngIf="!bReadonly$ else noEdit">
6+
<div *ngIf="bVisible$">
7+
<component-mapper
8+
name="RichTextEditor"
9+
[props]="{ placeholder, required: bRequired$, disabled: bDisabled$, label: label$, readonly: false, error, info, testId, value:value$ }"
10+
[parent]="this"
11+
[outputEvents]="{ onBlur: fieldOnBlur, onChange: fieldOnChange }"
12+
></component-mapper>
13+
</div>
14+
</div>
15+
</ng-template>
16+
<ng-template #noEdit>
17+
<div *ngIf="bVisible$ !== false">
18+
<component-mapper name="RichTextEditor" [props]="{ label: label$, value: value$, readonly: true, testId }" [parent]="this"></component-mapper>
19+
</div>
20+
</ng-template>

projects/angular-sdk-library/src/lib/_components/field/rich-text/rich-text.component.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)