mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-23 14:48:35 +00:00
Merge remote-tracking branch 'origin/main' into newui-all-in
This commit is contained in:
commit
99e67f13f2
@ -17,4 +17,4 @@ script_dir="$(
|
||||
|
||||
export SPIFFWORKFLOW_BACKEND_RUN_BACKGROUND_SCHEDULER_IN_CREATE_APP=false
|
||||
|
||||
exec poet run python "$script" "$@"
|
||||
exec poetry run python "$script" "$@"
|
||||
|
218
spiffworkflow-frontend/src/components/ProcessModelFileList.tsx
Normal file
218
spiffworkflow-frontend/src/components/ProcessModelFileList.tsx
Normal file
@ -0,0 +1,218 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Download, Edit, Favorite, TrashCan, View } from '@carbon/icons-react';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import { PureAbility } from '@casl/ability';
|
||||
import ButtonWithConfirmation from './ButtonWithConfirmation';
|
||||
import ProcessModelTestRun from './ProcessModelTestRun';
|
||||
import { ProcessFile } from '../interfaces';
|
||||
|
||||
interface ProcessModelFileListProps {
|
||||
processModel: any;
|
||||
ability: PureAbility;
|
||||
targetUris: any;
|
||||
modifiedProcessModelId: string;
|
||||
onDeleteFile: (fileName: string) => void;
|
||||
onSetPrimaryFile: (fileName: string) => void;
|
||||
isTestCaseFile: (processModelFile: ProcessFile) => boolean;
|
||||
}
|
||||
|
||||
export default function ProcessModelFileList({
|
||||
processModel,
|
||||
ability,
|
||||
targetUris,
|
||||
modifiedProcessModelId,
|
||||
onDeleteFile,
|
||||
onSetPrimaryFile,
|
||||
isTestCaseFile,
|
||||
}: ProcessModelFileListProps) {
|
||||
const profileModelFileEditUrl = (processModelFile: ProcessFile) => {
|
||||
if (processModel) {
|
||||
if (processModelFile.name.match(/\.(dmn|bpmn)$/)) {
|
||||
return `/editor/process-models/${modifiedProcessModelId}/files/${processModelFile.name}`;
|
||||
}
|
||||
if (processModelFile.name.match(/\.(json|md)$/)) {
|
||||
return `/process-models/${modifiedProcessModelId}/form/${processModelFile.name}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderButtonElements = (
|
||||
processModelFile: ProcessFile,
|
||||
isPrimaryBpmnFile: boolean,
|
||||
) => {
|
||||
const elements = [];
|
||||
|
||||
let icon = View;
|
||||
let actionWord = 'View';
|
||||
if (ability.can('PUT', targetUris.processModelFileCreatePath)) {
|
||||
icon = Edit;
|
||||
actionWord = 'Edit';
|
||||
}
|
||||
elements.push(
|
||||
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={icon}
|
||||
iconDescription={`${actionWord} File`}
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
|
||||
href={profileModelFileEditUrl(processModelFile)}
|
||||
/>
|
||||
</Can>,
|
||||
);
|
||||
elements.push(
|
||||
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Download}
|
||||
iconDescription="Download File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`/${targetUris.processModelFilePath}/${processModelFile.name}`,
|
||||
'_blank',
|
||||
)
|
||||
}
|
||||
/>
|
||||
</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"
|
||||
classNameForModal="modal-within-table-cell"
|
||||
/>
|
||||
</Can>,
|
||||
);
|
||||
}
|
||||
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
|
||||
elements.push(
|
||||
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Favorite}
|
||||
iconDescription="Set As Primary File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => onSetPrimaryFile(processModelFile.name)}
|
||||
/>
|
||||
</Can>,
|
||||
);
|
||||
}
|
||||
if (isTestCaseFile(processModelFile)) {
|
||||
elements.push(
|
||||
<Can I="POST" a={targetUris.processModelTestsPath} ability={ability}>
|
||||
<ProcessModelTestRun
|
||||
processModelFile={processModelFile}
|
||||
titleText="Run BPMN unit tests defined in this file"
|
||||
classNameForModal="modal-within-table-cell"
|
||||
/>
|
||||
</Can>,
|
||||
);
|
||||
}
|
||||
return elements;
|
||||
};
|
||||
|
||||
if (!processModel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tags = processModel.files
|
||||
.map((processModelFile: ProcessFile) => {
|
||||
if (!processModelFile.name.match(/\.(dmn|bpmn|json|md)$/)) {
|
||||
return undefined;
|
||||
}
|
||||
const isPrimaryBpmnFile =
|
||||
processModelFile.name === processModel.primary_file_name;
|
||||
|
||||
const actionsTableCell = (
|
||||
<TableCell key={`${processModelFile.name}-action`} align="right">
|
||||
{renderButtonElements(processModelFile, isPrimaryBpmnFile)}
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
let primarySuffix = null;
|
||||
if (isPrimaryBpmnFile) {
|
||||
primarySuffix = (
|
||||
<span>
|
||||
-{' '}
|
||||
<span className="primary-file-text-suffix">Primary File</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
let fileLink = null;
|
||||
const fileUrl = profileModelFileEditUrl(processModelFile);
|
||||
if (fileUrl) {
|
||||
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>;
|
||||
}
|
||||
return (
|
||||
<TableRow key={processModelFile.name}>
|
||||
<TableCell
|
||||
key={`${processModelFile.name}-cell`}
|
||||
className="process-model-file-table-filename"
|
||||
title={processModelFile.name}
|
||||
>
|
||||
{fileLink}
|
||||
{primarySuffix}
|
||||
</TableCell>
|
||||
{actionsTableCell}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
.filter((element: any) => element !== undefined);
|
||||
|
||||
if (tags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
size="lg"
|
||||
useZebraStyles={false}
|
||||
className="process-model-file-table"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader id="Name" key="Name">
|
||||
Name
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
id="Actions"
|
||||
key="Actions"
|
||||
className="table-header-right-align"
|
||||
>
|
||||
Actions
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{tags}</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FileUploader, Modal } from '@carbon/react';
|
||||
|
||||
interface ProcessModelFileUploadModalProps {
|
||||
showFileUploadModal: boolean;
|
||||
processModel: any;
|
||||
handleFileUploadCancel: () => void;
|
||||
checkDuplicateFile: (files: File[], forceOverwrite?: boolean) => void;
|
||||
doFileUpload: Function;
|
||||
setShowFileUploadModal: Function;
|
||||
}
|
||||
|
||||
export default function ProcessModelFileUploadModal({
|
||||
showFileUploadModal,
|
||||
processModel,
|
||||
handleFileUploadCancel,
|
||||
checkDuplicateFile,
|
||||
doFileUpload,
|
||||
setShowFileUploadModal,
|
||||
}: ProcessModelFileUploadModalProps) {
|
||||
const [filesToUpload, setFilesToUpload] = useState<File[] | null>(null);
|
||||
const [duplicateFilename, setDuplicateFilename] = useState<string>('');
|
||||
const [showOverwriteConfirmationPrompt, setShowOverwriteConfirmationPrompt] =
|
||||
useState(false);
|
||||
|
||||
const handleOverwriteFileConfirm = () => {
|
||||
setShowOverwriteConfirmationPrompt(false);
|
||||
doFileUpload(filesToUpload);
|
||||
};
|
||||
|
||||
const handleOverwriteFileCancel = () => {
|
||||
setShowFileUploadModal(true);
|
||||
setShowOverwriteConfirmationPrompt(false);
|
||||
};
|
||||
|
||||
const displayOverwriteConfirmation = (filename: string) => {
|
||||
setDuplicateFilename(filename);
|
||||
setShowFileUploadModal(false);
|
||||
setShowOverwriteConfirmationPrompt(true);
|
||||
};
|
||||
|
||||
const handleLocalFileUpload = () => {
|
||||
if (!filesToUpload) {
|
||||
return;
|
||||
}
|
||||
if (processModel) {
|
||||
let foundExistingFile = false;
|
||||
if (processModel.files && processModel.files.length > 0) {
|
||||
processModel.files.forEach((file: { name: string }) => {
|
||||
if (file.name === filesToUpload[0].name) {
|
||||
foundExistingFile = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (foundExistingFile) {
|
||||
displayOverwriteConfirmation(filesToUpload[0].name);
|
||||
} else {
|
||||
checkDuplicateFile(filesToUpload);
|
||||
setShowOverwriteConfirmationPrompt(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{confirmOverwriteFileDialog()}
|
||||
<Modal
|
||||
data-qa="modal-upload-file-dialog"
|
||||
open={showFileUploadModal}
|
||||
modalHeading="Upload File"
|
||||
primaryButtonText="Upload"
|
||||
primaryButtonDisabled={filesToUpload === null}
|
||||
secondaryButtonText="Cancel"
|
||||
onSecondarySubmit={handleFileUploadCancel}
|
||||
onRequestClose={handleFileUploadCancel}
|
||||
onRequestSubmit={handleLocalFileUpload}
|
||||
>
|
||||
<FileUploader
|
||||
labelTitle="Upload files"
|
||||
labelDescription="Max file size is 500mb. Only .bpmn, .dmn, .json, and .md files are supported."
|
||||
buttonLabel="Add file"
|
||||
buttonKind="primary"
|
||||
size="md"
|
||||
filenameStatus="edit"
|
||||
role="button"
|
||||
accept={['.bpmn', '.dmn', '.json', '.md']}
|
||||
disabled={false}
|
||||
iconDescription="Delete file"
|
||||
name=""
|
||||
multiple={false}
|
||||
onDelete={() => setFilesToUpload(null)}
|
||||
onChange={(event: any) => setFilesToUpload(event.target.files)}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { Button, Column, Grid } from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import { Edit } from '@carbon/icons-react';
|
||||
import { PureAbility } from '@casl/ability';
|
||||
import MarkdownDisplayForFile from './MarkdownDisplayForFile';
|
||||
import { ProcessFile } from '../interfaces';
|
||||
|
||||
interface ProcessModelReadmeAreaProps {
|
||||
readmeFile: ProcessFile | null;
|
||||
ability: PureAbility;
|
||||
targetUris: any;
|
||||
modifiedProcessModelId: string;
|
||||
}
|
||||
|
||||
export default function ProcessModelReadmeArea({
|
||||
readmeFile,
|
||||
ability,
|
||||
targetUris,
|
||||
modifiedProcessModelId,
|
||||
}: ProcessModelReadmeAreaProps) {
|
||||
if (readmeFile) {
|
||||
return (
|
||||
<div className="readme-container">
|
||||
<Grid condensed fullWidth className="megacondensed">
|
||||
<Column md={7} lg={15} sm={3}>
|
||||
<p className="with-icons">{readmeFile.name}</p>
|
||||
</Column>
|
||||
<Column md={1} lg={1} sm={1}>
|
||||
<Can
|
||||
I="PUT"
|
||||
a={targetUris.processModelFileCreatePath}
|
||||
ability={ability}
|
||||
>
|
||||
<Button
|
||||
kind="ghost"
|
||||
data-qa="process-model-readme-file-edit"
|
||||
renderIcon={Edit}
|
||||
iconDescription="Edit README.md"
|
||||
hasIconOnly
|
||||
href={`/process-models/${modifiedProcessModelId}/form/${readmeFile.name}`}
|
||||
/>
|
||||
</Can>
|
||||
</Column>
|
||||
</Grid>
|
||||
<hr />
|
||||
<MarkdownDisplayForFile
|
||||
apiPath={`/process-models/${modifiedProcessModelId}/files/${readmeFile.name}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<p>No README file found</p>
|
||||
<Can I="POST" a={targetUris.processModelFileCreatePath} ability={ability}>
|
||||
<Button
|
||||
className="with-top-margin"
|
||||
data-qa="process-model-readme-file-create"
|
||||
href={`/process-models/${modifiedProcessModelId}/form?file_ext=md&default_file_name=README.md`}
|
||||
size="md"
|
||||
>
|
||||
Add README.md
|
||||
</Button>
|
||||
</Can>
|
||||
</>
|
||||
);
|
||||
}
|
187
spiffworkflow-frontend/src/components/ProcessModelTabs.tsx
Normal file
187
spiffworkflow-frontend/src/components/ProcessModelTabs.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Column,
|
||||
Dropdown,
|
||||
Grid,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
} from '@carbon/react';
|
||||
import { Can } from '@casl/react'; // Corrected import
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PureAbility } from '@casl/ability';
|
||||
import ProcessInstanceListTable from './ProcessInstanceListTable';
|
||||
import ProcessModelFileList from './ProcessModelFileList';
|
||||
import { ProcessFile } from '../interfaces';
|
||||
import ProcessModelReadmeArea from './ProcessModelReadmeArea';
|
||||
|
||||
interface ProcessModelTabsProps {
|
||||
processModel: any;
|
||||
ability: PureAbility;
|
||||
targetUris: any;
|
||||
modifiedProcessModelId: string;
|
||||
selectedTabIndex: number;
|
||||
updateSelectedTab: (newTabIndex: any) => void;
|
||||
onDeleteFile: (fileName: string) => void;
|
||||
onSetPrimaryFile: (fileName: string) => void;
|
||||
isTestCaseFile: (processModelFile: ProcessFile) => boolean;
|
||||
readmeFile: ProcessFile | null;
|
||||
setShowFileUploadModal: Function;
|
||||
}
|
||||
|
||||
export default function ProcessModelTabs({
|
||||
processModel,
|
||||
ability,
|
||||
targetUris,
|
||||
modifiedProcessModelId,
|
||||
selectedTabIndex,
|
||||
updateSelectedTab,
|
||||
onDeleteFile,
|
||||
onSetPrimaryFile,
|
||||
isTestCaseFile,
|
||||
readmeFile,
|
||||
setShowFileUploadModal,
|
||||
}: ProcessModelTabsProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!processModel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let helpText = null;
|
||||
if (processModel.files.length === 0) {
|
||||
helpText = (
|
||||
<p className="no-results-message with-bottom-margin">
|
||||
<strong>
|
||||
**This process model does not have any files associated with it. Try
|
||||
creating a bpmn file by selecting "New BPMN File" in the
|
||||
dropdown below.**
|
||||
</strong>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const items = [
|
||||
'Upload File',
|
||||
'New BPMN File',
|
||||
'New DMN File',
|
||||
'New JSON File',
|
||||
'New Markdown File',
|
||||
].map((item) => ({
|
||||
text: item,
|
||||
}));
|
||||
|
||||
const addFileComponent = () => {
|
||||
return (
|
||||
<Dropdown
|
||||
id="inline"
|
||||
titleText=""
|
||||
size="lg"
|
||||
label="Add File"
|
||||
type="default"
|
||||
data-qa="process-model-add-file"
|
||||
onChange={(a: any) => {
|
||||
if (a.selectedItem.text === 'New BPMN File') {
|
||||
navigate(
|
||||
`/editor/process-models/${modifiedProcessModelId}/files?file_type=bpmn`,
|
||||
);
|
||||
} else if (a.selectedItem.text === 'Upload File') {
|
||||
// Handled by parent component via prop
|
||||
updateSelectedTab({ selectedIndex: 1 }); // Switch to Files tab
|
||||
// Open file upload modal (handled by parent)
|
||||
setShowFileUploadModal(true);
|
||||
} else if (a.selectedItem.text === 'New DMN File') {
|
||||
navigate(
|
||||
`/editor/process-models/${modifiedProcessModelId}/files?file_type=dmn`,
|
||||
);
|
||||
} else if (a.selectedItem.text === 'New JSON File') {
|
||||
navigate(
|
||||
`/process-models/${modifiedProcessModelId}/form?file_ext=json`,
|
||||
);
|
||||
} else if (a.selectedItem.text === 'New Markdown File') {
|
||||
navigate(
|
||||
`/process-models/${modifiedProcessModelId}/form?file_ext=md`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
items={items}
|
||||
itemToString={(item: any) => (item ? item.text : '')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs selectedIndex={selectedTabIndex} onChange={updateSelectedTab}>
|
||||
<TabList aria-label="List of tabs">
|
||||
<Tab>About</Tab>
|
||||
<Tab data-qa="process-model-files">Files</Tab>
|
||||
<Tab data-qa="process-instance-list-link">My process instances</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
{readmeFile && (
|
||||
<ProcessModelReadmeArea
|
||||
readmeFile={readmeFile}
|
||||
ability={ability}
|
||||
targetUris={targetUris}
|
||||
modifiedProcessModelId={modifiedProcessModelId}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Grid condensed fullWidth className="megacondensed">
|
||||
<Column md={6} lg={12} sm={4}>
|
||||
<Can
|
||||
I="POST"
|
||||
a={targetUris.processModelFileCreatePath}
|
||||
ability={ability}
|
||||
>
|
||||
{helpText}
|
||||
<div className="with-bottom-margin">
|
||||
Files
|
||||
{processModel &&
|
||||
processModel.bpmn_version_control_identifier &&
|
||||
` (revision ${processModel.bpmn_version_control_identifier})`}
|
||||
</div>
|
||||
{addFileComponent()}
|
||||
<br />
|
||||
</Can>
|
||||
<ProcessModelFileList
|
||||
processModel={processModel}
|
||||
ability={ability}
|
||||
targetUris={targetUris}
|
||||
modifiedProcessModelId={modifiedProcessModelId}
|
||||
onDeleteFile={onDeleteFile}
|
||||
onSetPrimaryFile={onSetPrimaryFile}
|
||||
isTestCaseFile={isTestCaseFile}
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{selectedTabIndex !== 2 ? null : (
|
||||
<Can
|
||||
I="POST"
|
||||
a={targetUris.processInstanceListForMePath}
|
||||
ability={ability}
|
||||
>
|
||||
<ProcessInstanceListTable
|
||||
additionalReportFilters={[
|
||||
{
|
||||
field_name: 'process_model_identifier',
|
||||
field_value: processModel.id,
|
||||
},
|
||||
]}
|
||||
perPageOptions={[2, 5, 25]}
|
||||
showLinkToReport
|
||||
variant="for-me"
|
||||
/>
|
||||
</Can>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
@ -1,33 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Download,
|
||||
Edit,
|
||||
Favorite,
|
||||
TrashCan,
|
||||
Upload,
|
||||
View,
|
||||
} from '@carbon/icons-react';
|
||||
import {
|
||||
Button,
|
||||
Column,
|
||||
Dropdown,
|
||||
FileUploader,
|
||||
Grid,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Tabs,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
} from '@carbon/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Upload, Edit, TrashCan } from '@carbon/icons-react';
|
||||
import { Button, Stack } from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
@ -45,8 +19,8 @@ import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import ProcessInstanceRun from '../components/ProcessInstanceRun';
|
||||
import { Notification } from '../components/Notification';
|
||||
import ProcessModelTestRun from '../components/ProcessModelTestRun';
|
||||
import MarkdownDisplayForFile from '../components/MarkdownDisplayForFile';
|
||||
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
|
||||
import ProcessModelTabs from '../components/ProcessModelTabs';
|
||||
import ProcessModelFileUploadModal from '../components/ProcessModelFileUploadModal';
|
||||
|
||||
export default function ProcessModelShow() {
|
||||
const params = useParams();
|
||||
@ -55,7 +29,6 @@ export default function ProcessModelShow() {
|
||||
|
||||
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
||||
const [reloadModel, setReloadModel] = useState<boolean>(false);
|
||||
const [filesToUpload, setFilesToUpload] = useState<any>(null);
|
||||
const [showFileUploadModal, setShowFileUploadModal] =
|
||||
useState<boolean>(false);
|
||||
const [processModelPublished, setProcessModelPublished] = useState<any>(null);
|
||||
@ -83,7 +56,7 @@ export default function ProcessModelShow() {
|
||||
let hasTestCaseFiles: boolean = false;
|
||||
|
||||
const isTestCaseFile = (processModelFile: ProcessFile) => {
|
||||
return processModelFile.name.match(/^test_.*\.json$/);
|
||||
return !!processModelFile.name.match(/^test_.*\.json$/);
|
||||
};
|
||||
|
||||
if (processModel) {
|
||||
@ -99,12 +72,14 @@ export default function ProcessModelShow() {
|
||||
setPageTitle([result.display_name]);
|
||||
|
||||
let newTabIndex = 1;
|
||||
let foundReadme = null;
|
||||
result.files.forEach((file: ProcessFile) => {
|
||||
if (file.name === 'README.md') {
|
||||
setReadmeFile(file);
|
||||
foundReadme = file;
|
||||
newTabIndex = 0;
|
||||
}
|
||||
});
|
||||
setReadmeFile(foundReadme);
|
||||
setSelectedTabIndex(newTabIndex);
|
||||
};
|
||||
HttpService.makeCallToBackend({
|
||||
@ -116,11 +91,11 @@ export default function ProcessModelShow() {
|
||||
const onUploadedCallback = () => {
|
||||
setReloadModel(true);
|
||||
};
|
||||
|
||||
const reloadModelOhYeah = (_httpResult: any) => {
|
||||
setReloadModel(!reloadModel);
|
||||
};
|
||||
|
||||
// Remove this code from
|
||||
const onDeleteFile = (fileName: string) => {
|
||||
const url = `/process-models/${modifiedProcessModelId}/files/${fileName}`;
|
||||
const httpMethod = 'DELETE';
|
||||
@ -145,51 +120,6 @@ export default function ProcessModelShow() {
|
||||
postBody: processModelToPass,
|
||||
});
|
||||
};
|
||||
const handleProcessModelFileResult = (processModelFile: ProcessFile) => {
|
||||
if (
|
||||
!('file_contents' in processModelFile) ||
|
||||
processModelFile.file_contents === undefined
|
||||
) {
|
||||
addError({
|
||||
message: `Could not file file contents for file: ${processModelFile.name}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let contentType = 'application/xml';
|
||||
if (processModelFile.type === 'json') {
|
||||
contentType = 'application/json';
|
||||
}
|
||||
const element = document.createElement('a');
|
||||
const file = new Blob([processModelFile.file_contents], {
|
||||
type: contentType,
|
||||
});
|
||||
const downloadFileName = processModelFile.name;
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = downloadFileName;
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
};
|
||||
|
||||
const downloadFile = (fileName: string) => {
|
||||
removeError();
|
||||
const processModelPath = `process-models/${modifiedProcessModelId}`;
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/${processModelPath}/files/${fileName}`,
|
||||
successCallback: handleProcessModelFileResult,
|
||||
});
|
||||
};
|
||||
|
||||
const profileModelFileEditUrl = (processModelFile: ProcessFile) => {
|
||||
if (processModel) {
|
||||
if (processModelFile.name.match(/\.(dmn|bpmn)$/)) {
|
||||
return `/editor/process-models/${modifiedProcessModelId}/files/${processModelFile.name}`;
|
||||
}
|
||||
if (processModelFile.name.match(/\.(json|md)$/)) {
|
||||
return `/process-models/${modifiedProcessModelId}/form/${processModelFile.name}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigateToProcessModels = (_result: any) => {
|
||||
navigate(
|
||||
@ -220,202 +150,17 @@ export default function ProcessModelShow() {
|
||||
});
|
||||
};
|
||||
|
||||
const navigateToFileEdit = (processModelFile: ProcessFile) => {
|
||||
const url = profileModelFileEditUrl(processModelFile);
|
||||
if (url) {
|
||||
navigate(url);
|
||||
const doFileUpload = (filesToUpload: File[], forceOverwrite = false) => {
|
||||
if (!filesToUpload || filesToUpload.length === 0) {
|
||||
return; // No files to upload
|
||||
}
|
||||
};
|
||||
|
||||
const renderButtonElements = (
|
||||
processModelFile: ProcessFile,
|
||||
isPrimaryBpmnFile: boolean,
|
||||
) => {
|
||||
const elements = [];
|
||||
|
||||
// So there is a bug in here. Since we use a react context for error messages, and since
|
||||
// its provider wraps the entire app, child components will re-render when there is an
|
||||
// error displayed. This is normally fine, but it interacts badly with the casl ability.can
|
||||
// functionality. We have observed that permissionsLoaded is never set to false. So when
|
||||
// you run a process and it fails, for example, process model show will re-render, the ability
|
||||
// will be cleared out and it will start fetching permissions from the server, but this
|
||||
// component still thinks permissionsLoaded is telling the truth (it says true, but it's actually false).
|
||||
// The only bad effect that we know of is that the Edit icon becomes an eye icon even for admins.
|
||||
let icon = View;
|
||||
let actionWord = 'View';
|
||||
if (ability.can('PUT', targetUris.processModelFileCreatePath)) {
|
||||
icon = Edit;
|
||||
actionWord = 'Edit';
|
||||
}
|
||||
elements.push(
|
||||
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={icon}
|
||||
iconDescription={`${actionWord} File`}
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
|
||||
onClick={() => navigateToFileEdit(processModelFile)}
|
||||
/>
|
||||
</Can>,
|
||||
);
|
||||
elements.push(
|
||||
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Download}
|
||||
iconDescription="Download File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => downloadFile(processModelFile.name)}
|
||||
/>
|
||||
</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"
|
||||
classNameForModal="modal-within-table-cell"
|
||||
/>
|
||||
</Can>,
|
||||
);
|
||||
}
|
||||
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
|
||||
elements.push(
|
||||
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Favorite}
|
||||
iconDescription="Set As Primary File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => onSetPrimaryFile(processModelFile.name)}
|
||||
/>
|
||||
</Can>,
|
||||
);
|
||||
}
|
||||
if (isTestCaseFile(processModelFile)) {
|
||||
elements.push(
|
||||
<Can I="POST" a={targetUris.processModelTestsPath} ability={ability}>
|
||||
<ProcessModelTestRun
|
||||
processModelFile={processModelFile}
|
||||
titleText="Run BPMN unit tests defined in this file"
|
||||
classNameForModal="modal-within-table-cell"
|
||||
/>
|
||||
</Can>,
|
||||
);
|
||||
}
|
||||
return elements;
|
||||
};
|
||||
|
||||
const processModelFileList = () => {
|
||||
if (!processModel || !permissionsLoaded) {
|
||||
return null;
|
||||
}
|
||||
let constructedTag;
|
||||
const tags = processModel.files
|
||||
.map((processModelFile: ProcessFile) => {
|
||||
if (!processModelFile.name.match(/\.(dmn|bpmn|json|md)$/)) {
|
||||
return undefined;
|
||||
}
|
||||
const isPrimaryBpmnFile =
|
||||
processModelFile.name === processModel.primary_file_name;
|
||||
|
||||
let actionsTableCell = null;
|
||||
if (processModelFile.name.match(/\.(dmn|bpmn|json|md)$/)) {
|
||||
actionsTableCell = (
|
||||
<TableCell key={`${processModelFile.name}-action`} align="right">
|
||||
{renderButtonElements(processModelFile, isPrimaryBpmnFile)}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
|
||||
let primarySuffix = null;
|
||||
if (isPrimaryBpmnFile) {
|
||||
primarySuffix = (
|
||||
<span>
|
||||
-{' '}
|
||||
<span className="primary-file-text-suffix">Primary File</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
let fileLink = null;
|
||||
const fileUrl = profileModelFileEditUrl(processModelFile);
|
||||
if (fileUrl) {
|
||||
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>;
|
||||
}
|
||||
constructedTag = (
|
||||
<TableRow key={processModelFile.name}>
|
||||
<TableCell
|
||||
key={`${processModelFile.name}-cell`}
|
||||
className="process-model-file-table-filename"
|
||||
title={processModelFile.name}
|
||||
>
|
||||
{fileLink}
|
||||
{primarySuffix}
|
||||
</TableCell>
|
||||
{actionsTableCell}
|
||||
</TableRow>
|
||||
);
|
||||
return constructedTag;
|
||||
})
|
||||
.filter((element: any) => element !== undefined);
|
||||
|
||||
if (tags.length > 0) {
|
||||
return (
|
||||
<Table
|
||||
size="lg"
|
||||
useZebraStyles={false}
|
||||
className="process-model-file-table"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader id="Name" key="Name">
|
||||
Name
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
id="Actions"
|
||||
key="Actions"
|
||||
className="table-header-right-align"
|
||||
>
|
||||
Actions
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{tags}</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const [fileUploadEvent, setFileUploadEvent] = useState(null);
|
||||
const [duplicateFilename, setDuplicateFilename] = useState<string>('');
|
||||
const [showOverwriteConfirmationPrompt, setShowOverwriteConfirmationPrompt] =
|
||||
useState(false);
|
||||
|
||||
const doFileUpload = (event: any) => {
|
||||
event.preventDefault();
|
||||
removeError();
|
||||
const url = `/process-models/${modifiedProcessModelId}/files`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', filesToUpload[0]);
|
||||
formData.append('fileName', filesToUpload[0].name);
|
||||
formData.append('overwrite', forceOverwrite.toString()); // Add overwrite parameter
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: onUploadedCallback,
|
||||
@ -423,279 +168,25 @@ export default function ProcessModelShow() {
|
||||
postBody: formData,
|
||||
failureCallback: addError,
|
||||
});
|
||||
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) {
|
||||
let foundExistingFile = false;
|
||||
if (processModel.files.length > 0) {
|
||||
processModel.files.forEach((file) => {
|
||||
if (file.name === filesToUpload[0].name) {
|
||||
foundExistingFile = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (foundExistingFile) {
|
||||
displayOverwriteConfirmation(filesToUpload[0].name);
|
||||
setFileUploadEvent(event);
|
||||
const checkDuplicateFile = (files: File[], forceOverwrite = false) => {
|
||||
if (forceOverwrite) {
|
||||
doFileUpload(files, true);
|
||||
} else {
|
||||
doFileUpload(event);
|
||||
doFileUpload(files);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: any) => {
|
||||
checkDuplicateFile(event);
|
||||
setShowFileUploadModal(false);
|
||||
};
|
||||
|
||||
const fileUploadModal = () => {
|
||||
return (
|
||||
<Modal
|
||||
data-qa="modal-upload-file-dialog"
|
||||
open={showFileUploadModal}
|
||||
modalHeading="Upload File"
|
||||
primaryButtonText="Upload"
|
||||
primaryButtonDisabled={filesToUpload === null}
|
||||
secondaryButtonText="Cancel"
|
||||
onSecondarySubmit={handleFileUploadCancel}
|
||||
onRequestClose={handleFileUploadCancel}
|
||||
onRequestSubmit={handleFileUpload}
|
||||
>
|
||||
<FileUploader
|
||||
labelTitle="Upload files"
|
||||
labelDescription="Max file size is 500mb. Only .bpmn, .dmn, .json, and .md files are supported."
|
||||
buttonLabel="Add file"
|
||||
buttonKind="primary"
|
||||
size="md"
|
||||
filenameStatus="edit"
|
||||
role="button"
|
||||
accept={['.bpmn', '.dmn', '.json', '.md']}
|
||||
disabled={false}
|
||||
iconDescription="Delete file"
|
||||
name=""
|
||||
multiple={false}
|
||||
onDelete={() => setFilesToUpload(null)}
|
||||
onChange={(event: any) => setFilesToUpload(event.target.files)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const items = [
|
||||
'Upload File',
|
||||
'New BPMN File',
|
||||
'New DMN File',
|
||||
'New JSON File',
|
||||
'New Markdown File',
|
||||
].map((item) => ({
|
||||
text: item,
|
||||
}));
|
||||
|
||||
const addFileComponent = () => {
|
||||
return (
|
||||
<Dropdown
|
||||
id="inline"
|
||||
titleText=""
|
||||
size="lg"
|
||||
label="Add File"
|
||||
type="default"
|
||||
data-qa="process-model-add-file"
|
||||
onChange={(a: any) => {
|
||||
if (a.selectedItem.text === 'New BPMN File') {
|
||||
navigate(
|
||||
`/editor/process-models/${modifiedProcessModelId}/files?file_type=bpmn`,
|
||||
);
|
||||
} else if (a.selectedItem.text === 'Upload File') {
|
||||
setShowFileUploadModal(true);
|
||||
} else if (a.selectedItem.text === 'New DMN File') {
|
||||
navigate(
|
||||
`/editor/process-models/${modifiedProcessModelId}/files?file_type=dmn`,
|
||||
);
|
||||
} else if (a.selectedItem.text === 'New JSON File') {
|
||||
navigate(
|
||||
`/process-models/${modifiedProcessModelId}/form?file_ext=json`,
|
||||
);
|
||||
} else if (a.selectedItem.text === 'New Markdown File') {
|
||||
navigate(
|
||||
`/process-models/${modifiedProcessModelId}/form?file_ext=md`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
items={items}
|
||||
itemToString={(item: any) => (item ? item.text : '')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const readmeFileArea = () => {
|
||||
if (readmeFile) {
|
||||
return (
|
||||
<div className="readme-container">
|
||||
<Grid condensed fullWidth className="megacondensed">
|
||||
<Column md={7} lg={15} sm={3}>
|
||||
<p className="with-icons">{readmeFile.name}</p>
|
||||
</Column>
|
||||
<Column md={1} lg={1} sm={1}>
|
||||
<Can
|
||||
I="PUT"
|
||||
a={targetUris.processModelFileCreatePath}
|
||||
ability={ability}
|
||||
>
|
||||
<Button
|
||||
kind="ghost"
|
||||
data-qa="process-model-readme-file-edit"
|
||||
renderIcon={Edit}
|
||||
iconDescription="Edit README.md"
|
||||
hasIconOnly
|
||||
href={`/process-models/${modifiedProcessModelId}/form/${readmeFile.name}`}
|
||||
/>
|
||||
</Can>
|
||||
</Column>
|
||||
</Grid>
|
||||
<hr />
|
||||
<MarkdownDisplayForFile
|
||||
apiPath={`/process-models/${modifiedProcessModelId}/files/${readmeFile.name}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<p>No README file found</p>
|
||||
<Can
|
||||
I="POST"
|
||||
a={targetUris.processModelFileCreatePath}
|
||||
ability={ability}
|
||||
>
|
||||
<Button
|
||||
className="with-top-margin"
|
||||
data-qa="process-model-readme-file-create"
|
||||
href={`/process-models/${modifiedProcessModelId}/form?file_ext=md&default_file_name=README.md`}
|
||||
size="md"
|
||||
>
|
||||
Add README.md
|
||||
</Button>
|
||||
</Can>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const updateSelectedTab = (newTabIndex: any) => {
|
||||
setSelectedTabIndex(newTabIndex.selectedIndex);
|
||||
};
|
||||
|
||||
const tabArea = () => {
|
||||
if (!processModel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let helpText = null;
|
||||
if (processModel.files.length === 0) {
|
||||
helpText = (
|
||||
<p className="no-results-message with-bottom-margin">
|
||||
<strong>
|
||||
**This process model does not have any files associated with it. Try
|
||||
creating a bpmn file by selecting "New BPMN File" in the
|
||||
dropdown below.**
|
||||
</strong>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs selectedIndex={selectedTabIndex} onChange={updateSelectedTab}>
|
||||
<TabList aria-label="List of tabs">
|
||||
<Tab>About</Tab>
|
||||
<Tab data-qa="process-model-files">Files</Tab>
|
||||
<Tab data-qa="process-instance-list-link">My process instances</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>{readmeFileArea()}</TabPanel>
|
||||
<TabPanel>
|
||||
<Grid condensed fullWidth className="megacondensed">
|
||||
<Column md={6} lg={12} sm={4}>
|
||||
<Can
|
||||
I="POST"
|
||||
a={targetUris.processModelFileCreatePath}
|
||||
ability={ability}
|
||||
>
|
||||
{helpText}
|
||||
<div className="with-bottom-margin">
|
||||
Files
|
||||
{processModel &&
|
||||
processModel.bpmn_version_control_identifier &&
|
||||
` (revision ${processModel.bpmn_version_control_identifier})`}
|
||||
</div>
|
||||
{addFileComponent()}
|
||||
<br />
|
||||
</Can>
|
||||
{processModelFileList()}
|
||||
</Column>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{selectedTabIndex !== 2 ? null : (
|
||||
<Can
|
||||
I="POST"
|
||||
a={targetUris.processInstanceListForMePath}
|
||||
ability={ability}
|
||||
>
|
||||
<ProcessInstanceListTable
|
||||
additionalReportFilters={[
|
||||
{
|
||||
field_name: 'process_model_identifier',
|
||||
field_value: processModel.id,
|
||||
},
|
||||
]}
|
||||
perPageOptions={[2, 5, 25]}
|
||||
showLinkToReport
|
||||
variant="for-me"
|
||||
/>
|
||||
</Can>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const processModelPublishMessage = () => {
|
||||
if (processModelPublished) {
|
||||
const prUrl: string = processModelPublished.pr_url;
|
||||
@ -721,18 +212,20 @@ export default function ProcessModelShow() {
|
||||
a={targetUris.processInstanceCreatePath}
|
||||
ability={ability}
|
||||
>
|
||||
<>
|
||||
<ProcessInstanceRun processModel={processModel} />
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
</Can>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{fileUploadModal()}
|
||||
{confirmOverwriteFileDialog()}
|
||||
<ProcessModelFileUploadModal
|
||||
showFileUploadModal={showFileUploadModal}
|
||||
processModel={processModel}
|
||||
doFileUpload={doFileUpload}
|
||||
handleFileUploadCancel={handleFileUploadCancel}
|
||||
checkDuplicateFile={checkDuplicateFile}
|
||||
setShowFileUploadModal={setShowFileUploadModal}
|
||||
/>
|
||||
<ProcessBreadcrumb
|
||||
hotCrumbs={[
|
||||
['Process Groups', '/process-groups'],
|
||||
@ -796,7 +289,23 @@ export default function ProcessModelShow() {
|
||||
{processModel.primary_file_name && processModel.is_executable
|
||||
? processStartButton
|
||||
: null}
|
||||
<div className="with-top-margin">{tabArea()}</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<ProcessModelTabs
|
||||
processModel={processModel}
|
||||
ability={ability}
|
||||
targetUris={targetUris}
|
||||
modifiedProcessModelId={modifiedProcessModelId}
|
||||
selectedTabIndex={selectedTabIndex}
|
||||
updateSelectedTab={updateSelectedTab}
|
||||
onDeleteFile={onDeleteFile}
|
||||
onSetPrimaryFile={onSetPrimaryFile}
|
||||
isTestCaseFile={isTestCaseFile}
|
||||
readmeFile={readmeFile}
|
||||
setShowFileUploadModal={setShowFileUploadModal}
|
||||
/>
|
||||
{permissionsLoaded ? (
|
||||
<span data-qa="process-model-show-permissions-loaded" />
|
||||
) : null}
|
||||
|
Loading…
x
Reference in New Issue
Block a user