import React, { type PropsWithChildren, useEffect, useLayoutEffect, useRef, useState } from 'react'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import makeStyles from '@mui/styles/makeStyles'; import Link from '@mui/material/Link'; import { createElement } from 'react'; import TableSortLabel from '@mui/material/TableSortLabel'; import MoreIcon from '@mui/icons-material/MoreVert'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import FilterListIcon from '@mui/icons-material/FilterList'; import SubjectIcon from '@mui/icons-material/Subject'; import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import Select from '@mui/material/Select'; import Button from '@mui/material/Button'; import TextField from '@mui/material/TextField'; import createPConnectComponent from '@pega/react-sdk-components/lib/bridge/react_pconnect'; import { Utils } from '@pega/react-sdk-components/lib/components/helpers/utils'; import { getReferenceList } from '@pega/react-sdk-components/lib/components/helpers/field-group-utils'; import { getDataPage } from '@pega/react-sdk-components/lib/components/helpers/data_page'; import { getGenericFieldsLocalizedValue } from '@pega/react-sdk-components/lib/components/helpers/common-utils'; import { buildFieldsForTable, filterData, getContext } from '@pega/react-sdk-components/lib/components/helpers/simpleTableHelpers'; import type { PConnProps } from '@pega/react-sdk-components/lib/types/PConnProps'; import { format } from '@pega/react-sdk-components/lib/components/helpers/formatters'; interface SimpleTableManualProps extends PConnProps { // If any, enter additional props that only exist on this component hideAddRow?: boolean; hideDeleteRow?: boolean; referenceList?: any[]; renderMode?: string; presets?: any[]; label?: string; showLabel?: boolean; dataPageName?: string; contextClass?: string; propertyLabel?: string; fieldMetadata?: any; editMode?: string; addAndEditRowsWithin?: any; viewForAddAndEditModal?: any; editModeConfig?: any; displayMode?: string; useSeparateViewForEdit: any; viewForEditModal: any; validatemessage?: string; required?: boolean; } const useStyles = makeStyles((/* theme */) => ({ label: { margin: '8px' }, tableLabel: { '&::after': { display: 'inline', content: '" *"', verticalAlign: 'top', color: 'var(--app-error-color)' } }, message: { margin: '8px', color: 'var(--app-error-color)', fontSize: '14px' }, header: { background: 'var(--table-header-background)' }, tableCell: { borderRight: '1px solid lightgray', padding: '8px' }, visuallyHidden: { border: 0, clip: 'rect(0 0 0 0)', height: 1, margin: -1, overflow: 'hidden', padding: 0, position: 'absolute', top: 20, width: 1 }, moreIcon: { verticalAlign: 'bottom' } })); let menuColumnId = ''; let menuColumnType = ''; let menuColumnLabel = ''; const filterByColumns: any[] = []; let myRows: any[]; export default function SimpleTableManual(props: PropsWithChildren) { const classes = useStyles(); const { getPConnect, referenceList = [], // if referenceList not in configProps$, default to empy list children, renderMode, presets, label: labelProp, showLabel, dataPageName, contextClass, hideAddRow, hideDeleteRow, propertyLabel, fieldMetadata, editMode, addAndEditRowsWithin, viewForAddAndEditModal, editModeConfig, displayMode, useSeparateViewForEdit, viewForEditModal, required, validatemessage } = props; const pConn = getPConnect(); const [rowData, setRowData] = useState([]); const [elements, setElementsData] = useState([]); const [order, setOrder] = useState('asc'); const [orderBy, setOrderBy] = useState(''); const [anchorEl, setAnchorEl] = useState(null); const [editAnchorEl, setEditAnchorEl] = useState(null); const [open, setOpen] = useState(false); const [filterBy, setFilterBy] = useState(); const [containsDateOrTime, setContainsDateOrTime] = useState(false); const [filterType, setFilterType] = useState('string'); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [displayDialogFilterName, setDisplayDialogFilterName] = useState(''); const [displayDialogContainsFilter, setDisplayDialogContainsFilter] = useState('contains'); const [displayDialogContainsValue, setDisplayDialogContainsValue] = useState(''); const [displayDialogDateFilter, setDisplayDialogDateFilter] = useState('notequal'); const [displayDialogDateValue, setDisplayDialogDateValue] = useState(''); const selectedRowIndex: any = useRef(null); const localizedVal = PCore.getLocaleUtils().getLocaleValue; const localeCategory = 'SimpleTable'; const parameters = fieldMetadata?.datasource?.parameters; const { referenceListStr } = getContext(getPConnect()); const label = labelProp || propertyLabel; const propsToUse = { label, showLabel, ...getPConnect().getInheritedProps() }; if (propsToUse.showLabel === false) { propsToUse.label = ''; } // Getting current context const context = getPConnect().getContextName(); const resolvedList = getReferenceList(pConn); pConn.setReferenceList(resolvedList); const menuIconOverride$ = Utils.getImageSrc('trash', Utils.getSDKStaticConentUrl()); const resolvedFields = children?.[0]?.children || presets?.[0].children?.[0].children; const primaryFieldsViewIndex = resolvedFields.findIndex(field => field.config.value === 'pyPrimaryFields'); // NOTE: props has each child.config with datasource and value undefined // but getRawMetadata() has each child.config with datasource and value showing their unresolved values (ex: "@P thePropName") // We need to use the prop name as the "glue" to tie the table dataSource, displayColumns and data together. // So, in the code below, we'll use the unresolved config.value (but replacing the space with an underscore to keep things happy) const rawMetadata: any = getPConnect().getRawMetadata(); // get raw config since @P and other annotations are processed and don't appear in the resolved config. // Destructure "raw" children into array var: "rawFields" // NOTE: when config.listType == "associated", the property can be found in either // config.value (ex: "@P .DeclarantChoice") or // config.datasource (ex: "@ASSOCIATED .DeclarantChoice") // Neither of these appear in the resolved props const rawConfig = rawMetadata?.config; const rawFields = rawConfig?.children?.[0]?.children || rawConfig?.presets?.[0].children?.[0]?.children; const isDisplayModeEnabled = displayMode === 'DISPLAY_ONLY'; const readOnlyMode = renderMode === 'ReadOnly'; const editableMode = renderMode === 'Editable'; const showAddRowButton = !readOnlyMode && !hideAddRow; const allowEditingInModal = (editMode ? editMode === 'modal' : addAndEditRowsWithin === 'modal') && !(renderMode === 'ReadOnly' || isDisplayModeEnabled); const showDeleteButton = editableMode && !hideDeleteRow; const defaultView = editModeConfig ? editModeConfig.defaultView : viewForAddAndEditModal; const bUseSeparateViewForEdit = editModeConfig ? editModeConfig.useSeparateViewForEdit : useSeparateViewForEdit; const editView = editModeConfig ? editModeConfig.editView : viewForEditModal; const fieldsWithPropNames = rawFields.map((field, index) => { return { ...resolvedFields[index], propName: field.config.value.replace('@P .', '') }; }); useEffect(() => { buildElementsForTable(); if (readOnlyMode || allowEditingInModal) { generateRowsData(); } }, [referenceList]); // fieldDefs will be an array where each entry will have a "name" which will be the // "resolved" property name (that we can use as the colId) though it's not really // resolved. The buildFieldsForTable helper just removes the "@P " (which is what // Constellation DX Components do). It will also have the "label", and "meta" contains the original, // unchanged config info. For now, much of the info here is carried over from // Constellation DX Components. const fieldDefs = buildFieldsForTable(rawFields, getPConnect(), showDeleteButton, { primaryFieldsViewIndex, fields: resolvedFields }); useLayoutEffect(() => { if (allowEditingInModal) { getPConnect() .getListActions() .initDefaultPageInstructions( getPConnect().getReferenceList(), fieldDefs.filter(item => item.name).map(item => item.name) ); } else { // @ts-expect-error - An argument for 'fields' was not provided getPConnect().getListActions().initDefaultPageInstructions(getPConnect().getReferenceList()); } }, []); const displayedColumns = fieldDefs.map(field => { return field.name ? field.name : field.cellRenderer; }); const getFormattedValue = (val, key) => { const rawField = fieldsWithPropNames.find(item => item.propName === key); let options = {}; if (rawField && ['Boolean', 'Checkbox'].includes(rawField.type)) { const { trueLabel, falseLabel } = rawField.config; options = { trueLabel, falseLabel }; } return rawField ? format(val, rawField.type, options) : val; }; // console.log(`SimpleTable displayedColumns:`); // console.log(displayedColumns); // return the value that should be shown as the contents for the given row data // of the given row field function getRowValue(inRowData: object, inColKey: string): any { // See what data (if any) we have to display const refKeys: string[] = inColKey?.split('.'); let valBuilder = inRowData; for (const key of refKeys) { valBuilder = valBuilder[key]; } return getFormattedValue(valBuilder, inColKey); } const formatRowsData = data => { if (!data) return {}; return data.map(item => { return displayedColumns.reduce((dataForRow, colKey) => { dataForRow[colKey] = getRowValue(item, colKey); return dataForRow; }, {}); }); }; function generateRowsData() { // if referenceList is empty and dataPageName property value exists then make a datapage fetch call and get the list of data. if (!referenceList.length && dataPageName) { getDataPage(dataPageName, parameters, context) .then(listData => { const data = formatRowsData(listData); myRows = data; setRowData(data); }) .catch(e => { console.log(e); }); } else { // The referenceList prop has the JSON data for each row to be displayed // in the table. So, iterate over referenceList to create the dataRows that // we're using as the table's dataSource const data: any = []; for (const row of referenceList) { const dataForRow: object = {}; for (const col of displayedColumns) { const colKey: string = col; const theVal = getRowValue(row, colKey); dataForRow[colKey] = theVal || ''; } data.push(dataForRow); myRows = data; } setRowData(data); } } // May be useful for debugging or understanding how it works. // These are the data structures referred to in the html file. // These are the relationships that make the table work // displayedColumns: key/value pairs where key is order of column and // value is the property shown in that column. Ex: 1: "FirstName" // rowData: array of each row's key/value pairs. Inside each row, // each key is an entry in displayedColumns: ex: "FirstName": "Charles" // Ex: { 1: {config: {label: "First Name", readOnly: true: name: "FirstName"}}, type: "TextInput" } // The "type" indicates the type of component that should be used for editing (when editing is enabled) // // console.log("SimpleTable displayedColumns:"); // console.log(displayedColumns); // console.log(`SimpleTable rowData (${rowData.length} row(s)):`); // console.log(JSON.stringify(rowData)); const addRecord = () => { if (allowEditingInModal && defaultView) { pConn .getActionsApi() // @ts-expect-error .openEmbeddedDataModal(defaultView, pConn, referenceListStr, referenceList.length, PCore.getConstants().RESOURCE_STATUS.CREATE); } else { pConn.getListActions().insert({ classID: contextClass }, referenceList.length); } getPConnect().clearErrorMessages({ property: (getPConnect().getStateProps() as any)?.referenceList?.substring(1) } as any); }; const editRecord = () => { setEditAnchorEl(null); if (typeof selectedRowIndex.current === 'number') { pConn .getActionsApi() // @ts-expect-error .openEmbeddedDataModal( bUseSeparateViewForEdit ? editView : defaultView, pConn, referenceListStr, selectedRowIndex.current, PCore.getConstants().RESOURCE_STATUS.UPDATE ); } }; const deleteRecord = () => { setEditAnchorEl(null); pConn.getListActions().deleteEntry(selectedRowIndex.current); }; const deleteRecordFromInlineEditable = (index: number) => { pConn.getListActions().deleteEntry(index); }; function buildElementsForTable() { const eleData: any = []; referenceList.forEach((element, index) => { const data: any = []; rawFields.forEach(item => { // removing label field from config to hide title in the table cell if (!item.config.hide) { item = { ...item, config: { ...item.config, label: '', displayMode: readOnlyMode || allowEditingInModal ? 'DISPLAY_ONLY' : undefined } }; const referenceListData = getReferenceList(pConn); const isDatapage = referenceListData.startsWith('D_'); const pageReferenceValue = isDatapage ? `${referenceListData}[${index}]` : `${pConn.getPageReference()}${referenceListData.substring(referenceListData.lastIndexOf('.'))}[${index}]`; const config = { meta: item, options: { context, pageReference: pageReferenceValue, referenceList: referenceListData, hasForm: true } }; const view = PCore.createPConnect(config); data.push(createElement(createPConnectComponent(), view)); } }); eleData.push(data); }); setElementsData(eleData); } const handleRequestSort = (event: React.MouseEvent, property: keyof any) => { const isAsc = orderBy === property && order === 'asc'; setOrder(isAsc ? 'desc' : 'asc'); setOrderBy(property); }; const createSortHandler = (property: keyof any) => (event: React.MouseEvent) => { handleRequestSort(event, property); }; function descendingComparator(a: T, b: T, orderedBy: keyof T) { if (!orderedBy || (!a[orderedBy] && !b[orderedBy])) { return 0; } if (!b[orderedBy] || b[orderedBy] < a[orderedBy]) { return -1; } if (!a[orderedBy] || b[orderedBy] > a[orderedBy]) { return 1; } return 0; } type Order = 'asc' | 'desc'; interface Comparator { (a: T, b: T): number; } function getComparator>(theOrder: Order, orderedBy: Key): Comparator { return theOrder === 'desc' ? (a: T, b: T) => descendingComparator(a, b, orderedBy) : (a: T, b: T) => -descendingComparator(a, b, orderedBy); } function stableSort(array: T[], comparator: (a: T, b: T) => number) { const stabilizedThis = array.map((el, index) => [el, index] as [T, number]); stabilizedThis.sort((a, b) => { const order = comparator(a[0], b[0]); if (order !== 0) return order; return a[1] - b[1]; }); const newElements = new Array(stabilizedThis.length); stabilizedThis.forEach((el, index) => { newElements[index] = elements[el[1]]; }); return newElements; } function _menuClick(event, columnId: string, columnType: string, labelValue: string) { menuColumnId = columnId; menuColumnType = columnType; menuColumnLabel = labelValue; setAnchorEl(event.currentTarget); } function editMenuClick(event, index) { selectedRowIndex.current = index; setEditAnchorEl(event.currentTarget); } function _menuClose() { setAnchorEl(null); setEditAnchorEl(null); } function _filterMenu() { setAnchorEl(null); let bFound = false; for (const filterObj of filterByColumns) { if (filterObj.ref === menuColumnId) { setFilterBy(menuColumnLabel); if (filterObj.type === 'Date' || filterObj.type === 'DateTime' || filterObj.type === 'Time') { setContainsDateOrTime(true); setFilterType(filterObj.type); setDisplayDialogDateFilter(filterObj.containsFilter); setDisplayDialogDateValue(filterObj.containsFilterValue); } else { setContainsDateOrTime(false); setFilterType('string'); setDisplayDialogContainsFilter(filterObj.containsFilter); setDisplayDialogContainsValue(filterObj.containsFilterValue); } bFound = true; break; } } if (!bFound) { setFilterBy(menuColumnLabel); setDisplayDialogFilterName(menuColumnId); setDisplayDialogContainsValue(''); switch (menuColumnType) { case 'Date': case 'DateTime': case 'Time': setContainsDateOrTime(true); setFilterType(menuColumnType); break; default: setContainsDateOrTime(false); setFilterType('string'); break; } } // open dialog setOpen(true); } function _groupMenu() {} function _closeDialog() { setOpen(false); } function _dialogContainsFilter(event) { setDisplayDialogContainsFilter(event.target.value); } function _dialogContainsValue(event) { setDisplayDialogContainsValue(event.target.value); } function _dialogDateFilter(event) { setDisplayDialogDateFilter(event.target.value); } function _dialogDateValue(event) { setDisplayDialogDateValue(event.target.value); } function filterSortGroupBy() { // get original data set let theData: any = myRows.slice(); // last filter config data is global theData = theData.filter(filterData(filterByColumns)); // move data to array and then sort setRowData(theData); } function updateFilterWithInfo() { let bFound = false; for (const filterObj of filterByColumns) { if (filterObj.ref === menuColumnId) { filterObj.type = filterType; if (containsDateOrTime) { filterObj.containsFilter = displayDialogDateFilter; filterObj.containsFilterValue = displayDialogDateValue; } else { filterObj.containsFilter = displayDialogContainsFilter; filterObj.containsFilterValue = displayDialogContainsValue; } bFound = true; break; } } if (!bFound) { // add in const filterObj: any = {}; filterObj.ref = menuColumnId; filterObj.type = filterType; if (containsDateOrTime) { filterObj.containsFilter = displayDialogDateFilter; filterObj.containsFilterValue = displayDialogDateValue; } else { filterObj.containsFilter = displayDialogContainsFilter; filterObj.containsFilterValue = displayDialogContainsValue; } filterByColumns.push(filterObj); } } function _submitFilter() { updateFilterWithInfo(); filterSortGroupBy(); setOpen(false); } function _showFilteredIcon(columnId) { for (const filterObj of filterByColumns) { if (filterObj.ref === columnId) { if (filterObj.containsFilterValue !== '') { return true; } return false; } } return false; } function results() { const len = editableMode ? elements.length : rowData.length; return len ? ( {len} result{len > 1 ? 's' : ''} ) : null; } return ( <> {propsToUse.label && (

{propsToUse.label} {results()}

)} {validatemessage &&
{validatemessage}
} {fieldDefs.map((field: any, index) => { if (field?.meta?.config?.hide) { return null; // Skip rendering if hide = true } return ( {(readOnlyMode || allowEditingInModal) && field.cellRenderer !== 'DeleteIcon' ? (
{field.label || '---'} {_showFilteredIcon(field.name) && } {orderBy === displayedColumns[index] ? ( {order === 'desc' ? 'sorted descending' : 'sorted ascending'} ) : null} { _menuClick(event, field.name, field.meta.type, field.label); }} />
) : ( field.label )}
); })}
{editableMode && !allowEditingInModal && elements.map((row: any, index) => { const theKey = `row-${index}`; return ( {row.map((item, childIndex) => { const theColKey = `data-${index}-${childIndex}`; return ( {item} ); })} {showDeleteButton && ( )} ); })} {(readOnlyMode || allowEditingInModal) && rowData && rowData.length > 0 && stableSort(rowData, getComparator(order, orderBy)) .slice(0) .map((row, index) => { return ( {row.map((item, childIndex) => { const theColKey = displayedColumns[childIndex]; return ( {item} ); })} {showDeleteButton && (
{ editMenuClick(event, index); }} /> editRecord()}>Edit deleteRecord()}>Delete
)}
); })}
{((readOnlyMode && (!rowData || rowData?.length === 0)) || (editableMode && (!referenceList || referenceList?.length === 0))) && (
{getGenericFieldsLocalizedValue('COSMOSFIELDS.lists', 'No records found.')}
)}
{showAddRowButton && (
+ {localizedVal('Add', localeCategory)}
)} Filter Group Filter: {filterBy} {containsDateOrTime ? ( <> {filterType === 'Date' && ( )} {filterType === 'DateTime' && ( )} {filterType === 'Time' && ( )} ) : ( <> )} ); }