From 93fdcdbeeb8aca2ce5db810a2bb97286fbe88bd6 Mon Sep 17 00:00:00 2001 From: manasa Date: Wed, 23 Sep 2026 17:38:05 +0530 Subject: [PATCH 1/4] feat(auto-complete): add create new record support --- .../src/lib/_bridge/angular-pconnect.ts | 1 + .../auto-complete.component.html | 13 + .../auto-complete.component.scss | 34 ++ .../auto-complete/auto-complete.component.ts | 163 +++++++- .../cancel-alert/cancel-alert.component.ts | 53 ++- .../modal-view-container.component.html | 47 ++- .../modal-view-container.component.ts | 362 ++++++++---------- sdk-config.json | 6 +- 8 files changed, 441 insertions(+), 238 deletions(-) diff --git a/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts b/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts index da8d80a0..fcb99760 100644 --- a/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts +++ b/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts @@ -9,6 +9,7 @@ export interface AngularPConnectData { compID?: string; unsubscribeFn?: Function; validateMessage?: string; + httpMessages?: any[]; actions?: { onChange: Function; onBlur: Function; diff --git a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html index a72d8b4d..0e98542a 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html +++ b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html @@ -46,6 +46,19 @@ +
+ + +
{{ helperText }} {{ getErrorMessage() }} diff --git a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss index d6f98a9e..cf168394 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss +++ b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss @@ -79,3 +79,37 @@ white-space: normal; } } + +// Footer row rendered inside the mat-autocomplete overlay panel (outside this component's view, +// hence ::ng-deep) — mirrors the MUI CustomPaper divider + "Create new" button pattern. +// Sticky (not scrolled) since it's a direct child of the panel's own scroll container. +::ng-deep .psdk-autocomplete-create-new-wrapper { + position: sticky; + bottom: 0; + z-index: 1; + background-color: var(--mat-sys-surface-container, var(--mat-sys-surface, #fff)); + + .mat-divider { + margin: 0; + } + + .psdk-autocomplete-create-new { + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + height: 48px; + padding: 0 16px; + text-align: left; + text-transform: none; + color: var(--mat-sys-primary); + + .mat-mdc-button-touch-target { + width: 100%; + } + + .mat-icon { + margin-right: 8px; + } + } +} diff --git a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts index ed05bfc2..6ab6840b 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts @@ -1,10 +1,13 @@ -import { Component, EventEmitter, OnInit, Output, forwardRef, inject } from '@angular/core'; +import { Component, EventEmitter, OnInit, Output, ViewChild, forwardRef, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatOptionModule } from '@angular/material/core'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatAutocompleteModule, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDividerModule } from '@angular/material/divider'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; @@ -41,6 +44,12 @@ interface AutoCompleteProps extends PConnFieldProps { parameters?: any; datasource: any; columns: any[]; + allowCreatingRecords?: boolean; + onCreateNew?: () => void; + createNewLabel?: string; + createNewRecord?: () => Promise; + contextClass?: string; + referenceType?: string; } @Component({ @@ -54,6 +63,9 @@ interface AutoCompleteProps extends PConnFieldProps { MatInputModule, MatAutocompleteModule, MatOptionModule, + MatButtonModule, + MatIconModule, + MatDividerModule, FieldWarningDirective, forwardRef(() => ComponentMapperComponent) ], @@ -64,18 +76,31 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { @Output() onRecordChange: EventEmitter = new EventEmitter(); + // The input's MatAutocompleteTrigger — used to close the options panel before navigating away + // (e.g. opening the create-new modal), so it doesn't remain open on top of it. + @ViewChild(MatAutocompleteTrigger) private autocompleteTrigger?: MatAutocompleteTrigger; + configProps$: AutoCompleteProps; options$: AutoCompleteOption[]; listType: string; columns: any[] = []; parameters: {}; + datasource: any; filteredOptions: Observable; // Grouped view of filteredOptions, only rendered when hasGroupBy is true groupedFilteredOptions$: Observable; hasGroupBy = false; filterValue = ''; + // "Create new" footer button state + showCreateButton = false; + createNewLabel = 'Create new'; + contextClass?: string; + referenceType?: string; + private onCreateNewFn?: () => void; + private createNewRecordFn?: () => Promise; + // Override ngOnInit method override async ngOnInit(): Promise { super.ngOnInit(); @@ -127,13 +152,22 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { this.updateComponentCommonProperties(this.configProps$); // Set component specific properties - const { value, listType, parameters } = this.configProps$; + const { value, listType, parameters, allowCreatingRecords, onCreateNew, createNewLabel, createNewRecord, contextClass, referenceType } = + this.configProps$; this.listType = listType; this.parameters = parameters; + this.showCreateButton = allowCreatingRecords === true; + this.createNewLabel = createNewLabel || 'Create new'; + this.contextClass = contextClass; + this.referenceType = referenceType; + this.onCreateNewFn = onCreateNew; + this.createNewRecordFn = createNewRecord; + const context = this.pConn$.getContextName(); const { columns, datasource } = this.generateColumnsAndDataSource(); + this.datasource = datasource; if (columns) { this.columns = this.preProcessColumns(columns); @@ -392,4 +426,127 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { this.onRecordChange.emit(value); } } + + // Re-fetches the options list (equivalent to initializeList in constellation-frontend) + refreshOptionsList(): void { + if (!this.displayMode$ && this.listType !== 'associated') { + const context = this.pConn$.getContextName(); + this.dataPageService + .getDataPageData(this.datasource, this.parameters, context) + .then((results: any) => this.fillOptions(results)) + .catch(e => console.error(e)); + } + } + + // Sets values for all columns that have setProperty defined (mirrors setValuesToOtherAdditionalFields in constellation-frontend) + setValuesToAdditionalFields(record: Record): void { + const setPropertyList = this.columns.filter(col => col.setProperty).map(col => ({ source: col.value, target: col.setProperty, key: col.key })); + + setPropertyList.forEach(prop => { + let valueToSet: string; + if (prop.key === 'true') { + valueToSet = record[prop.source]?.toString() || (record as any).pyGUID || ''; + } else { + valueToSet = record[prop.source]?.toString() || ''; + } + + if (prop.target === 'Associated property') { + handleEvent(this.actionsApi, 'changeNblur', this.propName, valueToSet); + } else { + const target = typeof prop.target === 'string' ? prop.target : ''; + const targetProp = target.startsWith('.') ? target : `.${target}`; + (this.actionsApi as any).updateFieldValue(targetProp, valueToSet, { associatedProperty: this.propName }); + (this.actionsApi as any).triggerFieldChange(targetProp, valueToSet); + } + }); + } + + createNewButtonHandler(): void { + // Close the options panel so it doesn't remain open on top of the create-new modal + this.autocompleteTrigger?.closePanel(); + + if (this.onCreateNewFn) { + this.onCreateNewFn(); + return; + } + + if (!this.contextClass) { + return; + } + + const context = this.pConn$.getContextName(); + const normalizedReferenceType = typeof this.referenceType === 'string' ? this.referenceType.toLowerCase() : ''; + const isDataReference = normalizedReferenceType === 'data'; + const { CREATE_STAGE_DONE } = PCore.getConstants().PUB_SUB_EVENTS.CASE_EVENTS; + const DATA_OBJECT_CREATED = (PCore.getConstants().PUB_SUB_EVENTS as any).DATA_EVENTS?.DATA_OBJECT_CREATED; + const eventType = isDataReference && DATA_OBJECT_CREATED ? DATA_OBJECT_CREATED : CREATE_STAGE_DONE; + const contextClass = this.contextClass; + + const createNewCallback = isDataReference + ? (data: { data?: { responseData?: Record } }) => { + // Clear contexted cache before re-fetching + PCore.getDataApi().clearContextedCache(context); + + const responseData = data?.data?.responseData; + if (responseData) { + this.setValuesToAdditionalFields(responseData); + const displayColumn = this.getDisplayFieldsMetaData(this.columns); + const newKey = responseData[displayColumn.key]?.toString() || (responseData as any).pyGUID; + if (this.onRecordChange && newKey) { + this.onRecordChange.emit({ id: newKey }); + } + } + + this.refreshOptionsList(); + PCore.getPubSubUtils().unsubscribe(eventType, contextClass); + } + : (data: { caseId?: string; caseType?: string; ID?: string }) => { + // Clear contexted cache before re-fetching + PCore.getDataApi().clearContextedCache(context); + + const newCaseId = data.caseId?.split(' ').pop(); + if (data.caseType === contextClass) { + const selectKey = data.ID || newCaseId; + + if (selectKey && this.listType !== 'associated' && this.datasource) { + this.dataPageService + .getDataPageData(this.datasource, this.parameters, context) + .then((results: any) => { + this.fillOptions(results); + + const displayColumn = this.getDisplayFieldsMetaData(this.columns); + const newRecord = results?.find((el: any) => el.ID === data.ID || (el[displayColumn.key] || el.pyGUID) === selectKey); + if (newRecord) { + this.setValuesToAdditionalFields(newRecord); + } else { + handleEvent(this.actionsApi, 'changeNblur', this.propName, selectKey); + } + if (this.onRecordChange) { + this.onRecordChange.emit({ id: selectKey }); + } + }) + .catch(e => console.error(e)); + } + PCore.getPubSubUtils().unsubscribe(eventType, contextClass); + } + }; + + // Build the create action if createNewRecord fn is not provided + const triggerCreate = this.createNewRecordFn + ? this.createNewRecordFn() + : isDataReference + ? this.pConn$.getActionsApi().showDataObjectCreateView(contextClass) + : this.pConn$.getActionsApi().createWork(contextClass, { + openCaseViewAfterCreate: false, + startingFields: {} + }); + + Promise.resolve(triggerCreate) + .then(() => { + PCore.getPubSubUtils().subscribe(eventType, createNewCallback, contextClass); + // Re-initialize the list (equivalent to initializeList() in constellation-frontend) + this.refreshOptionsList(); + }) + .catch(e => console.error(e)); + } } diff --git a/packages/angular-sdk-components/src/lib/_components/field/cancel-alert/cancel-alert.component.ts b/packages/angular-sdk-components/src/lib/_components/field/cancel-alert/cancel-alert.component.ts index afda0cd6..2c672751 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/cancel-alert/cancel-alert.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/cancel-alert/cancel-alert.component.ts @@ -14,6 +14,9 @@ import { ComponentMapperComponent } from '../../../_bridge/component-mapper/comp export class CancelAlertComponent implements OnChanges { @Input() pConn$: typeof PConnect; @Input() bShowAlert$: boolean; + @Input() hideDelete: boolean; + @Input() isDataObject: boolean; + @Input() skipReleaseLockRequest: any; @Output() onAlertState$: EventEmitter = new EventEmitter(); itemKey: string; @@ -61,7 +64,6 @@ export class CancelAlertComponent implements OnChanges { } buttonClick({ action }) { - const actionsAPI = this.pConn$.getActionsApi(); this.localizedVal = PCore.getLocaleUtils().getLocaleValue; switch (action) { @@ -70,23 +72,44 @@ export class CancelAlertComponent implements OnChanges { break; case 'discard': this.psService.sendMessage(true); - - // eslint-disable-next-line no-case-declarations - const deletePromise = actionsAPI.deleteCaseInCreateStage(this.itemKey); - - deletePromise - .then(() => { - this.psService.sendMessage(false); - this.dismissAlert(); - PCore.getPubSubUtils().publish(PCore.getConstants().PUB_SUB_EVENTS.EVENT_CANCEL); - }) - .catch(() => { - this.psService.sendMessage(false); - this.sendMessage(this.localizedVal('Delete failed.', this.localeCategory)); - }); + this.handleDiscard(); break; default: break; } } + + // Data objects and local/bulk actions don't have a create-stage case to delete, so each needs its own engine API + handleDiscard() { + const actionsAPI = this.pConn$.getActionsApi(); + // @ts-ignore - Property 'options' is private and only accessible within class 'C11nEnv'. + const isBulkAction = (this.pConn$ as any)?.options?.isBulkAction; + const isLocalAction = this.pConn$.getValue(PCore.getConstants().CASE_INFO.IS_LOCAL_ACTION); + + if (!this.isDataObject && !isLocalAction && !isBulkAction) { + actionsAPI + .deleteCaseInCreateStage(this.itemKey, this.hideDelete) + .then(() => { + this.psService.sendMessage(false); + this.dismissAlert(); + PCore.getPubSubUtils().publish(PCore.getConstants().PUB_SUB_EVENTS.EVENT_CANCEL); + }) + .catch(() => { + this.psService.sendMessage(false); + this.sendMessage(this.localizedVal('Delete failed.', this.localeCategory)); + }); + } else if (isLocalAction) { + this.psService.sendMessage(false); + this.dismissAlert(); + actionsAPI.cancelAssignment(this.itemKey, false); + } else if (isBulkAction) { + this.psService.sendMessage(false); + this.dismissAlert(); + actionsAPI.cancelBulkAction(this.itemKey); + } else { + this.psService.sendMessage(false); + this.dismissAlert(); + this.pConn$.getContainerManager().removeContainerItem({ containerItemID: this.itemKey, skipReleaseLockRequest: this.skipReleaseLockRequest }); + } + } } diff --git a/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.html b/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.html index ab426b3c..b0f2105f 100644 --- a/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.html +++ b/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.html @@ -1,27 +1,38 @@ -
-
-

{{ title$ }}

+
+
+

{{ modal.title }}

+
+ +
-
+
-
+
@@ -31,7 +42,13 @@

{{ title$ }}

diff --git a/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.ts b/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.ts index dfa28bcf..10e23ca6 100755 --- a/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.ts @@ -1,7 +1,6 @@ import { ChangeDetectorRef, Component, OnInit, Input, Output, EventEmitter, forwardRef, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import isEqual from 'fast-deep-equal'; import { AngularPConnectData, AngularPConnectService } from '../../../../_bridge/angular-pconnect'; import { ProgressSpinnerService } from '../../../../_messages/progress-spinner.service'; import { ComponentMapperComponent } from '../../../../_bridge/component-mapper/component-mapper.component'; @@ -12,6 +11,22 @@ import { getBanners } from '../../../../_helpers/case-utils'; * 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. */ +// One entry per open modal — supports stacking (e.g. "Create new" opened from within another modal) +interface ModalEntry { + key: string; + title: string; + createdViewPConn$: any; + arChildren$: any[]; + isMultiRecordData: boolean; + isDataObjectModal: boolean; + dataRecordKeys: string; + dataObjectActionID: string; + dataObjectAction: string; + dataObjectClassId: string; + context: string; + updateToken: number; +} + @Component({ selector: 'app-modal-view-container', templateUrl: './modal-view-container.component.html', @@ -27,41 +42,26 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { // Used with AngularPConnect angularPConnectData: AngularPConnectData = {}; - arChildren$: any[]; stateProps$: object; - banners: any; - templateName$: string; - buildName$: string; - context$: string; - title$ = ''; - bShowModal$ = false; - itemKey$: string; formGroup$: FormGroup; - oCaseInfo: object = {}; - - // for causing a change on assignment - updateToken$ = 0; routingInfoRef: any = {}; - // created object is now a View with a Template - // Use its PConnect to render the CaseView; DON'T replace this.pConn$ - createdViewPConn$: any; + // Stack of open modals — supports one modal opening another (e.g. "Create new" from within a modal). + // Keyed the same as PCore's routingInfo.items so open/update/close can be derived from accessedOrder. + modalStack: ModalEntry[] = []; + private modalCollection: Record = {}; bSubscribed = false; cancelPConn$?: typeof PConnect; + cancelHideDelete$: boolean; + cancelIsDataObject$: boolean; + cancelSkipReleaseLockRequest$: any; bShowCancelAlert$ = false; bAlertState: boolean; localizedVal: Function; localeCategory = 'Data Object'; - isMultiRecord = false; actionsDialog = false; - // Single-record data object modals own the new footer; multi-record modals keep their own. - bIsDataObjectRecord$ = false; - dataObjectAction$ = ''; - dataObjectActionID$ = ''; - dataRecordKeys$ = ''; - dataObjectClassID$ = ''; constructor( private angularPConnect: AngularPConnectService, @@ -77,22 +77,12 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { // First thing in initialization is registering and subscribing to the AngularPConnect service this.angularPConnectData = this.angularPConnect.registerAndSubscribeComponent(this, this.onStateChange); - const baseContext = this.pConn$.getContextName(); - const acName = this.pConn$.getContainerName(); - - // for now, in general this should be overridden by updateSelf(), and not be blank - if (this.itemKey$ === '') { - this.itemKey$ = baseContext.concat('/').concat(acName); - } - const containerMgr = this.pConn$.getContainerManager(); containerMgr.initializeContainers({ type: 'multiple' }); - // const { CONTAINER_TYPE, PUB_SUB_EVENTS } = PCore.getConstants(); - this.angularPConnect.shouldComponentUpdate(this); this.localizedVal = PCore.getLocaleUtils().getLocaleValue; } @@ -115,15 +105,8 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { onStateChange() { // Should always check the bridge to see if the component should // update itself (re-render) - const bUpdateSelf = this.angularPConnect.shouldComponentUpdate(this); - - // ONLY call updateSelf when the component should update - if (bUpdateSelf) { + if (this.angularPConnect.shouldComponentUpdate(this)) { this.updateSelf(); - } else if (this.bShowModal$) { - // right now onlu get one updated when initial diaplay. So, once modal is up - // let fall through and do a check with "compareCaseInfoIsDifferent" until fixed - // this.updateSelf(); } } @@ -140,153 +123,102 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { } catch (ex) { console.log(ex); } - // const configProps = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()); this.stateProps$ = this.pConn$.getStateProps(); - this.banners = this.getBanners(); - if (!loadingInfo) { - // turn off spinner - // this.psService.sendMessage(false); + if (!routingInfo || loadingInfo) { + return; } - if (routingInfo && !loadingInfo /* && this.bUpdate */) { - const currentOrder = routingInfo.accessedOrder; + const { accessedOrder, type } = routingInfo; - if (undefined == currentOrder) { - return; - } + if (undefined == accessedOrder) { + return; + } - const currentItems = routingInfo.items; - - const { key, latestItem } = this.getKeyAndLatestItem(routingInfo); - - if (currentOrder.length > 0) { - if (currentItems[key] && currentItems[key].view && Object.keys(currentItems[key].view).length > 0) { - const currentItem = currentItems[key]; - const rootView = currentItem.view; - const { context } = rootView.config; - const config: any = { meta: rootView }; - config.options = { - context: currentItem.context, - hasForm: true, - pageReference: context || this.pConn$.getPageReference() - }; - - if (!this.bSubscribed) { - this.bSubscribed = true; - const { PUB_SUB_EVENTS } = PCore.getConstants(); - PCore.getPubSubUtils().subscribe( - PUB_SUB_EVENTS.EVENT_SHOW_CANCEL_ALERT, - payload => { - this.showAlert(payload); - }, - PUB_SUB_EVENTS.EVENT_SHOW_CANCEL_ALERT - ); - } - - // let configObject = PCore.createPConnect(config); - - // THIS is where the ViewContainer creates a View - // The config has meta.config.type = "view" - this.createView(routingInfo, currentItem, latestItem, key); + const { MULTIPLE } = PCore.getConstants().CONTAINER_TYPE; + const { key, latestItem } = this.getKeyAndLatestItem(routingInfo); + + if (latestItem && type === MULTIPLE && (this.isOpenModalAction(accessedOrder) || this.isUpdateModalAction(accessedOrder))) { + const currentItem = routingInfo.items[key]; + if (currentItem?.view && Object.keys(currentItem.view).length > 0) { + if (!this.bSubscribed) { + this.bSubscribed = true; + const { PUB_SUB_EVENTS } = PCore.getConstants(); + PCore.getPubSubUtils().subscribe( + PUB_SUB_EVENTS.EVENT_SHOW_CANCEL_ALERT, + payload => { + this.showAlert(payload); + }, + PUB_SUB_EVENTS.EVENT_SHOW_CANCEL_ALERT + ); } - } else { - this.hideModal(); - } - } - } - createView(routingInfo, currentItem, latestItem, key) { - const configObject = this.getConfigObject(currentItem, null, false); - const newComp = configObject?.getPConnect(); - // const newCompName = newComp.getComponentName(); - const caseInfo = newComp && newComp.getDataObject() && newComp.getDataObject().caseInfo ? newComp.getDataObject().caseInfo : null; - // The metadata for pyDetails changed such that the "template": "CaseView" - // is no longer a child of the created View but is in the created View's - // config. So, we DON'T want to replace this.pConn$ since the created - // component is a View (and not a ViewContainer). We now look for the - // "template" type directly in the created component (newComp) and NOT - // as a child of the newly created component. - // console.log(`---> ModalViewContainer created new ${newCompName}`); - - // Use the newly created component (View) info but DO NOT replace - // this ModalViewContainer's pConn$, etc. - // Note that we're now using the newly created View's PConnect in the - // ViewContainer HTML template to guide what's rendered similar to what - // the Nebula/Constellation return of React.Fragment does - - // right now need to check caseInfo for changes, to trigger redraw, not getting - // changes from angularPconnect except for first draw - if (newComp && caseInfo && this.compareCaseInfoIsDifferent(caseInfo)) { - this.psService.sendMessage(false); - - this.createdViewPConn$ = newComp; - const newConfigProps = newComp.getConfigProps(); - this.templateName$ = 'template' in newConfigProps ? (newConfigProps.template as string) : ''; - - const { actionName } = latestItem; - const theNewCaseInfo = newComp.getCaseInfo(); - // const caseName = theNewCaseInfo.getName(); - const ID = theNewCaseInfo.getBusinessID() || theNewCaseInfo.getID(); - - const caseTypeName = theNewCaseInfo.getCaseTypeName(); - const isDataObject = routingInfo.items[latestItem.context].resourceType === PCore.getConstants().RESOURCE_TYPES.DATA; - const dataObjectAction = routingInfo.items[latestItem.context].resourceStatus; - this.isMultiRecord = routingInfo.items[latestItem.context].isMultiRecordData; - this.context$ = latestItem.context; - this.dataObjectAction$ = dataObjectAction; - this.dataObjectActionID$ = routingInfo.items[latestItem.context].actionID ?? ''; - // `key` arrives JSON-serialised; DataViewActionButtons parses it before calling the APIs. - this.dataRecordKeys$ = latestItem.key ?? ''; - this.dataObjectClassID$ = newComp.getValue('.classID') ?? ''; - this.bIsDataObjectRecord$ = isDataObject && !this.isMultiRecord; - this.title$ = this.getHeadingValue( - latestItem, - isDataObject, - actionName, - dataObjectAction, - caseTypeName, - ID, - this.createdViewPConn$?.getCaseLocaleReference() - ); - - const bIsRefComponent = this.checkIfRefComponent(newComp); - - if (bIsRefComponent) { - this.arChildren$ = [newComp.getComponent()]; - } else { - // update children with new view's children - this.arChildren$ = newComp.getChildren(); + this.upsertModal(routingInfo, latestItem, key, accessedOrder); } - - this.bShowModal$ = true; - - // for when non modal - this.modalVisibleChange.emit(this.bShowModal$); - - // save off itemKey to be used for finishAssignment, etc. - this.itemKey$ = key; - - // cause a change for assignment - this.updateToken$ = new Date().getTime(); - this.cdRef.markForCheck(); + } else if (this.isCloseModalAction(accessedOrder)) { + this.handleModalClose(accessedOrder); } } - hideModal() { - if (this.bShowModal$) { - // other code in Nebula/Constellation not needed currently, but if so later, - // should put here + // Builds/refreshes one modal-stack entry and pushes or updates it in-place + upsertModal(routingInfo, latestItem, key, accessedOrder) { + const entry = this.buildModalEntry(routingInfo, latestItem, key); + + if (this.isUpdateModalAction(accessedOrder)) { + this.modalStack = this.modalStack.map(modal => (modal.key === key ? entry : modal)); + } else if (this.isOpenModalAction(accessedOrder)) { + this.handleModalOpen(key); + this.modalStack = [...this.modalStack, entry]; } - this.bShowModal$ = false; + this.psService.sendMessage(false); + this.modalVisibleChange.emit(this.modalStack.length > 0); + this.cdRef.markForCheck(); + } - // for when non modal - this.modalVisibleChange.emit(this.bShowModal$); + buildModalEntry(routingInfo, latestItem, key): ModalEntry { + const configObject = this.getConfigObject(latestItem, null, false); + // latestItem is only reached once its view metadata is populated (see updateSelf), so the + // created component is always present here — same assumption the old single-modal code made. + const newComp = configObject!.getPConnect(); + + const { actionName } = latestItem; + const theCaseInfo = newComp.getCaseInfo(); + const ID = theCaseInfo.getBusinessID() || theCaseInfo.getID(); + const caseTypeName = theCaseInfo.getCaseTypeName(); + + const isDataObject = routingInfo.items[latestItem.context].resourceType === PCore.getConstants().RESOURCE_TYPES.DATA; + const dataObjectAction = routingInfo.items[latestItem.context].resourceStatus; + const isMultiRecordData = routingInfo.items[latestItem.context].isMultiRecordData; + + const title = this.getHeadingValue( + latestItem, + isMultiRecordData, + isDataObject, + actionName, + dataObjectAction, + caseTypeName, + ID, + newComp?.getCaseLocaleReference() + ); - this.bIsDataObjectRecord$ = false; - this.oCaseInfo = {}; - this.cdRef.markForCheck(); + const bIsRefComponent = this.checkIfRefComponent(newComp); + const arChildren$ = bIsRefComponent ? [newComp.getComponent()] : newComp.getChildren(); + + return { + key, + title, + createdViewPConn$: newComp, + arChildren$, + isMultiRecordData, + isDataObjectModal: isDataObject && !isMultiRecordData, + dataRecordKeys: latestItem.key || '', + dataObjectActionID: routingInfo.items[latestItem.context].actionID || '', + dataObjectAction: dataObjectAction || '', + dataObjectClassId: newComp.getValue('.classID') || '', + context: latestItem.context, + updateToken: new Date().getTime() + }; } getConfigObject(item, pConnect, isReverseCoexistence = false) { @@ -333,13 +265,17 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { this.bAlertState = bData; this.bShowCancelAlert$ = false; if (this.bAlertState) { - this.hideModal(); + // Discard confirmed — matches react-sdk: drop the whole stack rather than just the top entry + this.modalCollection = {}; + this.modalStack = []; + this.modalVisibleChange.emit(false); + this.cdRef.markForCheck(); } } showAlert(payload) { const { latestItem } = this.getKeyAndLatestItem(this.routingInfoRef.current); - const { isModalAction } = payload; + const { isModalAction, hideDelete, isDataObject, skipReleaseLockRequest } = payload; /* If we are in create stage full page mode, created a new case and trying to click on cancel button @@ -348,6 +284,9 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { if (latestItem && isModalAction && !this.actionsDialog) { const configObject = this.getConfigObject(latestItem, this.pConn$); this.cancelPConn$ = configObject?.getPConnect(); + this.cancelHideDelete$ = hideDelete; + this.cancelIsDataObject$ = isDataObject; + this.cancelSkipReleaseLockRequest$ = skipReleaseLockRequest; this.bShowCancelAlert$ = true; this.cdRef.markForCheck(); } @@ -371,31 +310,42 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { return {}; } - compareCaseInfoIsDifferent(oCurrentCaseInfo: object): boolean { - let bRet = false; - - // fast-deep-equal version - if (isEqual !== undefined) { - bRet = !isEqual(this.oCaseInfo, oCurrentCaseInfo); - } else { - const sCurrnentCaseInfo = JSON.stringify(oCurrentCaseInfo); - const sOldCaseInfo = JSON.stringify(this.oCaseInfo); - // stringify compare version - if (sCurrnentCaseInfo != sOldCaseInfo) { - bRet = true; - } - } + // Open/update/close are derived purely from how the count of known modals compares to + // routingInfo.accessedOrder — mirrors react-sdk's modal-stack fix so that closing one modal + // (removing one entry from accessedOrder) can never be mistaken for closing all of them. + isOpenModalAction(accessedOrder: string[]): boolean { + return Object.keys(this.modalCollection).length < accessedOrder.length; + } - // if different, save off new case info - if (bRet) { - this.oCaseInfo = JSON.parse(JSON.stringify(oCurrentCaseInfo)); - } + isUpdateModalAction(accessedOrder: string[]): boolean { + return Object.keys(this.modalCollection).length === accessedOrder.length; + } + + isCloseModalAction(accessedOrder: string[]): boolean { + return Object.keys(this.modalCollection).length > accessedOrder.length; + } - return bRet; + handleModalOpen(key: string) { + this.modalCollection = { ...this.modalCollection, [key]: {} }; } - getBanners() { - return getBanners({ target: this.itemKey$, ...this.stateProps$ }); + handleModalClose(accessedOrder: string[]) { + const closedModalKey = Object.keys(this.modalCollection).find(modalKey => !accessedOrder.includes(modalKey)); + + if (closedModalKey) { + const updatedModalCollection = { ...this.modalCollection }; + delete updatedModalCollection[closedModalKey]; + this.modalCollection = updatedModalCollection; + + this.modalStack = this.modalStack.filter(modal => modal.key !== closedModalKey); + this.modalVisibleChange.emit(this.modalStack.length > 0); + this.cdRef.markForCheck(); + } + } + + // AssignmentComponent renders its own validation banner via BannerService; this covers server-side errors (e.g. httpMessages) that arrive at the container instead + getBanners(itemKey: string) { + return getBanners({ target: itemKey, ...this.stateProps$, httpMessages: this.angularPConnectData.httpMessages }); } getModalHeading(dataObjectAction, actionName) { @@ -409,8 +359,8 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { } } - getHeadingValue(latestItem, isDataObject, actionName, dataObjectAction, caseTypeName, ID, caseLocaleRef) { - if (this.isMultiRecord) { + getHeadingValue(latestItem, isMultiRecordData, isDataObject, actionName, dataObjectAction, caseTypeName, ID, caseLocaleRef) { + if (isMultiRecordData) { return latestItem.heading; } if (isDataObject) { @@ -429,14 +379,22 @@ export class ModalViewContainerComponent implements OnInit, OnDestroy { return `${this.localizedVal('Create', this.localeCategory)} ${this.localizedVal(caseTypeName, undefined, caseLocaleRef)} (${ID})`; } - closeActionsDialog = () => { + // Closes one modal by key (bound per-instance in the template), or the topmost when omitted + closeActionsDialog = (modalKey?: string) => { this.actionsDialog = true; - this.bShowModal$ = false; - // for when non modal - this.modalVisibleChange.emit(this.bShowModal$); + this.modalStack = modalKey ? this.modalStack.filter(modal => modal.key !== modalKey) : this.modalStack.slice(0, -1); - this.oCaseInfo = {}; + this.modalVisibleChange.emit(this.modalStack.length > 0); this.cdRef.markForCheck(); }; + + // Binds closeActionsDialog to a specific modal's key so closing one stacked modal never affects the others + closeModalFor(modalKey: string) { + return () => this.closeActionsDialog(modalKey); + } + + trackByModalKey(index: number, modal: ModalEntry) { + return modal.key; + } } diff --git a/sdk-config.json b/sdk-config.json index 17aac2bc..e7225f24 100644 --- a/sdk-config.json +++ b/sdk-config.json @@ -8,20 +8,20 @@ "mashupClient_comment": "Client ID and Client secret from the OAuth 2.0 Client Registration record used for mashup use case", "mashupClient_comment2": "See SDK Guide for instructions on how to generate and obtain the proper values for the following entries", - "mashupClientId": "69184022781147469983", + "mashupClientId": "10837469341279910969", "mashupUserIdentifier": "customer@mediaco", "mashupClient_comment3": "Note: mashupPassword requires Base64 encoding", "mashupPassword": "", "portalClientId_comment": "Client ID from the OAuth 2.0 Client Registration record used for portal use case", - "portalClientId": "69184022781147469983" + "portalClientId": "10837469341279910969" }, "serverConfig": { "comment_serverConfig": "serverConfig is the block for SDK Content Server config entries", "infinityRestServerUrl_comment": "Full path to Infinity REST server", - "infinityRestServerUrl": "https://localhost:1080/prweb", + "infinityRestServerUrl": "https://lab-25012-ap-south-1.employee.pegalabs.io/prweb", "appAlias_comment": "appAlias of the application which operators will be accessing (e.g., MediaCo)", "appAlias": "", From 27d8e68617816de311fef2ee41b3cc4ba2efcf28 Mon Sep 17 00:00:00 2001 From: manasa Date: Wed, 23 Sep 2026 17:48:57 +0530 Subject: [PATCH 2/4] style(auto-complete): remove stale comments --- .../src/lib/_bridge/angular-pconnect.ts | 1 - .../field/auto-complete/auto-complete.component.scss | 3 --- .../field/auto-complete/auto-complete.component.ts | 8 +++----- sdk-config.json | 8 ++++---- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts b/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts index fcb99760..54ebc8e8 100644 --- a/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts +++ b/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts @@ -135,7 +135,6 @@ export class AngularPConnectService { // const componentName = inComp.constructor.name; - // The following comment is from the Nebula/Constellation version of this code. Meant as a reminder to check this occasionally // populate additional props which are component specific and not present in configurations // This block can be removed once all these props will be added as part of configs inComp.pConn$.populateAdditionalProps(compProps); diff --git a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss index cf168394..78c8b962 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss +++ b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss @@ -80,9 +80,6 @@ } } -// Footer row rendered inside the mat-autocomplete overlay panel (outside this component's view, -// hence ::ng-deep) — mirrors the MUI CustomPaper divider + "Create new" button pattern. -// Sticky (not scrolled) since it's a direct child of the panel's own scroll container. ::ng-deep .psdk-autocomplete-create-new-wrapper { position: sticky; bottom: 0; diff --git a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts index 6ab6840b..8c28a54f 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts @@ -76,8 +76,6 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { @Output() onRecordChange: EventEmitter = new EventEmitter(); - // The input's MatAutocompleteTrigger — used to close the options panel before navigating away - // (e.g. opening the create-new modal), so it doesn't remain open on top of it. @ViewChild(MatAutocompleteTrigger) private autocompleteTrigger?: MatAutocompleteTrigger; configProps$: AutoCompleteProps; @@ -427,7 +425,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { } } - // Re-fetches the options list (equivalent to initializeList in constellation-frontend) + // Re-fetches the options list refreshOptionsList(): void { if (!this.displayMode$ && this.listType !== 'associated') { const context = this.pConn$.getContextName(); @@ -438,7 +436,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { } } - // Sets values for all columns that have setProperty defined (mirrors setValuesToOtherAdditionalFields in constellation-frontend) + // Sets values for all columns that have setProperty defined setValuesToAdditionalFields(record: Record): void { const setPropertyList = this.columns.filter(col => col.setProperty).map(col => ({ source: col.value, target: col.setProperty, key: col.key })); @@ -544,7 +542,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { Promise.resolve(triggerCreate) .then(() => { PCore.getPubSubUtils().subscribe(eventType, createNewCallback, contextClass); - // Re-initialize the list (equivalent to initializeList() in constellation-frontend) + // Re-initialize the list this.refreshOptionsList(); }) .catch(e => console.error(e)); diff --git a/sdk-config.json b/sdk-config.json index e7225f24..101484f1 100644 --- a/sdk-config.json +++ b/sdk-config.json @@ -8,20 +8,20 @@ "mashupClient_comment": "Client ID and Client secret from the OAuth 2.0 Client Registration record used for mashup use case", "mashupClient_comment2": "See SDK Guide for instructions on how to generate and obtain the proper values for the following entries", - "mashupClientId": "10837469341279910969", + "mashupClientId": "69184022781147469983", "mashupUserIdentifier": "customer@mediaco", "mashupClient_comment3": "Note: mashupPassword requires Base64 encoding", "mashupPassword": "", "portalClientId_comment": "Client ID from the OAuth 2.0 Client Registration record used for portal use case", - "portalClientId": "10837469341279910969" + "portalClientId": "69184022781147469983" }, "serverConfig": { "comment_serverConfig": "serverConfig is the block for SDK Content Server config entries", "infinityRestServerUrl_comment": "Full path to Infinity REST server", - "infinityRestServerUrl": "https://lab-25012-ap-south-1.employee.pegalabs.io/prweb", + "infinityRestServerUrl": "https://localhost:1080/prweb", "appAlias_comment": "appAlias of the application which operators will be accessing (e.g., MediaCo)", "appAlias": "", @@ -33,7 +33,7 @@ "appPortal": "", "appMashupCaseType_comment": "If specified, uses this case type for mashup. Otherwise, uses the first case type found for the app", - "appMashupCaseType": "DIXL-MediaCo-Work-PurchasePhone", + "appMashupCaseType": "", "excludePortals_comment": "Array of specific portals to avoid attempting to load with SDK", "excludePortals": ["pxExpress", "Developer", "pxPredictionStudio", "pxAdminStudio", "pyCaseWorker", "pyCaseManager7"], From 8e3221812f52e7ac2fdc190a85e51eaf5c687b87 Mon Sep 17 00:00:00 2001 From: manasa Date: Wed, 23 Sep 2026 17:54:37 +0530 Subject: [PATCH 3/4] docs: update changelog --- CHANGELOG.md | 2 ++ sdk-config.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb81f7b1..44d2df48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ * Github: [PR-576](https://github.com/pegasystems/angular-sdk-components/pull/576) * **Added support for Data Object actions in the case view, and Submit/Cancel controls in the Data Object modal.** * Github: [PR-577](https://github.com/pegasystems/angular-sdk-components/pull/577) +* **Added support for creating new records for the Autocomplete DataReference and CaseReference components.** + * Github: [PR-585](https://github.com/pegasystems/angular-sdk-components/pull/585) ### **Bug fixes** * **Fixed DataReference not making an api call on state change.** diff --git a/sdk-config.json b/sdk-config.json index 101484f1..17aac2bc 100644 --- a/sdk-config.json +++ b/sdk-config.json @@ -33,7 +33,7 @@ "appPortal": "", "appMashupCaseType_comment": "If specified, uses this case type for mashup. Otherwise, uses the first case type found for the app", - "appMashupCaseType": "", + "appMashupCaseType": "DIXL-MediaCo-Work-PurchasePhone", "excludePortals_comment": "Array of specific portals to avoid attempting to load with SDK", "excludePortals": ["pxExpress", "Developer", "pxPredictionStudio", "pxAdminStudio", "pyCaseWorker", "pyCaseManager7"], From c80245946fa36798147f006db6e0653f8fc3c855 Mon Sep 17 00:00:00 2001 From: Siva Rama Krishna Date: Wed, 23 Sep 2026 20:31:25 +0530 Subject: [PATCH 4/4] feat(modal-view): integrate BannerService for improved error handling --- .../src/lib/_bridge/angular-pconnect.ts | 1 - .../modal-view-container.component.html | 3 --- .../modal-view-container.component.ts | 17 +++++++++-------- .../src/lib/_services/banner.service.ts | 12 +++++++++++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts b/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts index 54ebc8e8..d97c9926 100644 --- a/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts +++ b/packages/angular-sdk-components/src/lib/_bridge/angular-pconnect.ts @@ -9,7 +9,6 @@ export interface AngularPConnectData { compID?: string; unsubscribeFn?: Function; validateMessage?: string; - httpMessages?: any[]; actions?: { onChange: Function; onBlur: Function; diff --git a/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.html b/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.html index b0f2105f..e0c1e71c 100644 --- a/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.html +++ b/packages/angular-sdk-components/src/lib/_components/infra/Containers/modal-view-container/modal-view-container.component.html @@ -1,9 +1,6 @@

{{ modal.title }}

-
- -