moved some more files w/ burnettk

This commit is contained in:
jasquat 2025-01-27 14:12:44 -05:00
parent 559589a5b7
commit 5cdfa5a135
No known key found for this signature in database
9 changed files with 478 additions and 5 deletions

View File

@ -0,0 +1,183 @@
import React from 'react';
import { Alert, AlertTitle } from '@mui/material';
import useAPIError from '../hooks/UseApiError';
import {
ErrorForDisplay,
ProcessInstanceEventErrorDetail,
ProcessInstanceLogEntry,
TestCaseErrorDetails,
} from '../interfaces';
const defaultMessageClassName = 'failure-string';
function errorDetailDisplay(
errorObject: any,
propertyName: string,
title: string,
) {
// Creates a bit of html for displaying a single error property if it exists.
let value = errorObject[propertyName];
if (propertyName in errorObject && value) {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
return (
<div className="error_info">
<span className="error_title">{title}:</span>
{value}
</div>
);
}
return null;
}
export const errorForDisplayFromString = (errorMessage: string) => {
const errorForDisplay: ErrorForDisplay = {
message: errorMessage,
messageClassName: defaultMessageClassName,
};
return errorForDisplay;
};
export const errorForDisplayFromProcessInstanceErrorDetail = (
processInstanceEvent: ProcessInstanceLogEntry,
processInstanceErrorEventDetail: ProcessInstanceEventErrorDetail,
) => {
const errorForDisplay: ErrorForDisplay = {
message: processInstanceErrorEventDetail.message,
messageClassName: defaultMessageClassName,
task_name: processInstanceEvent.task_definition_name,
task_id: processInstanceEvent.task_definition_identifier,
line_number: processInstanceErrorEventDetail.task_line_number,
error_line: processInstanceErrorEventDetail.task_line_contents,
task_trace: processInstanceErrorEventDetail.task_trace,
stacktrace: processInstanceErrorEventDetail.stacktrace,
};
return errorForDisplay;
};
export const errorForDisplayFromTestCaseErrorDetails = (
testCaseErrorDetails: TestCaseErrorDetails,
) => {
const errorForDisplay: ErrorForDisplay = {
message: testCaseErrorDetails.error_messages.join('\n'),
messageClassName: defaultMessageClassName,
task_name: testCaseErrorDetails.task_bpmn_name,
task_id: testCaseErrorDetails.task_bpmn_identifier,
line_number: testCaseErrorDetails.task_line_number,
error_line: testCaseErrorDetails.task_line_contents,
task_trace: testCaseErrorDetails.task_trace,
stacktrace: testCaseErrorDetails.stacktrace,
task_type: testCaseErrorDetails.task_bpmn_type,
output_data: testCaseErrorDetails.output_data,
expected_data: testCaseErrorDetails.expected_data,
};
return errorForDisplay;
};
export const childrenForErrorObject = (errorObject: ErrorForDisplay) => {
let sentryLinkTag = null;
if (errorObject.sentry_link) {
sentryLinkTag = (
<span>
{
': Find details about this error here (it may take a moment to become available): '
}
<a href={errorObject.sentry_link} target="_blank" rel="noreferrer">
{errorObject.sentry_link}
</a>
</span>
);
}
const message = (
<div className={errorObject.messageClassName}>{errorObject.message}</div>
);
const taskName = errorDetailDisplay(errorObject, 'task_name', 'Task Name');
const taskId = errorDetailDisplay(errorObject, 'task_id', 'Task ID');
const fileName = errorDetailDisplay(errorObject, 'file_name', 'File Name');
const lineNumber = errorDetailDisplay(
errorObject,
'line_number',
'Line Number',
);
const errorLine = errorDetailDisplay(errorObject, 'error_line', 'Context');
const taskType = errorDetailDisplay(errorObject, 'task_type', 'Task Type');
const outputData = errorDetailDisplay(
errorObject,
'output_data',
'Output Data',
);
const expectedData = errorDetailDisplay(
errorObject,
'expected_data',
'Expected Data',
);
let codeTrace = null;
if (errorObject.task_trace && errorObject.task_trace.length > 0) {
codeTrace = (
<div className="error_info">
<span className="error_title">Call Activity Trace:</span>
{errorObject.task_trace.reverse().join(' -> ')}
</div>
);
} else if (errorObject.stacktrace) {
codeTrace = (
<pre className="error_info">
<span className="error_title">Stacktrace:</span>
{errorObject.stacktrace.reverse().map((a) => (
<>
{a}
<br />
</>
))}
</pre>
);
}
return [
message,
<br />,
sentryLinkTag,
taskName,
taskId,
fileName,
lineNumber,
errorLine,
codeTrace,
taskType,
outputData,
expectedData,
];
};
export function errorDisplayStateless(
errorObject: ErrorForDisplay,
onClose?: Function,
) {
const title = 'Error:';
const hideCloseButton = !onClose;
return (
<Alert
severity="error"
onClose={onClose}
action={hideCloseButton ? null : <button onClick={onClose}>Close</button>}
>
<AlertTitle>{title}</AlertTitle>
{childrenForErrorObject(errorObject)}
</Alert>
);
}
export default function ErrorDisplay() {
const errorObject = useAPIError().error;
const { removeError } = useAPIError();
let errorTag = null;
if (errorObject) {
errorTag = errorDisplayStateless(errorObject, removeError);
}
return errorTag;
}

View File

@ -0,0 +1,15 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import UserService from '../services/UserService';
export default function LoginHandler() {
const navigate = useNavigate();
useEffect(() => {
if (!UserService.isLoggedIn()) {
navigate(`/login?original_url=${UserService.getCurrentLocation()}`);
}
}, [navigate]);
return null;
}

View File

@ -0,0 +1,19 @@
import MDEditor from '@uiw/react-md-editor';
import FormattingService from '../services/FormattingService';
export default function MarkdownRenderer(props: any) {
const { source } = props;
const newMarkdown = FormattingService.checkForSpiffFormats(source);
let wrapperClassName = '';
const propsToUse = props;
if ('wrapperClassName' in propsToUse) {
wrapperClassName = propsToUse.wrapperClassName;
delete propsToUse.wrapperClassName;
}
return (
<div data-color-mode="light" className={wrapperClassName}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<MDEditor.Markdown {...{ ...propsToUse, ...{ source: newMarkdown } }} />
</div>
);
}

View File

@ -15,7 +15,7 @@ import InstructionsForEndUser from './InstructionsForEndUser';
import {
errorDisplayStateless,
errorForDisplayFromProcessInstanceErrorDetail,
} from '../../components/ErrorDisplay';
} from './ErrorDisplay';
type OwnProps = {
processInstanceId: number;

View File

@ -0,0 +1,165 @@
import { useNavigate } from 'react-router-dom';
import Button from '@mui/material/Button'; // Updated import for MUI Button
import { Can } from '@casl/react';
import { useState } from 'react';
import {
PermissionsToCheck,
ProcessInstance,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import HttpService from '../services/HttpService';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { usePermissionFetcher } from '../hooks/PermissionService';
import useAPIError from '../hooks/UseApiError';
const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel,
) => {
// All values stored in localStorage are strings.
// Grab our recentProcessModels string from localStorage.
const stringFromLocalStorage = window.localStorage.getItem(
'recentProcessModels',
);
// adapted from https://stackoverflow.com/a/59424458/6090676
// If that value is null (meaning that we've never saved anything to that spot in localStorage before), use an empty array as our array. Otherwise, use the value we parse out.
let array: RecentProcessModel[] = [];
if (stringFromLocalStorage !== null) {
// Then parse that string into an actual value.
array = JSON.parse(stringFromLocalStorage);
}
// Here's the value we want to add
const value = {
processModelIdentifier: processModelForStorage.id,
processModelDisplayName: processModelForStorage.display_name,
};
// anything with a processGroupIdentifier is old and busted. leave it behind.
array = array.filter((item) => item.processGroupIdentifier === undefined);
// If our parsed/empty array doesn't already have this value in it...
const matchingItem = array.find(
(item) => item.processModelIdentifier === value.processModelIdentifier,
);
if (matchingItem === undefined) {
// add the value to the beginning of the array
array.unshift(value);
// Keep the array to 3 items
if (array.length > 3) {
array.pop();
}
}
// once the old and busted serializations are gone, we can put these two statements inside the above if statement
// turn the array WITH THE NEW VALUE IN IT into a string to prepare it to be stored in localStorage
const stringRepresentingArray = JSON.stringify(array);
// and store it in localStorage as "recentProcessModels"
window.localStorage.setItem('recentProcessModels', stringRepresentingArray);
};
type OwnProps = {
processModel: ProcessModel;
className?: string;
checkPermissions?: boolean;
buttonText?: string;
};
export default function ProcessInstanceRun({
processModel,
className,
checkPermissions = true,
buttonText = 'Start',
}: OwnProps) {
const navigate = useNavigate();
const { addError, removeError } = useAPIError();
const [disableStartButton, setDisableStartButton] = useState<boolean>(false);
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
processModel.id,
);
const processInstanceCreatePath = `/v1.0/process-instances/${modifiedProcessModelId}`;
let permissionRequestData: PermissionsToCheck = {
[processInstanceCreatePath]: ['POST'],
};
if (!checkPermissions) {
permissionRequestData = {};
}
const { ability } = usePermissionFetcher(permissionRequestData);
const onProcessInstanceRun = (processInstance: ProcessInstance) => {
const processInstanceId = processInstance.id;
if (processInstance.process_model_uses_queued_execution) {
navigate(
`/process-instances/for-me/${modifiedProcessModelId}/${processInstanceId}/progress`,
);
} else {
navigate(
`/process-instances/for-me/${modifiedProcessModelId}/${processInstanceId}/interstitial`,
);
}
};
const processModelRun = (processInstance: ProcessInstance) => {
removeError();
if (processModel) {
storeRecentProcessModelInLocalStorage(processModel);
}
HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
successCallback: onProcessInstanceRun,
failureCallback: (result: any) => {
addError(result);
setDisableStartButton(false);
},
httpMethod: 'POST',
});
};
const processInstanceCreateAndRun = () => {
removeError();
setDisableStartButton(true);
HttpService.makeCallToBackend({
path: processInstanceCreatePath,
successCallback: processModelRun,
failureCallback: (result: any) => {
addError(result);
setDisableStartButton(false);
},
httpMethod: 'POST',
});
};
let startButton = null;
if (processModel.primary_file_name && processModel.is_executable) {
startButton = (
<Button
data-qa="start-process-instance"
onClick={processInstanceCreateAndRun}
className={className}
disabled={disableStartButton}
variant="contained" // MUI specific prop
size="medium" // MUI specific prop
>
{buttonText}
</Button>
);
}
// if checkPermissions is false then assume the page using this component has already checked the permissions
if (checkPermissions) {
return (
<Can I="POST" a={processInstanceCreatePath} ability={ability}>
{startButton}
</Can>
);
}
return startButton;
}

View File

@ -0,0 +1,46 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Tabs, Tab } from '@mui/material'; // Importing MUI components
import { SpiffTab } from '../interfaces';
type OwnProps = {
tabs: SpiffTab[];
};
export default function SpiffTabs({ tabs }: OwnProps) {
const location = useLocation();
const [selectedTabIndex, setSelectedTabIndex] = useState<number | null>(null);
const navigate = useNavigate();
useEffect(() => {
let newSelectedTabIndex = tabs.findIndex((spiffTab: SpiffTab) => {
return location.pathname === spiffTab.path;
});
if (newSelectedTabIndex === -1) {
newSelectedTabIndex = 0;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location, tabs]);
const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => {
navigate(tabs[newValue].path);
};
if (selectedTabIndex !== null && tabs.length > selectedTabIndex) {
return (
<>
<Tabs
value={selectedTabIndex}
onChange={handleTabChange}
aria-label="List of tabs"
>
{tabs.map((spiffTab: SpiffTab, index: number) => (
<Tab key={index} label={spiffTab.display_name} />
))}
</Tabs>
<br />
</>
);
}
return null;
}

View File

@ -0,0 +1,12 @@
// src/common/hooks/useAPIError/index.js
import { useContext } from 'react';
// Import MUI components if needed
// import { SomeMUIComponent } from '@mui/material';
import { APIErrorContext } from '../contexts/APIErrorContext';
function useAPIError() {
const { error, addError, removeError } = useContext(APIErrorContext);
return { error, addError, removeError };
}
export default useAPIError;

View File

@ -0,0 +1,33 @@
import DateAndTimeService from './DateAndTimeService';
const spiffFormatFunctions: { [key: string]: Function } = {
convert_seconds_to_date_time_for_display: DateAndTimeService.formatDateTime,
convert_seconds_to_duration_for_display:
DateAndTimeService.formatDurationForDisplay,
convert_date_to_date_for_display:
DateAndTimeService.ymdDateStringToConfiguredFormat,
};
const checkForSpiffFormats = (markdown: string) => {
const replacer = (
match: string,
spiffFormat: string,
originalValue: string,
) => {
if (spiffFormat in spiffFormatFunctions) {
return spiffFormatFunctions[spiffFormat](originalValue);
}
console.warn(
`attempted: ${match}, but ${spiffFormat} is not a valid conversion function`,
);
return match;
};
return markdown.replaceAll(/SPIFF_FORMAT:::(\w+)\(([^)]+)\)/g, replacer);
};
const FormattingService = {
checkForSpiffFormats,
};
export default FormattingService;

View File

@ -22,8 +22,8 @@ import ProcessInstanceRun from '../components/ProcessInstanceRun';
import MarkdownRenderer from '../components/MarkdownRenderer';
import LoginHandler from '../components/LoginHandler';
import SpiffTabs from '../components/SpiffTabs';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import CreateNewInstance from './CreateNewInstance';
// import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
// import CreateNewInstance from './CreateNewInstance';
type OwnProps = {
pageIdentifier?: string;
@ -63,10 +63,10 @@ export default function Extension({
const { addError, removeError } = useAPIError();
const supportedComponents: SupportedComponentList = {
CreateNewInstance,
// CreateNewInstance,
CustomForm,
MarkdownRenderer,
ProcessInstanceListTable,
// ProcessInstanceListTable,
ProcessInstanceRun,
SpiffTabs,
};