use a state for hotCrumbs with using ProcessBreadCrumb to avoid unnecessary renderings and network calls w/ burnettk

This commit is contained in:
jasquat 2023-12-11 14:51:04 -05:00
parent c00d810704
commit c36e342d1e
3 changed files with 202 additions and 36 deletions

146
spiffworkflow-frontend/' Normal file
View File

@ -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 (
<BreadcrumbItem key={parentGroup.id} href={fullUrl}>
{parentGroup.display_name}
</BreadcrumbItem>
);
}
);
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(
<BreadcrumbItem
key={processEntity.id}
href={fullUrl}
data-qa={dataQaTag}
>
{processEntity.display_name}
</BreadcrumbItem>
);
} else {
breadcrumbs.push(
<BreadcrumbItem key={processEntity.id} isCurrentPage>
{processEntity.display_name}
</BreadcrumbItem>
);
}
return breadcrumbs;
}
if (Array.isArray(crumb)) {
const valueLabel = crumb[0];
const url = crumb[1];
if (!url && valueLabel) {
return (
<BreadcrumbItem isCurrentPage key={valueLabel}>
{valueLabel}
</BreadcrumbItem>
);
}
if (url && valueLabel) {
return (
<BreadcrumbItem key={valueLabel} href={url}>
{valueLabel}
</BreadcrumbItem>
);
}
}
return null;
}
);
return <Breadcrumb noTrailingSlash>{leadingCrumbLinks}</Breadcrumb>;
}
return null;
};
return <Breadcrumb noTrailingSlash>{hotCrumbElement()}</Breadcrumb>;
})

View File

@ -12,6 +12,12 @@ import {
} from '../interfaces'; } from '../interfaces';
import HttpService from '../services/HttpService'; 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 = { type OwnProps = {
hotCrumbs?: HotCrumbItem[]; hotCrumbs?: HotCrumbItem[];
}; };

View File

@ -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 { useNavigate, useParams } from 'react-router-dom';
import { Grid, Column, Button, ButtonSet, Loading } from '@carbon/react'; import { Grid, Column, Button, ButtonSet, Loading } from '@carbon/react';
@ -16,6 +16,7 @@ import {
BasicTask, BasicTask,
ErrorForDisplay, ErrorForDisplay,
EventDefinition, EventDefinition,
HotCrumbItem,
Task, Task,
} from '../interfaces'; } from '../interfaces';
import CustomForm from '../components/CustomForm'; import CustomForm from '../components/CustomForm';
@ -31,6 +32,14 @@ export default function TaskShow() {
const [basicTask, setBasicTask] = useState<BasicTask | null>(null); const [basicTask, setBasicTask] = useState<BasicTask | null>(null);
const [taskWithTaskData, setTaskWithTaskData] = useState<Task | null>(null); const [taskWithTaskData, setTaskWithTaskData] = useState<Task | null>(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<HotCrumbItem[]>([]);
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [formButtonsDisabled, setFormButtonsDisabled] = useState(false); const [formButtonsDisabled, setFormButtonsDisabled] = useState(false);
@ -49,7 +58,8 @@ export default function TaskShow() {
// if a user can complete a task then the for-me page should // 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 // always work for them so use that since it will work in all cases
const navigateToInterstitial = (myTask: BasicTask) => { const navigateToInterstitial = useCallback(
(myTask: BasicTask) => {
if (UserService.onlyGuestTaskCompletion()) { if (UserService.onlyGuestTaskCompletion()) {
setGuestConfirmationText('Thank you!'); setGuestConfirmationText('Thank you!');
} else { } else {
@ -59,10 +69,12 @@ export default function TaskShow() {
)}/${myTask.process_instance_id}/interstitial` )}/${myTask.process_instance_id}/interstitial`
); );
} }
}; },
[navigate]
);
useEffect(() => { const processBasicTaskResult = useCallback(
const processBasicTaskResult = (result: BasicTask) => { (result: BasicTask) => {
setBasicTask(result); setBasicTask(result);
setPageTitle([result.name_for_display]); setPageTitle([result.name_for_display]);
if (!result.can_complete) { if (!result.can_complete) {
@ -76,7 +88,28 @@ export default function TaskShow() {
navigateToInterstitial(result); 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) => { const processTaskWithDataResult = (result: Task) => {
setTaskWithTaskData(result); 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 // 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 // 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 // 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 // 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) || !('allowGuest' in basicTask.extensions) ||
basicTask.extensions.allowGuest !== 'true' basicTask.extensions.allowGuest !== 'true'
) { ) {
pageElements.push( pageElements.push(<ProcessBreadcrumb hotCrumbs={hotCrumbs} />);
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/process-groups'],
{
entityToExplode: basicTask.process_model_identifier,
entityType: 'process-model-id',
linkLastItem: true,
checkPermission: true,
},
[
`Process Instance Id: ${basicTask.process_instance_id}`,
`/process-instances/for-me/${modifyProcessIdentifierForPathParam(
basicTask.process_model_identifier
)}/${basicTask.process_instance_id}`,
],
[`Task: ${basicTask.name_for_display || basicTask.id}`],
]}
/>
);
pageElements.push( pageElements.push(
<h3> <h3>
Task: {basicTask.name_for_display} ( Task: {basicTask.name_for_display} (