mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-02 17:53:30 +00:00
AAE-50098 Fix unrendered rich text expressions in form submissions (#12169)
This commit is contained in:
@@ -36,6 +36,58 @@ describe('FormFieldModel', () => {
|
||||
expect(model.json).toBe(json);
|
||||
});
|
||||
|
||||
it('should return an isolated authored value snapshot', () => {
|
||||
const authoredValue = { blocks: [{ data: { text: '${field.name}' } }] };
|
||||
const model = new FormFieldModel(new FormModel(), { id: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue });
|
||||
|
||||
const snapshot = model.authoredValue as typeof authoredValue;
|
||||
snapshot.blocks[0].data.text = 'changed';
|
||||
|
||||
expect((model.authoredValue as typeof authoredValue).blocks[0].data.text).toBe('${field.name}');
|
||||
expect(authoredValue.blocks[0].data.text).toBe('${field.name}');
|
||||
});
|
||||
|
||||
it('should not capture authored values for other field types', () => {
|
||||
const model = new FormFieldModel(new FormModel(), { id: 'json', type: FormFieldTypes.JSON, value: { content: 'value' } });
|
||||
|
||||
expect(model.authoredValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for authored values that cannot be cloned', () => {
|
||||
const circularValue: { self?: unknown } = {};
|
||||
circularValue.self = circularValue;
|
||||
|
||||
const circularValueModel = new FormFieldModel(new FormModel(), {
|
||||
id: 'circular',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: circularValue
|
||||
});
|
||||
const bigintValueModel = new FormFieldModel(new FormModel(), {
|
||||
id: 'bigint',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: BigInt(1)
|
||||
});
|
||||
|
||||
expect(circularValueModel.authoredValue).toBeUndefined();
|
||||
expect(bigintValueModel.authoredValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should preserve authored value when form data overrides the field value', () => {
|
||||
const authoredValue = { blocks: [{ data: { text: '${field.name}' } }] };
|
||||
const savedValue = { blocks: [{ data: { text: 'John' } }] };
|
||||
const form = new FormModel(
|
||||
{
|
||||
fields: [{ id: 'richText', name: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue }]
|
||||
},
|
||||
{ richText: savedValue }
|
||||
);
|
||||
const model = form.getFieldById('richText');
|
||||
|
||||
expect(model.value).toEqual(savedValue);
|
||||
expect(model.authoredValue).toEqual(authoredValue);
|
||||
expect(model.authoredValue).not.toBe(authoredValue);
|
||||
});
|
||||
|
||||
it('should setup with json config', () => {
|
||||
const json = {
|
||||
fieldType: '<fieldType>',
|
||||
|
||||
@@ -38,12 +38,28 @@ export type FieldOptionType = 'rest' | 'manual' | 'variable';
|
||||
export type FieldSelectionType = 'single' | 'multiple';
|
||||
export type FieldAlignmentType = 'vertical' | 'horizontal';
|
||||
|
||||
const isJsonPrimitive = (value: unknown): value is null | string | number | boolean =>
|
||||
value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
||||
|
||||
const cloneJsonCompatibleValue = (value: unknown): unknown => {
|
||||
if (value === undefined || isJsonPrimitive(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Maps to FormFieldRepresentation
|
||||
export class FormFieldModel extends FormWidgetModel {
|
||||
private _value: string;
|
||||
private _readOnly: boolean = false;
|
||||
private _isValid: boolean = true;
|
||||
private _required: boolean = false;
|
||||
private readonly _authoredValue: unknown;
|
||||
|
||||
readonly defaultDateFormat: string = 'D-M-YYYY';
|
||||
readonly defaultDateTimeFormat: string = 'D-M-YYYY hh:mm A';
|
||||
@@ -123,6 +139,10 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
}
|
||||
}
|
||||
|
||||
get authoredValue(): unknown {
|
||||
return cloneJsonCompatibleValue(this._authoredValue);
|
||||
}
|
||||
|
||||
get readOnly(): boolean {
|
||||
if (this.form?.readOnly) {
|
||||
return true;
|
||||
@@ -183,6 +203,7 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
|
||||
constructor(form: any, json?: any, parent?: RepeatableSectionModel) {
|
||||
super(form, json);
|
||||
this._authoredValue = json?.type === FormFieldTypes.DISPLAY_RICH_TEXT ? cloneJsonCompatibleValue(json.value) : undefined;
|
||||
if (json) {
|
||||
this.fieldType = json.fieldType;
|
||||
this.id = this.getId(json.id, parent);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { VersionCompatibilityService, AlfrescoApiService } from '@alfresco/adf-content-services';
|
||||
import {
|
||||
ADF_DISPLAY_TEXT_SETTINGS,
|
||||
ContentLinkModel,
|
||||
CoreModule,
|
||||
FormFieldModel,
|
||||
@@ -117,7 +118,8 @@ describe('FormCloudComponent', () => {
|
||||
useValue: {}
|
||||
},
|
||||
{ provide: FormRenderingService, useClass: CloudFormRenderingService },
|
||||
{ provide: FORM_CLOUD_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] }
|
||||
{ provide: FORM_CLOUD_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] },
|
||||
{ provide: ADF_DISPLAY_TEXT_SETTINGS, useValue: { enableExpressionEvaluation: true } }
|
||||
]
|
||||
});
|
||||
const apiService = TestBed.inject(AlfrescoApiService);
|
||||
@@ -874,6 +876,39 @@ describe('FormCloudComponent', () => {
|
||||
expect(savedForm).toEqual(formModel);
|
||||
});
|
||||
|
||||
it('should materialize unrendered rich text expressions when saving a task form', () => {
|
||||
spyOn(formCloudService, 'saveTaskForm').and.returnValue(of(undefined));
|
||||
const formModel = new FormModel({
|
||||
id: '23',
|
||||
taskId: '123-223',
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
const originalValues = JSON.parse(JSON.stringify(formModel.values));
|
||||
formComponent.form = formModel;
|
||||
formComponent.taskId = formModel.taskId;
|
||||
formComponent.appName = 'test-app';
|
||||
|
||||
formComponent.saveTaskForm();
|
||||
|
||||
expect(formCloudService.saveTaskForm).toHaveBeenCalledWith(
|
||||
'test-app',
|
||||
formModel.taskId,
|
||||
undefined,
|
||||
formModel.id,
|
||||
jasmine.objectContaining({
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }] }
|
||||
})
|
||||
);
|
||||
expect(formModel.values).toEqual(originalValues);
|
||||
});
|
||||
|
||||
it('should handle error during form save', () => {
|
||||
const error = 'Error';
|
||||
spyOn(formCloudService, 'saveTaskForm').and.callFake(() => throwError(error));
|
||||
@@ -981,6 +1016,39 @@ describe('FormCloudComponent', () => {
|
||||
expect(completedForm).toBe(formComponent.form);
|
||||
});
|
||||
|
||||
it('should materialize unrendered rich text expressions when completing a task form', () => {
|
||||
spyOn(formCloudService, 'completeTaskForm').and.returnValue(of(undefined));
|
||||
const formModel = new FormModel({
|
||||
id: '23',
|
||||
taskId: '123-223',
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
formComponent.form = formModel;
|
||||
formComponent.taskId = formModel.taskId;
|
||||
formComponent.appName = 'test-app';
|
||||
|
||||
formComponent.completeTaskForm('complete');
|
||||
|
||||
expect(formCloudService.completeTaskForm).toHaveBeenCalledWith(
|
||||
'test-app',
|
||||
formModel.taskId,
|
||||
undefined,
|
||||
formModel.id,
|
||||
jasmine.objectContaining({
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'John' } }] }
|
||||
}),
|
||||
'complete',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('should open confirmation dialog on complete task', async () => {
|
||||
formComponent.form = new FormModel({
|
||||
confirmMessage: {
|
||||
|
||||
@@ -35,9 +35,12 @@ import { filter, map, switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
ConfirmDialogComponent,
|
||||
ContentLinkModel,
|
||||
ADF_DISPLAY_TEXT_SETTINGS,
|
||||
DisplayTextWidgetSettings,
|
||||
FormatSpacePipe,
|
||||
FormBaseComponent,
|
||||
FormEvent,
|
||||
FormExpressionService,
|
||||
FormFieldModel,
|
||||
FormRulesEvent,
|
||||
FormFieldValidator,
|
||||
@@ -67,6 +70,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from '../services/form-cloud-submission-values';
|
||||
|
||||
interface FormFieldRuntimeState {
|
||||
value: any;
|
||||
@@ -228,6 +232,8 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
protected changeDetector = inject(ChangeDetectorRef);
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly expressions = inject(FormExpressionService);
|
||||
private enableExpressionEvaluation = false;
|
||||
|
||||
private get currentForm(): FormModel | undefined {
|
||||
return super.form;
|
||||
@@ -252,6 +258,9 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
constructor() {
|
||||
const injectedFieldValidators = inject(FORM_CLOUD_FIELD_VALIDATORS_TOKEN, { optional: true });
|
||||
const tabNavEnabledToken = inject(ADF_FORM_TAB_NAV_ENABLED, { optional: true });
|
||||
const displayTextSettings = inject<Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings>(ADF_DISPLAY_TEXT_SETTINGS, {
|
||||
optional: true
|
||||
});
|
||||
|
||||
super();
|
||||
this.loadInjectedFieldValidators(injectedFieldValidators);
|
||||
@@ -270,6 +279,12 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
}
|
||||
}
|
||||
|
||||
getExpressionEvaluationEnabled$(displayTextSettings)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((enabled) => {
|
||||
this.enableExpressionEvaluation = enabled;
|
||||
});
|
||||
|
||||
this.formService.formContentClicked.pipe(takeUntilDestroyed()).subscribe((content) => {
|
||||
if (content instanceof UploadWidgetContentLinkModel) {
|
||||
this.form.setNodeIdValueForViewersLinkedToUploadWidget(content);
|
||||
@@ -482,7 +497,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
saveTaskForm() {
|
||||
if (this.form && this.appName && this.taskId) {
|
||||
this.formCloudService
|
||||
.saveTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values)
|
||||
.saveTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.getSubmissionValues())
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@@ -523,7 +538,15 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
private completeForm(outcome?: string, outcomeId?: string) {
|
||||
if (this.form && this.appName && this.taskId) {
|
||||
this.formCloudService
|
||||
.completeTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values, outcome, this.appVersion)
|
||||
.completeTaskForm(
|
||||
this.appName,
|
||||
this.taskId,
|
||||
this.processInstanceId,
|
||||
`${this.form.id}`,
|
||||
this.getSubmissionValues(),
|
||||
outcome,
|
||||
this.appVersion
|
||||
)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@@ -536,6 +559,10 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
}
|
||||
}
|
||||
|
||||
private getSubmissionValues(): FormValues {
|
||||
return materializeSubmissionValues(this.form, { enableExpressionEvaluation: this.enableExpressionEvaluation }, this.expressions);
|
||||
}
|
||||
|
||||
parseForm(formCloudRepresentationJSON?: any): FormModel | null {
|
||||
if (formCloudRepresentationJSON) {
|
||||
const formValues: FormValues = {};
|
||||
|
||||
+45
@@ -200,6 +200,51 @@ describe('DisplayRichTextWidgetComponent', () => {
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should resolve from authored value after saved data rehydrates the field', () => {
|
||||
const form = new FormModel(
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
name: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }]
|
||||
}
|
||||
},
|
||||
{ id: 'name', name: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
},
|
||||
{
|
||||
richText1: {
|
||||
blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }]
|
||||
},
|
||||
name: 'Jane'
|
||||
}
|
||||
);
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello Jane');
|
||||
});
|
||||
|
||||
it('should preserve the current value when the authored value is unavailable', () => {
|
||||
const form = new FormModel({
|
||||
fields: [{ id: 'richText1', type: 'display-rich-text' }]
|
||||
});
|
||||
const currentValue = {
|
||||
blocks: [{ type: 'paragraph', data: { text: 'Current value' } }]
|
||||
};
|
||||
const field = form.getFieldById('richText1');
|
||||
field.value = currentValue;
|
||||
widget.field = field;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe(currentValue);
|
||||
});
|
||||
|
||||
it('should resolve expressions in multiple blocks', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
|
||||
+10
-13
@@ -22,6 +22,7 @@ import { BaseDisplayTextWidgetComponent } from '@alfresco/adf-core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RichTextParserService } from '../../../services/rich-text-parser.service';
|
||||
import { resolveRichTextExpressions } from './rich-text-expression-resolver';
|
||||
|
||||
export const RICH_TEXT_PARSER_TOKEN = new InjectionToken<RichTextParserService>('RichTextParserService', {
|
||||
factory: () => new RichTextParserService()
|
||||
@@ -66,7 +67,10 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone
|
||||
|
||||
protected storeOriginalValue(): void {
|
||||
if (this.field) {
|
||||
this.originalFieldValue = JSON.stringify(this.field.value);
|
||||
const authoredValue = this.field.authoredValue;
|
||||
if (authoredValue !== undefined) {
|
||||
this.originalFieldValue = JSON.stringify(authoredValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +79,10 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone
|
||||
return;
|
||||
}
|
||||
|
||||
const value = JSON.parse(JSON.stringify(this.field.value));
|
||||
this.applyExpressionsToBlocks(value);
|
||||
const authoredValue = this.field.authoredValue;
|
||||
if (authoredValue !== undefined) {
|
||||
this.applyExpressionsToBlocks(authoredValue);
|
||||
}
|
||||
}
|
||||
|
||||
protected reevaluateExpressions(): void {
|
||||
@@ -89,16 +95,7 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone
|
||||
}
|
||||
|
||||
private applyExpressionsToBlocks(value: any): void {
|
||||
for (const block of value.blocks) {
|
||||
if (block.type === 'list') {
|
||||
for (const item of block.data.items) {
|
||||
item.content = this.resolveExpressions(item.content, true);
|
||||
}
|
||||
} else {
|
||||
block.data.text = this.resolveExpressions(block.data.text, true);
|
||||
}
|
||||
}
|
||||
this.field.value = value;
|
||||
this.field.value = resolveRichTextExpressions(value, (content) => this.resolveExpressions(content, true));
|
||||
}
|
||||
|
||||
private parseAndSanitize(): void {
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { resolveRichTextExpressions } from './rich-text-expression-resolver';
|
||||
|
||||
describe('resolveRichTextExpressions', () => {
|
||||
const resolve = (value: string) => value.replaceAll('${field.name}', 'John').replaceAll('${variable.status}', 'Active');
|
||||
|
||||
it('should resolve supported rich text content without mutating the input', () => {
|
||||
const value = {
|
||||
time: 1,
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}',
|
||||
caption: 'Status: ${variable.status}',
|
||||
content: [
|
||||
['Cell ${field.name}'],
|
||||
{
|
||||
label: '${variable.status}'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
content: '${field.name}',
|
||||
items: [{ content: '${variable.status}' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
version: '2.30.0'
|
||||
};
|
||||
const originalValue = JSON.parse(JSON.stringify(value));
|
||||
|
||||
const result = resolveRichTextExpressions(value, resolve);
|
||||
|
||||
expect(result).toEqual({
|
||||
time: 1,
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello John',
|
||||
caption: 'Status: Active',
|
||||
content: [['Cell John'], { label: 'Active' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
data: {
|
||||
items: [{ content: 'John', items: [{ content: 'Active' }] }]
|
||||
}
|
||||
}
|
||||
],
|
||||
version: '2.30.0'
|
||||
});
|
||||
expect(value).toEqual(originalValue);
|
||||
expect(result).not.toBe(value);
|
||||
});
|
||||
|
||||
it('should resolve a caller-owned clone without cloning it again', () => {
|
||||
const value = {
|
||||
blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }]
|
||||
};
|
||||
|
||||
const result = resolveRichTextExpressions(value, resolve, { cloneValue: false }) as typeof value;
|
||||
|
||||
expect(result).toBe(value);
|
||||
expect(result.blocks[0].data.text).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should preserve unknown blocks and properties', () => {
|
||||
const value = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'custom',
|
||||
data: {
|
||||
label: '${field.name}'
|
||||
},
|
||||
metadata: '${variable.status}'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(resolveRichTextExpressions(value, resolve)).toEqual(value);
|
||||
});
|
||||
|
||||
it('should not introduce missing content properties', () => {
|
||||
const result = resolveRichTextExpressions({ blocks: [{ type: 'paragraph', data: {} }] }, resolve) as {
|
||||
blocks: Array<{ data: Record<string, unknown> }>;
|
||||
};
|
||||
|
||||
expect(result.blocks[0].data).toEqual({});
|
||||
});
|
||||
|
||||
it('should return malformed values unchanged', () => {
|
||||
const malformedValues = [null, undefined, 'text', [], {}, { blocks: null }];
|
||||
|
||||
malformedValues.forEach((value) => {
|
||||
expect(resolveRichTextExpressions(value, resolve)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return non-cloneable values without mutating them', () => {
|
||||
const value: {
|
||||
blocks: Array<{ data: { text: string } }>;
|
||||
self?: unknown;
|
||||
} = {
|
||||
blocks: [{ data: { text: '${field.name}' } }]
|
||||
};
|
||||
value.self = value;
|
||||
|
||||
expect(resolveRichTextExpressions(value, resolve)).toBe(value);
|
||||
expect(value.blocks[0].data.text).toBe('${field.name}');
|
||||
});
|
||||
});
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
export type RichTextExpressionResolver = (value: string) => string;
|
||||
|
||||
export interface RichTextExpressionResolverOptions {
|
||||
cloneValue?: boolean;
|
||||
}
|
||||
|
||||
const isJsonObject = (value: unknown): value is JsonObject => typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const cloneJsonValue = (value: unknown): unknown => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveNestedContent = (value: unknown, resolve: RichTextExpressionResolver): unknown => {
|
||||
if (typeof value === 'string') {
|
||||
return resolve(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => resolveNestedContent(entry, resolve));
|
||||
}
|
||||
|
||||
if (isJsonObject(value)) {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveNestedContent(entry, resolve)]));
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const resolveListItems = (items: unknown, resolve: RichTextExpressionResolver): unknown => {
|
||||
if (!Array.isArray(items)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.map((item) => {
|
||||
if (!isJsonObject(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (Object.hasOwn(item, 'content')) {
|
||||
item.content = resolveNestedContent(item.content, resolve);
|
||||
}
|
||||
|
||||
if (Object.hasOwn(item, 'items')) {
|
||||
item.items = resolveListItems(item.items, resolve);
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
export const resolveRichTextExpressions = (
|
||||
value: unknown,
|
||||
resolve: RichTextExpressionResolver,
|
||||
options: RichTextExpressionResolverOptions = {}
|
||||
): unknown => {
|
||||
if (!isJsonObject(value) || !Array.isArray(value.blocks)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const resolvedValue = options.cloneValue === false ? value : cloneJsonValue(value);
|
||||
if (!isJsonObject(resolvedValue) || !Array.isArray(resolvedValue.blocks)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
resolvedValue.blocks.forEach((block) => {
|
||||
if (!isJsonObject(block) || !isJsonObject(block.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof block.data.text === 'string') {
|
||||
block.data.text = resolve(block.data.text);
|
||||
}
|
||||
|
||||
if (typeof block.data.caption === 'string') {
|
||||
block.data.caption = resolve(block.data.caption);
|
||||
}
|
||||
|
||||
if (Object.hasOwn(block.data, 'content')) {
|
||||
block.data.content = resolveNestedContent(block.data.content, resolve);
|
||||
}
|
||||
|
||||
if (block.type === 'list' && Object.hasOwn(block.data, 'items')) {
|
||||
block.data.items = resolveListItems(block.data.items, resolve);
|
||||
}
|
||||
});
|
||||
|
||||
return resolvedValue;
|
||||
};
|
||||
@@ -43,5 +43,6 @@ export * from './services/form-cloud.service';
|
||||
export * from './services/content-cloud-node-selector.service';
|
||||
export * from './services/process-cloud-content.service';
|
||||
export * from './services/display-mode.service';
|
||||
export * from './services/form-cloud-submission-values';
|
||||
|
||||
export * from './form-cloud.module';
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { FormExpressionService, FormFieldModel, FormFieldTypes, FormModel } from '@alfresco/adf-core';
|
||||
import { firstValueFrom, of } from 'rxjs';
|
||||
import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from './form-cloud-submission-values';
|
||||
|
||||
describe('getExpressionEvaluationEnabled$', () => {
|
||||
it('should return the configured static value', async () => {
|
||||
const enabled = await firstValueFrom(getExpressionEvaluationEnabled$({ enableExpressionEvaluation: true }));
|
||||
|
||||
expect(enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should return values emitted by observable settings', async () => {
|
||||
const enabled = await firstValueFrom(getExpressionEvaluationEnabled$(of({ enableExpressionEvaluation: true })));
|
||||
|
||||
expect(enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when settings are unavailable', async () => {
|
||||
const enabled = await firstValueFrom(getExpressionEvaluationEnabled$(undefined));
|
||||
|
||||
expect(enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('materializeSubmissionValues', () => {
|
||||
let expressions: FormExpressionService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [FormExpressionService]
|
||||
});
|
||||
expressions = TestBed.inject(FormExpressionService);
|
||||
});
|
||||
|
||||
it('should resolve root rich text values from the authored template without mutating the form', () => {
|
||||
const authoredValue = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name} - ${variable.status} - ${field.missing} - ${field.unsafe}'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'richText', name: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue },
|
||||
{ id: 'name', name: 'name', type: FormFieldTypes.TEXT, value: 'John' },
|
||||
{ id: 'unsafe', name: 'unsafe', type: FormFieldTypes.TEXT, value: '<b>John</b>' }
|
||||
],
|
||||
variables: [{ id: 'status', name: 'status', type: 'string', value: 'Active' }]
|
||||
});
|
||||
const richTextField = form.getFieldById('richText');
|
||||
richTextField.value = { blocks: [{ type: 'paragraph', data: { text: 'stale rendered value' } }] };
|
||||
const originalValues = JSON.parse(JSON.stringify(form.values));
|
||||
const originalDefinition = JSON.parse(JSON.stringify(form.json));
|
||||
|
||||
const values = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions);
|
||||
|
||||
expect(values.richText).toEqual({
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello John - Active - - <b>John</b>'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(form.values).toEqual(originalValues);
|
||||
expect(form.json).toEqual(originalDefinition);
|
||||
expect(richTextField.value).toEqual({ blocks: [{ type: 'paragraph', data: { text: 'stale rendered value' } }] });
|
||||
});
|
||||
|
||||
it('should produce stable values across repeated materialization', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
const firstValues = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions);
|
||||
const secondValues = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions);
|
||||
|
||||
expect(secondValues).toEqual(firstValues);
|
||||
});
|
||||
|
||||
it('should return a shallow clone without resolving expressions when evaluation is disabled', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
const values = materializeSubmissionValues(form, { enableExpressionEvaluation: false }, expressions);
|
||||
|
||||
expect(values).toEqual(form.values);
|
||||
expect(values).not.toBe(form.values);
|
||||
expect(values.richText).toBe(form.values.richText);
|
||||
});
|
||||
|
||||
it('should isolate materialized repeatable section rows', () => {
|
||||
const form = new FormModel();
|
||||
form.values = {
|
||||
section: [
|
||||
{ richText: 'saved row one', untouched: 'one' },
|
||||
{ richText: 'saved row two', untouched: 'two' }
|
||||
],
|
||||
name: 'John'
|
||||
};
|
||||
const nameField = new FormFieldModel(form, { id: 'name', type: FormFieldTypes.TEXT, value: 'John' });
|
||||
const firstField = new FormFieldModel(
|
||||
form,
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: 'First ${field.name}' } }] }
|
||||
},
|
||||
{ id: 'section', uid: 'richText-Row1', fields: {}, rowIndex: 0 }
|
||||
);
|
||||
const secondField = new FormFieldModel(
|
||||
form,
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: 'Second ${field.name}' } }] }
|
||||
},
|
||||
{ id: 'section', uid: 'richText-Row2', fields: {}, rowIndex: 1 }
|
||||
);
|
||||
form.fieldsCache = [nameField, firstField, secondField];
|
||||
form.values.section = [
|
||||
{ richText: 'saved row one', untouched: 'one' },
|
||||
{ richText: 'saved row two', untouched: 'two' }
|
||||
];
|
||||
const originalSection = form.values.section;
|
||||
const originalFirstRow = form.values.section[0];
|
||||
const originalSecondRow = form.values.section[1];
|
||||
|
||||
const values = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions);
|
||||
|
||||
expect(values.section).toEqual([
|
||||
{
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'First John' } }] },
|
||||
untouched: 'one'
|
||||
},
|
||||
{
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'Second John' } }] },
|
||||
untouched: 'two'
|
||||
}
|
||||
]);
|
||||
expect(values.section).not.toBe(originalSection);
|
||||
expect(values.section[0]).not.toBe(originalFirstRow);
|
||||
expect(values.section[1]).not.toBe(originalSecondRow);
|
||||
expect(form.values.section).toBe(originalSection);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DisplayTextWidgetSettings, FormExpressionService, FormFieldTypes, FormModel, FormValues, ROW_ID_PREFIX } from '@alfresco/adf-core';
|
||||
import { isObservable, Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { resolveRichTextExpressions } from '../components/widgets/display-rich-text/rich-text-expression-resolver';
|
||||
|
||||
type SubmissionRow = Record<string, unknown>;
|
||||
|
||||
const isSubmissionRow = (value: unknown): value is SubmissionRow => typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
export interface FormCloudSubmissionValuesOptions {
|
||||
enableExpressionEvaluation: boolean;
|
||||
}
|
||||
|
||||
export const getExpressionEvaluationEnabled$ = (
|
||||
settings: Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings | null | undefined
|
||||
): Observable<boolean> =>
|
||||
isObservable(settings)
|
||||
? settings.pipe(map((value) => value?.enableExpressionEvaluation ?? false))
|
||||
: of(settings?.enableExpressionEvaluation ?? false);
|
||||
|
||||
export const materializeSubmissionValues = (
|
||||
form: FormModel,
|
||||
options: FormCloudSubmissionValuesOptions,
|
||||
expressions: FormExpressionService
|
||||
): FormValues => {
|
||||
const values = { ...form.values };
|
||||
|
||||
if (!options.enableExpressionEvaluation) {
|
||||
return values;
|
||||
}
|
||||
|
||||
for (const field of form.getFormFields([FormFieldTypes.DISPLAY_RICH_TEXT])) {
|
||||
const { authoredValue, parent } = field;
|
||||
if (authoredValue === undefined || parent?.isTemplate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const materializedValue = resolveRichTextExpressions(authoredValue, (content) => expressions.resolveExpressions(form, content, true), {
|
||||
cloneValue: false
|
||||
});
|
||||
|
||||
if (!parent) {
|
||||
values[field.id] = materializedValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
const sectionValues = values[parent.id];
|
||||
const sectionRow = Array.isArray(sectionValues) ? sectionValues[parent.rowIndex] : undefined;
|
||||
if (!isSubmissionRow(sectionRow)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const materializedRows = [...sectionValues];
|
||||
const fieldId = field.id.split(ROW_ID_PREFIX)[0];
|
||||
materializedRows[parent.rowIndex] = {
|
||||
...sectionRow,
|
||||
[fieldId]: materializedValue
|
||||
};
|
||||
values[parent.id] = materializedRows;
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
+55
-4
@@ -17,8 +17,8 @@
|
||||
|
||||
import { DebugElement, SimpleChange } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { ADF_DISPLAY_TEXT_SETTINGS, FormFieldTypes, FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core';
|
||||
import { Subject, of, throwError } from 'rxjs';
|
||||
import { StartProcessCloudService } from '../services/start-process-cloud.service';
|
||||
import { FormCloudService } from '../../../form/services/form-cloud.service';
|
||||
import { FormCloudComponent } from '../../../form/components/form-cloud.component';
|
||||
@@ -47,7 +47,7 @@ import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing';
|
||||
import { MatButtonHarness } from '@angular/material/button/testing';
|
||||
import { FormCloudDisplayMode } from '../../../services/form-fields.interfaces';
|
||||
import { FormCloudDisplayMode, FormContent } from '../../../services/form-fields.interfaces';
|
||||
import { MatDialogHarness } from '@angular/material/dialog/testing';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
@@ -92,7 +92,10 @@ describe('StartProcessCloudComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [StartProcessCloudComponent, ReactiveFormsModule, StartProcessScreenCloudComponent],
|
||||
providers: [provideScreen(screenId, MockedTaskScreenCloudComponent)]
|
||||
providers: [
|
||||
provideScreen(screenId, MockedTaskScreenCloudComponent),
|
||||
{ provide: ADF_DISPLAY_TEXT_SETTINGS, useValue: { enableExpressionEvaluation: true } }
|
||||
]
|
||||
});
|
||||
processService = TestBed.inject(StartProcessCloudService);
|
||||
formCloudService = TestBed.inject(FormCloudService);
|
||||
@@ -417,6 +420,54 @@ describe('StartProcessCloudComponent', () => {
|
||||
expect(startBtn.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should keep the start action unavailable while the form definition is loading', async () => {
|
||||
const formDefinition = new Subject<FormContent>();
|
||||
formDefinitionSpy.and.returnValue(formDefinition);
|
||||
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
|
||||
await selectOptionByName('processwithform');
|
||||
|
||||
const startButton = fixture.nativeElement.querySelector('#button-start');
|
||||
expect(startButton).toBeNull();
|
||||
expect(startProcessWithFormSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should materialize unrendered rich text expressions when starting a process', async () => {
|
||||
formDefinitionSpy.and.returnValue(of(fakeStartForm));
|
||||
component.processDefinitionCurrent = fakeProcessDefinitions[2];
|
||||
component.processPayloadCloud.processDefinitionKey = fakeProcessDefinitions[2].key;
|
||||
component.processInstanceName.setValue('My process');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
const formElement = fixture.debugElement.query(By.css('adf-cloud-form'));
|
||||
const startButton = fixture.debugElement.query(By.css('#button-start'));
|
||||
|
||||
formElement.triggerEventHandler('formLoaded', form);
|
||||
fixture.detectChanges();
|
||||
startButton.triggerEventHandler('click', null);
|
||||
|
||||
expect(startProcessWithFormSpy).toHaveBeenCalledWith(
|
||||
component.appName,
|
||||
fakeProcessDefinitions[2].formKey,
|
||||
fakeProcessDefinitions[2].version,
|
||||
jasmine.objectContaining({
|
||||
values: jasmine.objectContaining({
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }] }
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to start a process with form full display mode', async () => {
|
||||
component.displayModeConfigurations = [
|
||||
{
|
||||
|
||||
+35
-3
@@ -30,10 +30,14 @@ import {
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ADF_DISPLAY_TEXT_SETTINGS,
|
||||
ConfirmDialogComponent,
|
||||
ContentLinkModel,
|
||||
DisplayTextWidgetSettings,
|
||||
FormExpressionService,
|
||||
FormModel,
|
||||
FormOutcomeEvent,
|
||||
FormValues,
|
||||
IconModule,
|
||||
InplaceFormInputComponent,
|
||||
LocalizedDatePipe,
|
||||
@@ -65,6 +69,7 @@ import { FormCustomOutcomesComponent } from '../../../form/components/form-cloud
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { StartProcessScreenCloudComponent } from '../../../screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component';
|
||||
import { TaskTypeResolverService } from '../../../services/task-type-resolver/task-type-resolver.service';
|
||||
import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from '../../../form/services/form-cloud-submission-values';
|
||||
|
||||
const MAX_NAME_LENGTH: number = 255;
|
||||
const PROCESS_DEFINITION_DEBOUNCE: number = 300;
|
||||
@@ -211,6 +216,11 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
private readonly hasVisibleOutcomesSubject = new BehaviorSubject<boolean>(false);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly taskTypeResolverService = inject(TaskTypeResolverService);
|
||||
private readonly expressions = inject(FormExpressionService);
|
||||
private readonly displayTextSettings = inject<Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings>(ADF_DISPLAY_TEXT_SETTINGS, {
|
||||
optional: true
|
||||
});
|
||||
private enableExpressionEvaluation = false;
|
||||
|
||||
private screenSubmitPayload: unknown;
|
||||
|
||||
@@ -218,8 +228,12 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
showCompleteButton = false;
|
||||
|
||||
get isProcessFormValid(): boolean {
|
||||
if (this.hasForm && this.isFormCloudLoaded) {
|
||||
return (this.formCloud ? !Object.keys(this.formCloud.values).length : false) || this.formCloud?.isValid || this.isProcessStarting;
|
||||
if (this.hasForm) {
|
||||
if (!this.isFormCloudLoaded || !this.formCloud) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !Object.keys(this.formCloud.values).length || this.formCloud.isValid || this.isProcessStarting;
|
||||
} else if (this.hasScreen) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -268,6 +282,12 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
constructor() {
|
||||
this.startProcessButtonLabel = this.defaultStartProcessButtonLabel;
|
||||
this.cancelButtonLabel = this.defaultCancelProcessButtonLabel;
|
||||
|
||||
getExpressionEvaluationEnabled$(this.displayTextSettings)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((enabled) => {
|
||||
this.enableExpressionEvaluation = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -482,6 +502,14 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
}
|
||||
|
||||
startProcessWithoutConfirmation() {
|
||||
let submissionValues = this.screenSubmitPayload;
|
||||
if (this.hasForm) {
|
||||
if (!this.formCloud) {
|
||||
return;
|
||||
}
|
||||
submissionValues = this.getFormSubmissionValues(this.formCloud);
|
||||
}
|
||||
|
||||
this.isProcessStarting = true;
|
||||
|
||||
let action: Observable<ProcessInstanceCloud>;
|
||||
@@ -495,7 +523,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
processName: this.processInstanceName.value,
|
||||
processDefinitionKey: this.processPayloadCloud.processDefinitionKey,
|
||||
variables: this.variables ?? {},
|
||||
values: this.hasForm ? this.formCloud.values : this.screenSubmitPayload,
|
||||
values: submissionValues,
|
||||
outcome: this.customOutcomeName
|
||||
})
|
||||
);
|
||||
@@ -524,6 +552,10 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private getFormSubmissionValues(form: FormModel): FormValues {
|
||||
return materializeSubmissionValues(form, { enableExpressionEvaluation: this.enableExpressionEvaluation }, this.expressions);
|
||||
}
|
||||
|
||||
startProcess() {
|
||||
if (!this.formCloud?.confirmMessage?.show) {
|
||||
this.startProcessWithoutConfirmation();
|
||||
|
||||
Reference in New Issue
Block a user