diff --git a/packages/angular-sdk-components/src/lib/_bridge/helpers/sdk-pega-component-map.ts b/packages/angular-sdk-components/src/lib/_bridge/helpers/sdk-pega-component-map.ts index e0bab3b2..6361cb92 100644 --- a/packages/angular-sdk-components/src/lib/_bridge/helpers/sdk-pega-component-map.ts +++ b/packages/angular-sdk-components/src/lib/_bridge/helpers/sdk-pega-component-map.ts @@ -31,6 +31,7 @@ import { GroupComponent } from '../../_components/field/group/group.component'; import { IntegerComponent } from '../../_components/field/integer/integer.component'; import { ListViewActionButtonsComponent } from '../../_components/field/list-view-action-buttons/list-view-action-buttons.component'; import { LocationComponent } from '../../_components/field/location/location.component'; +import { EmbeddedDataMultiComponent } from '../../_components/field/embedded-data-multi/embedded-data-multi.component'; import { ObjectReferenceComponent } from '../../_components/field/object-reference/object-reference.component'; import { PercentageComponent } from '../../_components/field/percentage/percentage.component'; import { PhoneComponent } from '../../_components/field/phone/phone.component'; @@ -175,6 +176,7 @@ const pegaSdkComponentMap = { Dropdown: DropdownComponent, DynamicTabs: DynamicTabsComponent, Email: EmailComponent, + EmbeddedDataMulti: EmbeddedDataMultiComponent, ErrorBoundary: ErrorBoundaryComponent, FeedContainer: FeedContainerComponent, FieldGroup: FieldGroupComponent, diff --git a/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.html b/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.html index 3ab25ba9..c9a6b34b 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.html +++ b/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.html @@ -18,7 +18,7 @@
- + + +
diff --git a/packages/angular-sdk-components/src/lib/_components/field/embedded-data-multi/embedded-data-multi.component.scss b/packages/angular-sdk-components/src/lib/_components/field/embedded-data-multi/embedded-data-multi.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/packages/angular-sdk-components/src/lib/_components/field/embedded-data-multi/embedded-data-multi.component.ts b/packages/angular-sdk-components/src/lib/_components/field/embedded-data-multi/embedded-data-multi.component.ts new file mode 100644 index 00000000..044fe113 --- /dev/null +++ b/packages/angular-sdk-components/src/lib/_components/field/embedded-data-multi/embedded-data-multi.component.ts @@ -0,0 +1,173 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input, OnInit, OnDestroy, forwardRef } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { AngularPConnectData, AngularPConnectService } from '../../../_bridge/angular-pconnect'; +import { ComponentMapperComponent } from '../../../_bridge/component-mapper/component-mapper.component'; + +interface EmbeddedDataMultiProps { + addEditAction?: string; + addEditView?: string; + displayAs?: string; + editAction?: string; + editMode?: string; + editType?: string; + editView?: string; + readOnly?: boolean; + repeatingView?: string; + targetObjectClass?: string; + useSeparateActionForEdit?: boolean; + useSeparateViewForEdit?: boolean; + visibility?: boolean; +} + +@Component({ + selector: 'app-embedded-data-multi', + templateUrl: './embedded-data-multi.component.html', + styleUrls: ['./embedded-data-multi.component.scss'], + imports: [CommonModule, forwardRef(() => ComponentMapperComponent)] +}) +export class EmbeddedDataMultiComponent implements OnInit, OnDestroy { + @Input() pConn$: typeof PConnect; + @Input() formGroup$: FormGroup; + + angularPConnectData: AngularPConnectData = {}; + simpleTablePConn: typeof PConnect; + simpleTableComponentName = ''; + bVisible$ = true; + + constructor(private angularPConnect: AngularPConnectService) {} + + ngOnInit(): void { + this.angularPConnectData = this.angularPConnect.registerAndSubscribeComponent(this, this.onStateChange); + this.checkAndUpdate(); + } + + ngOnDestroy(): void { + if (this.angularPConnectData.unsubscribeFn) { + this.angularPConnectData.unsubscribeFn(); + } + } + + onStateChange = () => { + this.checkAndUpdate(); + }; + + checkAndUpdate() { + const bUpdateSelf = this.angularPConnect.shouldComponentUpdate(this); + if (bUpdateSelf) { + this.updateSelf(); + } + } + + updateSelf() { + const configProps = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()) as EmbeddedDataMultiProps; + const { + addEditAction, + addEditView, + displayAs, + editAction, + editMode, + editType, + editView, + readOnly = false, + repeatingView, + targetObjectClass, + useSeparateActionForEdit, + useSeparateViewForEdit, + visibility = true + } = configProps; + + this.bVisible$ = visibility !== false; + + const rawMetadata = this.pConn$.getRawMetadata(); + const rawMetadataConfig = rawMetadata?.config as any; + + if (!rawMetadataConfig) { + return; + } + + const renderMode = readOnly ? 'ReadOnly' : 'Editable'; + + let columnsChildren; + if (displayAs === 'table' || displayAs === 'simpleTable') { + columnsChildren = [ + { + children: rawMetadataConfig.columns || [], + name: 'Columns', + type: 'Region' + } + ]; + } + + let regionWithView: any[] = []; + if (displayAs === 'repeatingView') { + regionWithView = [ + { + children: [ + { + type: 'reference', + config: { + type: 'view', + name: repeatingView + } + } + ], + name: 'view', + type: 'Region' + } + ]; + } + + const { pagelistValue } = rawMetadataConfig; + const authorContext = pagelistValue?.startsWith('@P') ? pagelistValue?.substring(3) : pagelistValue; + + const componentConfig = { + type: 'View', + config: { + template: 'SimpleTable', + type: 'multirecordlist', + authorContext, + name: authorContext?.substring(1), + renderMode, + multiRecordDisplayAs: displayAs === 'repeatingView' ? 'fieldGroup' : displayAs, + referenceList: pagelistValue, + contextClass: targetObjectClass, + editMode, + editModeConfig: { + editType, + defaultView: addEditView, + defaultAction: addEditAction, + useSeparateViewForEdit, + useSeparateActionForEdit, + editView, + editAction + }, + label: rawMetadataConfig.label, + hideLabel: rawMetadataConfig.hideLabel, + children: columnsChildren, + displayField: rawMetadataConfig.displayField, + uniqueField: rawMetadataConfig.uniqueField, + targetClassLabel: rawMetadataConfig.targetClassLabel, + targetClassLabelOption: rawMetadataConfig.targetClassLabelOption, + fieldHeader: rawMetadataConfig.repeatingViewHeadingSource, + heading: rawMetadataConfig.repeatingViewHeading, + allowActions: { + allowAdd: rawMetadataConfig.allowAdd ?? true, + allowEdit: rawMetadataConfig.allowEdit ?? true, + allowDelete: rawMetadataConfig.allowDelete ?? true, + allowDragDrop: rawMetadataConfig.allowDragDrop ?? true + }, + allowRowDelete: rawMetadataConfig.allowRowDelete ?? true, + allowRowEdit: rawMetadataConfig.allowRowEdit ?? true + }, + children: regionWithView + }; + + const component = this.pConn$.createComponent(componentConfig as any, '', 0, {}); + if (component) { + this.simpleTablePConn = component.getPConnect(); + this.simpleTableComponentName = this.simpleTablePConn.getComponentName() ?? ''; + } + } +} diff --git a/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.ts b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.ts index 9ad8a22d..8fabcc63 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.ts @@ -91,13 +91,32 @@ export class MultiselectComponent extends FieldBase { } ]; let secondaryColumns: any = []; - if (secondaryFields) { - secondaryColumns = secondaryFields.map(secondaryField => ({ - value: secondaryField, + // Read columnsFormatter from raw (unresolved) metadata to get field property paths + // (e.g. "@P .pyLabel") instead of resolved data values (e.g. "New Complex Fields"). + // This matches Constellation's approach: pConn.getRawMetadata()?.config?.columnsFormatter + const columnsFormatter = (this.pConn$.getRawMetadata()?.config as any)?.columnsFormatter; + const secondaryFieldsFromFormatter = columnsFormatter + ? columnsFormatter + .map(item => { + const property = item?.config?.value; + if (typeof property === 'string') { + if (property.startsWith('@USER ')) return property.substring(6); + if (property.startsWith('@P ')) return property.substring(3); + return property; + } + return undefined; + }) + .filter(Boolean) + : undefined; + const updatedSecondaryFields = secondaryFieldsFromFormatter || secondaryFields; + if (updatedSecondaryFields) { + secondaryColumns = updatedSecondaryFields.map(secondaryField => ({ + value: typeof secondaryField === 'string' ? secondaryField : undefined, display: 'true', secondary: 'true', useForSearch: 'true' })); + secondaryColumns = secondaryColumns.filter(col => col.value); } else { secondaryColumns = [ { diff --git a/packages/angular-sdk-components/src/lib/_components/field/multiselect/utils.ts b/packages/angular-sdk-components/src/lib/_components/field/multiselect/utils.ts index 5915fc48..926afe4d 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/multiselect/utils.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/multiselect/utils.ts @@ -11,9 +11,11 @@ export function setVisibilityForList(c11nEnv, visibility) { function preProcessColumns(columns) { return columns?.map(col => { const tempColObj = { ...col }; - tempColObj.value = col.value && col.value.startsWith('.') ? col.value.substring(1) : col.value; - if (tempColObj.setProperty) { - tempColObj.setProperty = col.setProperty && col.setProperty.startsWith('.') ? col.setProperty.substring(1) : col.setProperty; + if (typeof col.value === 'string') { + tempColObj.value = col.value.startsWith('.') ? col.value.substring(1) : col.value; + } + if (typeof col.setProperty === 'string') { + tempColObj.setProperty = col.setProperty.startsWith('.') ? col.setProperty.substring(1) : col.setProperty; } return tempColObj; }); diff --git a/packages/angular-sdk-components/src/lib/_components/field/object-reference/object-reference.component.html b/packages/angular-sdk-components/src/lib/_components/field/object-reference/object-reference.component.html index 17cf877d..94a19dd6 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/object-reference/object-reference.component.html +++ b/packages/angular-sdk-components/src/lib/_components/field/object-reference/object-reference.component.html @@ -1,17 +1,32 @@ -
- -
- -
+ + +
+ +
+ + +
+ +
+ + +
- - - - + + +
+ +
+ + +
+ +
+
diff --git a/packages/angular-sdk-components/src/lib/_components/field/object-reference/object-reference.component.ts b/packages/angular-sdk-components/src/lib/_components/field/object-reference/object-reference.component.ts index d3d594be..cca848d5 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/object-reference/object-reference.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/object-reference/object-reference.component.ts @@ -4,7 +4,19 @@ import { FormGroup } from '@angular/forms'; import { ComponentMetadataConfig } from '@pega/pcore-pconnect-typedefs/interpreter/types'; import { AngularPConnectData, AngularPConnectService } from '../../../_bridge/angular-pconnect'; import { ComponentMapperComponent } from '../../../_bridge/component-mapper/component-mapper.component'; -import { generateColumns, getDataRelationshipContextFromKey } from '../../../_helpers/objectReference-utils'; +import { + SELECTION_MODE, + SIMPLE_TABLE_MANUAL_READONLY, + generateColumns, + addCompositeKeysToConfig, + getDataRelationshipContextFromKey, + generateDetailsDisplay, + createNewRecord, + getAdditionalInfo, + camelCase +} from '../../../_helpers/objectReference-utils'; +import { componentCachePersistUtils, getMappedKey } from '../../template/advanced-search/search-group/persist-utils'; +import { DataReferenceAdvancedSearchService } from '../../../_services/data-reference-advanced-search.service'; import { PConnFieldProps } from '../../../_types/PConnProps.interface'; interface ObjectReferenceProps extends PConnFieldProps { @@ -14,8 +26,17 @@ interface ObjectReferenceProps extends PConnFieldProps { mode: string; targetObjectType: any; allowAndPersistChangesInReviewMode: boolean; + allowCreatingRecords?: boolean; + linkReference?: boolean; + matchPosition?: string; + additionalFields?: any; } +/** + * Rendering modes for the template + */ +type RenderMode = 'singleReferenceReadonly' | 'multiReferenceReadonly' | 'semanticLink' | 'searchAndSelect' | 'dynamicComponent'; + @Component({ selector: 'app-object-reference', imports: [CommonModule, forwardRef(() => ComponentMapperComponent)], @@ -28,9 +49,7 @@ export class ObjectReferenceComponent implements OnInit, OnDestroy { angularPConnectData: AngularPConnectData = {}; configProps: ObjectReferenceProps; - value: { [key: string]: any }; readOnly: boolean; - isForm: boolean; type: string; isDisplayModeEnabled: boolean; canBeChangedInReviewMode: boolean; @@ -38,7 +57,25 @@ export class ObjectReferenceComponent implements OnInit, OnDestroy { newPconn: typeof PConnect; rawViewMetadata: ComponentMetadataConfig | undefined; - constructor(private angularPConnect: AngularPConnectService) {} + // Rendering mode + renderMode: RenderMode = 'dynamicComponent'; + + // Whether the dynamic component supports @Output() onRecordChange binding + useOutputEvents = false; + + // Visibility flag — prevents rendering when platform sets visibility to false + bVisible$ = true; + + // For SearchAndSelect + searchSelectCacheKey: string; + + // For parameterized CheckboxGroup datasource + parameterizedDataSource: any[] = []; + + constructor( + private angularPConnect: AngularPConnectService, + private dataRefAdvancedSearchService: DataReferenceAdvancedSearchService + ) {} ngOnInit() { this.angularPConnectData = this.angularPConnect.registerAndSubscribeComponent(this, this.onStateChange); @@ -63,85 +100,262 @@ export class ObjectReferenceComponent implements OnInit, OnDestroy { } updateSelf() { + this.useOutputEvents = false; this.configProps = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()) as ObjectReferenceProps; + + const { visibility = true } = this.configProps; + this.bVisible$ = visibility !== false; const { - displayMode, allowAndPersistChangesInReviewMode: editableInReview = false, + allowCreatingRecords, targetObjectType, - mode, + mode = '', parameters, - hideLabel, - inline, - showPromotedFilters + hideLabel = false, + inline = false, + showPromotedFilters = false, + linkReference, + matchPosition = 'contains' } = this.configProps; - const referenceType: string = targetObjectType === 'case' ? 'Case' : 'Data'; + let displayMode = this.configProps.displayMode ?? ''; + + const referenceType: string = targetObjectType?.toLowerCase() === 'case' ? 'Case' : 'Data'; this.rawViewMetadata = this.pConn$.getRawMetadata(); - const refFieldMetadata = this.pConn$.getFieldMetadata(this.rawViewMetadata?.config?.value?.split('.', 2)[1] ?? ''); - const propsToUse = { ...this.pConn$.getInheritedProps(), ...this.configProps }; + const rawConfig = this.rawViewMetadata?.config as any; + + const refFieldMetadata = this.pConn$.getFieldMetadata( + rawConfig?.mode === SELECTION_MODE.SINGLE ? rawConfig?.value?.split('.', 2)[1] : rawConfig?.pagelistValue?.substring(4) + ); + + const propsToUse: any = { ...this.pConn$.getInheritedProps(), ...this.configProps }; + if (!propsToUse.label) { + propsToUse.label = this.configProps.label; + } + + this.normalizeComboboxType(rawConfig, mode); + + if (rawConfig?.parameters && ['CheckboxGroup'].includes(rawConfig.componentType)) { + this.loadParameterizedDataSource(rawConfig, parameters ?? {}); + } + + displayMode = this.resolveDisplayMode(rawConfig, displayMode, mode); this.isDisplayModeEnabled = displayMode === 'DISPLAY_ONLY'; - this.type = this.getComponentType(); - this.canBeChangedInReviewMode = editableInReview && ['Autocomplete', 'Dropdown'].includes(this.type); - - if (this.type === 'SemanticLink' && !this.canBeChangedInReviewMode) { - const config: any = { - ...this.rawViewMetadata?.config, - primaryField: (this.rawViewMetadata?.config as any).displayField, - caseClass: (this.rawViewMetadata?.config as any).targetObjectClass, - text: (this.rawViewMetadata?.config as any).displayField, - caseID: (this.rawViewMetadata?.config as any).value, - contextPage: `@P .${(this.rawViewMetadata?.config as any).displayField ? getDataRelationshipContextFromKey((this.rawViewMetadata?.config as any).displayField) : null}`, - resourceParams: { workID: (this.rawViewMetadata?.config as any).value }, - resourcePayload: { caseClassName: (this.rawViewMetadata?.config as any).targetObjectClass } - }; - this.createSemanticLinkPConnect(config, displayMode ?? '', referenceType, hideLabel); + this.type = rawConfig?.componentType; + this.canBeChangedInReviewMode = editableInReview && ['AutoComplete', 'Dropdown'].includes(this.type); + + if (this.handleReadOnlyMode(rawConfig, mode)) { return; } - if (this.type !== 'SemanticLink' && !this.isDisplayModeEnabled) { - const config: any = { ...this.rawViewMetadata?.config }; - generateColumns(config, this.pConn$, referenceType); - config.deferDatasource = true; - config.listType = 'datapage'; - if (['Dropdown', 'AutoComplete'].includes(this.type) && !config.placeholder) { - config.placeholder = '@L Select...'; - } - config.showPromotedFilters = showPromotedFilters; - if (!this.canBeChangedInReviewMode) { - config.displayMode = displayMode; + this.routeToEditableComponent( + rawConfig, + mode, + refFieldMetadata, + propsToUse, + referenceType, + displayMode, + parameters, + hideLabel, + inline, + showPromotedFilters, + matchPosition, + linkReference, + targetObjectType, + allowCreatingRecords + ); + } + + private normalizeComboboxType(rawConfig: any, mode: string): void { + if (rawConfig?.componentType === 'Combobox') { + rawConfig.componentType = mode === SELECTION_MODE.MULTI ? 'Multiselect' : 'AutoComplete'; + } + } + + private resolveDisplayMode(rawConfig: any, displayMode: string, mode: string): string { + if (displayMode === 'DISPLAY_ONLY' || !this.configProps.readOnly) { + return displayMode; + } + if (rawConfig?.componentType === 'Multiselect') { + rawConfig.componentType = 'SemanticLink'; + return displayMode; + } + if (mode === 'readonly-single') { + return this.configProps.displayMode ?? displayMode; + } + return 'DISPLAY_ONLY'; + } + + private handleReadOnlyMode(rawConfig: any, mode: string): boolean { + const isReadOnlyOrSemanticLink = (this.configProps.readOnly || this.type === 'SemanticLink') && !this.canBeChangedInReviewMode; + if (isReadOnlyOrSemanticLink) { + if (mode !== 'readonly-multi') { + this.buildSingleReferenceReadonly(rawConfig); + } else { + this.buildMultiReferenceReadonly(); } - config.parameters = parameters; + return true; + } + if (this.isDisplayModeEnabled && !this.canBeChangedInReviewMode) { + this.buildSingleReferenceReadonly(rawConfig); + return true; + } + return false; + } - this.createOtherComponentPConnect(config, propsToUse, mode, refFieldMetadata, referenceType, hideLabel, inline); + private resolveCreatePermissions(rawConfig: any, allowCreatingRecords: boolean | undefined) { + const { disableStartingFieldsForReference = false } = (PCore as any).getEnvironmentInfo().environmentInfoObject?.features?.form || {}; + const contextClass = rawConfig.targetObjectClass; + const formFeaturesAvailable = (PCore as any).getEnvironmentInfo().environmentInfoObject?.features?.form; + const createAuthoringEnabled = allowCreatingRecords ?? formFeaturesAvailable?.isCreateNewReferenceEnabled; + const userHasCreateAccess = formFeaturesAvailable + ? formFeaturesAvailable.isCreateNewReferenceEnabled && PCore.getAccessPrivilege().hasCreateAccess(contextClass) + : PCore.getAccessPrivilege().hasCreateAccess(contextClass); + return { + isCreateNewReferenceEnabled: createAuthoringEnabled && userHasCreateAccess, + disableStartingFieldsForReference, + contextClass + }; + } + + private routeToEditableComponent( + rawConfig: any, + mode: string, + refFieldMetadata: any, + propsToUse: any, + referenceType: string, + displayMode: string, + parameters: any, + hideLabel: boolean, + inline: boolean, + showPromotedFilters: boolean, + matchPosition: string, + linkReference: any, + targetObjectType: any, + allowCreatingRecords: boolean | undefined + ): void { + if (this.type === 'EmbeddedInsightTable') { + this.buildEmbeddedInsightTableChild(rawConfig, referenceType, propsToUse); + this.renderMode = 'dynamicComponent'; + return; + } + if (this.type === 'Cards') { + this.buildCardsChild(rawConfig); + this.renderMode = 'dynamicComponent'; + return; } + if (this.type === 'Map') { + this.buildMapChild(rawConfig, targetObjectType); + this.renderMode = 'dynamicComponent'; + return; + } + if (this.type === 'CheckboxGroup') { + this.buildCheckboxGroupChild(rawConfig, mode, refFieldMetadata, propsToUse, hideLabel); + this.renderMode = 'dynamicComponent'; + return; + } + if (this.type === 'Table' || this.type === 'SimpleTable') { + this.buildTableChild(this.type, rawConfig, mode, referenceType, propsToUse); + this.renderMode = 'dynamicComponent'; + return; + } + + generateColumns(rawConfig, this.pConn$, referenceType); + addCompositeKeysToConfig(rawConfig, this.pConn$); + rawConfig.deferDatasource = true; + rawConfig.listType = 'datapage'; + if (['Dropdown', 'AutoComplete'].includes(this.type) && !rawConfig.placeholder) { + rawConfig.placeholder = '@L Select...'; + } + rawConfig.showPromotedFilters = showPromotedFilters; + if (!this.canBeChangedInReviewMode) { + rawConfig.displayMode = displayMode; + } + + const fieldMetaData = this.buildFieldMetaData(rawConfig, parameters); + const { isCreateNewReferenceEnabled, disableStartingFieldsForReference, contextClass } = this.resolveCreatePermissions( + rawConfig, + allowCreatingRecords + ); + + if (this.type === 'SearchAndSelect') { + this.buildSearchAndSelectChild( + rawConfig, + refFieldMetadata, + propsToUse, + referenceType, + hideLabel, + inline, + matchPosition, + isCreateNewReferenceEnabled, + disableStartingFieldsForReference + ); + this.renderMode = 'searchAndSelect'; + return; + } + if (this.type === 'Multiselect') { + this.buildMultiselectChild( + rawConfig, + mode, + refFieldMetadata, + fieldMetaData, + propsToUse, + referenceType, + hideLabel, + inline, + isCreateNewReferenceEnabled, + disableStartingFieldsForReference, + contextClass + ); + this.renderMode = 'dynamicComponent'; + return; + } + this.buildDefaultChild( + rawConfig, + mode, + refFieldMetadata, + fieldMetaData, + propsToUse, + referenceType, + hideLabel, + inline, + isCreateNewReferenceEnabled, + disableStartingFieldsForReference, + contextClass, + linkReference + ); + this.renderMode = 'dynamicComponent'; } - onRecordChange(value) { - const caseKey = this.pConn$.getCaseInfo().getKey() ?? ''; - const refreshOptions = { autoDetectRefresh: true, propertyName: '' }; - refreshOptions.propertyName = this.rawViewMetadata?.config?.value ?? ''; + onRecordChange = event => { + const pConn = this.pConn$; + const caseKey = pConn.getCaseInfo().getKey() ?? ''; + const refreshOptions: any = { autoDetectRefresh: true, propertyName: '' }; + refreshOptions.propertyName = (this.rawViewMetadata?.config as any)?.value ?? ''; + + const { allowImplicitRefresh } = (PCore as any).getFieldDefaultUtils().fieldDefaults.DataReference || {}; - if (!this.canBeChangedInReviewMode || !this.pConn$.getValue('__currentPageTabViewName')) { - const pgRef = this.pConn$.getPageReference().replace('caseInfo.content', '') ?? ''; + if (!this.canBeChangedInReviewMode || !pConn.getValue('__currentPageTabViewName') || allowImplicitRefresh) { + const pgRef = pConn.getPageReference().replace('caseInfo.content', '') ?? ''; const viewName = this.rawViewMetadata?.name; if (viewName && viewName.length > 0) { - getPConnect().getActionsApi().refreshCaseView(caseKey, viewName, pgRef, refreshOptions); + pConn.getActionsApi().refreshCaseView(caseKey, viewName, pgRef, refreshOptions); } } - const propValue = value; + const propValue = event?.id || event?.target?.value || event; const propName = - this.rawViewMetadata?.type === 'SimpleTableSelect' && this.configProps.mode === 'multi' - ? PCore.getAnnotationUtils().getPropertyName(this.rawViewMetadata?.config?.selectionList ?? '') - : PCore.getAnnotationUtils().getPropertyName(this.rawViewMetadata?.config?.value ?? ''); + this.rawViewMetadata?.type === 'SimpleTableSelect' && this.configProps.mode === SELECTION_MODE.MULTI + ? PCore.getAnnotationUtils().getPropertyName((this.rawViewMetadata?.config as any)?.selectionList ?? '') + : PCore.getAnnotationUtils().getPropertyName((this.rawViewMetadata?.config as any)?.value ?? ''); if (propValue && this.canBeChangedInReviewMode && this.isDisplayModeEnabled) { PCore.getCaseUtils() .getCaseEditLock(caseKey, '') .then(caseResponse => { - const pageTokens = this.pConn$.getPageReference().replace('caseInfo.content', '').split('.'); - let curr = {}; + const pageTokens = pConn.getPageReference().replace('caseInfo.content', '').split('.'); + let curr: any = {}; const commitData = curr; pageTokens?.forEach(el => { @@ -151,7 +365,6 @@ export class ObjectReferenceComponent implements OnInit, OnDestroy { } }); - // expecting format like {Customer: {pyID:"C-100"}} const propArr = propName.split('.'); propArr.forEach((element, idx) => { if (idx + 1 === propArr.length) { @@ -163,75 +376,584 @@ export class ObjectReferenceComponent implements OnInit, OnDestroy { }); PCore.getCaseUtils() - .updateCaseEditFieldsData(caseKey, { [caseKey]: commitData }, caseResponse.headers.etag, this.pConn$?.getContextName() ?? '') + .updateCaseEditFieldsData(caseKey, { [caseKey]: commitData }, caseResponse.headers.etag, pConn.getContextName() ?? '') .then(response => { - PCore.getContainerUtils().updateParentLastUpdateTime(this.pConn$.getContextName() ?? '', response.data.data.caseInfo.lastUpdateTime); - PCore.getContainerUtils().updateRelatedContextEtag(this.pConn$.getContextName() ?? '', response.headers.etag); + PCore.getContainerUtils().updateParentLastUpdateTime(pConn.getContextName() ?? '', (response.data as any).data.caseInfo.lastUpdateTime); + PCore.getContainerUtils().updateRelatedContextEtag(pConn.getContextName() ?? '', response.headers.etag); }); }); } + }; + + private loadParameterizedDataSource(rawConfig: any, parameters: any) { + const { value, key, text } = { + key: `@P ${rawConfig.selectionKey}`, + text: `@P ${rawConfig.displayField}`, + value: `@P ${rawConfig.selectionKey}` + }; + const refList = rawConfig.referenceList; + (PCore as any) + .getDataApiUtils() + .getData(refList, { dataViewParameters: parameters }) + .then((res: any) => { + if (res.data.data !== null) { + this.parameterizedDataSource = res.data.data + .map((listItem: any) => ({ + key: listItem[key.split(' .', 2)[1]], + text: listItem[text.split(' .', 2)[1]], + value: listItem[value.split(' .', 2)[1]] + })) + .filter((item: any) => item.key); + } else { + this.parameterizedDataSource = []; + } + }) + .catch(() => { + this.parameterizedDataSource = []; + }); + } + + private buildFieldMetaData(rawConfig: any, parameters: any) { + const fieldMetaData: any = { + datasourceMetadata: { datasource: { parameters: {} } } + }; + if (rawConfig.parameters) { + fieldMetaData.datasourceMetadata.datasource.parameters = parameters; + } + fieldMetaData.datasourceMetadata.datasource.propertyForDisplayText = rawConfig.datasource?.fields?.text?.startsWith('@P') + ? rawConfig.datasource.fields.text.substring(3) + : rawConfig.datasource?.fields?.text; + fieldMetaData.datasourceMetadata.datasource.propertyForValue = rawConfig.datasource?.fields?.value?.startsWith('@P') + ? rawConfig.datasource.fields.value.substring(3) + : rawConfig.datasource?.fields?.value; + fieldMetaData.datasourceMetadata.datasource.name = rawConfig.referenceList; + return fieldMetaData; } - private getComponentType(): string { - // componentType is not defined in ComponentMetadataConfig type so using any - return (this.rawViewMetadata?.config as any)?.componentType; + private buildSingleReferenceReadonly(rawConfig: any) { + // SingleReferenceReadonly is rendered via component-mapper with pConn$ which internally resolves config + // Setting properties needed by the child component on config + rawConfig.primaryField = rawConfig.displayField; + rawConfig.text = rawConfig.displayField; + rawConfig.caseClass = rawConfig.targetObjectClass; + rawConfig.caseID = rawConfig.value; + rawConfig.contextPage = `@P .${rawConfig.displayField ? getDataRelationshipContextFromKey(rawConfig.displayField) : null}`; + this.renderMode = 'singleReferenceReadonly'; } - private createSemanticLinkPConnect(config: any, displayMode: string, referenceType: string, hideLabel: boolean) { - const semanticLinkConfig = { - ...config, - displayMode, - referenceType, - hideLabel, - dataRelationshipContext: config.displayField ? getDataRelationshipContextFromKey(config.displayField) : null + private buildMultiReferenceReadonly() { + this.renderMode = 'multiReferenceReadonly'; + } + + private buildCardsChild(rawConfig: any) { + const selectionMode = rawConfig.mode; + const datasourceKeyField = + rawConfig.selectionKey || (rawConfig.isCalculated ? '.pzInsKey' : `.${rawConfig.value.trim().split('.').pop()?.trim()}`); + + const componentMeta = { + type: selectionMode === 'single' ? 'RadioButtons' : 'Checkbox', + config: { + ...rawConfig, + label: rawConfig.label, + value: selectionMode === 'single' ? rawConfig.value : undefined, + referenceList: rawConfig.referenceList, + contextClass: rawConfig.targetObjectClass, + referenceType: rawConfig.targetObjectType === 'case' ? 'Case' : 'Data', + readonlyContextList: selectionMode.includes('multi') ? rawConfig.pagelistValue : undefined, + ...(selectionMode.includes('multi') + ? { + selectionList: rawConfig.pagelistValue.substring(3), + selectionKey: rawConfig.selectionKey || (rawConfig.isCalculated ? '.pzInsKey' : undefined) + } + : { selectionList: rawConfig.contextPage?.substring(3) }), + selectionMode: selectionMode.includes('multi') ? 'multi' : undefined, + displayMode: rawConfig.isCalculated ? 'DISPLAY_ONLY' : undefined, + renderMode: !rawConfig.isCalculated && rawConfig.mode === 'readonly-multi' ? 'ReadOnly' : undefined, + variant: 'card', + inlineDisplay: rawConfig.inlineDisplay ?? true, + hideFieldLabels: rawConfig.hideFieldLabels, + presets: [ + { + children: [ + { + children: rawConfig.secondaryFields, + name: 'AdditionalDetails', + type: 'Region' + } + ], + config: {}, + id: 'P_', + label: '', + name: 'presets', + template: 'Cards' + } + ], + datasource: { + fields: { + key: `@P ${datasourceKeyField}`, + text: + typeof rawConfig.displayField === 'string' && rawConfig.displayField.trim() + ? `@P .${rawConfig.displayField.trim().split('.').pop()?.trim()}` + : `@P ${datasourceKeyField}`, + value: `@P ${datasourceKeyField}` + }, + filterDownloadedFields: true, + source: `@DATASOURCE ${rawConfig.referenceList}.pxResults` + }, + displayAs: 'cards', + hideLabel: rawConfig.hideLabel, + imagePosition: rawConfig.imagePosition, + image: rawConfig.image, + imageSize: rawConfig.imageSize, + showImageDescription: rawConfig.showImageDescription, + imageDescription: rawConfig.imageDescription, + required: rawConfig.required, + readOnly: !!rawConfig.isCalculated, + disabled: rawConfig.disabled, + labelOption: rawConfig.labelOption, + primaryField: rawConfig.displayField || datasourceKeyField, + dataRelationshipContext: rawConfig.displayField ? getDataRelationshipContextFromKey(rawConfig.displayField) : null + } }; - const component = this.pConn$.createComponent({ type: 'SemanticLink', config: semanticLinkConfig }, '', 0, {}); + const component = this.pConn$.createComponent(componentMeta as any, '', 0, {}); + this.newComponentName = component?.getPConnect().getComponentName(); this.newPconn = component?.getPConnect(); + + // For multi-mode, pre-set the reference list on the child PConnect so that + // getListActions() resolves the correct path even if the child's updateSelf() + // doesn't re-run after a setInput update. + if (selectionMode.includes('multi') && this.newPconn) { + const selectionList = rawConfig.pagelistValue.substring(3); + this.newPconn.setReferenceList(selectionList); + } } - private createOtherComponentPConnect( - config: any, - propsToUse: any, - mode: string, + private buildMapChild(rawConfig: any, targetObjectType: any) { + const displayField = getDataRelationshipContextFromKey(rawConfig.displayField); + const displayFieldMetadata = this.pConn$.getFieldMetadata(displayField); + + const componentMeta = { + type: 'MapView', + config: { + contextClass: rawConfig.targetObjectClass, + displayAs: 'map', + hideLabel: false, + label: rawConfig.label, + referenceType: targetObjectType?.toLowerCase() === 'case' ? 'Case' : 'Data', + localeReference: rawConfig.localeReference, + classId: SIMPLE_TABLE_MANUAL_READONLY, + detailsDisplay: generateDetailsDisplay({ + isCaseType: targetObjectType?.toLowerCase() === 'case', + fieldForDisplay: displayField, + fieldNameForKey: rawConfig.selectionKey, + displayFieldMetadata + }), + presets: [ + { + children: [ + { + children: [], + name: 'Columns', + type: 'Region' + }, + { + children: rawConfig.secondaryFields, + name: 'AdditionalDetails', + type: 'Region' + } + ], + config: {}, + id: 'P_', + label: '', + name: 'presets', + locationDetails: rawConfig.locationDetails, + template: 'Map' + } + ], + readonlyContextList: rawConfig.pagelistValue, + referenceList: rawConfig.pagelistValue, + renderMode: 'ReadOnly', + selectionMode: 'multi', + required: rawConfig.required, + readOnly: false, + disabled: rawConfig.disabled, + visibility: rawConfig.visibility + } + }; + + const component = this.pConn$.createComponent(componentMeta as any, '', 0, {}); + this.newComponentName = component?.getPConnect().getComponentName(); + this.newPconn = component?.getPConnect(); + } + + private buildCheckboxGroupChild(rawConfig: any, mode: string, refFieldMetadata: any, propsToUse: any, hideLabel: boolean) { + const displayField = rawConfig.displayField; + const primaryField = displayField?.startsWith('@P') ? displayField?.substring(3) : displayField; + const readOnly = !(mode === 'multi' || mode === 'single'); + const pageListValueFromConfig = rawConfig.pagelistValue; + const selectionList = pageListValueFromConfig?.startsWith('@P') ? pageListValueFromConfig?.substring(3) : pageListValueFromConfig; + + const component = this.pConn$.createComponent( + { + type: 'Checkbox', + config: { + ...rawConfig, + contextClass: rawConfig.targetObjectClass, + datasource: { + fields: { + key: `@P ${rawConfig.selectionKey}`, + text: `@P ${rawConfig.displayField}`, + value: `@P ${rawConfig.selectionKey}` + }, + source: !rawConfig.parameters ? `@DATASOURCE ${rawConfig.referenceList}.pxResults` : this.parameterizedDataSource + }, + descriptors: mode === SELECTION_MODE.SINGLE ? refFieldMetadata?.descriptors : null, + displayAs: 'checkboxgroup', + hideLabel, + inline: rawConfig.inline ?? false, + label: propsToUse.label, + primaryField, + readOnly, + readonlyContextList: pageListValueFromConfig, + referenceType: rawConfig.targetObjectType, + selectionKey: rawConfig.selectionKey, + selectionList, + selectionMode: mode, + disabled: propsToUse.disabled, + required: propsToUse.required, + visibility: propsToUse.visibility, + additionalInfo: refFieldMetadata?.additionalInformation ? { content: refFieldMetadata.additionalInformation } : undefined + } + } as any, + '', + 0, + {} + ); + this.newComponentName = component?.getPConnect().getComponentName(); + this.newPconn = component?.getPConnect(); + + // For multi-mode, pre-set the reference list on the child PConnect so that + // getListActions() resolves the correct path on every re-render. + if (mode === 'multi' && this.newPconn && selectionList) { + this.newPconn.setReferenceList(selectionList); + } + } + + private buildTableChild(type: string, rawConfig: any, mode: string, referenceType: string, propsToUse: any) { + const presets = [ + { + children: [{ children: rawConfig.columns, name: 'Columns', type: 'Region' }], + config: { filterExpression: rawConfig.filterExpression }, + id: 'P_', + label: '', + name: 'presets', + template: 'Table' + } + ]; + const tableDisplayAs = camelCase(type); + + let componentConfig: any; + + if (mode === 'readonly-multi') { + componentConfig = { + type: 'SimpleTableSelect', + config: { + contextClass: rawConfig.targetObjectClass, + defaultRowHeight: rawConfig.defaultRowHeight, + displayAs: tableDisplayAs, + hideLabel: false, + label: propsToUse.label, + localeReference: rawConfig.localeReference, + presets, + readOnly: true, + readonlyContextList: rawConfig.pagelistValue, + referenceList: rawConfig.pagelistValue, + referenceType, + renderMode: 'ReadOnly', + rowHeader: rawConfig.rowHeader, + selectionMode: 'multi', + required: propsToUse.required, + visibility: propsToUse.visibility, + disabled: propsToUse.disabled, + toggleFieldVisibility: rawConfig.toggleFieldVisibility + } + }; + } else if (mode === 'single' || mode === 'multi') { + const contextPageFromConfig = mode === 'single' ? rawConfig.contextPage : rawConfig.pagelistValue; + const contextPageValue = contextPageFromConfig?.startsWith('@P') ? contextPageFromConfig?.substring(3) : contextPageFromConfig; + + componentConfig = { + type: 'SimpleTableSelect', + config: { + ...(mode === 'single' ? { selectionKey: rawConfig.value, value: rawConfig.value } : {}), + dataRelationshipContext: contextPageValue, + defaultRowHeight: rawConfig.defaultRowHeight, + displayAs: tableDisplayAs, + hideLabel: false, + inline: false, + label: propsToUse.label, + localeReference: rawConfig.localeReference, + presets, + readOnly: false, + referenceList: rawConfig.referenceList, + referenceType, + rowHeader: rawConfig.rowHeader, + parameters: rawConfig.parameters, + selectionList: contextPageValue, + selectionMode: mode, + showPromotedFilters: false, + required: propsToUse.required, + visibility: propsToUse.visibility, + disabled: propsToUse.disabled, + toggleFieldVisibility: rawConfig.toggleFieldVisibility + } + }; + } + + if (componentConfig) { + const component = this.pConn$.createComponent(componentConfig, '', 0, {}); + this.newComponentName = component?.getPConnect().getComponentName(); + this.newPconn = component?.getPConnect(); + } + } + + private buildSearchAndSelectChild( + rawConfig: any, refFieldMetadata: any, + propsToUse: any, referenceType: string, hideLabel: boolean, - inline: boolean + inline: boolean, + matchPosition: string, + isCreateNewReferenceEnabled: boolean, + disableStartingFieldsForReference: boolean ) { - const fieldMetaData = { - datasourceMetadata: { - datasource: { - parameters: config.parameters ?? {}, - propertyForDisplayText: config.datasource?.fields?.text?.substring(3) ?? config.datasource?.fields?.text, - propertyForValue: config.datasource?.fields?.value?.substring(3) ?? config.datasource?.fields?.value, - name: config.referenceList ?? '' - } - } - }; + const selectionMode = rawConfig.mode; + const firstChildMeta = structuredClone((this.rawViewMetadata as any)?.children?.[0]); + const pyID = getMappedKey('pyID'); + const additionalInfo = refFieldMetadata?.additionalInformation ? { content: refFieldMetadata.additionalInformation } : undefined; + const pageListValueFromConfig = rawConfig.pagelistValue; + const selectionList = pageListValueFromConfig?.startsWith('@P') ? pageListValueFromConfig?.substring(3) : pageListValueFromConfig; + const contextPageFromConfig = rawConfig.contextPage; + const unannotatedContextPageFromConfig = contextPageFromConfig?.startsWith('@P') ? contextPageFromConfig?.substring(3) : contextPageFromConfig; + const name = (selectionList ?? unannotatedContextPageFromConfig)?.replace(/^\./, ''); - const componentConfig = { - ...config, - descriptors: mode === 'single' ? refFieldMetadata?.descriptors : null, - datasourceMetadata: fieldMetaData.datasourceMetadata, + const dataReferenceConfigToChild: any = { + selectionMode, + additionalInfo, + descriptors: selectionMode === SELECTION_MODE.SINGLE ? refFieldMetadata?.descriptors : null, required: propsToUse.required, visibility: propsToUse.visibility, disabled: propsToUse.disabled, label: propsToUse.label, + displayAs: 'advancedSearch', readOnly: false, - ...(mode === 'single' && { referenceType }), - contextClass: config.targetObjectClass, - primaryField: config.displayField, - dataRelationshipContext: config.displayField ? getDataRelationshipContextFromKey(config.displayField) : null, + matchPosition, + ...(selectionMode === SELECTION_MODE.SINGLE && { referenceType }), + ...(selectionMode === SELECTION_MODE.SINGLE && { + value: rawConfig.value, + contextPage: contextPageFromConfig + }), + ...(selectionMode === SELECTION_MODE.MULTI && { + selectionList, + readonlyContextList: pageListValueFromConfig, + referenceType: referenceType || firstChildMeta?.config?.referenceType + }), + dataRelationshipContext: rawConfig.targetObjectClass && name ? name : null, hideLabel, + onRecordChange: this.onRecordChange, + getAdditionalInfo: getAdditionalInfo.bind(this, this.pConn$, rawConfig?.authorContext), + createNewRecord: isCreateNewReferenceEnabled ? createNewRecord : undefined, inline }; - const component = this.pConn$.createComponent({ type: this.type, config: componentConfig }, '', 0, {}); + this.searchSelectCacheKey = componentCachePersistUtils.getComponentStateKey(this.pConn$, name); + + // Set advanced search context via service + this.dataRefAdvancedSearchService.setConfig({ + dataReferenceConfigToChild, + isCreateNewReferenceEnabled, + disableStartingFieldsForReference, + pyID, + searchSelectKey: this.searchSelectCacheKey + }); + } + + private buildMultiselectChild( + rawConfig: any, + mode: string, + refFieldMetadata: any, + fieldMetaData: any, + propsToUse: any, + referenceType: string, + hideLabel: boolean, + inline: boolean, + isCreateNewReferenceEnabled: boolean, + disableStartingFieldsForReference: boolean, + contextClass: string + ) { + const component = this.pConn$.createComponent( + { + type: 'Multiselect', + config: { + ...rawConfig, + descriptors: refFieldMetadata?.descriptors, + datasourceMetadata: fieldMetaData?.datasourceMetadata, + selectionList: rawConfig.pagelistValue?.substring(3), + readonlyContextList: rawConfig.pagelistValue, + selectionKey: rawConfig.selectionKey, + selectionMode: SELECTION_MODE.MULTI, + required: propsToUse.required, + visibility: propsToUse.visibility, + disabled: propsToUse.disabled, + label: propsToUse.label, + parameters: rawConfig.parameters, + readOnly: false, + localeReference: rawConfig.localeReference, + ...(mode === SELECTION_MODE.MULTI ? { referenceType } : {}), + contextClass: rawConfig.targetObjectClass, + primaryField: rawConfig.displayField?.startsWith('@P') ? rawConfig.displayField.slice(3) : rawConfig.displayField, + dataRelationshipContext: rawConfig.pagelistValue?.substring(4), + hideLabel: hideLabel ?? false, + onRecordChange: this.onRecordChange, + createNewRecord: isCreateNewReferenceEnabled + ? () => + createNewRecord({ + referenceType: rawConfig.targetObjectType === 'case' ? 'Case' : 'Data', + pConn: this.pConn$, + getPConnect: () => this.pConn$, + disableStartingFieldsForReference, + startingFields: {}, + contextClass + }) + : undefined, + inline, + columnsFormatter: rawConfig.secondaryFields, + additionalInfo: refFieldMetadata?.additionalInformation ? { content: refFieldMetadata.additionalInformation } : undefined + } + } as any, + '', + 0, + {} + ); this.newComponentName = component?.getPConnect().getComponentName(); this.newPconn = component?.getPConnect(); - if (this.rawViewMetadata?.config) { - this.rawViewMetadata.config = { ...config }; + + // Pre-set the reference list on the child PConnect so that + // getListActions() resolves the correct path on every re-render. + const selectionList = rawConfig.pagelistValue?.substring(3); + if (this.newPconn && selectionList) { + this.newPconn.setReferenceList(selectionList); } } + + private buildEmbeddedInsightTableChild(rawConfig: any, referenceType: string, propsToUse: any) { + // Extract columns from insightModel.query.columns and convert to presets format + const insightModel = propsToUse.insightModel; + const insightColumns: any[] = insightModel?.query?.columns ?? []; + + const columnChildren = insightColumns + .filter((col: any) => col.type === 'column' && col.field) + .map((col: any) => ({ + type: 'TextInput', + config: { + value: `@P .${col.field.fieldID}`, + label: col.field.name || col.field.fieldID + } + })); + + const presets = [ + { + children: [{ children: columnChildren, name: 'Columns', type: 'Region' }], + config: {}, + id: 'P_', + label: '', + name: 'presets', + template: 'Table' + } + ]; + + const componentConfig = { + type: 'SimpleTableManual', + config: { + contextClass: rawConfig.targetObjectClass, + presets, + label: propsToUse.label, + readonlyContextList: rawConfig.pagelistValue, + referenceList: rawConfig.pagelistValue, + renderMode: 'ReadOnly', + dataPageName: rawConfig.referenceList, + fieldMetadata: { + datasource: { + parameters: this.configProps.parameters ?? {} + } + } + } + }; + + const component = this.pConn$.createComponent(componentConfig as any, '', 0, {}); + this.newComponentName = component?.getPConnect().getComponentName(); + this.newPconn = component?.getPConnect(); + } + + private buildDefaultChild( + rawConfig: any, + mode: string, + refFieldMetadata: any, + fieldMetaData: any, + propsToUse: any, + referenceType: string, + hideLabel: boolean, + inline: boolean, + isCreateNewReferenceEnabled: boolean, + disableStartingFieldsForReference: boolean, + contextClass: string, + linkReference: any + ) { + this.useOutputEvents = true; + const component = this.pConn$.createComponent( + { + type: this.type, + config: { + ...rawConfig, + descriptors: mode === SELECTION_MODE.SINGLE ? refFieldMetadata?.descriptors : null, + datasourceMetadata: fieldMetaData?.datasourceMetadata, + required: propsToUse.required, + visibility: propsToUse.visibility, + disabled: propsToUse.disabled, + label: propsToUse.label, + parameters: rawConfig.parameters, + readOnly: false, + localeReference: rawConfig.localeReference, + ...(mode === SELECTION_MODE.SINGLE ? { referenceType } : {}), + contextClass: rawConfig.targetObjectClass, + primaryField: rawConfig.displayField, + dataRelationshipContext: rawConfig.displayField ? getDataRelationshipContextFromKey(rawConfig.displayField) : null, + hideLabel, + onRecordChange: this.onRecordChange, + createNewRecord: isCreateNewReferenceEnabled + ? () => + createNewRecord({ + referenceType: rawConfig.targetObjectType === 'case' ? 'Case' : 'Data', + pConn: this.pConn$, + getPConnect: () => this.pConn$, + disableStartingFieldsForReference, + startingFields: {}, + contextClass + }) + : undefined, + inline, + columnsFormatter: rawConfig.secondaryFields, + getAdditionalInfo: getAdditionalInfo.bind(this, this.pConn$, rawConfig.displayField), + linkReference: this.type === 'AutoComplete' ? linkReference : undefined + } + } as any, + '', + 0, + {} + ); + this.newComponentName = component?.getPConnect().getComponentName(); + this.newPconn = component?.getPConnect(); + } } diff --git a/packages/angular-sdk-components/src/lib/_components/field/selectable-card/selectable-card.component.html b/packages/angular-sdk-components/src/lib/_components/field/selectable-card/selectable-card.component.html index 345ba763..71910278 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/selectable-card/selectable-card.component.html +++ b/packages/angular-sdk-components/src/lib/_components/field/selectable-card/selectable-card.component.html @@ -17,6 +17,7 @@ [checked]="cardContent.commonCardProps.selected" [disabled]="disabled || readOnly" [attr.data-test-id]="testId + ':' + cardContent.commonCardProps.label" + (click)="$event.stopPropagation()" (change)="handleChangeMultiMode($event, cardContent.commonCardProps)" (blur)="fieldOnBlur()" >{{ cardContent.commonCardProps.label }} this.pConn$ }; + } else { + this.deferLoadedTabs = this.pConn$.getChildren()[2]; + } const cache: any = PCore.getNavigationUtils().getComponentCache(this.searchSelectCacheKey) ?? {}; const { selectedCategory } = cache; const firstTabId = getFirstVisibleTabId(this.deferLoadedTabs, selectedCategory); diff --git a/packages/angular-sdk-components/src/lib/_components/template/simple-table-manual/simple-table-manual.component.ts b/packages/angular-sdk-components/src/lib/_components/template/simple-table-manual/simple-table-manual.component.ts index 2aaa4d74..7b945a66 100644 --- a/packages/angular-sdk-components/src/lib/_components/template/simple-table-manual/simple-table-manual.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/template/simple-table-manual/simple-table-manual.component.ts @@ -159,6 +159,9 @@ export class SimpleTableManualComponent implements OnInit, OnDestroy { referenceListStr: any; bUseSeparateViewForEdit: any; editView: any; + editType: any; + defaultActionId: any; + editActionId: any; settingsSvgIcon$: string; isInitialized = false; @@ -308,6 +311,10 @@ export class SimpleTableManualComponent implements OnInit, OnDestroy { this.defaultView = editModeConfig ? editModeConfig.defaultView : viewForAddAndEditModal; this.bUseSeparateViewForEdit = editModeConfig ? editModeConfig.useSeparateViewForEdit : useSeparateViewForEdit; this.editView = editModeConfig ? editModeConfig.editView : viewForEditModal; + this.editType = editModeConfig?.editType; + this.defaultActionId = this.editType === 'action' ? editModeConfig?.defaultAction : undefined; + this.editActionId = + this.editType === 'action' && editModeConfig?.useSeparateActionForEdit ? editModeConfig?.editAction : editModeConfig?.defaultAction; const primaryFieldsViewIndex = resolvedFields.findIndex(field => field.config.value === 'pyPrimaryFields'); // const showDeleteButton = !this.readOnlyMode && !hideDeleteRow; @@ -384,17 +391,15 @@ export class SimpleTableManualComponent implements OnInit, OnDestroy { } initializeDefaultPageInstructions() { - if (this.isInitialized) { + if (this.allowEditingInModal) { + this.pConn$.getListActions().initDefaultPageInstructions( + this.pConn$.getReferenceList(), + this.fieldDefs.filter(item => item.name).map(item => item.name) + ); + } else if (this.isInitialized) { this.isInitialized = false; - if (this.allowEditingInModal) { - this.pConn$.getListActions().initDefaultPageInstructions( - this.pConn$.getReferenceList(), - this.fieldDefs.filter(item => item.name).map(item => item.name) - ); - } else { - // @ts-ignore - An argument for 'propertyNames' was not provided. - this.pConn$.getListActions().initDefaultPageInstructions(this.pConn$.getReferenceList()); - } + // @ts-ignore - An argument for 'propertyNames' was not provided. + this.pConn$.getListActions().initDefaultPageInstructions(this.pConn$.getReferenceList()); } } @@ -958,17 +963,18 @@ export class SimpleTableManualComponent implements OnInit, OnDestroy { } addRecord() { - if (this.allowEditingInModal && this.defaultView) { + if (this.allowEditingInModal && (this.defaultView || this.defaultActionId)) { this.pConn$ .getActionsApi() - // @ts-expect-error .openEmbeddedDataModal( this.defaultView, this.pConn$ as any, this.referenceListStr, this.referenceList.length, PCore.getConstants().RESOURCE_STATUS.CREATE, - this.targetClassLabel + this.targetClassLabel, + this.editType, + this.defaultActionId ); } else { this.pConn$.getListActions().insert({ classID: this.contextClass }, this.referenceList.length); @@ -981,16 +987,18 @@ export class SimpleTableManualComponent implements OnInit, OnDestroy { editRecord(data, index) { if (data) { + const viewForEdit = this.bUseSeparateViewForEdit ? this.editView : this.defaultView; this.pConn$ .getActionsApi() - // @ts-expect-error .openEmbeddedDataModal( - this.bUseSeparateViewForEdit ? this.editView : this.defaultView, + viewForEdit, this.pConn$ as any, this.referenceListStr, index, PCore.getConstants().RESOURCE_STATUS.UPDATE, - this.targetClassLabel + this.targetClassLabel, + this.editType, + this.editActionId ); } } diff --git a/packages/angular-sdk-components/src/lib/_components/template/simple-table/simple-table.component.ts b/packages/angular-sdk-components/src/lib/_components/template/simple-table/simple-table.component.ts index 1ad6f2ff..005771e4 100644 --- a/packages/angular-sdk-components/src/lib/_components/template/simple-table/simple-table.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/template/simple-table/simple-table.component.ts @@ -82,6 +82,11 @@ export class SimpleTableComponent implements OnInit, OnDestroy { // moved this from ngOnInit() and call this from there instead... this.configProps$ = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()) as SimpleTableProps; + // Reset rendering state on every update + this.fieldGroupProps = null; + this.listViewProps = null; + this.refToPConnect = null; + if (this.configProps$.visibility != null) { this.bVisible$ = this.bVisible$ = this.utils.getBooleanValue(this.configProps$.visibility); } @@ -95,6 +100,7 @@ export class SimpleTableComponent implements OnInit, OnDestroy { } if (multiRecordDisplayAs === 'fieldGroup') { this.fieldGroupProps = { ...this.configProps$, contextClass }; + return; } const { diff --git a/packages/angular-sdk-components/src/lib/_helpers/objectReference-utils.ts b/packages/angular-sdk-components/src/lib/_helpers/objectReference-utils.ts index 10cb611b..75fa6b5d 100644 --- a/packages/angular-sdk-components/src/lib/_helpers/objectReference-utils.ts +++ b/packages/angular-sdk-components/src/lib/_helpers/objectReference-utils.ts @@ -1,12 +1,17 @@ +export const SELECTION_MODE = { SINGLE: 'single', MULTI: 'multi' }; + +export const SIMPLE_TABLE_MANUAL_READONLY = 'SimpleTableManualReadOnly'; + const PERIOD = '.'; const AT = '@'; const SQUARE_BRACKET_START = '['; const SQUARE_BRACKET_END = ']'; function getMappedKey(key) { - const mappedKey = PCore.getEnvironmentInfo().getKeyMapping(key); + const qualifiedKey = (PCore as any).getNameSpaceUtils().getDefaultQualifiedName(key); + const mappedKey = PCore.getEnvironmentInfo().getKeyMapping(qualifiedKey); if (!mappedKey) { - return key; + return qualifiedKey; } return mappedKey; } @@ -16,7 +21,7 @@ function updatePageListPropertyValue(value) { return value; } -function getPropertyValue(value) { +export function getPropertyValue(value) { if (value.startsWith(AT)) { value = value.substring(value.indexOf(' ') + 1); if (value.startsWith(PERIOD)) value = value.substring(1); @@ -28,30 +33,45 @@ function getPropertyValue(value) { } function getLeafNameFromPropertyName(property): string { - return property?.substr(property.lastIndexOf('.')); + return property?.substr(property.lastIndexOf('.') + 1); +} + +export function isSelfReferencedProperty(param, referenceProp): boolean { + const [, parentPropName] = param.split('.'); + const referencePropParent = referenceProp?.split('.').pop(); + return parentPropName === referencePropParent; } -function isSelfReferencedProperty(param, referenceProp): boolean { - return param === referenceProp?.split('.', 2)[1]; +function getReferenceProp(config): string { + if (config.mode === SELECTION_MODE.MULTI) { + return config?.pagelistValue?.substring(4) ?? ''; + } + const property = config.value; + const arr = property?.split('.') ?? []; + if (arr.length > 1) { + arr.pop(); + return arr.slice(1).join('.'); + } + return ''; } function getCompositeKeys(c11nEnv, property): any { const { datasource: { parameters = {} } = {} } = c11nEnv.getFieldMetadata(property) || {}; return Object.values(parameters).reduce((compositeKeys: any, param: any) => { - if (isSelfReferencedProperty(property, param)) { + if (isSelfReferencedProperty(param, property)) { let propName = getPropertyValue(param); - propName = propName.substring(propName.indexOf('.')); + propName = propName.substring(propName.indexOf('.') + 1); compositeKeys.push(propName); } return compositeKeys; }, []); } -function generateColumns(config, pConn, referenceType) { +export function generateColumns(config, pConn, referenceType) { const displayField = getLeafNameFromPropertyName(config.displayField); - const referenceProp = config.value.split('.', 2)[1]; + const referenceProp = getReferenceProp(config); const compositeKeys = getCompositeKeys(pConn, referenceProp); - let value = getLeafNameFromPropertyName(config.value); + let value = getLeafNameFromPropertyName(config.mode === SELECTION_MODE.MULTI ? config.selectionKey : config.value); const columns: any[] = []; if (displayField) { @@ -63,6 +83,9 @@ function generateColumns(config, pConn, referenceType) { }); } if (value && compositeKeys.indexOf(value) !== -1) { + if (!config.value) { + config.value = `@P .${referenceProp}.${value}`; + } columns.push({ value, setProperty: 'Associated property', @@ -70,7 +93,7 @@ function generateColumns(config, pConn, referenceType) { }); } else { const actualValue = compositeKeys.length > 0 ? compositeKeys[0] : value; - config.value = `@P .${referenceProp}${actualValue}`; + config.value = `@P .${referenceProp}.${actualValue}`; value = actualValue; columns.push({ value: actualValue, @@ -81,9 +104,9 @@ function generateColumns(config, pConn, referenceType) { config.datasource = { fields: { - key: getLeafNameFromPropertyName(config.value), - text: getLeafNameFromPropertyName(config.displayField), - value: getLeafNameFromPropertyName(config.value) + key: `.${getLeafNameFromPropertyName(config.value)}`, + text: `.${getLeafNameFromPropertyName(config.displayField)}`, + value: `.${getLeafNameFromPropertyName(config.value)}` } }; @@ -97,21 +120,105 @@ function generateColumns(config, pConn, referenceType) { } compositeKeys.forEach(key => { + const descriptorsFieldName = `.${key}`; if (value !== key) columns.push({ - value: key, + value: descriptorsFieldName, display: 'false', secondary: 'true', useForSearch: false, - setProperty: `.${referenceProp}${key}` + setProperty: `.${referenceProp}.${key}` }); }); config.columns = columns; } -function getDataRelationshipContextFromKey(key) { - return key.split('.', 2)[1]; +export function addCompositeKeysToConfig(config, pConn) { + const referenceProp = getReferenceProp(config); + const fieldMetadata = pConn.getFieldMetadata(referenceProp) || {}; + const { datasource: { parameters: fieldParameters = {} } = {} } = fieldMetadata; + const compositeKeys: string[] = []; + Object.values(fieldParameters).forEach((param: any) => { + if (isSelfReferencedProperty(param, referenceProp)) { + compositeKeys.push(param); + } + }); + config.compositeKeys = compositeKeys; +} + +export function getDataRelationshipContextFromKey(key) { + const firstIndexOfDot = key.indexOf('.'); + if (firstIndexOfDot > -1) { + const lastIndexOfDot = key.lastIndexOf('.'); + if (lastIndexOfDot > -1) { + return key.substring(firstIndexOfDot + 1, lastIndexOfDot); + } + } + return ''; +} + +export function createNewRecord({ referenceType, disableStartingFieldsForReference, pConn, contextClass, startingFields, getPConnect }) { + if (referenceType === 'Case') { + if (!disableStartingFieldsForReference) { + startingFields[(PCore as any).getNameSpaceUtils().getDefaultQualifiedName('pyAddCaseContextPage')] = { + pyID: pConn.getCaseInfo().getKey()?.split(' ')?.pop() + }; + } + return pConn.getActionsApi().createWork(contextClass, { + openCaseViewAfterCreate: false, + startingFields + }); + } + if (referenceType === 'Data') { + return getPConnect().getActionsApi().showDataObjectCreateView(contextClass); + } +} + +export function generateDetailsDisplay({ isCaseType, fieldForDisplay, fieldNameForKey, displayFieldMetadata }) { + const displayDetails: any[] = [ + { + config: { + label: `@L ${fieldForDisplay}`, + value: `@P .${fieldForDisplay}`, + ...(isCaseType && { + additionalDetails: { + type: 'DISPLAY_LINK', + params: {} + } + }) + }, + type: displayFieldMetadata?.type || 'TextInput' + } + ]; + if (isCaseType) { + displayDetails.push({ + config: { + additionalDetails: { + params: {}, + type: 'DISPLAY_LINK' + }, + label: '@L Case ID', + previewKey: '@P .pzInsKey', + value: `@P ${fieldNameForKey || 'pyID'}` + }, + type: 'TextInput' + }); + } + return displayDetails; +} + +export function getAdditionalInfo(pConn, propertyName) { + const parentFieldMetadata = pConn.getFieldMetadata(getDataRelationshipContextFromKey(propertyName)); + return parentFieldMetadata?.additionalInformation + ? { + content: parentFieldMetadata.additionalInformation + } + : undefined; +} + +export function camelCase(str: string): string { + return str.replace(/[-_\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : '')).replace(/^[A-Z]/, char => char.toLowerCase()); } -export { getLeafNameFromPropertyName, isSelfReferencedProperty, getCompositeKeys, generateColumns, getDataRelationshipContextFromKey }; +export { getLeafNameFromPropertyName, getCompositeKeys }; diff --git a/projects/angular-test-app/tests/e2e/DigV2/NewComplexFields/CaseReference.spec.js b/projects/angular-test-app/tests/e2e/DigV2/NewComplexFields/CaseReference.spec.js new file mode 100644 index 00000000..68618686 --- /dev/null +++ b/projects/angular-test-app/tests/e2e/DigV2/NewComplexFields/CaseReference.spec.js @@ -0,0 +1,179 @@ +const { test, expect } = require('@playwright/test'); +const config = require('../../../config'); +const common = require('../../../common'); + +test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 1920, height: 1080 }); + await page.goto(config.config.baseUrl, { waitUntil: 'networkidle' }); +}); + +const selectComplexFieldType = async (type, page) => { + await page.locator('mat-select[data-test-id="d1b79d3f-1b8c-4347-bdd8-f86a5967bebf"]').click(); + await page.getByRole('option', { name: type, exact: true }).click(); +}; + +const selectMode = async (mode, page) => { + const modeDropdown = page.locator('mat-select[data-test-id="5131692e-09b6-45f1-83fa-3c280566f0fa"]'); + await modeDropdown.waitFor({ state: 'visible' }); + await expect(async () => { + await modeDropdown.click(); + await expect(page.locator('.cdk-overlay-pane mat-option').first()).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 30000 }); + await page.locator(`mat-option > span:has-text("${mode}")`).click(); +}; + +const selectDisplayAs = async (displayAs, page) => { + const displayDropdown = page.locator('mat-select[data-test-id="a97b483b-34e0-48c5-8f48-a09cee7d74a3"]'); + await displayDropdown.waitFor({ state: 'visible' }); + await displayDropdown.click(); + await page.locator(`mat-option > span:has-text("${displayAs}")`).click(); +}; + +test.describe('Single select mode of Case Reference', () => { + test('Verify Single select mode of Case Reference displayed as Cards', async ({ page }) => { + await common.login(config.config.apps.digv2.user.username, config.config.apps.digv2.user.password, page); + + await common.verifyHomePage(page); + + // Creating New complex case type + await common.createCase('New Complex Fields', page); + + // Get the case ID of the newly created case + await expect(page.locator('div[id="caseId"]')).not.toBeEmpty(); + const caseID = await page.locator('div[id="caseId"]').textContent(); + + // Select Case Reference from the category dropdown + await selectComplexFieldType('CaseReference', page); + await page.locator('button:has-text("submit")').click(); + + await page.locator('h2:has-text("CaseReference")').waitFor({ state: 'visible' }); + await page.waitForLoadState('networkidle'); + + // Select Single Select mode and Cards as display option for the Case Reference field + await selectMode('Single Select', page); + await selectDisplayAs('Cards', page); + + // Select current case reference field from the list of cards + await page.locator(`mat-card:has(label:has-text("${caseID}")) mat-radio-button`).click(); + await page.locator('button:has-text("submit")').click(); + + // Verify the selected case reference displays the correct case ID + await expect(page.locator('app-semantic-link a.psdk-value')).toBeVisible(); + await expect(page.locator('app-semantic-link a.psdk-value')).toHaveText(caseID); + }, 10000); + + test('Verify Single select mode of Case Reference displayed as Combobox', async ({ page }) => { + await common.login(config.config.apps.digv2.user.username, config.config.apps.digv2.user.password, page); + + await common.verifyHomePage(page); + + // Creating New complex case type + await common.createCase('New Complex Fields', page); + + // Get the case ID of the newly created case + await expect(page.locator('div[id="caseId"]')).not.toBeEmpty(); + const caseID = await page.locator('div[id="caseId"]').textContent(); + + // Select Case Reference from the category dropdown + await selectComplexFieldType('CaseReference', page); + await page.locator('button:has-text("submit")').click(); + + await page.locator('h2:has-text("CaseReference")').waitFor({ state: 'visible' }); + await page.waitForLoadState('networkidle'); + + // Select Single Select mode and Cards as display option for the Case Reference field + await selectMode('Single Select', page); + await selectDisplayAs('Combobox', page); + + // Select current case reference field from the list of combobox options + const combobox = page.locator('input[role="combobox"]'); + await combobox.click(); + await combobox.fill(caseID); + await page.locator(`mat-option:has-text("${caseID}")`).click(); + await page.locator('button:has-text("submit")').click(); + + // Verify the selected case reference displays the correct case ID + await expect(page.locator('app-semantic-link a.psdk-value')).toBeVisible(); + await expect(page.locator('app-semantic-link a.psdk-value')).toHaveText(caseID); + }, 10000); + + test('Verify Single select mode of Case Reference displayed as Dropdown', async ({ page }) => { + await common.login(config.config.apps.digv2.user.username, config.config.apps.digv2.user.password, page); + + await common.verifyHomePage(page); + + // Creating New complex case type + await common.createCase('New Complex Fields', page); + + // Get the case ID of the newly created case + await expect(page.locator('div[id="caseId"]')).not.toBeEmpty(); + const caseID = await page.locator('div[id="caseId"]').textContent(); + + // Select Case Reference from the category dropdown + await selectComplexFieldType('CaseReference', page); + await page.locator('button:has-text("submit")').click(); + + await page.locator('h2:has-text("CaseReference")').waitFor({ state: 'visible' }); + await page.waitForLoadState('networkidle'); + + // Select Single Select mode and Cards as display option for the Case Reference field + await selectMode('Single Select', page); + await selectDisplayAs('Dropdown', page); + + // Select current case reference field from the list of dropdown options + const dropdown = page.locator('mat-select[role="combobox"]').last(); + await dropdown.click(); + await page.locator(`mat-option:has-text("${caseID}")`).click(); + await page.locator('button:has-text("submit")').click(); + + // Verify the selected case reference displays the correct case ID + await expect(page.locator('app-semantic-link a.psdk-value')).toBeVisible(); + await expect(page.locator('app-semantic-link a.psdk-value')).toHaveText(caseID); + }, 10000); + + test('Verify Single select mode of Case Reference displayed as Search and select', async ({ page }) => { + await common.login(config.config.apps.digv2.user.username, config.config.apps.digv2.user.password, page); + + await common.verifyHomePage(page); + + // Creating New complex case type + await common.createCase('New Complex Fields', page); + + // Get the case ID of the newly created case + await expect(page.locator('div[id="caseId"]')).not.toBeEmpty(); + const caseID = await page.locator('div[id="caseId"]').textContent(); + + // Select Case Reference from the category dropdown + await selectComplexFieldType('CaseReference', page); + await page.locator('button:has-text("submit")').click(); + + await page.locator('h2:has-text("CaseReference")').waitFor({ state: 'visible' }); + await page.waitForLoadState('networkidle'); + + // Select Single Select mode and Search and select as display option for the Case Reference field + await selectMode('Single Select', page); + await selectDisplayAs('Search and select', page); + + // Select current case reference field from the Search and select options + const targetCaseID = caseID.trim(); + const searchInput = page.getByLabel('Case ID'); + await searchInput.click(); + await searchInput.fill(targetCaseID); + await page.locator('button:has(span:has-text("Search"))').click(); + + // Wait for search results to load and select the correct case reference + const searchResultRow = page.locator('tr[role="row"]', { hasText: targetCaseID }); + await searchResultRow.waitFor({ state: 'visible' }); + await searchResultRow.locator('mat-radio-button').click(); + + await page.locator('button:has-text("submit")').click(); + + // Verify the selected case reference displays the correct case ID + await expect(page.locator('app-semantic-link a.psdk-value')).toBeVisible(); + await expect(page.locator('app-semantic-link a.psdk-value')).toHaveText(caseID); + }, 10000); +}); + +test.afterEach(async ({ page }) => { + await page.close(); +});