import { useEffect, useState, useCallback } from 'react'; import type { ReactNode } from 'react'; import { TextField, Paper, Button, Divider } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; import isDeepEqual from 'fast-deep-equal/react'; import Utils from '@pega/react-sdk-components/lib/components/helpers/utils'; import { getDataPage } from '@pega/react-sdk-components/lib/components/helpers/data_page'; import { getComponentFromMap } from '@pega/react-sdk-components/lib/bridge/helpers/sdk_component_map'; import type { PConnFieldProps } from '@pega/react-sdk-components/lib/types/PConnProps'; import useStatus from '@pega/react-sdk-components/lib/hooks/useStatus'; import { getFieldSx } from '@pega/react-sdk-components/lib/components/helpers/field-utils'; import handleEvent from '@pega/react-sdk-components/lib/components/helpers/event-utils'; interface IOption { key: string; value: string; secondary?: ReactNode[]; secondaryRaw?: string[]; group?: string; } const preProcessColumns = columnList => { return columnList.map(col => { const tempColObj = { ...col }; tempColObj.value = col.value && col.value.startsWith('.') ? col.value.substring(1) : col.value; return tempColObj; }); }; const getPropertyName = (rawValue: string): string => { let propName; if (rawValue.startsWith('@USER ')) { propName = rawValue.substring(6).trim(); } else if (rawValue.startsWith('@P ')) { propName = rawValue.substring(3).trim(); } else { propName = rawValue.trim(); } return propName.startsWith('.') ? propName.substring(1) : propName; }; const getDisplayFieldsMetaData = columnList => { const displayColumns = columnList.filter(col => col.display === 'true'); const metaDataObj: any = { key: '', primary: '', secondary: [] }; const keyCol = columnList.filter(col => col.key === 'true'); metaDataObj.key = keyCol.length > 0 ? keyCol[0].value : 'auto'; for (let index = 0; index < displayColumns.length; index += 1) { if (displayColumns[index].primary === 'true') { metaDataObj.primary = displayColumns[index].value; } else { metaDataObj.secondary.push(displayColumns[index].value); } } return metaDataObj; }; interface AutoCompleteProps extends PConnFieldProps { // If any, enter additional props that only exist on AutoComplete here' displayMode?: string; deferDatasource?: boolean; datasourceMetadata?: any; status?: string; onRecordChange?: any; additionalProps?: object; listType: string; parameters?: any; datasource: any; columns: any[]; showFieldMessage?: boolean; messageConfig?: { content?: string; visibility?: boolean; }; variant?: string; showCreateNew?: boolean; onCreateNew?: () => void; createNewLabel?: string; createNewRecord?: () => Promise; contextClass?: string; referenceType?: string; allowCreatingRecords?: boolean; } export default function AutoComplete(props: AutoCompleteProps) { // Get emitted components from map (so we can get any override that may exist) const TextInput = getComponentFromMap('TextInput'); const FieldValueList = getComponentFromMap('FieldValueList'); const { getPConnect, label, required, placeholder, value = '', validatemessage, readOnly, testId, displayMode, deferDatasource, datasourceMetadata, helperText, hideLabel, onRecordChange, showFieldMessage, messageConfig = {}, onCreateNew, createNewLabel = 'Create new', createNewRecord, contextClass, referenceType } = props; const context = getPConnect().getContextName(); let { listType, parameters, datasource = [], columns = [] } = props; const [inputValue, setInputValue] = useState(''); const [options, setOptions] = useState([]); const [theDatasource, setDatasource] = useState(null); let selectedValue: any = null; const eligibleForFieldWarning = showFieldMessage && messageConfig.visibility && !readOnly; const helperTextToDisplay = validatemessage || (eligibleForFieldWarning ? messageConfig.content : helperText); const thePConn = getPConnect(); const actionsApi = thePConn.getActionsApi(); const propName = (thePConn.getStateProps() as any).value; const status = useStatus({ showFieldMessage, messageVisibility: messageConfig.visibility, validatemessage, readOnly }); if (!isDeepEqual(datasource, theDatasource)) { // inbound datasource is different, so update theDatasource (to trigger useEffect) setDatasource(datasource); } const flattenParameters = (params = {}) => { const flatParams = {}; Object.keys(params).forEach(key => { const { name, value: theVal } = params[key]; flatParams[name] = theVal; }); return flatParams; }; // convert associated to datapage listtype and transform props // Process deferDatasource when datapage name is present. WHhen tableType is promptList / localList if (deferDatasource && datasourceMetadata?.datasource?.name) { listType = 'datapage'; datasource = datasourceMetadata.datasource.name; const { parameters: dataSourceParameters, propertyForDisplayText, propertyForValue } = datasourceMetadata.datasource; parameters = flattenParameters(dataSourceParameters); const displayProp = propertyForDisplayText.startsWith('@P') ? propertyForDisplayText.substring(3) : propertyForDisplayText; const valueProp = propertyForValue.startsWith('@P') ? propertyForValue.substring(3) : propertyForValue; columns = [ { key: 'true', setProperty: 'Associated property', value: valueProp }, { display: 'true', primary: 'true', useForSearch: true, value: displayProp } ]; } // Add secondary columns from columnsFormatter metadata const columnsFormatter = (thePConn.getRawMetadata?.()?.config as any)?.columnsFormatter; if (Array.isArray(columnsFormatter) && columnsFormatter.length > 0) { columnsFormatter.forEach(formatter => { if (formatter.config?.value) { columns.push({ value: getPropertyName(formatter.config.value), display: 'true', useForSearch: true, secondary: 'true' }); } }); } // Read groupsFields from raw metadata for grouping support const groupsFields = (thePConn.getRawMetadata?.()?.config as any)?.groupsFields; const groupKeyFields: string[] = Array.isArray(groupsFields) ? groupsFields.map((entry: any) => getPropertyName(entry?.config?.value || '')).filter(Boolean) : []; const isGrouped = groupKeyFields.length > 0; // Ensure group fields are included in columns so they appear in the data page response groupKeyFields.forEach(field => { if (!columns.find(col => col.value === field || col.value === `.${field}`)) { columns.push({ value: field, display: 'false', useForSearch: false }); } }); columns = preProcessColumns(columns); useEffect(() => { if (listType === 'associated') { setOptions(Utils.getOptionList(props, getPConnect().getDataObject(''))); } }, [theDatasource]); // Builds options with primary + secondary data from datapage results const buildOptionsFromResults = (results: any[]): IOption[] => { const optionsData: IOption[] = []; const displayColumn = getDisplayFieldsMetaData(columns); results?.forEach(element => { const val = element[displayColumn.primary]?.toString() || ''; let secondaryNodes: ReactNode[] = []; let secondaryRaw: string[] = []; if (Array.isArray(columnsFormatter) && columnsFormatter.length > 0) { columnsFormatter.forEach((formatter, idx) => { if (formatter.config?.value) { const fmtPropName = getPropertyName(formatter.config.value); secondaryRaw.push(element[fmtPropName]?.toString() || ''); try { const fieldMeta = { meta: { ...formatter, config: { ...formatter.config, displayMode: 'DISPLAY_ONLY', contextName: context, variant: 'inline-compact' } }, useCustomContext: element }; const configObj = PCore.createPConnect(fieldMeta); const resolvedPConn = configObj.getPConnect(); const resolvedProps = resolvedPConn.resolveConfigProps(resolvedPConn.getConfigProps()); const Component = getComponentFromMap(formatter.type); if (Component) { secondaryNodes.push( resolvedPConn} displayMode='DISPLAY_ONLY' />); } else { secondaryNodes.push(element[fmtPropName]?.toString() || ''); } } catch { secondaryNodes.push(element[fmtPropName]?.toString() || ''); } } }); } else { secondaryRaw = displayColumn.secondary.map(col => element[col]?.toString()).filter(Boolean); secondaryNodes = [...secondaryRaw]; } const obj: IOption = { key: String(element[displayColumn.key] ?? element.pyGUID ?? ''), value: val, ...(secondaryNodes.length > 0 && { secondary: secondaryNodes }), ...(secondaryRaw.length > 0 && { secondaryRaw }), ...(isGrouped && { group: element[groupKeyFields[0]]?.toString() || '' }) }; optionsData.push(obj); }); // Sort options by group value so MUI renders groups contiguously if (isGrouped) { optionsData.sort((a, b) => (a.group || '').localeCompare(b.group || '')); } return optionsData; }; useEffect(() => { if (!displayMode && listType !== 'associated') { getDataPage(datasource, parameters, context) .then((results: any) => { setOptions(buildOptionsFromResults(results)); }) .catch(e => { console.error(e); }); } }, []); if (displayMode === 'DISPLAY_ONLY') { return ; } if (displayMode === 'STACKED_LARGE_VAL') { return ; } if (value) { const index = options?.findIndex(element => element.key === value); if (index > -1) { selectedValue = options[index].value; } else { selectedValue = value; } } useEffect(() => { setInputValue(selectedValue || ''); }, [selectedValue]); const handleChange = (event: object, newValue) => { const val = newValue ? newValue.key : ''; if (!newValue) { setInputValue(''); } actionsApi.updateFieldValue(propName, val); const changePromise = (actionsApi as any).triggerFieldChange(propName, val); if (onRecordChange) { if (changePromise && changePromise.then) { changePromise .then(() => { onRecordChange({ ...event, id: val }); }) .catch(e => { console.error(e); }); } else { onRecordChange({ ...event, id: val }); } } }; const handleInputValue = (event, newInputValue, reason) => { if (reason === 'selectOption') { setInputValue(newInputValue); } }; // Sets values for all columns that have setProperty defined (mirrors setValuesToOtherAdditionalFields in constellation-frontend) const setValuesToAdditionalFields = (record: Record) => { const setPropertyList = columns .filter(col => col.setProperty) .map(col => ({ source: col.value, target: col.setProperty, key: col.key, primary: col.primary })); if (setPropertyList.length > 0) { 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(actionsApi, 'changeNblur', propName, valueToSet); } else { const target = typeof prop.target === 'string' ? prop.target : ''; const targetProp = target.startsWith('.') ? target : `.${target}`; actionsApi.updateFieldValue(targetProp, valueToSet, { associatedProperty: propName }); actionsApi.triggerFieldChange(targetProp, valueToSet); } }); } }; // Re-fetches the options list (equivalent to initializeList in constellation-frontend) const refreshOptionsList = () => { if (!displayMode && listType !== 'associated') { getDataPage(datasource, parameters, context) .then((results: any) => { setOptions(buildOptionsFromResults(results)); }) .catch(e => { console.error(e); }); } }; const createNewButtonHandler = () => { if (onCreateNew) { onCreateNew(); return; } if (!contextClass) return; const normalizedReferenceType = typeof referenceType === 'string' ? 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 createNewCallback = isDataReference ? (data: { classId?: string; data?: { responseData?: Record } }) => { // Clear contexted cache before re-fetching PCore.getDataApi().clearContextedCache(context); const responseData = data?.data?.responseData; if (responseData) { // Set all mapped property values from the newly created record setValuesToAdditionalFields(responseData); const displayColumn = getDisplayFieldsMetaData(columns); if (onRecordChange) { const newKey = responseData[displayColumn.key]?.toString() || (responseData as any).pyGUID; onRecordChange({ id: newKey }); } } // Refresh the options list 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 && listType !== 'associated' && datasource) { // Re-fetch data to find the newly created record and set all mapped properties getDataPage(datasource, parameters, context) .then((results: any) => { setOptions(buildOptionsFromResults(results)); // Find the newly created record by ID or caseId and set all properties const displayColumn = getDisplayFieldsMetaData(columns); const newRecord = results?.find((el: any) => el.ID === data.ID || (el[displayColumn.key] || el.pyGUID) === selectKey); if (newRecord) { setValuesToAdditionalFields(newRecord); } else { // Fallback: just set the key value handleEvent(actionsApi, 'changeNblur', propName, selectKey); } if (onRecordChange) { onRecordChange({ id: selectKey }); } }) .catch(e => { console.error(e); }); } PCore.getPubSubUtils().unsubscribe(eventType, contextClass); } }; // Build the create action if createNewRecord prop is not provided const triggerCreate = createNewRecord ? createNewRecord() : isDataReference ? getPConnect().getActionsApi().showDataObjectCreateView(contextClass) : thePConn.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) refreshOptionsList(); }) .catch(e => { console.error(e); }); }; const showCreateButton = props.allowCreatingRecords === true; const CustomPaper = useCallback( (paperProps: React.HTMLAttributes) => { return ( {paperProps.children} {showCreateButton && ( <> )} ); }, [showCreateButton, createNewButtonHandler, createNewLabel] ); if (readOnly) { const theValAsString = options?.find(opt => opt.key === value)?.value; return ; } // Need to use both getOptionLabel and getOptionSelected to map our // key/value structure to what Autocomplete expects return ( { return option.value ? option.value : ''; }} {...(isGrouped && { groupBy: (option: IOption) => option.group || '' })} isOptionEqualToValue={(option: any) => { return option.value ? option.value : ''; }} fullWidth onChange={handleChange} value={selectedValue} inputValue={inputValue} onInputChange={handleInputValue} filterOptions={createFilterOptions({ stringify: option => { const parts = [option.value || '']; if (option.secondaryRaw) { parts.push(...option.secondaryRaw); } return parts.join(' '); } })} renderOption={(renderProps, option: IOption) => (
  • {option.value} {option.secondary && option.secondary.length > 0 && (
    {option.secondary.map((sec, i) => ( {sec} {i < option.secondary!.length - 1 && {'\u00B7'}} ))}
    )}
  • )} renderInput={params => ( )} /> ); }