diff --git a/spiffworkflow-frontend/' b/spiffworkflow-frontend/' new file mode 100644 index 000000000..9c54c427a --- /dev/null +++ b/spiffworkflow-frontend/' @@ -0,0 +1,146 @@ +// @ts-ignore +import { Breadcrumb, BreadcrumbItem } from '@carbon/react'; +import React, { useEffect, useState } from 'react'; +import { modifyProcessIdentifierForPathParam } from '../helpers'; +import { + HotCrumbItem, + HotCrumbItemArray, + HotCrumbItemObject, + ProcessGroup, + ProcessGroupLite, + ProcessModel, +} from '../interfaces'; +import HttpService from '../services/HttpService'; + +type OwnProps = { + hotCrumbs?: HotCrumbItem[]; +}; + +// export default function ProcessBreadcrumb({ hotCrumbs }: OwnProps) { +export const default ProcessBreadcrumb = React.memo(({ hotCrumbs }: OwnProps) => { + const [processEntity, setProcessEntity] = useState< + ProcessGroup | ProcessModel | null + >(null); + + useEffect(() => { + const explodeCrumbItemObject = (crumb: HotCrumbItem) => { + if ('entityToExplode' in crumb) { + const { entityToExplode, entityType } = crumb; + if (entityType === 'process-model-id') { + HttpService.makeCallToBackend({ + path: `/process-models/${modifyProcessIdentifierForPathParam( + entityToExplode as string + )}`, + successCallback: setProcessEntity, + onUnauthorized: () => {}, + }); + } else if (entityType === 'process-group-id') { + HttpService.makeCallToBackend({ + path: `/process-groups/${modifyProcessIdentifierForPathParam( + entityToExplode as string + )}`, + successCallback: setProcessEntity, + onUnauthorized: () => {}, + }); + } else { + setProcessEntity(entityToExplode as any); + } + } + }; + if (hotCrumbs) { + hotCrumbs.forEach(explodeCrumbItemObject); + } + }, [hotCrumbs]); + + const checkPermissions = (crumb: HotCrumbItemObject) => { + if (!crumb.checkPermission) { + return true; + } + return ( + processEntity && + 'actions' in processEntity && + processEntity.actions && + 'read' in processEntity.actions + ); + }; + + // eslint-disable-next-line sonarjs/cognitive-complexity + const hotCrumbElement = () => { + if (hotCrumbs) { + const leadingCrumbLinks = hotCrumbs.map( + (crumb: HotCrumbItemArray | HotCrumbItemObject) => { + if ( + 'entityToExplode' in crumb && + processEntity && + processEntity.parent_groups && + checkPermissions(crumb) + ) { + const breadcrumbs = processEntity.parent_groups.map( + (parentGroup: ProcessGroupLite) => { + const fullUrl = `/process-groups/${modifyProcessIdentifierForPathParam( + parentGroup.id + )}`; + return ( + + {parentGroup.display_name} + + ); + } + ); + + if (crumb.linkLastItem) { + let apiBase = '/process-groups'; + let dataQaTag = ''; + if (crumb.entityType.startsWith('process-model')) { + apiBase = '/process-models'; + dataQaTag = 'process-model-breadcrumb-link'; + } + const fullUrl = `${apiBase}/${modifyProcessIdentifierForPathParam( + processEntity.id + )}`; + breadcrumbs.push( + + {processEntity.display_name} + + ); + } else { + breadcrumbs.push( + + {processEntity.display_name} + + ); + } + return breadcrumbs; + } + if (Array.isArray(crumb)) { + const valueLabel = crumb[0]; + const url = crumb[1]; + if (!url && valueLabel) { + return ( + + {valueLabel} + + ); + } + if (url && valueLabel) { + return ( + + {valueLabel} + + ); + } + } + return null; + } + ); + return {leadingCrumbLinks}; + } + return null; + }; + + return {hotCrumbElement()}; +}) diff --git a/spiffworkflow-frontend/src/components/ProcessBreadcrumb.tsx b/spiffworkflow-frontend/src/components/ProcessBreadcrumb.tsx index 8a6db736f..465a49247 100644 --- a/spiffworkflow-frontend/src/components/ProcessBreadcrumb.tsx +++ b/spiffworkflow-frontend/src/components/ProcessBreadcrumb.tsx @@ -12,6 +12,12 @@ import { } from '../interfaces'; import HttpService from '../services/HttpService'; +// it is recommend to use a state for hotCrumbs so ProcessBreadCrumb does not attmept +// to re-render. This is because javascript cannot tell if an array or object has changed +// but react states can. If we simply initialize a ProcessBreadCrumb when +// the component that uses it renders, we may get a request to process model show +// every time a state changes in the parent component (any state, not even a related state). +// For an example of usage see TaskShow. type OwnProps = { hotCrumbs?: HotCrumbItem[]; }; diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index df40865f2..cd5aadfc9 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Grid, Column, Button, ButtonSet, Loading } from '@carbon/react'; @@ -16,6 +16,7 @@ import { BasicTask, ErrorForDisplay, EventDefinition, + HotCrumbItem, Task, } from '../interfaces'; import CustomForm from '../components/CustomForm'; @@ -31,6 +32,14 @@ export default function TaskShow() { const [basicTask, setBasicTask] = useState(null); const [taskWithTaskData, setTaskWithTaskData] = useState(null); + // put this in a state so ProcessBreadCrumb does not attempt to re-render + // since javascript cannot tell if an array or object has changed + // but react states can. If we simply initialize a ProcessBreadCrumb when + // this parent component renders, we get a request to process model show + // every time someone types a character into a user task form (because we change ANY + // state. in this case, setTaskData). + const [hotCrumbs, setHotCrumbs] = useState([]); + const params = useParams(); const navigate = useNavigate(); const [formButtonsDisabled, setFormButtonsDisabled] = useState(false); @@ -49,20 +58,23 @@ export default function TaskShow() { // if a user can complete a task then the for-me page should // always work for them so use that since it will work in all cases - const navigateToInterstitial = (myTask: BasicTask) => { - if (UserService.onlyGuestTaskCompletion()) { - setGuestConfirmationText('Thank you!'); - } else { - navigate( - `/process-instances/for-me/${modifyProcessIdentifierForPathParam( - myTask.process_model_identifier - )}/${myTask.process_instance_id}/interstitial` - ); - } - }; + const navigateToInterstitial = useCallback( + (myTask: BasicTask) => { + if (UserService.onlyGuestTaskCompletion()) { + setGuestConfirmationText('Thank you!'); + } else { + navigate( + `/process-instances/for-me/${modifyProcessIdentifierForPathParam( + myTask.process_model_identifier + )}/${myTask.process_instance_id}/interstitial` + ); + } + }, + [navigate] + ); - useEffect(() => { - const processBasicTaskResult = (result: BasicTask) => { + const processBasicTaskResult = useCallback( + (result: BasicTask) => { setBasicTask(result); setPageTitle([result.name_for_display]); if (!result.can_complete) { @@ -76,7 +88,28 @@ export default function TaskShow() { navigateToInterstitial(result); } } - }; + const hotCrumbList: HotCrumbItem[] = [ + ['Process Groups', '/process-groups'], + { + entityToExplode: result.process_model_identifier, + entityType: 'process-model-id', + linkLastItem: true, + checkPermission: true, + }, + [ + `Process Instance Id: ${result.process_instance_id}`, + `/process-instances/for-me/${modifyProcessIdentifierForPathParam( + result.process_model_identifier + )}/${result.process_instance_id}`, + ], + [`Task: ${result.name_for_display || result.id}`], + ]; + setHotCrumbs(hotCrumbList); + }, + [navigateToInterstitial, navigate] + ); + + useEffect(() => { const processTaskWithDataResult = (result: Task) => { setTaskWithTaskData(result); @@ -102,7 +135,7 @@ export default function TaskShow() { }); // FIXME: not sure what to do about addError. adding it to this array causes the page to endlessly reload // eslint-disable-next-line react-hooks/exhaustive-deps - }, [params]); + }, [params, processBasicTaskResult]); // Before we auto-saved form data, we remembered what data was in the form, and then created a synthetic submit event // in order to implement a "Save and close" button. That button no longer saves (since we have auto-save), but the crazy @@ -422,26 +455,7 @@ export default function TaskShow() { !('allowGuest' in basicTask.extensions) || basicTask.extensions.allowGuest !== 'true' ) { - pageElements.push( - - ); + pageElements.push(); pageElements.push(

Task: {basicTask.name_for_display} (