From 4437f0a1c26d28d7e2f6d2c7a1d05e5b2196a7bf Mon Sep 17 00:00:00 2001 From: Richard Marsot Date: Mon, 26 Aug 2024 23:08:37 -0400 Subject: [PATCH] add support for case hierarchy widget --- .../CustomTree.styles.ts | 136 ++++++++ .../CustomTree.tsx | 317 ++++++++++++++++++ .../CustomTree.types.ts | 47 +++ .../Pega_Extensions_CaseHierarchy/Docs.mdx | 15 + .../Pega_Extensions_CaseHierarchy/config.json | 33 ++ .../demo.stories.tsx | 203 +++++++++++ .../demo.test.tsx | 11 + .../Pega_Extensions_CaseHierarchy/index.tsx | 134 ++++++++ .../Pega_Extensions_CaseHierarchy/styles.ts | 6 + 9 files changed, 902 insertions(+) create mode 100644 src/components/Pega_Extensions_CaseHierarchy/CustomTree.styles.ts create mode 100644 src/components/Pega_Extensions_CaseHierarchy/CustomTree.tsx create mode 100644 src/components/Pega_Extensions_CaseHierarchy/CustomTree.types.ts create mode 100644 src/components/Pega_Extensions_CaseHierarchy/Docs.mdx create mode 100644 src/components/Pega_Extensions_CaseHierarchy/config.json create mode 100644 src/components/Pega_Extensions_CaseHierarchy/demo.stories.tsx create mode 100644 src/components/Pega_Extensions_CaseHierarchy/demo.test.tsx create mode 100644 src/components/Pega_Extensions_CaseHierarchy/index.tsx create mode 100644 src/components/Pega_Extensions_CaseHierarchy/styles.ts diff --git a/src/components/Pega_Extensions_CaseHierarchy/CustomTree.styles.ts b/src/components/Pega_Extensions_CaseHierarchy/CustomTree.styles.ts new file mode 100644 index 0000000..ae36d31 --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/CustomTree.styles.ts @@ -0,0 +1,136 @@ +import styled, { css } from 'styled-components'; +import { transparentize } from 'polished'; + +import { + Tree, + StyledTreeListItem, + Icon, + StyledIcon, + defaultThemeProp, + useDirection +} from '@pega/cosmos-react-core'; + +export const StyledToggleIcon = styled(Icon)``; + +export const StyledLabelContent = styled.div` + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + display: inline; +`; + +export const StyledCustomTreeItemSubTree = styled.div(() => { + return css` + position: relative; + `; +}); + +StyledCustomTreeItemSubTree.defaultProps = defaultThemeProp; + +export const StyledNodeInteraction = styled.div(({ theme }) => { + return css` + color: ${theme.base.palette['foreground-color']}; + text-decoration: none; + min-height: 2.25rem; + padding-inline-start: calc(var(--parent-caret-width) * var(--depth) + var(--initial-padding)); + padding-inline-end: calc(var(--initial-padding) + ${theme.base.spacing}); + cursor: pointer; + + &:focus-visible { + outline: none; + } + + &:hover { + background-color: ${transparentize(0.9, theme.base.palette['foreground-color'])}; + } + + &[aria-current='page'], + &[aria-current='true'] { + background-color: ${transparentize(0.95, theme.base.palette['foreground-color'])}; + + & > ${StyledLabelContent}, & > :first-child > ${StyledLabelContent} { + color: ${theme.base.palette.interactive}; + } + } + `; +}); + +StyledNodeInteraction.defaultProps = defaultThemeProp; + +export const StyledCustomTreeLeaf = styled.div(() => { + return css` + ${StyledNodeInteraction} { + padding-inline-start: calc( + var(--initial-padding) + (var(--parent-caret-width) * var(--depth)) + + (var(--parent-caret-spacing-width) * (max(var(--has-parent-sibling), var(--has-parent)))) + ); + } + `; +}); + +StyledCustomTreeLeaf.defaultProps = defaultThemeProp; + +// FIXME: any is used since typeof StyledNodeInteraction not playing nicely. +export const StyledCustomTreeParent: any = styled(StyledNodeInteraction)(({ theme }) => { + const { ltr } = useDirection(); + + return css` + display: flex; + align-items: center; + min-height: 2rem; + &[aria-expanded='true'] ${StyledToggleIcon} { + transform: rotate(90deg); + } + + &[aria-expanded='false'] { + ${StyledToggleIcon} { + transform: rotate(${ltr ? '0' : '180'}deg); + } + + + ${StyledCustomTreeItemSubTree} { + display: none; + } + } + + &:hover + ${StyledCustomTreeItemSubTree}::before { + opacity: 0.5; + } + + ${StyledToggleIcon} { + transition: transform calc(${theme.base.animation.speed} / 2) + ${theme.base.animation.timing.ease}; + } + `; +}); + +StyledCustomTreeParent.defaultProps = defaultThemeProp; + +export const StyledCustomTreeNode = styled.div( + ({ theme }) => css` + --initial-padding: calc(${theme.base.spacing} * 0.5); + --parent-caret-width: 1em; + --parent-caret-spacing-width: max( + (var(--parent-caret-width) + (2 * ${theme.components.button['border-width']})), + ${theme.base['hit-area'].compact} + ); + + ${StyledIcon} { + width: var(--parent-caret-width); + height: var(--parent-caret-width); + } + ` +); + +StyledCustomTreeNode.defaultProps = defaultThemeProp; + +export const StyledCustomTree = styled(Tree)(() => { + return css` + ${StyledTreeListItem} { + display: block; + } + `; +}); + +StyledCustomTree.defaultProps = defaultThemeProp; + +(StyledCustomTree as typeof Tree & { defaultProps: object }).defaultProps = defaultThemeProp; diff --git a/src/components/Pega_Extensions_CaseHierarchy/CustomTree.tsx b/src/components/Pega_Extensions_CaseHierarchy/CustomTree.tsx new file mode 100644 index 0000000..4fb1ec2 --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/CustomTree.tsx @@ -0,0 +1,317 @@ +import { + forwardRef, + createContext, + useMemo, + useContext, + useCallback, + useState, + type KeyboardEvent, + useEffect, + useRef +} from 'react'; +import type { FunctionComponent, PropsWithoutRef, MouseEvent, CSSProperties } from 'react'; + +import { + Flex, + useConsolidatedRef, + useFocusWithin, + treeHelpers, + Button, + Link +} from '@pega/cosmos-react-core'; +import type { ForwardProps, TreeProps } from '@pega/cosmos-react-core'; + +import { + StyledCustomTreeParent, + StyledCustomTreeItemSubTree, + StyledCustomTree, + StyledNodeInteraction, + StyledLabelContent, + StyledToggleIcon, + StyledCustomTreeLeaf, + StyledCustomTreeNode +} from './CustomTree.styles'; + +import type { + CustomTreeContextProps, + CustomTreeNode, + CustomTreeProps, + CustomTreePropsWithDefaults +} from './CustomTree.types'; + +const CustomTreeContext = createContext< + Pick< + CustomTreePropsWithDefaults, + | 'currentNodeId' + | 'onNodeClick' + | 'onNodeToggle' + | 'firstNodeId' + | 'lastNodeId' + | 'getPConnect' + | 'focusedNodeId' + | 'changeFocusedNodeId' + > +>({ + currentNodeId: undefined, + onNodeClick: () => {}, + onNodeToggle: () => {}, + firstNodeId: undefined, + lastNodeId: undefined, + getPConnect: undefined, + focusedNodeId: undefined, + changeFocusedNodeId: () => {} +}); + +const NodeRenderer: TreeProps['nodeRenderer'] = ({ + id, + label, + depth, + hasParentSibling, + nodes, + expanded = false, + subTree, + onClick, + href, + objclass +}) => { + const { + currentNodeId, + onNodeClick, + onNodeToggle, + focusedNodeId, + changeFocusedNodeId, + + getPConnect + } = useContext(CustomTreeContext); + const current = currentNodeId === id; + const focusedEl = focusedNodeId === id; + + const ariaCurrent = useMemo(() => { + return href ? 'page' : 'true'; + }, [href]); + + const handleParentClick = useCallback( + (e: MouseEvent | KeyboardEvent) => { + onNodeClick?.(id, e); + changeFocusedNodeId(id); + }, + [id, onNodeClick, changeFocusedNodeId] + ); + + const handleParentToggle = useCallback( + (e: MouseEvent | KeyboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + onNodeToggle?.(id, e); + changeFocusedNodeId(id); + }, + [id, onNodeToggle, changeFocusedNodeId] + ); + + const elRef = useRef(null); + + useEffect(() => { + if (focusedEl) { + elRef?.current?.focus(); + } + }, [focusedEl]); + + const labelContent = useMemo(() => { + const internal = href ? ( + + { + getPConnect() + .getActionsApi() + .showCasePreview(encodeURI(id), { caseClassName: objclass }); + }} + onClick={(e: MouseEvent) => { + /* for links - need to set onClick for spa to avoid full reload - (cmd | ctrl) + click for opening in new tab */ + if (!e.metaKey && !e.ctrlKey) { + e.preventDefault(); + getPConnect().getActionsApi().openWorkByHandle(id, objclass); + } + }} + > + {label} + + + ) : ( + + {label} + + ); + + return !nodes && (onClick ?? onNodeClick) ? ( + ) => { + onClick?.(id, e); + onNodeClick?.(id, e); + changeFocusedNodeId(id); + }} + ref={elRef} + > + {internal} + + ) : ( + internal + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id, label, nodes, onClick, onNodeClick, current, focusedEl, changeFocusedNodeId]); + + return ( +
+ + {nodes ? ( + <> + + + {depth === 0 ? null : ( + + )} + {labelContent} + + + + {subTree} + + + ) : ( + {labelContent} + )} + +
+ ); +}; + +const CustomTreeWithNodes: FunctionComponent = forwardRef( + function CustomTreeWithNodes( + { + nodes, + getPConnect, + currentNodeId, + onNodeClick, + onNodeToggle, + ...restProps + }: PropsWithoutRef, + ref: CustomTreeProps['ref'] + ) { + const [focusedNodeId, setFocusedNodedId] = useState(); + const treeRef = useConsolidatedRef(ref); + + const lastNodeId = useMemo(() => { + return treeHelpers.getDeepestNode(nodes, nodes[nodes.length - 1].id)?.id; + }, [nodes]); + + const changeFocusedNodeId: CustomTreeContextProps['changeFocusedNodeId'] = useCallback( + (id, type) => { + switch (type) { + case 'up': { + const previousNode = treeHelpers.getPreviousNode(nodes, id); + if (previousNode) setFocusedNodedId(previousNode.id); + break; + } + case 'down': { + const nextNode = treeHelpers.getNextNode(nodes, id); + if (nextNode) setFocusedNodedId(nextNode.id); + break; + } + case 'left': { + const parentNode = treeHelpers.getParentNode(nodes, id); + if (parentNode) setFocusedNodedId(parentNode.id); + break; + } + case 'right': { + const childNode = treeHelpers.getFirstChildNode(nodes, id); + if (childNode) setFocusedNodedId(childNode.id); + break; + } + default: { + if (id !== focusedNodeId) setFocusedNodedId(id); + break; + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [nodes] + ); + + const onFocusChange = (focused: boolean) => { + if (!focused) setFocusedNodedId(''); + }; + + useFocusWithin([treeRef], onFocusChange); + + return ( + ({ + currentNodeId, + focusedNodeId, + lastNodeId, + getPConnect, + firstNodeId: nodes[0].id, + changeFocusedNodeId, + onNodeClick, + onNodeToggle + }), + [ + currentNodeId, + focusedNodeId, + lastNodeId, + getPConnect, + nodes, + changeFocusedNodeId, + onNodeClick, + onNodeToggle + ] + )} + > + {/* FIXME: Types are having issues when styled(Tree) is typeof Tree. */} + + + ); + } +); + +const CustomTree: FunctionComponent = forwardRef( + function CustomTree(props: PropsWithoutRef, ref: CustomTreeProps['ref']) { + return props.nodes.length > 0 ? : null; + } +); + +export default CustomTree; diff --git a/src/components/Pega_Extensions_CaseHierarchy/CustomTree.types.ts b/src/components/Pega_Extensions_CaseHierarchy/CustomTree.types.ts new file mode 100644 index 0000000..fed6178 --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/CustomTree.types.ts @@ -0,0 +1,47 @@ +import type { MouseEvent, KeyboardEvent } from 'react'; + +import type { PropsWithDefaults, TreeProps, TreeNode } from '@pega/cosmos-react-core'; + +export interface CustomTreeNode extends TreeNode { + /** A set of nested tree nodes. */ + nodes?: CustomTreeNode[]; + /** The display text of the tree node. */ + label: string; + /** + * If true, the node's children will be displayed. + * @default false + */ + expanded?: boolean; + /** Click handler for the tree node. */ + onClick?: (id: TreeNode['id'], e: MouseEvent | KeyboardEvent) => void; + /** URL or DOM id to navigate to. This will render the tree node as a link. */ + href: string; + id: string; + objclass: string; +} + +export interface CustomTreeProps extends TreeProps { + /** The id of the currently active tree node. */ + currentNodeId?: TreeNode['id']; + getPConnect: any; + /** Callback function for click events on tree nodes. This will only be called on parent nodes if selectableParents is true. It will always be called on leaf nodes. */ + onNodeClick?: ( + id: TreeNode['id'], + e: MouseEvent | KeyboardEvent + ) => void; + /** Callback function for toggling tree nodes between expanded/collapsed states. This is only ever called on parent nodes. */ + onNodeToggle?: ( + id: TreeNode['id'], + e?: MouseEvent | KeyboardEvent + ) => void; +} + +export interface CustomTreeContextProps extends CustomTreeProps { + firstNodeId?: TreeNode['id']; + lastNodeId?: TreeNode['id']; + focusedNodeId?: TreeNode['id']; + changeFocusedNodeId: (id: TreeNode['id'], type?: 'up' | 'down' | 'left' | 'right') => void; + getPConnect: any; +} + +export type CustomTreePropsWithDefaults = PropsWithDefaults; diff --git a/src/components/Pega_Extensions_CaseHierarchy/Docs.mdx b/src/components/Pega_Extensions_CaseHierarchy/Docs.mdx new file mode 100644 index 0000000..617e2ad --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/Docs.mdx @@ -0,0 +1,15 @@ +import { Meta, Primary, Controls, Story } from '@storybook/blocks'; +import * as DemoStories from './demo.stories'; + + + +# Overview + +The Case Hierarchy Widget allows to display a tree representation of the parent-child relationship for the current case as a utility widget in the Case View. +It is possible to show the top level parent of the current case and the complete tree. + + + +## Props + + diff --git a/src/components/Pega_Extensions_CaseHierarchy/config.json b/src/components/Pega_Extensions_CaseHierarchy/config.json new file mode 100644 index 0000000..dcef2b2 --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/config.json @@ -0,0 +1,33 @@ +{ + "name": "Pega_Extensions_CaseHierarchy", + "label": "Case Hierarchy Utility", + "description": "Hierarchy of the cases", + "organization": "Pega", + "version": "1.0.0", + "library": "Extensions", + "allowedApplications": [], + "componentKey": "Pega_Extensions_CaseHierarchy", + "type": "Widget", + "subtype": "CASE", + "properties": [ + { + "name": "heading", + "label": "Heading", + "format": "TEXT" + }, + { + "name": "dataPage", + "label": "Data Page name", + "format": "TEXT" + }, + { + "name": "showParent", + "label": "Show parent case type", + "format": "BOOLEAN" + } + ], + "defaultConfig": { + "heading": "Case Hierarchy", + "showParent": false + } +} diff --git a/src/components/Pega_Extensions_CaseHierarchy/demo.stories.tsx b/src/components/Pega_Extensions_CaseHierarchy/demo.stories.tsx new file mode 100644 index 0000000..29ce0fc --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/demo.stories.tsx @@ -0,0 +1,203 @@ +import type { StoryObj } from '@storybook/react'; +import { PegaExtensionsCaseHierarchy } from './index'; + +export default { + title: 'Widgets/Case Hierarchy', + argTypes: { + dataPage: { + table: { + disable: true + } + }, + getPConnect: { + table: { + disable: true + } + } + }, + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'aria-required-children', + enabled: false + }, + { + id: 'listitem', + enabled: false + } + ] + } + } + }, + component: PegaExtensionsCaseHierarchy +}; + +const setPCore = () => { + (window as any).PCore = { + getConstants: () => { + return { + CASE_INFO: { + CASE_INFO_ID: 'ID' + } + }; + }, + getSemanticUrlUtils: () => { + return { + getResolvedSemanticURL: () => { + return '/case/case-1'; + }, + getActions: () => { + return { + ACTION_OPENWORKBYHANDLE: 'openWorkByHandle' + }; + } + }; + }, + getDataPageUtils: () => { + return { + getPageDataAsync: (data: string, context: string, parameters: { showParent: boolean }) => { + const { showParent } = parameters; + if (showParent) { + return Promise.resolve({ + pzInsKey: 'OPGO8L-CARINSUR-WORK A-1', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '1', + pyLabel: 'Parent', + pxResults: [ + { + pzInsKey: 'OPGO8L-CARINSUR-WORK A-2', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '2', + pyLabel: 'Child Case 1', + pxResults: [ + { + pzInsKey: 'OPGO8L-TY-WORK A-3', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '3', + pyLabel: 'Child Child Case 1' + }, + { + pzInsKey: 'OPGO8L-TY-WORK A-4', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '4', + pyLabel: 'Child Child Case 2' + } + ] + }, + { + pzInsKey: 'OPGO8L-CARINSUR-WORK A-5', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '5', + pyLabel: 'Child Case 2', + pxResults: [ + { + pzInsKey: 'OPGO8L-TY-WORK A-6', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '6', + pyLabel: 'Scheduling' + }, + { + pzInsKey: 'OPGO8L-TY-WORK A-7', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '7', + pyLabel: 'Provisioning' + } + ] + }, + { + pzInsKey: 'OPGO8L-CARINSUR-WORK A-18', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '18', + pyLabel: 'Request Approval' + }, + { + pzInsKey: 'OPGO8L-CARINSUR-WORK A-8', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '8', + pyLabel: 'Service Request', + pxResults: [ + { + pzInsKey: 'OPGO8L-TY-WORK A-9', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '9', + pyLabel: 'Scheduling Child' + } + ] + } + ] + }); + } else { + return Promise.resolve({ + pzInsKey: 'OPGO8L-CARINSUR-WORK A-5', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '5', + pyLabel: 'Child Case type 2 with a very long description', + pxResults: [ + { + pzInsKey: 'OPGO8L-TY-WORK A-6', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '6', + pyLabel: 'Scheduling case type with a very long description' + }, + { + pzInsKey: 'OPGO8L-TY-WORK A-7', + pxObjClass: 'Work', + pyClassName: 'OPGO8L-CarInsur-Work', + pyID: '7', + pyLabel: 'Provisioning' + } + ] + }); + } + } + }; + } + }; +}; + +type Story = StoryObj; +export const Default: Story = { + render: args => { + setPCore(); + const props = { + ...args, + getPConnect: () => { + return { + getActionsApi: () => { + return { + openWorkByHandle: () => { + /* nothing */ + }, + showCasePreview: () => { + /* nothing */ + } + }; + }, + getContextName: () => '', + getValue: () => 'OPGO8L-CARINSUR-WORK A-5' + }; + } + }; + return ; + }, + args: { + heading: 'Case Hierarchy', + dataPage: 'D_myCases', + showParent: false + } +}; diff --git a/src/components/Pega_Extensions_CaseHierarchy/demo.test.tsx b/src/components/Pega_Extensions_CaseHierarchy/demo.test.tsx new file mode 100644 index 0000000..ab5cd1f --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/demo.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from '@testing-library/react'; +import { composeStories } from '@storybook/react'; +import * as DemoStories from './demo.stories'; + +const { Default } = composeStories(DemoStories); + +test('renders Case Hierarchy component with default args', async () => { + render(); + expect(await screen.findByText('Case Hierarchy')).toBeVisible(); + expect(await screen.findByText('Child Case type 2 with a very long description')).toBeVisible(); +}); diff --git a/src/components/Pega_Extensions_CaseHierarchy/index.tsx b/src/components/Pega_Extensions_CaseHierarchy/index.tsx new file mode 100644 index 0000000..399fecf --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/index.tsx @@ -0,0 +1,134 @@ +import { useState, useEffect, useCallback } from 'react'; +import { + withConfiguration, + Flex, + treeHelpers, + registerIcon, + Card, + Icon, + Text +} from '@pega/cosmos-react-core'; +import CustomTree from './CustomTree'; +import { type CustomTreeNode } from './CustomTree.types'; +import { StyledSummaryListHeader, StyledSummaryListContent } from './styles'; +import '../create-nonce'; + +import * as caretRightIcon from '@pega/cosmos-react-core/lib/components/Icon/icons/caret-right.icon'; +import * as FolderNestedIcon from '@pega/cosmos-react-core/lib/components/Icon/icons/folder-nested.icon'; + +registerIcon(caretRightIcon, FolderNestedIcon); +type CaseHierarchyProps = { + heading?: string; + dataPage: string; + showParent: boolean; + getPConnect: any; +}; + +export const PegaExtensionsCaseHierarchy = (props: CaseHierarchyProps) => { + const { heading = 'Case Hierarchy', dataPage = '', showParent = false, getPConnect } = props; + const [objects, setObjects] = useState([]); + const [loading, setLoading] = useState(true); + const [currentNodeId, setCurrentNodeId] = useState(); + + const loadTree = useCallback( + (item: any, cases: Array, caseInstanceKey: string) => { + const childcases: Array = []; + if (item.pxResults) { + item.pxResults.forEach((childcase: any) => { + loadTree(childcase, childcases, caseInstanceKey); + }); + } + const linkURL = + caseInstanceKey === item.pzInsKey + ? '' + : (window as any).PCore.getSemanticUrlUtils().getResolvedSemanticURL( + (window as any).PCore.getSemanticUrlUtils().getActions().ACTION_OPENWORKBYHANDLE, + { caseClassName: item.pyClassName }, + { workID: item.pyID } + ); + cases.push({ + id: item.pzInsKey, + label: item.pyLabel, + objclass: item.pyClassName, + expanded: true, + href: linkURL, + ...(childcases.length > 0 ? { nodes: childcases } : null) + }); + }, + [] + ); + + useEffect(() => { + const caseInstanceKey = getPConnect().getValue( + (window as any).PCore.getConstants().CASE_INFO.CASE_INFO_ID + ); + const loadObjects = (response: any) => { + const cases: Array = []; + loadTree(response, cases, caseInstanceKey); + setObjects(cases); + setLoading(false); + }; + + if (dataPage) { + const context = getPConnect().getContextName(); + (window as any).PCore.getDataPageUtils() + .getPageDataAsync( + dataPage, + context, + { caseInstanceKey, showParent }, + { invalidateCache: true } + ) + .then((response: any) => { + if (response !== null) { + loadObjects(response); + } else { + setLoading(false); + } + }) + .catch(() => { + setLoading(false); + }); + } + }, [dataPage, getPConnect, loadTree, showParent]); + + if (!dataPage) return null; + if (loading) return null; + + return ( + + + + + {heading} + + + + { + e.preventDefault(); + setCurrentNodeId(id); + }} + onNodeToggle={id => { + const clickedNode = treeHelpers.getNode(objects, id); + // If a leaf node, just set to current + if (!clickedNode?.nodes) return; + + setObjects(tree => + treeHelpers.mapNode(tree, id, node => { + return { + ...node, + expanded: !node.expanded, + loading: node.nodes?.length === 0 + }; + }) + ); + }} + /> + + + ); +}; +export default withConfiguration(PegaExtensionsCaseHierarchy); diff --git a/src/components/Pega_Extensions_CaseHierarchy/styles.ts b/src/components/Pega_Extensions_CaseHierarchy/styles.ts new file mode 100644 index 0000000..810b898 --- /dev/null +++ b/src/components/Pega_Extensions_CaseHierarchy/styles.ts @@ -0,0 +1,6 @@ +import { CardHeader, CardContent } from '@pega/cosmos-react-core'; +import styled, { css } from 'styled-components'; + +export const StyledSummaryListHeader = styled(CardHeader)(() => css``); + +export const StyledSummaryListContent = styled(CardContent)(() => css``);