diff --git a/spiffworkflow-backend/bin/run_local_python_script b/spiffworkflow-backend/bin/run_local_python_script index 83f3ddd12..e8683fa86 100755 --- a/spiffworkflow-backend/bin/run_local_python_script +++ b/spiffworkflow-backend/bin/run_local_python_script @@ -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" "$@" diff --git a/spiffworkflow-frontend/src/components/ProcessModelFileList.tsx b/spiffworkflow-frontend/src/components/ProcessModelFileList.tsx new file mode 100644 index 000000000..1260a89ed --- /dev/null +++ b/spiffworkflow-frontend/src/components/ProcessModelFileList.tsx @@ -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( + + + + + ); +} diff --git a/spiffworkflow-frontend/src/components/ProcessModelTabs.tsx b/spiffworkflow-frontend/src/components/ProcessModelTabs.tsx new file mode 100644 index 000000000..37f8e03ea --- /dev/null +++ b/spiffworkflow-frontend/src/components/ProcessModelTabs.tsx @@ -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 = ( +

+ + **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.** + +

+ ); + } + + const items = [ + 'Upload File', + 'New BPMN File', + 'New DMN File', + 'New JSON File', + 'New Markdown File', + ].map((item) => ({ + text: item, + })); + + const addFileComponent = () => { + return ( + { + 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 ( + + + About + Files + My process instances + + + + {readmeFile && ( + + )} + + + + + + {helpText} +
+ Files + {processModel && + processModel.bpmn_version_control_identifier && + ` (revision ${processModel.bpmn_version_control_identifier})`} +
+ {addFileComponent()} +
+
+ +
+
+
+ + {selectedTabIndex !== 2 ? null : ( + + + + )} + +
+
+ ); +} diff --git a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx index 4f03c1864..97a6a9d99 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx @@ -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(null); const [reloadModel, setReloadModel] = useState(false); - const [filesToUpload, setFilesToUpload] = useState(null); const [showFileUploadModal, setShowFileUploadModal] = useState(false); const [processModelPublished, setProcessModelPublished] = useState(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( - - - - - ); - }; - const updateSelectedTab = (newTabIndex: any) => { setSelectedTabIndex(newTabIndex.selectedIndex); }; - const tabArea = () => { - if (!processModel) { - return null; - } - - let helpText = null; - if (processModel.files.length === 0) { - helpText = ( -

- - **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.** - -

- ); - } - - return ( - - - About - Files - My process instances - - - {readmeFileArea()} - - - - - {helpText} -
- Files - {processModel && - processModel.bpmn_version_control_identifier && - ` (revision ${processModel.bpmn_version_control_identifier})`} -
- {addFileComponent()} -
-
- {processModelFileList()} -
-
-
- - {selectedTabIndex !== 2 ? null : ( - - - - )} - -
-
- ); - }; - const processModelPublishMessage = () => { if (processModelPublished) { const prUrl: string = processModelPublished.pr_url; @@ -721,18 +212,20 @@ export default function ProcessModelShow() { a={targetUris.processInstanceCreatePath} ability={ability} > - <> - -
-
- + ); return ( <> - {fileUploadModal()} - {confirmOverwriteFileDialog()} + {tabArea()} + +
+
+ + {permissionsLoaded ? ( ) : null}