Feature/pm readme file (#495)

* refactored process model show page to use tabs

* added ability to view and edit a process model readme file w/ burnettk

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2023-09-20 11:14:20 -04:00 committed by GitHub
parent 5e19d68d49
commit 00158df03d
5 changed files with 208 additions and 75 deletions

View File

@ -0,0 +1,34 @@
import MDEditor from '@uiw/react-md-editor';
import { useEffect, useState } from 'react';
import HttpService from '../services/HttpService';
type OwnProps = {
apiPath: string;
};
export default function MarkdownDisplayForFile({ apiPath }: OwnProps) {
const [markdownContents, setMarkdownContents] = useState<string | null>(null);
useEffect(() => {
const processResult = (result: any) => {
if (result.file_contents) {
setMarkdownContents(result.file_contents);
}
};
HttpService.makeCallToBackend({
path: apiPath,
successCallback: processResult,
});
}, [apiPath]);
if (markdownContents) {
return (
<div data-color-mode="light" className="with-bottom-margin">
<MDEditor.Markdown linkTarget="_blank" source={markdownContents} />
</div>
);
}
return null;
}

View File

@ -1840,6 +1840,9 @@ export default function ProcessInstanceListTable({
</Column> </Column>
); );
} }
if (!headerElement && !filterButtonLink) {
return null;
}
return ( return (
<> <>
<Column <Column

View File

@ -216,6 +216,14 @@ h1.with-icons {
margin-top: 5px; margin-top: 5px;
} }
.with-icons {
margin-top: 10px;
}
.readme-container {
max-width: 640px;
}
dl { dl {
display: block; display: block;
grid-template-columns: 35% 65%; grid-template-columns: 35% 65%;
@ -488,11 +496,6 @@ th.table-header-right-align .cds--data-table, th.table-header-right-align .cds--
margin-bottom: 1rem; margin-bottom: 1rem;
} }
/* top and bottom margin since this is sort of the middle of three sections on the process model show page */
.process-model-files-section {
margin: 2rem 0;
}
.filter-icon { .filter-icon {
text-align: right; text-align: right;
padding-bottom: 10px; padding-bottom: 10px;

View File

@ -9,8 +9,6 @@ import {
View, View,
} from '@carbon/icons-react'; } from '@carbon/icons-react';
import { import {
Accordion,
AccordionItem,
Button, Button,
Column, Column,
Dropdown, Dropdown,
@ -24,6 +22,11 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
Tabs,
Tab,
TabList,
TabPanels,
TabPanel,
} from '@carbon/react'; } from '@carbon/react';
import { Can } from '@casl/react'; import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
@ -48,10 +51,12 @@ import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import ProcessInstanceRun from '../components/ProcessInstanceRun'; import ProcessInstanceRun from '../components/ProcessInstanceRun';
import { Notification } from '../components/Notification'; import { Notification } from '../components/Notification';
import ProcessModelTestRun from '../components/ProcessModelTestRun'; import ProcessModelTestRun from '../components/ProcessModelTestRun';
import MarkdownDisplayForFile from '../components/MarkdownDisplayForFile';
export default function ProcessModelShow() { export default function ProcessModelShow() {
const params = useParams(); const params = useParams();
const { addError, removeError } = useAPIError(); const { addError, removeError } = useAPIError();
const navigate = useNavigate();
const [processModel, setProcessModel] = useState<ProcessModel | null>(null); const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
const [processInstance, setProcessInstance] = const [processInstance, setProcessInstance] =
@ -62,7 +67,8 @@ export default function ProcessModelShow() {
useState<boolean>(false); useState<boolean>(false);
const [processModelPublished, setProcessModelPublished] = useState<any>(null); const [processModelPublished, setProcessModelPublished] = useState<any>(null);
const [publishDisabled, setPublishDisabled] = useState<boolean>(false); const [publishDisabled, setPublishDisabled] = useState<boolean>(false);
const navigate = useNavigate(); const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const [readmeFile, setReadmeFile] = useState<ProcessFile | null>(null);
const { targetUris } = useUriListForPermissions(); const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = { const permissionRequestData: PermissionsToCheck = {
@ -98,6 +104,15 @@ export default function ProcessModelShow() {
setProcessModel(result); setProcessModel(result);
setReloadModel(false); setReloadModel(false);
setPageTitle([result.display_name]); setPageTitle([result.display_name]);
let newTabIndex = 1;
result.files.forEach((file: ProcessFile) => {
if (file.name === 'README.md') {
setReadmeFile(file);
newTabIndex = 0;
}
});
setSelectedTabIndex(newTabIndex);
}; };
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}`, path: `/process-models/${modifiedProcessModelId}`,
@ -562,44 +577,124 @@ export default function ProcessModelShow() {
); );
}; };
const processModelFilesSection = () => { const readmeFileArea = () => {
return ( if (readmeFile) {
<Grid return (
condensed <div className="readme-container">
fullWidth <Grid condensed fullWidth className="megacondensed">
className="megacondensed process-model-files-section" <Column md={7} lg={15} sm={3}>
> <p className="with-icons">{readmeFile.name}</p>
<Column md={8} lg={14} sm={4}> </Column>
<Accordion align="end" open className="megacondensed-button"> <Column md={1} lg={1} sm={1}>
<AccordionItem
open
data-qa="files-accordion"
title={
<Stack orientation="horizontal">
<span>
<Button size="sm" kind="ghost">
Files
{processModel &&
processModel.bpmn_version_control_identifier &&
` (revision ${processModel.bpmn_version_control_identifier})`}
</Button>
</span>
</Stack>
}
>
<Can <Can
I="POST" I="GET"
a={targetUris.processModelFileCreatePath} a={targetUris.processModelFileCreatePath}
ability={ability} ability={ability}
> >
{addFileComponent()} <Button
<br /> kind="ghost"
data-qa="process-model-readme-file-edit"
renderIcon={Edit}
iconDescription="Edit README.md"
hasIconOnly
href={`/admin/process-models/${modifiedProcessModelId}/form/${readmeFile.name}`}
/>
</Can> </Can>
{processModelFileList()} </Column>
</AccordionItem> </Grid>
</Accordion> <hr />
</Column> <MarkdownDisplayForFile
</Grid> 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={`/admin/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;
}
return (
<Tabs selectedIndex={selectedTabIndex} onChange={updateSelectedTab}>
<TabList aria-label="List of tabs">
<Tab>About</Tab>
<Tab>Files</Tab>
<Tab>My Process instances</Tab>
</TabList>
<TabPanels>
<TabPanel>{readmeFileArea()}</TabPanel>
<TabPanel>
<Grid condensed fullWidth className="megacondensed">
<Column md={4} lg={8} sm={4}>
<Can
I="POST"
a={targetUris.processModelFileCreatePath}
ability={ability}
>
<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
filtersEnabled={false}
showLinkToReport
variant="for-me"
additionalReportFilters={[
{
field_name: 'process_model_identifier',
field_value: processModel.id,
},
]}
perPageOptions={[2, 5, 25]}
showReports={false}
/>
<span data-qa="process-model-show-permissions-loaded" />
</Can>
)}
</TabPanel>
</TabPanels>
</Tabs>
); );
}; };
@ -666,9 +761,7 @@ export default function ProcessModelShow() {
iconDescription="Edit Process Model" iconDescription="Edit Process Model"
hasIconOnly hasIconOnly
href={`/admin/process-models/${modifiedProcessModelId}/edit`} href={`/admin/process-models/${modifiedProcessModelId}/edit`}
> />
Edit process model
</Button>
</Can> </Can>
<Can I="DELETE" a={targetUris.processModelShowPath} ability={ability}> <Can I="DELETE" a={targetUris.processModelShowPath} ability={ability}>
<ButtonWithConfirmation <ButtonWithConfirmation
@ -707,28 +800,7 @@ export default function ProcessModelShow() {
</Stack> </Stack>
<p className="process-description">{processModel.description}</p> <p className="process-description">{processModel.description}</p>
{processModel.primary_file_name ? processStartButton : null} {processModel.primary_file_name ? processStartButton : null}
{processModelFilesSection()} <div className="with-top-margin">{tabArea()}</div>
<Can
I="POST"
a={targetUris.processInstanceListForMePath}
ability={ability}
>
<ProcessInstanceListTable
headerElement={<h2>My Process Instances</h2>}
filtersEnabled={false}
showLinkToReport
variant="for-me"
additionalReportFilters={[
{
field_name: 'process_model_identifier',
field_value: processModel.id,
},
]}
perPageOptions={[2, 5, 25]}
showReports={false}
/>
<span data-qa="process-model-show-permissions-loaded" />
</Can>
</> </>
); );
} }

View File

@ -6,6 +6,7 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
// @ts-ignore // @ts-ignore
import { Button, ButtonSet, Modal } from '@carbon/react'; import { Button, ButtonSet, Modal } from '@carbon/react';
import { Can } from '@casl/react'; import { Can } from '@casl/react';
import MDEditor from '@uiw/react-md-editor';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
@ -57,6 +58,7 @@ export default function ReactFormEditor() {
const hasDiagram = fileExtension === 'bpmn' || fileExtension === 'dmn'; const hasDiagram = fileExtension === 'bpmn' || fileExtension === 'dmn';
const hasFormBuilder = fileExtension === 'json'; const hasFormBuilder = fileExtension === 'json';
const defaultFileName = searchParams.get('default_file_name');
const editorDefaultLanguage = (() => { const editorDefaultLanguage = (() => {
if (fileExtension === 'json') { if (fileExtension === 'json') {
@ -106,7 +108,8 @@ export default function ReactFormEditor() {
setProcessModelFile(file); setProcessModelFile(file);
} }
if (!params.file_name) { if (!params.file_name) {
const fileNameWithExtension = `${newFileName}.${fileExtension}`; const fileNameWithExtension =
defaultFileName ?? `${newFileName}.${fileExtension}`;
navigate( navigate(
`/admin/process-models/${modifiedProcessModelId}/form/${fileNameWithExtension}` `/admin/process-models/${modifiedProcessModelId}/form/${fileNameWithExtension}`
); );
@ -119,7 +122,7 @@ export default function ReactFormEditor() {
let url = `/process-models/${modifiedProcessModelId}/files`; let url = `/process-models/${modifiedProcessModelId}/files`;
let httpMethod = 'PUT'; let httpMethod = 'PUT';
let fileNameWithExtension = params.file_name; let fileNameWithExtension = params.file_name || defaultFileName;
if (newFileName) { if (newFileName) {
fileNameWithExtension = `${newFileName}.${fileExtension}`; fileNameWithExtension = `${newFileName}.${fileExtension}`;
@ -219,6 +222,30 @@ export default function ReactFormEditor() {
return null; return null;
}; };
const editorArea = () => {
if (fileExtension === 'md') {
return (
<div data-color-mode="light">
<MDEditor
height={600}
highlightEnable={false}
value={processModelFileContents || ''}
onChange={(value) => setProcessModelFileContents(value || '')}
/>
</div>
);
}
return (
<Editor
height={600}
width="auto"
defaultLanguage={editorDefaultLanguage}
defaultValue={processModelFileContents || ''}
onChange={(value) => setProcessModelFileContents(value || '')}
/>
);
};
if (processModelFile || !params.file_name) { if (processModelFile || !params.file_name) {
const processModelFileName = processModelFile ? processModelFile.name : ''; const processModelFileName = processModelFile ? processModelFile.name : '';
const formBuildFileParam = params.file_name const formBuildFileParam = params.file_name
@ -318,13 +345,7 @@ export default function ReactFormEditor() {
<ActiveUsers /> <ActiveUsers />
</Can> </Can>
</ButtonSet> </ButtonSet>
<Editor {editorArea()}
height={600}
width="auto"
defaultLanguage={editorDefaultLanguage}
defaultValue={processModelFileContents || ''}
onChange={(value) => setProcessModelFileContents(value || '')}
/>
</main> </main>
); );
} }