Merge remote-tracking branch 'origin/main' into newui-all-in

This commit is contained in:
jasquat 2025-02-05 14:58:53 -05:00
commit 99e67f13f2
No known key found for this signature in database
6 changed files with 635 additions and 539 deletions

View File

@ -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" "$@"

View 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>
&nbsp;-{' '}
<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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View 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 &quot;New BPMN File&quot; 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>
);
}

View File

@ -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>
&nbsp;-{' '}
<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 &quot;New BPMN File&quot; 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}