@pega/dx-component-builder-sdk
Version:
Utility for building custom UI components
613 lines (497 loc) • 21.5 kB
text/typescript
import { Component, OnInit, Input, ChangeDetectorRef, NgZone, forwardRef, OnDestroy, Injector } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { publicConstants } from '@pega/pcore-pconnect-typedefs/constants';
import { ProgressSpinnerService } from '@pega/angular-sdk-components';
import { ReferenceComponent } from '@pega/angular-sdk-components';
import { Utils } from '@pega/angular-sdk-components';
import { getToDoAssignments, hasAssignments, showBanner } from './helpers';
import { ComponentMapperComponent } from '@pega/angular-sdk-components';
import { FlowContainerBaseComponent } from '@pega/angular-sdk-components';
/**
* WARNING: It is not expected that this file should be modified. It is part of infrastructure code that works with
* Redux and creation/update of Redux containers and PConnect. Modifying this code could have undesireable results and
* is totally at your own risk.
*/
interface FlowContainerProps {
// If any, enter additional props that only exist on this component
children?: any[];
name?: string;
routingInfo?: any;
pageMessages: any[];
}
export class FlowContainerComponent extends FlowContainerBaseComponent implements OnInit, OnDestroy {
pConn$: typeof PConnect;
pCoreConstants: typeof publicConstants;
configProps$: FlowContainerProps;
formGroup$: FormGroup;
arChildren$: any[];
itemKey$ = '';
containerName$: string;
buildName$: string;
// todo
todo_showTodo$ = false;
todo_caseInfoID$: string;
todo_showTodoList$ = false;
todo_datasource$: any;
todo_headerText$ = 'To do';
todo_type$: string;
todo_context$: string;
todo_pConn$: typeof PConnect;
bHasCancel = false;
// messages
caseMessages$: string;
bHasCaseMessages$ = false;
checkSvg$: string;
TODO: any;
bShowConfirm = false;
bShowBanner: boolean;
confirm_pconn: any;
localizedVal: any;
localeCategory = 'Messages';
localeReference: any;
banners: any[];
// itemKey: string = ""; // JA - this is what Nebula/Constellation uses to pass to finishAssignment, navigateToStep
pConnectOfActiveContainerItem;
constructor(
injector: Injector,
private cdRef: ChangeDetectorRef,
private psService: ProgressSpinnerService,
private fb: FormBuilder,
private ngZone: NgZone,
private utils: Utils
) {
super(injector);
// create the formGroup
this.formGroup$ = this.fb.group({ hideRequired: false });
}
ngOnInit() {
// First thing in initialization is registering and subscribing to the AngularPConnect service
this.angularPConnectData = this.angularPConnect.registerAndSubscribeComponent(this, this.onStateChange);
this.localizedVal = PCore.getLocaleUtils().getLocaleValue;
const caseInfo = this.pConn$.getCaseInfo();
this.localeReference = `${caseInfo?.getClassName()}!CASE!${caseInfo.getName()}`.toUpperCase();
// Then, continue on with other initialization
// get the PCore constants
this.pCoreConstants = PCore.getConstants();
const { TODO } = this.pCoreConstants;
this.TODO = TODO;
// with init, force children to be loaded of global pConn
this.initComponent(true);
this.initContainer();
this.checkAndUpdate();
PCore.getPubSubUtils().subscribe(
PCore.getConstants().PUB_SUB_EVENTS.EVENT_CANCEL,
() => {
this.handleCancel();
},
'cancelAssignment'
);
PCore.getPubSubUtils().subscribe(
'cancelPressed',
() => {
this.handleCancelPressed();
},
'cancelPressed'
);
PCore.getPubSubUtils().subscribe(
'clearBannerMessages',
() => {
this.banners = [];
},
'clearBannerMessages'
);
}
ngOnDestroy() {
if (this.angularPConnectData.unsubscribeFn) {
this.angularPConnectData.unsubscribeFn();
}
PCore.getPubSubUtils().unsubscribe(PCore.getConstants().PUB_SUB_EVENTS.EVENT_CANCEL, 'cancelAssignment');
PCore.getPubSubUtils().unsubscribe('cancelPressed', 'cancelPressed');
PCore.getPubSubUtils().unsubscribe('clearBannerMessages', 'clearBannerMessages');
}
handleCancel() {
// cancel happened, so ok to initialize the flow container
sessionStorage.setItem('okToInitFlowContainer', 'true');
}
handleCancelPressed() {
this.bHasCancel = true;
}
// Callback passed when subscribing to store change
onStateChange() {
this.checkAndUpdate();
}
checkAndUpdate() {
// Should always check the bridge to see if the component should update itself (re-render)
const bUpdateSelf = this.angularPConnect.shouldComponentUpdate(this);
const pConn = this.pConnectOfActiveContainerItem || this.pConn$;
const caseViewModeFromProps = this.angularPConnect.getComponentProp(this, 'caseViewMode');
const caseViewModeFromRedux = pConn.getValue('context_data.caseViewMode', '');
const completeProps = this.angularPConnect.getCurrentCompleteProps(this) as FlowContainerProps;
// ONLY call updateSelf when the component should update
// AND removing the "gate" that was put there since shouldComponentUpdate
// should be the real "gate"
// eslint-disable-next-line sonarjs/no-collapsible-if
if (bUpdateSelf || caseViewModeFromProps !== caseViewModeFromRedux) {
// don't want to redraw the flow container when there are page messages, because
// the redraw causes us to loose the errors on the elements
if (!completeProps.pageMessages || completeProps.pageMessages.length == 0) {
// with a cancel, need to timeout so todo will update correctly
if (this.bHasCancel) {
this.bHasCancel = false;
setTimeout(() => {
this.updateSelf();
}, 500);
} else {
this.updateSelf();
}
}
}
this.showPageMessages(completeProps);
}
showPageMessages(completeProps: FlowContainerProps) {
this.ngZone.run(() => {
const pageMessages = completeProps.pageMessages;
this.banners = [{ messages: pageMessages?.map(msg => this.localizedVal(msg.message, 'Messages')), variant: 'urgent' }];
});
}
getTodoVisibilty() {
const caseViewMode = this.pConn$.getValue('context_data.caseViewMode');
if (caseViewMode && caseViewMode === 'review') {
const kid = this.pConn$.getChildren()[0];
const todoKid = kid.getPConnect().getChildren()[0];
this.todo_pConn$ = todoKid.getPConnect();
return true;
}
return !(caseViewMode && caseViewMode === 'perform');
}
initContainer() {
const containerMgr = this.pConn$.getContainerManager();
const baseContext = this.pConn$.getContextName();
const containerName = this.pConn$.getContainerName();
const containerType = 'single';
const flowContainerTarget = `${baseContext}/${containerName}`;
const isContainerItemAvailable = PCore.getContainerUtils().getActiveContainerItemName(flowContainerTarget);
// clear out since we are initializing
sessionStorage.setItem('okToInitFlowContainer', 'false');
if (!isContainerItemAvailable) {
containerMgr.initializeContainers({
type: containerType
});
/* remove commented out code when update React/WC
*** instead of getting values here to pass to addContainerItem, we call the function below "addContainerItem"
*** which comes from flow container helpers in Nebula
*/
// containerMgr.addContainerItem({
// semanticURL: "",
// key: this.pConn$.getValue("key"),
// flowName: this.pConn$.getValue("flowName"),
// caseViewMode: "perform",
// data: this.pConn$.getDataObject(baseContext),
// containerType
// });
this.addContainerItem(this.pConn$);
}
}
initComponent(bLoadChildren: boolean) {
this.configProps$ = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()) as FlowContainerProps;
this.showPageMessages(this.configProps$);
// when true, update arChildren from pConn, otherwise, arChilren will be updated in updateSelf()
if (bLoadChildren) {
this.arChildren$ = this.pConn$.getChildren();
}
// const oData = this.pConn$.getDataObject();
// const activeActionLabel: string = '';
// const { getPConnect } = this.arChildren$[0].getPConnect();
this.todo_showTodo$ = this.getTodoVisibilty();
// create pointers to functions
// const containerMgr = this.pConn$.getContainerManager();
// const actionsAPI = this.pConn$.getActionsApi();
const baseContext = this.pConn$.getContextName();
const acName = this.pConn$.getContainerName();
if (this.itemKey$ === '') {
this.itemKey$ = baseContext.concat('/').concat(acName);
}
this.pConn$.isBoundToState();
// inside
// get fist kid, get the name and display
// pass first kid to a view container, which will disperse it to a view which will use one column, two column, etc.
const oWorkItem = this.arChildren$[0].getPConnect();
// const oWorkMeta = oWorkItem.getRawMetadata();
const oWorkData = oWorkItem.getDataObject();
// this.containerName$ = oWorkMeta["name"];
if (bLoadChildren && oWorkData) {
this.containerName$ = this.localizedVal(this.getActiveViewLabel() || oWorkData.caseInfo.assignments[0].name, undefined, this.localeReference);
}
// turn off spinner
this.psService.sendMessage(false);
}
isCaseWideLocalAction() {
const actionID = this.pConn$.getValue(this.pCoreConstants.CASE_INFO.ACTIVE_ACTION_ID);
const caseActions = this.pConn$.getValue(this.pCoreConstants.CASE_INFO.AVAILABLEACTIONS);
let bCaseWideAction = false;
if (caseActions && actionID) {
const actionObj = caseActions.find(caseAction => caseAction.ID === actionID);
if (actionObj) {
bCaseWideAction = actionObj.type === 'Case';
}
}
return bCaseWideAction;
}
hasChildCaseAssignments() {
const childCases = this.pConn$.getValue(this.pCoreConstants.CASE_INFO.CHILD_ASSIGNMENTS);
return childCases && childCases.length > 0;
}
getActiveViewLabel() {
let activeActionLabel = '';
const { CASE_INFO: CASE_CONSTS } = PCore.getConstants();
const caseActions = this.pConn$.getValue(CASE_CONSTS.CASE_INFO_ACTIONS) as any[];
const activeActionID = this.pConn$.getValue(CASE_CONSTS.ACTIVE_ACTION_ID);
const activeAction = caseActions?.find(action => action.ID === activeActionID);
if (activeAction) {
activeActionLabel = activeAction.name;
}
return activeActionLabel;
}
findCurrentIndicies(arStepperSteps: any[], arIndicies: number[], depth: number): number[] {
let count = 0;
arStepperSteps.forEach(step => {
if (step.visited_status == 'current') {
arIndicies[depth] = count;
// add in
step.step_status = '';
} else if (step.visited_status == 'success') {
count++;
step.step_status = 'completed';
} else {
count++;
step.step_status = '';
}
if (step.steps) {
arIndicies = this.findCurrentIndicies(step.steps, arIndicies, depth + 1);
}
});
return arIndicies;
}
// Called when bridge shouldComponentUpdate indicates that this component
// should update itself (re-render)
updateSelf() {
// for now
// const { getPConnect } = this.arChildren$[0].getPConnect();
const localPConn = this.arChildren$[0].getPConnect();
this.pConnectOfActiveContainerItem = this.getPConnectOfActiveContainerItem(this.pConn$) || this.pConn$;
const caseViewMode = this.pConnectOfActiveContainerItem.getValue('context_data.caseViewMode');
this.bShowBanner = showBanner(this.pConn$);
if (caseViewMode && caseViewMode == 'review') {
this.loadReviewPage(localPConn);
// in Nebula/Constellation, when cancel is called, somehow the constructor for flowContainer is called which
// does init/add of containers. This mimics that
if (sessionStorage.getItem('okToInitFlowContainer') == 'true') {
this.initContainer();
}
} else if (caseViewMode && caseViewMode === 'perform') {
// perform
this.todo_showTodo$ = false;
// this is different than Angular SDK, as we need to initContainer if root container reloaded
if (sessionStorage.getItem('okToInitFlowContainer') == 'true') {
this.initContainer();
}
}
// if have caseMessage show message and end
this.showCaseMessages();
this.updateFlowContainerChildren();
}
loadReviewPage(localPConn) {
const { CASE_INFO: CASE_CONSTS } = PCore.getConstants();
setTimeout(() => {
this.ngZone.run(() => {
/*
*** renove this commmented out code when React/WC is updated
*** this code is replace with the call to "getToDoAssigments" function below
const assignmentsList = localPConn.getValue(
CASE_CONSTS.D_CASE_ASSIGNMENTS_RESULTS
);
// add status
const status = localPConn.getValue("caseInfo.status");
let localAssignment = JSON.parse(JSON.stringify(assignmentsList[0]));
localAssignment.status = status;
let locaAssignmentsList: Array<any> = [];
locaAssignmentsList.push(localAssignment);
const caseActions = localPConn.getValue(CASE_CONSTS.CASE_INFO_ACTIONS);
*/
const todoAssignments = getToDoAssignments(this.pConn$);
if (todoAssignments && todoAssignments.length > 0) {
this.todo_caseInfoID$ = this.pConn$.getValue(CASE_CONSTS.CASE_INFO_ID);
this.todo_datasource$ = { source: todoAssignments };
}
/* remove this commented out code when update React/WC */
// let kid = this.pConn$.getChildren()[0];
// kid.getPConnect() can be a Reference component. So normalize it just in case
// let todoKid = ReferenceComponent.normalizePConn(kid.getPConnect()).getChildren()[0];
// this.todo_pConn$ = todoKid.getPConnect();
/* code change here to note for React/WC */
// todo now needs pConn to open the work item on click "go"
this.todo_pConn$ = this.pConn$;
// still needs the context of the original work item
this.todo_context$ = localPConn.getContextName();
this.todo_showTodo$ = true;
this.psService.sendMessage(false);
});
});
}
showCaseMessages() {
this.caseMessages$ = this.localizedVal(this.pConn$.getValue('caseMessages'), this.localeCategory);
// caseMessages's behavior has changed in 24.2, and hence it doesn't let Optional Action work.
// Changing the below condition for now. Was: (theCaseMessages || !hasAssignments())
if (!hasAssignments(this.pConn$)) {
this.bHasCaseMessages$ = true;
this.bShowConfirm = true;
this.checkSvg$ = this.utils.getImageSrc('check', this.utils.getSDKStaticContentUrl());
// Temp fix for 8.7 change: confirmationNote no longer coming through in caseMessages$.
// So, if we get here and caseMessages$ is empty, use default value in DX API response
if (!this.caseMessages$) {
this.caseMessages$ = this.localizedVal('Thank you! The next step in this case has been routed appropriately.', this.localeCategory);
}
this.psService.sendMessage(false);
} else {
this.bHasCaseMessages$ = false;
this.bShowConfirm = false;
}
}
updateFlowContainerChildren() {
// routingInfo was added as component prop in populateAdditionalProps
const routingInfo = this.angularPConnect.getComponentProp(this, 'routingInfo');
this.confirm_pconn = null;
let loadingInfo: any;
try {
// @ts-ignore - Property 'getLoadingStatus' is private and only accessible within class 'C11nEnv'
loadingInfo = this.pConn$.getLoadingStatus();
} catch (ex) {
/* empty */
}
// this check in routingInfo, mimic Nebula/Constellation (React) to check and get the internals of the
// flowContainer and force updates to pConnect/redux
if (routingInfo && loadingInfo !== undefined) {
const currentOrder = routingInfo.accessedOrder;
const currentItems = routingInfo.items;
const type = routingInfo.type;
if (currentOrder && currentItems) {
// JA - making more similar to Nebula/Constellation
const key = currentOrder[currentOrder.length - 1];
// save off itemKey to be used for finishAssignment, etc.
// timeout and detectChanges to avoid ExpressionChangedAfterItHasBeenCheckedError
setTimeout(() => {
if (key && key != '') {
this.itemKey$ = key;
this.cdRef.detectChanges();
}
});
// eslint-disable-next-line sonarjs/no-collapsible-if
if (currentOrder.length > 0) {
if (currentItems[key] && currentItems[key].view && type === 'single' && Object.keys(currentItems[key].view).length > 0) {
// when we get here, it it because the flow action data has changed
// from the server, and need to add to pConnect and update children
this.addPConnectAndUpdateChildren(currentItems[key], key);
}
}
}
}
}
addPConnectAndUpdateChildren(currentItem, key) {
const localPConn = this.arChildren$[0].getPConnect();
const rootView = currentItem.view;
const { context, name: ViewName } = rootView.config;
const config: any = { meta: rootView };
// Don't go ahead if View doesn't exist
if (!ViewName) {
return;
}
this.todo_context$ = currentItem.context;
config.options = {
context: currentItem.context,
pageReference: context || localPConn.getPageReference(),
hasForm: true,
isFlowContainer: true,
containerName: localPConn.getContainerName(),
containerItemName: key,
parentPageReference: localPConn.getPageReference()
};
const configObject = PCore.createPConnect(config);
this.confirm_pconn = configObject.getPConnect();
// 8.7 - config might be a Reference component so, need to normalize it to get
// the View if it is a Reference component. And need to pass in the getPConnect
// to have normalize do a c11Env createComponent (that makes sure options.hasForm
// is passed along to all the component's children)
const normalizedConfigObject = ReferenceComponent.normalizePConn(configObject.getPConnect());
// We want the children to be the PConnect itself, not the result of calling getPConnect(),
// So need to get the PConnect of the normalized component we just created...
const normalizedConfigObjectAsPConnect = normalizedConfigObject.getComponent();
// makes sure Angular tracks these changes
this.ngZone.run(() => {
this.buildName$ = this.getBuildName();
// what comes back now in configObject is the children of the flowContainer
this.arChildren$ = [];
this.arChildren$.push(normalizedConfigObjectAsPConnect);
this.psService.sendMessage(false);
const oWorkItem = configObject.getPConnect();
const oWorkData: any = oWorkItem.getDataObject();
this.containerName$ = this.localizedVal(this.getActiveViewLabel() || oWorkData.caseInfo.assignments?.[0].name, undefined, this.localeReference);
});
}
getBuildName(): string {
// let { getPConnect, name } = this.pConn$.props;
const context = this.pConn$.getContextName();
let viewContainerName = this.pConn$.getContainerName();
if (!viewContainerName) viewContainerName = '';
return `${context.toUpperCase()}/${viewContainerName.toUpperCase()}`;
}
formValid(): boolean {
this.touchAll();
return this.formGroup$.valid;
}
touchAll(): void {
Object.values(this.formGroup$.controls).forEach(control => {
control.markAsTouched();
});
}
topViewRefresh(): void {
Object.values(this.formGroup$.controls).forEach(control => {
control.markAsTouched();
});
}
// helpers - copyied from flow container helpers.js
addContainerItem(pConnect) {
// copied from flow container helper.js
const containerManager = pConnect.getContainerManager();
const contextName = pConnect.getContextName(); // here we will get parent context name, as flow container is child of view container
const caseViewMode = pConnect.getValue('context_data.caseViewMode');
let key;
let flowName;
if (caseViewMode !== 'review') {
const target = contextName.substring(0, contextName.lastIndexOf('_'));
const activeContainerItemID = PCore.getContainerUtils().getActiveContainerItemName(target);
const containerItemData = PCore.getContainerUtils().getContainerItemData(target, activeContainerItemID);
if (containerItemData) {
({ key, flowName } = containerItemData);
}
}
containerManager.addContainerItem({
semanticURL: '',
key,
flowName,
caseViewMode: 'perform',
resourceType: 'ASSIGNMENT',
data: pConnect.getDataObject(contextName)
});
}
// helpers end
}