mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-22 06:08:29 +00:00
can add, remove, delete process groups and models w/ burnettk
This commit is contained in:
parent
7dccce611e
commit
2bba376ab3
@ -0,0 +1,178 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, TextField, Stack } from '@mui/material';
|
||||
import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers';
|
||||
import HttpService from '../services/HttpService';
|
||||
import { ProcessGroup } from '../interfaces';
|
||||
|
||||
type OwnProps = {
|
||||
mode: string;
|
||||
processGroup: ProcessGroup;
|
||||
setProcessGroup: (..._args: any[]) => any;
|
||||
};
|
||||
|
||||
export default function ProcessGroupForm({
|
||||
mode,
|
||||
processGroup,
|
||||
setProcessGroup,
|
||||
}: OwnProps) {
|
||||
const [identifierInvalid, setIdentifierInvalid] = useState<boolean>(false);
|
||||
const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] =
|
||||
useState<boolean>(false);
|
||||
const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
let newProcessGroupId = processGroup.id;
|
||||
|
||||
const handleProcessGroupUpdateResponse = (_result: any) => {
|
||||
if (newProcessGroupId) {
|
||||
navigate(
|
||||
`/newui/process-groups/${modifyProcessIdentifierForPathParam(
|
||||
newProcessGroupId,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const hasValidIdentifier = (identifierToCheck: string) => {
|
||||
return identifierToCheck.match(/^[a-z0-9][0-9a-z-]*[a-z0-9]$/);
|
||||
};
|
||||
|
||||
const handleFormSubmission = (event: any) => {
|
||||
const searchParams = new URLSearchParams(document.location.search);
|
||||
const parentGroupId = searchParams.get('parentGroupId');
|
||||
|
||||
event.preventDefault();
|
||||
let hasErrors = false;
|
||||
if (mode === 'new' && !hasValidIdentifier(processGroup.id)) {
|
||||
setIdentifierInvalid(true);
|
||||
hasErrors = true;
|
||||
}
|
||||
if (processGroup.display_name === '') {
|
||||
setDisplayNameInvalid(true);
|
||||
hasErrors = true;
|
||||
}
|
||||
if (hasErrors) {
|
||||
return;
|
||||
}
|
||||
let path = '/process-groups';
|
||||
if (mode === 'edit') {
|
||||
path = `/process-groups/${modifyProcessIdentifierForPathParam(
|
||||
processGroup.id,
|
||||
)}`;
|
||||
}
|
||||
let httpMethod = 'POST';
|
||||
if (mode === 'edit') {
|
||||
httpMethod = 'PUT';
|
||||
}
|
||||
const postBody = {
|
||||
display_name: processGroup.display_name,
|
||||
description: processGroup.description,
|
||||
messages: processGroup.messages,
|
||||
};
|
||||
if (mode === 'new') {
|
||||
if (parentGroupId) {
|
||||
newProcessGroupId = `${parentGroupId}/${processGroup.id}`;
|
||||
}
|
||||
Object.assign(postBody, {
|
||||
id: parentGroupId
|
||||
? `${parentGroupId}/${processGroup.id}`
|
||||
: `${processGroup.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path,
|
||||
successCallback: handleProcessGroupUpdateResponse,
|
||||
httpMethod,
|
||||
postBody,
|
||||
});
|
||||
};
|
||||
|
||||
const updateProcessGroup = (newValues: any) => {
|
||||
const processGroupToCopy = {
|
||||
...processGroup,
|
||||
};
|
||||
Object.assign(processGroupToCopy, newValues);
|
||||
setProcessGroup(processGroupToCopy);
|
||||
};
|
||||
|
||||
const onDisplayNameChanged = (newDisplayName: any) => {
|
||||
setDisplayNameInvalid(false);
|
||||
const updateDict = { display_name: newDisplayName };
|
||||
if (!idHasBeenUpdatedByUser && mode === 'new') {
|
||||
Object.assign(updateDict, { id: slugifyString(newDisplayName) });
|
||||
}
|
||||
updateProcessGroup(updateDict);
|
||||
};
|
||||
|
||||
const formElements = () => {
|
||||
const textInputs = [
|
||||
<TextField
|
||||
id="process-group-display-name"
|
||||
data-qa="process-group-display-name-input"
|
||||
name="display_name"
|
||||
error={displayNameInvalid}
|
||||
helperText={displayNameInvalid ? 'Display Name is required.' : ''}
|
||||
label="Display Name*"
|
||||
value={processGroup.display_name}
|
||||
onChange={(event: any) => onDisplayNameChanged(event.target.value)}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (mode === 'new') {
|
||||
textInputs.push(
|
||||
<TextField
|
||||
id="process-group-identifier"
|
||||
name="id"
|
||||
error={identifierInvalid}
|
||||
helperText={
|
||||
identifierInvalid
|
||||
? 'Identifier is required and must be all lowercase characters and hyphens.'
|
||||
: ''
|
||||
}
|
||||
label="Identifier*"
|
||||
value={processGroup.id}
|
||||
onChange={(event: any) => {
|
||||
updateProcessGroup({ id: event.target.value });
|
||||
// was invalid, and now valid
|
||||
if (identifierInvalid && hasValidIdentifier(event.target.value)) {
|
||||
setIdentifierInvalid(false);
|
||||
}
|
||||
setIdHasBeenUpdatedByUser(true);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
textInputs.push(
|
||||
<TextField
|
||||
id="process-group-description"
|
||||
name="description"
|
||||
label="Description"
|
||||
multiline
|
||||
value={processGroup.description}
|
||||
onChange={(event: any) =>
|
||||
updateProcessGroup({ description: event.target.value })
|
||||
}
|
||||
/>,
|
||||
);
|
||||
return textInputs;
|
||||
};
|
||||
|
||||
const formButtons = () => {
|
||||
return (
|
||||
<Button type="submit" variant="contained">
|
||||
Submit
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmission}>
|
||||
<Stack spacing={2}>
|
||||
{formElements()}
|
||||
{formButtons()}
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -267,7 +267,7 @@ function SideNav({
|
||||
sx={{ ml: isCollapsed ? 'auto' : 0 }}
|
||||
>
|
||||
{isMobile ? <CloseIcon /> : collapseOrExpandIcon}
|
||||
</IconButton>{' '}
|
||||
</IconButton>
|
||||
</Box>
|
||||
<List>
|
||||
{navItems.map((item) => (
|
||||
|
@ -33,6 +33,8 @@ import ProcessInstanceList from './ProcessInstanceList'; // Import the new compo
|
||||
import { UiSchemaUxElement } from '../extension_ui_schema_interfaces';
|
||||
import { extensionUxElementMap } from '../components/ExtensionUxElementForDisplay';
|
||||
import Extension from './Extension';
|
||||
import ProcessGroupNew from './ProcessGroupNew';
|
||||
import ProcessGroupEdit from './ProcessGroupEdit';
|
||||
|
||||
type OwnProps = {
|
||||
setAdditionalNavElement: Function;
|
||||
@ -181,6 +183,11 @@ export default function BaseRoutes({
|
||||
path="/process-instances"
|
||||
element={<ProcessInstanceList variant="all" />}
|
||||
/>
|
||||
<Route path="/process-groups/new" element={<ProcessGroupNew />} />
|
||||
<Route
|
||||
path="/process-groups/:process_group_id/edit"
|
||||
element={<ProcessGroupEdit />}
|
||||
/>
|
||||
</Routes>
|
||||
</Box>
|
||||
);
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ProcessGroupForm from '../components/ProcessGroupForm';
|
||||
import { ProcessGroup } from '../interfaces';
|
||||
import { setPageTitle } from '../helpers';
|
||||
|
||||
export default function ProcessGroupEdit() {
|
||||
const params = useParams();
|
||||
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const setProcessGroupsFromResult = (result: any) => {
|
||||
setProcessGroup(result);
|
||||
};
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-groups/${params.process_group_id}`,
|
||||
successCallback: setProcessGroupsFromResult,
|
||||
});
|
||||
}, [params.process_group_id]);
|
||||
|
||||
if (processGroup) {
|
||||
setPageTitle([`Editing ${processGroup.display_name}`]);
|
||||
return (
|
||||
<>
|
||||
<ProcessBreadcrumb
|
||||
hotCrumbs={[
|
||||
['Process Groups', '/process-groups'],
|
||||
{
|
||||
entityToExplode: processGroup,
|
||||
entityType: 'process-group',
|
||||
linkLastItem: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<h1>Edit Process Group: {(processGroup as any).id}</h1>
|
||||
<ProcessGroupForm
|
||||
mode="edit"
|
||||
processGroup={processGroup}
|
||||
setProcessGroup={setProcessGroup}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import ProcessGroupForm from '../components/ProcessGroupForm';
|
||||
import { ProcessGroup, HotCrumbItem } from '../interfaces';
|
||||
import { setPageTitle } from '../helpers';
|
||||
|
||||
export default function ProcessGroupNew() {
|
||||
const searchParams = new URLSearchParams(document.location.search);
|
||||
const parentGroupId = searchParams.get('parentGroupId');
|
||||
const [processGroup, setProcessGroup] = useState<ProcessGroup>({
|
||||
id: '',
|
||||
display_name: '',
|
||||
description: '',
|
||||
});
|
||||
setPageTitle(['New Process Group']);
|
||||
|
||||
const hotCrumbs: HotCrumbItem[] = [['Process Groups', '/process-groups']];
|
||||
if (parentGroupId) {
|
||||
hotCrumbs.push({
|
||||
entityToExplode: parentGroupId,
|
||||
entityType: 'process-group-id',
|
||||
linkLastItem: true,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProcessBreadcrumb hotCrumbs={hotCrumbs} />
|
||||
<h1>Add Process Group</h1>
|
||||
<ProcessGroupForm
|
||||
mode="new"
|
||||
processGroup={processGroup}
|
||||
setProcessGroup={setProcessGroup}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -4,13 +4,16 @@ import {
|
||||
AccordionSummary,
|
||||
Box,
|
||||
Container,
|
||||
IconButton,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Can } from '@casl/react';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import StarRateIcon from '@mui/icons-material/StarRate';
|
||||
import { Delete, Edit, Add } from '@mui/icons-material';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import useProcessGroups from '../../hooks/useProcessGroups';
|
||||
@ -25,11 +28,16 @@ import {
|
||||
import SpiffBreadCrumbs, { Crumb, SPIFF_ID } from './SpiffBreadCrumbs';
|
||||
import { modifyProcessIdentifierForPathParam } from '../../../helpers';
|
||||
import {
|
||||
PermissionsToCheck,
|
||||
ProcessGroup,
|
||||
ProcessGroupLite,
|
||||
ProcessModelAction,
|
||||
} from '../../interfaces';
|
||||
import { unModifyProcessIdentifierForPathParam } from '../../helpers';
|
||||
import { useUriListForPermissions } from '../../hooks/UriListForPermissions';
|
||||
import { usePermissionFetcher } from '../../hooks/PermissionService';
|
||||
import ButtonWithConfirmation from '../../components/ButtonWithConfirmation';
|
||||
import HttpService from '../../services/HttpService';
|
||||
|
||||
type OwnProps = {
|
||||
setNavElementCallback?: Function;
|
||||
@ -76,7 +84,10 @@ export default function ProcessModelTreePage({
|
||||
// On load, there are always groups and never models, expand accordingly.
|
||||
const [groupsExpanded, setGroupsExpanded] = useState(true);
|
||||
const [modelsExpanded, setModelsExpanded] = useState(false);
|
||||
const [lastSelected, setLastSelected] = useState<Record<string, any>>({});
|
||||
const [currentProcessGroup, setCurrentProcessGroup] = useState<Record<
|
||||
string,
|
||||
any
|
||||
> | null>(null);
|
||||
const [crumbs, setCrumbs] = useState<Crumb[]>([]);
|
||||
// const [treeCollapsed, setTreeCollapsed] = useState(false);
|
||||
const [treeCollapsed] = useState(false);
|
||||
@ -92,6 +103,15 @@ export default function ProcessModelTreePage({
|
||||
gridGap: 20,
|
||||
};
|
||||
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const permissionRequestData: PermissionsToCheck = {
|
||||
[targetUris.dataStoreListPath]: ['POST'],
|
||||
[targetUris.processGroupListPath]: ['POST'],
|
||||
[targetUris.processGroupShowPath]: ['PUT', 'DELETE'],
|
||||
[targetUris.processModelCreatePath]: ['POST'],
|
||||
};
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
const processCrumbs = (
|
||||
item: Record<string, any>,
|
||||
flattened: Record<string, any>,
|
||||
@ -116,7 +136,7 @@ export default function ProcessModelTreePage({
|
||||
|
||||
const handleClickStream = (item: Record<string, any>) => {
|
||||
setCrumbs(processCrumbs(item, flatItems));
|
||||
setLastSelected(item);
|
||||
setCurrentProcessGroup(item);
|
||||
let itemToUse: any = { ...item };
|
||||
// Duck type to find out if this is a model ore a group.
|
||||
// If model, we want its parent group, which can be found in the id.
|
||||
@ -233,6 +253,7 @@ export default function ProcessModelTreePage({
|
||||
setModelsExpanded(false);
|
||||
setGroupsExpanded(true);
|
||||
setCrumbs([]);
|
||||
setCurrentProcessGroup(null);
|
||||
treeRef.current?.clearExpanded();
|
||||
navigate('/newui/process-groups');
|
||||
return;
|
||||
@ -242,6 +263,7 @@ export default function ProcessModelTreePage({
|
||||
if (found) {
|
||||
clickStream.next(found);
|
||||
const processEntityId = modifyProcessIdentifierForPathParam(crumb.id);
|
||||
setCurrentProcessGroup(null);
|
||||
navigate(`/newui/process-groups/${processEntityId}`);
|
||||
}
|
||||
};
|
||||
@ -324,6 +346,7 @@ export default function ProcessModelTreePage({
|
||||
setGroups(foundProcessGroup.process_groups || null);
|
||||
setModels(foundProcessGroup.process_models || []);
|
||||
setCrumbs(processCrumbs(foundProcessGroup, flattened));
|
||||
setCurrentProcessGroup(foundProcessGroup);
|
||||
} else {
|
||||
setGroups(processGroupsLite);
|
||||
setCrumbs([]);
|
||||
@ -351,6 +374,29 @@ export default function ProcessModelTreePage({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [clickStream]);
|
||||
|
||||
const deleteProcessGroup = () => {
|
||||
if (currentProcessGroup) {
|
||||
const modifiedGroupId = modifyProcessIdentifierForPathParam(
|
||||
currentProcessGroup.id,
|
||||
);
|
||||
let parendGroupId = modifiedGroupId.replace(/:[^:]+$/, '');
|
||||
// this means it was a root group
|
||||
if (parendGroupId === modifiedGroupId) {
|
||||
parendGroupId = SPIFF_ID;
|
||||
}
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-groups/${modifiedGroupId}`,
|
||||
successCallback: () => {
|
||||
handleCrumbClick({
|
||||
id: parendGroupId.replaceAll(':', '/'),
|
||||
displayName: 'NOT_USED',
|
||||
});
|
||||
},
|
||||
httpMethod: 'DELETE',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h1" sx={{ mb: 2 }}>
|
||||
@ -429,6 +475,57 @@ export default function ProcessModelTreePage({
|
||||
)}
|
||||
<SpiffBreadCrumbs crumbs={crumbs} callback={handleCrumbClick} />
|
||||
</Stack>
|
||||
{currentProcessGroup ? (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
width: '100%',
|
||||
paddingBottom: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h3" className="with-icons">
|
||||
Process Group: {currentProcessGroup.display_name}
|
||||
</Typography>
|
||||
<Can
|
||||
I="PUT"
|
||||
a={targetUris.processGroupShowPath}
|
||||
ability={ability}
|
||||
>
|
||||
<IconButton
|
||||
data-qa="edit-process-group-button"
|
||||
href={`/newui/process-groups/${modifyProcessIdentifierForPathParam(currentProcessGroup.id)}/edit`}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</Can>
|
||||
<Can
|
||||
I="DELETE"
|
||||
a={targetUris.processGroupShowPath}
|
||||
ability={ability}
|
||||
>
|
||||
<ButtonWithConfirmation
|
||||
data-qa="delete-process-group-button"
|
||||
renderIcon={<Delete />}
|
||||
iconDescription="Delete Process Group"
|
||||
hasIconOnly
|
||||
description={`Delete process group: ${currentProcessGroup.display_name}`}
|
||||
onConfirmation={deleteProcessGroup}
|
||||
confirmButtonLabel="Delete"
|
||||
/>
|
||||
</Can>
|
||||
</Stack>
|
||||
) : null}
|
||||
{currentProcessGroup ? (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
width: '100%',
|
||||
paddingBottom: 2,
|
||||
}}
|
||||
>
|
||||
{currentProcessGroup.description}
|
||||
</Stack>
|
||||
) : null}
|
||||
<Accordion
|
||||
expanded={modelsExpanded}
|
||||
onChange={() => setModelsExpanded((prev) => !prev)}
|
||||
@ -438,6 +535,12 @@ export default function ProcessModelTreePage({
|
||||
aria-controls="Process Models Accordion"
|
||||
>
|
||||
({models.length}) Process Models
|
||||
<IconButton
|
||||
data-qa="add-process-model-button"
|
||||
href={`/newui/process-models/${modifyProcessIdentifierForPathParam(currentProcessGroup ? currentProcessGroup.id : '')}/new`}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box sx={gridProps}>
|
||||
@ -446,7 +549,7 @@ export default function ProcessModelTreePage({
|
||||
key={model.id}
|
||||
model={model}
|
||||
stream={clickStream}
|
||||
lastSelected={lastSelected}
|
||||
lastSelected={currentProcessGroup || {}}
|
||||
processModelAction={processModelAction}
|
||||
onStartProcess={() => {
|
||||
if (setNavElementCallback) {
|
||||
@ -473,6 +576,12 @@ export default function ProcessModelTreePage({
|
||||
aria-controls="Process Groups Accordion"
|
||||
>
|
||||
({groups?.length}) Process Groups
|
||||
<IconButton
|
||||
data-qa="add-process-group-button"
|
||||
href={`/newui/process-groups/new${currentProcessGroup ? `?parentGroupId=${currentProcessGroup.id}` : ''}`}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box sx={gridProps}>
|
||||
|
Loading…
x
Reference in New Issue
Block a user