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 { 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: This file is part of the infrastructure component responsible for working with Redux and managing the creation and update of Redux containers and PConnect. * You may override Material components within this component if needed, but do not modify any container-related logic. Changing this logic can lead to unexpected behavior. */ interface FlowContainerProps { // If any, enter additional props that only exist on this component children?: any[]; name?: string; routingInfo?: any; pageMessages: any[]; } @Component({ selector: 'app-flow-container', templateUrl: './flow-container.component.html', styleUrls: ['./flow-container.component.scss'], providers: [Utils], imports: [CommonModule, MatCardModule, forwardRef(() => ComponentMapperComponent)] }) export class FlowContainerComponent extends FlowContainerBaseComponent implements OnInit, OnDestroy { @Input() 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; isMultiStep: any; 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; this.localeReference = this.pConn$?.getCaseLocaleReference(); // 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" 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 !!this.todo_pConn$; } 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 ); this.isMultiStep = this.utils.getBooleanValue(oWorkData.caseInfo.assignments?.[0]?.isMultiStep); } // 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 = []; 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 = 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) { console.log(ex); } // 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(); } }); 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 = 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 }