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} (