Squashed 'spiffworkflow-frontend/' changes from 55607af9..16dc9a7c

16dc9a7c Merge pull request #75 from sartography/bug/replace-file-warning
c9d376a3 Merge pull request #74 from sartography/bug/delete-primary-warning
dcf5b5f2 Merge pull request #73 from sartography/bug/save-file-message
d4ae393b added new api endpoint to get task-info so users with access to process instances can see the tasks but not the data
3d6c0acb get all of the process identifiers that the diagram knows about so we can display the correct task info
818fdbbd Allow viewing/editing xml of bpmn and dmn files (#76)
e8a86a5a words
04ee4be6 process model cypress tests are passing w/ burnettk
23754677 some fixes for ci w/ burnettk
4ceb9364 throw error if not logged in w/ burnettk
7272eec0 force login if not logged when navigating to frontend w/ burnettk
e285d9a4 Merge pull request #72 from sartography/feature/view_call_activity_diagram
db8c9ec8 pyl and fix test w/ burnettk
7f0416cb use forEach
54881ae8 gitignore things
f09d3aab Add a message when file is saved.
3bb56589 some fixes to ensure we display the correct task data for the diagram elements w/ burnettk
29533216 Don't show delete button for primary file
4fbeba49 allow viewing the diagram for a specific process identifier
43d7bfed Confirm before overwriting file when uploading file with same name

git-subtree-dir: spiffworkflow-frontend
git-subtree-split: 16dc9a7c4535eea9f15374fad8ecff77d6027a66
This commit is contained in:
jasquat 2022-12-16 13:23:58 -05:00
parent e4e0056581
commit 6b38b62ccf
23 changed files with 380 additions and 125 deletions

3
.gitignore vendored
View File

@ -8,6 +8,9 @@
# testing
/coverage
# in case we accidentally run backend tests in frontend. :D
/.coverage.*
# production
/build

View File

@ -1,4 +1,5 @@
import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
import { miscDisplayName } from '../support/helpers';
describe('process-models', () => {
beforeEach(() => {
@ -16,7 +17,7 @@ describe('process-models', () => {
const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`;
const newModelDisplayName = `${modelDisplayName} edited`;
cy.contains('99-Shared Resources').click();
cy.contains(miscDisplayName).click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
@ -34,7 +35,7 @@ describe('process-models', () => {
cy.contains(`Process Model: ${newModelDisplayName}`);
// go back to process model show by clicking on the breadcrumb
cy.contains(modelId).click();
cy.contains(modelDisplayName).click();
cy.getBySel('delete-process-model-button').click();
cy.contains('Are you sure');
@ -46,6 +47,7 @@ describe('process-models', () => {
`process-groups/${modifyProcessIdentifierForPathParam(groupId)}`
);
cy.contains(modelId).should('not.exist');
cy.contains(modelDisplayName).should('not.exist');
});
it('can create new bpmn, dmn, and json files', () => {
@ -61,11 +63,11 @@ describe('process-models', () => {
const dmnFileName = `dmn_test_file_${id}`;
const jsonFileName = `json_test_file_${id}`;
cy.contains('99-Shared Resources').click();
cy.contains(miscDisplayName).click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(directParentGroupId).click();
cy.contains(groupDisplayName).click();
cy.contains(modelDisplayName).click();
cy.url().should(
'include',
@ -90,7 +92,7 @@ describe('process-models', () => {
cy.get('input[name=file_name]').type(bpmnFileName);
cy.contains('Save Changes').click();
cy.contains(`Process Model File: ${bpmnFileName}`);
cy.contains(modelId).click();
cy.contains(modelDisplayName).click();
cy.contains(`Process Model: ${modelDisplayName}`);
// cy.getBySel('files-accordion').click();
cy.contains(`${bpmnFileName}.bpmn`).should('exist');
@ -108,7 +110,7 @@ describe('process-models', () => {
cy.get('input[name=file_name]').type(dmnFileName);
cy.contains('Save Changes').click();
cy.contains(`Process Model File: ${dmnFileName}`);
cy.contains(modelId).click();
cy.contains(modelDisplayName).click();
cy.contains(`Process Model: ${modelDisplayName}`);
// cy.getBySel('files-accordion').click();
cy.contains(`${dmnFileName}.dmn`).should('exist');
@ -124,7 +126,7 @@ describe('process-models', () => {
cy.contains(`Process Model File: ${jsonFileName}`);
// wait for json to load before clicking away to avoid network errors
cy.wait(500);
cy.contains(modelId).click();
cy.contains(modelDisplayName).click();
cy.contains(`Process Model: ${modelDisplayName}`);
// cy.getBySel('files-accordion').click();
cy.contains(`${jsonFileName}.json`).should('exist');
@ -151,12 +153,12 @@ describe('process-models', () => {
const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`;
cy.contains('Add a process group');
cy.contains('99-Shared Resources').click();
cy.contains(miscDisplayName).click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(`${directParentGroupId}`).click();
cy.contains(`${groupDisplayName}`).click();
cy.contains('Add a process model');
cy.contains(modelDisplayName).click();
cy.url().should(
@ -186,7 +188,7 @@ describe('process-models', () => {
.click();
// in breadcrumb
cy.contains(modelId).click();
cy.contains(modelDisplayName).click();
cy.getBySel('delete-process-model-button').click();
cy.contains('Are you sure');
@ -203,7 +205,7 @@ describe('process-models', () => {
// process models no longer has pagination post-tiles
// it.only('can paginate items', () => {
// cy.contains('99-Shared Resources').click();
// cy.contains(miscDisplayName).click();
// cy.wait(500);
// cy.contains('Acceptance Tests Group One').click();
// cy.basicPaginationTest();

View File

@ -1,5 +1,6 @@
import { string } from 'prop-types';
import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
import { miscDisplayName } from './helpers';
// ***********************************************
// This example commands.js shows you how to
@ -86,15 +87,15 @@ Cypress.Commands.add('createModel', (groupId, modelId, modelDisplayName) => {
Cypress.Commands.add(
'runPrimaryBpmnFile',
(expectAutoRedirectToHumanTask = false) => {
cy.contains('Run').click();
cy.contains('Start').click();
if (expectAutoRedirectToHumanTask) {
// the url changes immediately, so also make sure we get some content from the next page, "Task:", or else when we try to interact with the page, it'll re-render and we'll get an error with cypress.
cy.url().should('include', `/tasks/`);
cy.contains('Task: ');
} else {
cy.contains(/Process Instance.*kicked off/);
cy.contains(/Process Instance.*[kK]icked [oO]ff/);
cy.reload(true);
cy.contains(/Process Instance.*kicked off/).should('not.exist');
cy.contains(/Process Instance.*[kK]icked [oO]ff/).should('not.exist');
}
}
);
@ -103,8 +104,8 @@ Cypress.Commands.add(
'navigateToProcessModel',
(groupDisplayName, modelDisplayName, modelIdentifier) => {
cy.navigateToAdmin();
cy.contains('99-Shared Resources').click();
cy.contains(`Process Group: 99-Shared Resources`, { timeout: 10000 });
cy.contains(miscDisplayName).click();
cy.contains(`Process Group: ${miscDisplayName}`, { timeout: 10000 });
cy.contains(groupDisplayName).click();
cy.contains(`Process Group: ${groupDisplayName}`);
// https://stackoverflow.com/q/51254946/6090676

View File

@ -0,0 +1 @@
export const miscDisplayName = 'Shared Resources';

16
package-lock.json generated
View File

@ -68,7 +68,7 @@
"@cypress/grep": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^10.8.0",
"cypress": "^12",
"eslint": "^8.19.0",
"eslint_d": "^12.2.0",
"eslint-config-airbnb": "^19.0.4",
@ -9850,9 +9850,9 @@
"integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A=="
},
"node_modules/cypress": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz",
"integrity": "sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==",
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz",
"integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@ -9903,7 +9903,7 @@
"cypress": "bin/cypress"
},
"engines": {
"node": ">=12.0.0"
"node": "^14.0.0 || ^16.0.0 || >=18.0.0"
}
},
"node_modules/cypress/node_modules/@types/node": {
@ -38586,9 +38586,9 @@
"integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A=="
},
"cypress": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz",
"integrity": "sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==",
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz",
"integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==",
"dev": true,
"requires": {
"@cypress/request": "^2.88.10",

View File

@ -104,7 +104,7 @@
"@cypress/grep": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^10.8.0",
"cypress": "^12",
"eslint": "^8.19.0",
"eslint_d": "^12.2.0",
"eslint-config-airbnb": "^19.0.4",

View File

@ -13,6 +13,7 @@ import AdminRoutes from './routes/AdminRoutes';
import { ErrorForDisplay } from './interfaces';
import { AbilityContext } from './contexts/Can';
import UserService from './services/UserService';
export default function App() {
const [errorMessage, setErrorMessage] = useState<ErrorForDisplay | null>(
@ -24,6 +25,11 @@ export default function App() {
[errorMessage]
);
if (!UserService.isLoggedIn()) {
UserService.doLogin();
return null;
}
const ability = defineAbility(() => {});
let errorTag = null;

View File

@ -24,6 +24,7 @@ import UserService from '../services/UserService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
import { UnauthenticatedError } from '../services/HttpService';
// for ref: https://react-bootstrap.github.io/components/navbar/
export default function NavigationBar() {
@ -39,6 +40,11 @@ export default function NavigationBar() {
const [activeKey, setActiveKey] = useState<string>('');
const { targetUris } = useUriListForPermissions();
// App.jsx forces login (which redirects to keycloak) so we should never get here if we're not logged in.
if (!UserService.isLoggedIn()) {
throw new UnauthenticatedError('You must be authenticated to do this.');
}
const permissionRequestData: PermissionsToCheck = {
[targetUris.authenticationListPath]: ['GET'],
[targetUris.messageInstanceListPath]: ['GET'],
@ -135,6 +141,9 @@ export default function NavigationBar() {
};
const headerMenuItems = () => {
if (!UserService.isLoggedIn()) {
return null;
}
return (
<>
<HeaderMenuItem href="/" isCurrentPage={isActivePage('/')}>

View File

@ -300,8 +300,13 @@ export default function ProcessInstanceListTable({
checkFiltersAndRun();
if (autoReload) {
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, checkFiltersAndRun);
return refreshAtInterval(
REFRESH_INTERVAL,
REFRESH_TIMEOUT,
checkFiltersAndRun
);
}
return undefined;
}, [
autoReload,
searchParams,
@ -838,8 +843,8 @@ export default function ProcessInstanceListTable({
return null;
}}
shouldFilterItem={shouldFilterReportColumn}
placeholder="Choose a report column"
titleText="Report Column"
placeholder="Choose a column to show"
titleText="Column"
/>
);
}
@ -888,7 +893,7 @@ export default function ProcessInstanceListTable({
kind="ghost"
size="sm"
className={`button-tag-icon ${tagTypeClass}`}
title={`Edit ${reportColumnForEditing.accessor}`}
title={`Edit ${reportColumnForEditing.accessor} column`}
onClick={() => {
setReportColumnToOperateOn(reportColumnForEditing);
setShowReportColumnForm(true);
@ -916,7 +921,7 @@ export default function ProcessInstanceListTable({
<Button
data-qa="add-column-button"
renderIcon={AddAlt}
iconDescription="Filter Options"
iconDescription="Column options"
className="with-tiny-top-margin"
kind="ghost"
hasIconOnly

View File

@ -52,22 +52,25 @@ import TouchModule from 'diagram-js/lib/navigation/touch';
// @ts-expect-error TS(7016) FIXME
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
import { useNavigate } from 'react-router-dom';
import { Can } from '@casl/react';
import HttpService from '../services/HttpService';
import ButtonWithConfirmation from './ButtonWithConfirmation';
import { makeid } from '../helpers';
import { getBpmnProcessIdentifiers, makeid } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { PermissionsToCheck, ProcessInstanceTask } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
type OwnProps = {
processModelId: string;
diagramType: string;
readyOrWaitingBpmnTaskIds?: string[] | null;
completedTasksBpmnIds?: string[] | null;
readyOrWaitingProcessInstanceTasks?: ProcessInstanceTask[] | null;
completedProcessInstanceTasks?: ProcessInstanceTask[] | null;
saveDiagram?: (..._args: any[]) => any;
onDeleteFile?: (..._args: any[]) => any;
isPrimaryFile?: boolean;
onSetPrimaryFile?: (..._args: any[]) => any;
diagramXML?: string | null;
fileName?: string;
@ -88,10 +91,11 @@ type OwnProps = {
export default function ReactDiagramEditor({
processModelId,
diagramType,
readyOrWaitingBpmnTaskIds,
completedTasksBpmnIds,
readyOrWaitingProcessInstanceTasks,
completedProcessInstanceTasks,
saveDiagram,
onDeleteFile,
isPrimaryFile,
onSetPrimaryFile,
diagramXML,
fileName,
@ -119,6 +123,7 @@ export default function ReactDiagramEditor({
[targetUris.processModelFileShowPath]: ['POST', 'GET', 'PUT', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const navigate = useNavigate();
useEffect(() => {
if (diagramModelerState) {
@ -227,7 +232,11 @@ export default function ReactDiagramEditor({
function handleElementClick(event: any) {
if (onElementClick) {
onElementClick(event.element);
const canvas = diagramModeler.get('canvas');
const bpmnProcessIdentifiers = getBpmnProcessIdentifiers(
canvas.getRootElement()
);
onElementClick(event.element, bpmnProcessIdentifiers);
}
}
@ -350,12 +359,19 @@ export default function ReactDiagramEditor({
function highlightBpmnIoElement(
canvas: any,
taskBpmnId: string,
bpmnIoClassName: string
processInstanceTask: ProcessInstanceTask,
bpmnIoClassName: string,
bpmnProcessIdentifiers: string[]
) {
if (checkTaskCanBeHighlighted(taskBpmnId)) {
if (checkTaskCanBeHighlighted(processInstanceTask.name)) {
try {
canvas.addMarker(taskBpmnId, bpmnIoClassName);
if (
bpmnProcessIdentifiers.includes(
processInstanceTask.process_identifier
)
) {
canvas.addMarker(processInstanceTask.name, bpmnIoClassName);
}
} catch (bpmnIoError: any) {
// the task list also contains task for processes called from call activities which will
// not exist in this diagram so just ignore them for now.
@ -394,21 +410,29 @@ export default function ReactDiagramEditor({
// highlighting a field
// Option 3 at:
// https://github.com/bpmn-io/bpmn-js-examples/tree/master/colors
if (readyOrWaitingBpmnTaskIds) {
readyOrWaitingBpmnTaskIds.forEach((readyOrWaitingBpmnTaskId) => {
if (readyOrWaitingProcessInstanceTasks) {
const bpmnProcessIdentifiers = getBpmnProcessIdentifiers(
canvas.getRootElement()
);
readyOrWaitingProcessInstanceTasks.forEach((readyOrWaitingBpmnTask) => {
highlightBpmnIoElement(
canvas,
readyOrWaitingBpmnTaskId,
'active-task-highlight'
readyOrWaitingBpmnTask,
'active-task-highlight',
bpmnProcessIdentifiers
);
});
}
if (completedTasksBpmnIds) {
completedTasksBpmnIds.forEach((completedTaskBpmnId) => {
if (completedProcessInstanceTasks) {
const bpmnProcessIdentifiers = getBpmnProcessIdentifiers(
canvas.getRootElement()
);
completedProcessInstanceTasks.forEach((completedTask) => {
highlightBpmnIoElement(
canvas,
completedTaskBpmnId,
'completed-task-highlight'
completedTask,
'completed-task-highlight',
bpmnProcessIdentifiers
);
});
}
@ -484,8 +508,8 @@ export default function ReactDiagramEditor({
diagramType,
diagramXML,
diagramXMLString,
readyOrWaitingBpmnTaskIds,
completedTasksBpmnIds,
readyOrWaitingProcessInstanceTasks,
completedProcessInstanceTasks,
fileName,
performingXmlUpdates,
processModelId,
@ -533,6 +557,8 @@ export default function ReactDiagramEditor({
});
};
const canViewXml = fileName !== undefined;
const userActionOptions = () => {
if (diagramType !== 'readonly') {
return (
@ -549,7 +575,7 @@ export default function ReactDiagramEditor({
a={targetUris.processModelFileShowPath}
ability={ability}
>
{fileName && (
{fileName && !isPrimaryFile && (
<ButtonWithConfirmation
description={`Delete file ${fileName}?`}
onConfirmation={handleDelete}
@ -571,6 +597,23 @@ export default function ReactDiagramEditor({
>
<Button onClick={downloadXmlFile}>Download</Button>
</Can>
<Can
I="GET"
a={targetUris.processModelFileShowPath}
ability={ability}
>
{canViewXml && (
<Button
onClick={() => {
navigate(
`/admin/process-models/${processModelId}/form/${fileName}`
);
}}
>
View XML
</Button>
)}
</Can>
</>
);
}

View File

@ -41,7 +41,7 @@ export default function MyOpenProcesses() {
});
};
getTasks();
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
return refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
const buildTable = () => {

View File

@ -41,7 +41,7 @@ export default function TasksWaitingForMyGroups() {
});
};
getTasks();
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
return refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
const buildTable = () => {

View File

@ -208,5 +208,29 @@ export const refreshAtInterval = (
() => clearInterval(intervalRef),
timeout * 1000
);
return [intervalRef, timeoutRef];
return () => {
clearInterval(intervalRef);
clearTimeout(timeoutRef);
};
};
const getChildProcesses = (bpmnElement: any) => {
let elements: string[] = [];
bpmnElement.children.forEach((c: any) => {
if (c.type === 'bpmn:Participant') {
if (c.businessObject.processRef) {
elements.push(c.businessObject.processRef.id);
}
elements = [...elements, ...getChildProcesses(c)];
} else if (c.type === 'bpmn:SubProcess') {
elements.push(c.id);
}
});
return elements;
};
export const getBpmnProcessIdentifiers = (rootBpmnElement: any) => {
const childProcesses = getChildProcesses(rootBpmnElement);
childProcesses.push(rootBpmnElement.businessObject.id);
return childProcesses;
};

View File

@ -14,7 +14,8 @@ export const useUriListForPermissions = () => {
processInstanceListPath: '/v1.0/process-instances',
processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`,
processInstanceReportListPath: '/v1.0/process-instances/reports',
processInstanceTaskListPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`,
processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`,
processInstanceTaskListDataPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`,
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,

View File

@ -11,6 +11,13 @@ export interface RecentProcessModel {
processModelDisplayName: string;
}
export interface ProcessInstanceTask {
id: string;
state: string;
process_identifier: string;
name: string;
}
export interface ProcessReference {
name: string; // The process or decision Display name.
identifier: string; // The unique id of the process
@ -39,6 +46,7 @@ export interface ProcessInstance {
id: number;
process_model_identifier: string;
process_model_display_name: string;
spiff_step?: number;
}
export interface MessageCorrelationProperties {

View File

@ -21,10 +21,11 @@ export default function ProcessInstanceList() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModelFullIdentifier}`,
`process_model:${processModelFullIdentifier}:link`,
],
{
entityToExplode: processModelFullIdentifier,
entityType: 'process-model-id',
linkLastItem: true,
},
['Process Instances'],
]}
/>

View File

@ -1,6 +1,11 @@
import { useContext, useEffect, useState } from 'react';
import Editor from '@monaco-editor/react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import {
useParams,
useNavigate,
Link,
useSearchParams,
} from 'react-router-dom';
import {
TrashCan,
StopOutline,
@ -34,15 +39,21 @@ import {
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ErrorContext from '../contexts/ErrorContext';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import {
PermissionsToCheck,
ProcessInstance,
ProcessInstanceTask,
} from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function ProcessInstanceShow() {
const navigate = useNavigate();
const params = useParams();
const [searchParams] = useSearchParams();
const [processInstance, setProcessInstance] = useState(null);
const [tasks, setTasks] = useState<Array<object> | null>(null);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
const [tasks, setTasks] = useState<ProcessInstanceTask[] | null>(null);
const [tasksCallHadError, setTasksCallHadError] = useState<boolean>(false);
const [taskToDisplay, setTaskToDisplay] = useState<object | null>(null);
const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>('');
@ -59,8 +70,10 @@ export default function ProcessInstanceShow() {
const permissionRequestData: PermissionsToCheck = {
[targetUris.messageInstanceListPath]: ['GET'],
[targetUris.processInstanceTaskListPath]: ['GET'],
[targetUris.processInstanceTaskListDataPath]: ['GET', 'PUT'],
[targetUris.processInstanceActionPath]: ['DELETE'],
[targetUris.processInstanceLogListPath]: ['GET'],
[targetUris.processModelShowPath]: ['PUT'],
[`${targetUris.processInstanceActionPath}/suspend`]: ['PUT'],
[`${targetUris.processInstanceActionPath}/terminate`]: ['PUT'],
[`${targetUris.processInstanceActionPath}/resume`]: ['PUT'],
@ -80,17 +93,28 @@ export default function ProcessInstanceShow() {
const processTaskFailure = () => {
setTasksCallHadError(true);
};
let queryParams = '';
const processIdentifier = searchParams.get('process_identifier');
if (processIdentifier) {
queryParams = `?process_identifier=${processIdentifier}`;
}
HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}`,
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}${queryParams}`,
successCallback: setProcessInstance,
});
let taskParams = '?all_tasks=true';
if (typeof params.spiff_step !== 'undefined') {
taskParams = `${taskParams}&spiff_step=${params.spiff_step}`;
}
if (ability.can('GET', targetUris.processInstanceTaskListPath)) {
let taskPath = '';
if (ability.can('GET', targetUris.processInstanceTaskListDataPath)) {
taskPath = `${targetUris.processInstanceTaskListDataPath}${taskParams}`;
} else if (ability.can('GET', targetUris.processInstanceTaskListPath)) {
taskPath = `${targetUris.processInstanceTaskListPath}${taskParams}`;
}
if (taskPath) {
HttpService.makeCallToBackend({
path: `${targetUris.processInstanceTaskListPath}${taskParams}`,
path: taskPath,
successCallback: setTasks,
failureCallback: processTaskFailure,
});
@ -98,7 +122,14 @@ export default function ProcessInstanceShow() {
setTasksCallHadError(true);
}
}
}, [params, modifiedProcessModelId, permissionsLoaded, ability, targetUris]);
}, [
params,
modifiedProcessModelId,
permissionsLoaded,
ability,
targetUris,
searchParams,
]);
const deleteProcessInstance = () => {
HttpService.makeCallToBackend({
@ -140,12 +171,12 @@ export default function ProcessInstanceShow() {
const getTaskIds = () => {
const taskIds = { completed: [], readyOrWaiting: [] };
if (tasks) {
tasks.forEach(function getUserTasksElement(task: any) {
tasks.forEach(function getUserTasksElement(task: ProcessInstanceTask) {
if (task.state === 'COMPLETED') {
(taskIds.completed as any).push(task.name);
(taskIds.completed as any).push(task);
}
if (task.state === 'READY' || task.state === 'WAITING') {
(taskIds.readyOrWaiting as any).push(task.name);
(taskIds.readyOrWaiting as any).push(task);
}
});
}
@ -175,15 +206,18 @@ export default function ProcessInstanceShow() {
label: any,
distance: number
) => {
const processIdentifier = searchParams.get('process_identifier');
let queryParams = '';
if (processIdentifier) {
queryParams = `?process_identifier=${processIdentifier}`;
}
return (
<Link
reloadDocument
data-qa="process-instance-step-link"
to={`/admin/process-instances/${
params.process_model_id
}/process-instances/${params.process_instance_id}/${
currentSpiffStep(processInstanceToUse) + distance
}`}
to={`/admin/process-instances/${params.process_model_id}/${
params.process_instance_id
}/${currentSpiffStep(processInstanceToUse) + distance}${queryParams}`}
>
{label}
</Link>
@ -364,10 +398,15 @@ export default function ProcessInstanceShow() {
}
};
const handleClickedDiagramTask = (shapeElement: any) => {
const handleClickedDiagramTask = (
shapeElement: any,
bpmnProcessIdentifiers: any
) => {
if (tasks) {
const matchingTask: any = tasks.find(
(task: any) => task.name === shapeElement.id
(task: any) =>
task.name === shapeElement.id &&
bpmnProcessIdentifiers.includes(task.process_identifier)
);
if (matchingTask) {
setTaskToDisplay(matchingTask);
@ -411,7 +450,9 @@ export default function ProcessInstanceShow() {
const canEditTaskData = (task: any) => {
return (
task.state === 'READY' && showingLastSpiffStep(processInstance as any)
ability.can('PUT', targetUris.processInstanceTaskListDataPath) &&
task.state === 'READY' &&
showingLastSpiffStep(processInstance as any)
);
};
@ -460,7 +501,10 @@ export default function ProcessInstanceShow() {
const taskDataButtons = (task: any) => {
const buttons = [];
if (task.type === 'Script Task') {
if (
task.type === 'Script Task' &&
ability.can('PUT', targetUris.processModelShowPath)
) {
buttons.push(
<Button
data-qa="create-script-unit-test-button"
@ -471,19 +515,28 @@ export default function ProcessInstanceShow() {
);
}
if (task.type === 'Call Activity') {
buttons.push(
<Link
data-qa="go-to-call-activity-result"
to={`${window.location.pathname}?process_identifier=${task.call_activity_process_identifier}`}
target="_blank"
>
View Call Activity Diagram
</Link>
);
}
if (canEditTaskData(task)) {
if (editingTaskData) {
buttons.push(
<Button
data-qa="create-script-unit-test-button"
onClick={saveTaskData}
>
<Button data-qa="save-task-data-button" onClick={saveTaskData}>
Save
</Button>
);
buttons.push(
<Button
data-qa="create-script-unit-test-button"
data-qa="cancel-task-data-edit-button"
onClick={cancelEditingTaskData}
>
Cancel
@ -492,7 +545,7 @@ export default function ProcessInstanceShow() {
} else {
buttons.push(
<Button
data-qa="create-script-unit-test-button"
data-qa="edit-task-data-button"
onClick={() => setEditingTaskData(true)}
>
Edit
@ -622,8 +675,8 @@ export default function ProcessInstanceShow() {
processModelId={processModelId || ''}
diagramXML={processInstanceToUse.bpmn_xml_file_contents || ''}
fileName={processInstanceToUse.bpmn_xml_file_contents || ''}
readyOrWaitingBpmnTaskIds={taskIds.readyOrWaiting}
completedTasksBpmnIds={taskIds.completed}
readyOrWaitingProcessInstanceTasks={taskIds.readyOrWaiting}
completedProcessInstanceTasks={taskIds.completed}
diagramType="readonly"
onElementClick={handleClickedDiagramTask}
/>

View File

@ -25,6 +25,7 @@ import {
ProcessReference,
} from '../interfaces';
import ProcessSearch from '../components/ProcessSearch';
import { Notification } from '../components/Notification';
export default function ProcessModelEditDiagram() {
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
@ -157,6 +158,8 @@ export default function ProcessModelEditDiagram() {
}
};
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const saveDiagram = (bpmnXML: any, fileName = params.file_name) => {
setErrorMessage(null);
setBpmnXmlForDiagramRendering(bpmnXML);
@ -192,6 +195,7 @@ export default function ProcessModelEditDiagram() {
// after saving the file, make sure we null out newFileName
// so it does not get used over the params
setNewFileName('');
setDisplaySaveFileMessage(true);
};
const onDeleteFile = (fileName = params.file_name) => {
@ -819,6 +823,7 @@ export default function ProcessModelEditDiagram() {
processModelId={params.process_model_id || ''}
saveDiagram={saveDiagram}
onDeleteFile={onDeleteFile}
isPrimaryFile={params.file_name === processModel?.primary_file_name}
onSetPrimaryFile={onSetPrimaryFileCallback}
diagramXML={bpmnXmlForDiagramRendering}
fileName={params.file_name}
@ -836,6 +841,20 @@ export default function ProcessModelEditDiagram() {
);
};
const saveFileMessage = () => {
if (displaySaveFileMessage) {
return (
<Notification
title="File Saved: "
onClose={() => setDisplaySaveFileMessage(false)}
>
Changes to the file were saved.
</Notification>
);
}
return null;
};
// if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it
if ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) {
const processModelFileName = processModelFile ? processModelFile.name : '';
@ -856,6 +875,7 @@ export default function ProcessModelEditDiagram() {
Process Model File{processModelFile ? ': ' : ''}
{processModelFileName}
</h1>
{saveFileMessage()}
{appropriateEditor()}
{newFileNameBox()}
{scriptEditor()}

View File

@ -264,25 +264,27 @@ export default function ProcessModelShow() {
</Can>
);
elements.push(
<Can
I="DELETE"
a={targetUris.processModelFileCreatePath}
ability={ability}
>
<ButtonWithConfirmation
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete File"
hasIconOnly
description={`Delete file: ${processModelFile.name}`}
onConfirmation={() => {
onDeleteFile(processModelFile.name);
}}
confirmButtonLabel="Delete"
/>
</Can>
);
if (!isPrimaryBpmnFile) {
elements.push(
<Can
I="DELETE"
a={targetUris.processModelFileCreatePath}
ability={ability}
>
<ButtonWithConfirmation
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete File"
hasIconOnly
description={`Delete file: ${processModelFile.name}`}
onConfirmation={() => {
onDeleteFile(processModelFile.name);
}}
confirmButtonLabel="Delete"
/>
</Can>
);
}
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
elements.push(
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
@ -360,27 +362,76 @@ export default function ProcessModelShow() {
);
};
const [fileUploadEvent, setFileUploadEvent] = useState(null);
const [duplicateFilename, setDuplicateFilename] = useState<String>('');
const [showOverwriteConfirmationPrompt, setShowOverwriteConfirmationPrompt] =
useState(false);
const doFileUpload = (event: any) => {
event.preventDefault();
const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData();
formData.append('file', filesToUpload[0]);
formData.append('fileName', filesToUpload[0].name);
HttpService.makeCallToBackend({
path: url,
successCallback: onUploadedCallback,
httpMethod: 'POST',
postBody: formData,
});
setFilesToUpload(null);
};
const handleFileUploadCancel = () => {
setShowFileUploadModal(false);
setFilesToUpload(null);
};
const handleOverwriteFileConfirm = () => {
setShowOverwriteConfirmationPrompt(false);
doFileUpload(fileUploadEvent);
};
const handleOverwriteFileCancel = () => {
setShowOverwriteConfirmationPrompt(false);
setFilesToUpload(null);
};
const confirmOverwriteFileDialog = () => {
return (
<Modal
danger
open={showOverwriteConfirmationPrompt}
data-qa="file-overwrite-modal-confirmation-dialog"
modalHeading={`Overwrite the file: ${duplicateFilename}`}
modalLabel="Overwrite file?"
primaryButtonText="Yes"
secondaryButtonText="Cancel"
onSecondarySubmit={handleOverwriteFileCancel}
onRequestSubmit={handleOverwriteFileConfirm}
onRequestClose={handleOverwriteFileCancel}
/>
);
};
const displayOverwriteConfirmation = (filename: String) => {
setDuplicateFilename(filename);
setShowOverwriteConfirmationPrompt(true);
};
const checkDuplicateFile = (event: any) => {
if (processModel && processModel.files.length > 0) {
processModel.files.forEach((file) => {
if (file.name === filesToUpload[0].name) {
displayOverwriteConfirmation(file.name);
setFileUploadEvent(event);
}
});
}
};
const handleFileUpload = (event: any) => {
if (processModel) {
event.preventDefault();
const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData();
formData.append('file', filesToUpload[0]);
formData.append('fileName', filesToUpload[0].name);
HttpService.makeCallToBackend({
path: url,
successCallback: onUploadedCallback,
httpMethod: 'POST',
postBody: formData,
});
checkDuplicateFile(event);
}
setShowFileUploadModal(false);
setFilesToUpload(null);
};
const fileUploadModal = () => {
@ -548,6 +599,7 @@ export default function ProcessModelShow() {
return (
<>
{fileUploadModal()}
{confirmOverwriteFileDialog()}
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],

View File

@ -36,7 +36,20 @@ export default function ReactFormEditor() {
return searchParams.get('file_ext') ?? 'json';
})();
const editorDefaultLanguage = fileExtension === 'md' ? 'markdown' : 'json';
const hasDiagram = fileExtension === 'bpmn' || fileExtension === 'dmn';
const editorDefaultLanguage = (() => {
if (fileExtension === 'json') {
return 'json';
}
if (hasDiagram) {
return 'xml';
}
if (fileExtension === 'md') {
return 'markdown';
}
return 'text';
})();
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
`${params.process_model_id}`
@ -193,6 +206,19 @@ export default function ReactFormEditor() {
buttonLabel="Delete"
/>
) : null}
{hasDiagram ? (
<Button
onClick={() =>
navigate(
`/admin/process-models/${modifiedProcessModelId}/files/${params.file_name}`
)
}
variant="danger"
data-qa="view-diagram-button"
>
View Diagram
</Button>
) : null}
<Editor
height={600}
width="auto"

View File

@ -40,7 +40,7 @@ export default function TaskShow() {
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceTaskListPath]: ['GET'],
[targetUris.processInstanceTaskListDataPath]: ['GET'],
};
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
@ -50,7 +50,7 @@ export default function TaskShow() {
if (permissionsLoaded) {
const processResult = (result: any) => {
setTask(result);
if (ability.can('GET', targetUris.processInstanceTaskListPath)) {
if (ability.can('GET', targetUris.processInstanceTaskListDataPath)) {
HttpService.makeCallToBackend({
path: `/task-data/${modifyProcessIdentifierForPathParam(
result.process_model_identifier

View File

@ -26,7 +26,7 @@ type backendCallProps = {
postBody?: any;
};
class UnauthenticatedError extends Error {
export class UnauthenticatedError extends Error {
constructor(message: string) {
super(message);
this.name = 'UnauthenticatedError';

View File

@ -27,8 +27,8 @@ const doLogout = () => {
const idToken = getIdToken();
localStorage.removeItem('jwtAccessToken');
localStorage.removeItem('jwtIdToken');
const redirctUrl = `${window.location.origin}/`;
const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirctUrl}&id_token=${idToken}`;
const redirectUrl = `${window.location.origin}`;
const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirectUrl}&id_token=${idToken}`;
window.location.href = url;
};