some more updates for group forms w/ burnettk

This commit is contained in:
jasquat 2022-11-03 15:55:50 -04:00
parent f0cc086e09
commit 7fffa650b0
12 changed files with 266 additions and 166 deletions

View File

@ -153,7 +153,6 @@ paths:
description: The number of groups to show per page. Defaults to page 10.
schema:
type: integer
# process_groups_list
get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_groups_list
summary: get list
@ -168,7 +167,6 @@ paths:
type: array
items:
$ref: "#/components/schemas/ProcessModelCategory"
# process_group_add
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_add
summary: Add process group

View File

@ -1,5 +1,6 @@
"""Process_group."""
from __future__ import annotations
import dataclasses
from dataclasses import dataclass
from dataclasses import field
@ -20,6 +21,7 @@ class ProcessGroup:
id: str # A unique string name, lower case, under scores (ie, 'my_group')
display_name: str
description: str | None = None
display_order: int | None = 0
admin: bool | None = False
process_models: list[ProcessModelInfo] = field(
@ -38,6 +40,11 @@ class ProcessGroup:
return True
return False
@property
def serialized(self) -> dict:
original_dict = dataclasses.asdict(self)
return {x: original_dict[x] for x in original_dict if x not in ["sort_index"]}
class ProcessGroupSchema(Schema):
"""ProcessGroupSchema."""

View File

@ -43,7 +43,7 @@ from spiffworkflow_backend.models.message_triggerable_process_model import (
MessageTriggerableProcessModel,
)
from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.process_group import ProcessGroupSchema
from spiffworkflow_backend.models.process_group import ProcessGroup, ProcessGroupSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
@ -135,17 +135,13 @@ def permissions_check(body: Dict[str, Dict[str, list[str]]]) -> flask.wrappers.R
def process_group_add(
body: Dict[str, Union[str, bool, int]]
body: dict
) -> flask.wrappers.Response:
"""Add_process_group."""
process_model_service = ProcessModelService()
process_group = ProcessGroupSchema().load(body)
process_group = ProcessGroup(**body)
process_model_service.add_process_group(process_group)
return Response(
json.dumps(ProcessGroupSchema().dump(process_group)),
status=201,
mimetype="application/json",
)
return make_response(jsonify(process_group), 201)
def process_group_delete(process_group_id: str) -> flask.wrappers.Response:
@ -155,12 +151,12 @@ def process_group_delete(process_group_id: str) -> flask.wrappers.Response:
def process_group_update(
process_group_id: str, body: Dict[str, Union[str, bool, int]]
) -> Dict[str, Union[str, bool, int]]:
process_group_id: str, body: dict
) -> flask.wrappers.Response:
"""Process Group Update."""
process_group = ProcessGroupSchema().load(body)
process_group = ProcessGroup(id=process_group_id, **body)
ProcessModelService().update_process_group(process_group)
return ProcessGroupSchema().dump(process_group) # type: ignore
return make_response(jsonify(process_group), 200)
def process_groups_list(page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
@ -173,6 +169,7 @@ def process_groups_list(page: int = 1, per_page: int = 100) -> flask.wrappers.Re
remainder = len(process_groups) % per_page
if remainder > 0:
pages += 1
response_json = {
"results": ProcessGroupSchema(many=True).dump(batch),
"pagination": {
@ -198,7 +195,7 @@ def process_group_show(
status_code=400,
)
) from exception
return ProcessGroupSchema().dump(process_group)
return make_response(jsonify(process_group), 200)
def process_model_add(

View File

@ -1,4 +1,5 @@
"""Process_model_service."""
import dataclasses
import json
import os
import shutil
@ -170,7 +171,7 @@ class ProcessModelService(FileSystemService):
json_path = os.path.join(cat_path, self.CAT_JSON_FILE)
with open(json_path, "w") as cat_json:
json.dump(
self.GROUP_SCHEMA.dump(process_group),
process_group.serialized,
cat_json,
indent=4,
sort_keys=True,

View File

@ -1,4 +1,5 @@
"""Test Process Api Blueprint."""
import dataclasses
import io
import json
import time
@ -8,6 +9,7 @@ import pytest
from flask.app import Flask
from flask.testing import FlaskClient
from flask_bpmn.models.db import db
from spiffworkflow_backend import MyJSONEncoder
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
@ -17,7 +19,7 @@ from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_group import ProcessGroupSchema
# from spiffworkflow_backend.models.process_group import ProcessGroupSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance_report import (
@ -388,25 +390,30 @@ class TestProcessApi(BaseTest):
display_name="Another Test Category",
display_order=0,
admin=False,
description="Test Description"
)
response = client.post(
"/v1.0/process-groups",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
data=json.dumps(ProcessGroupSchema().dump(process_group)),
data=json.dumps(process_group.serialized),
)
assert response.status_code == 201
assert response.json
# Check what is returned
result = ProcessGroupSchema().loads(response.get_data(as_text=True))
result = ProcessGroup(**response.json)
assert result is not None
assert result.display_name == "Another Test Category"
assert result.id == "test"
assert result.description == "Test Description"
# Check what is persisted
persisted = ProcessModelService().get_process_group("test")
assert persisted.display_name == "Another Test Category"
assert persisted.id == "test"
assert persisted.description == "Test Description"
def test_process_group_delete(
self,

View File

@ -41,23 +41,35 @@ export default function ButtonWithConfirmation({
const confirmationDialog = () => {
return (
<Modal
show={showConfirmationPrompt}
onHide={handleConfirmationPromptCancel}
>
<Modal.Header closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
{modalBodyElement()}
<Modal.Footer>
<Button variant="secondary" onClick={handleConfirmationPromptCancel}>
Cancel
</Button>
<Button variant="primary" onClick={handleConfirmation}>
{confirmButtonLabel}
</Button>
</Modal.Footer>
</Modal>
open={showConfirmationPrompt}
danger
modalHeading={description}
modalLabel={title}
primaryButtonText={confirmButtonLabel}
secondaryButtonText="Cancel"
onSecondarySubmit={handleConfirmationPromptCancel}
onRequestSubmit={handleConfirmation}
/>
);
// return (
// <Modal
// show={showConfirmationPrompt}
// onHide={handleConfirmationPromptCancel}
// >
// <Modal.Header closeButton>
// <Modal.Title>{title}</Modal.Title>
// </Modal.Header>
// {modalBodyElement()}
// <Modal.Footer>
// <Button variant="secondary" onClick={handleConfirmationPromptCancel}>
// Cancel
// </Button>
// <Button variant="primary" onClick={handleConfirmation}>
// {confirmButtonLabel}
// </Button>
// </Modal.Footer>
// </Modal>
// );
};
return (

View File

@ -0,0 +1,178 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
// @ts-ignore
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
import { slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
import { ProcessGroup } from '../interfaces';
import ButtonWithConfirmation from './ButtonWithConfirmation';
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();
const navigateToProcessGroup = (_result: any) => {
if (processGroup) {
navigate(`/admin/process-groups/${processGroup.id}`);
}
};
const navigateToProcessGroups = (_result: any) => {
navigate(`/admin/process-groups`);
};
const hasValidIdentifier = (identifierToCheck: string) => {
return identifierToCheck.match(/^[a-z0-9][0-9a-z-]+[a-z0-9]$/);
};
const deleteProcessGroup = () => {
HttpService.makeCallToBackend({
path: `/process-groups/${processGroup.id}`,
successCallback: navigateToProcessGroups,
httpMethod: 'DELETE',
});
};
const handleFormSubmission = (event: any) => {
event.preventDefault();
let hasErrors = false;
if (!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/${processGroup.id}`;
}
let httpMethod = 'POST';
if (mode === 'edit') {
httpMethod = 'PUT';
}
const postBody = {
display_name: processGroup.display_name,
description: processGroup.description,
};
if (mode === 'new') {
Object.assign(postBody, { id: processGroup.id });
}
HttpService.makeCallToBackend({
path,
successCallback: navigateToProcessGroup,
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) {
Object.assign(updateDict, { id: slugifyString(newDisplayName) });
}
updateProcessGroup(updateDict);
};
const formElements = () => {
const textInputs = [
<TextInput
id="process-group-display-name"
invalidText="Display Name is required."
invalid={displayNameInvalid}
labelText="Display Name*"
value={processGroup.display_name}
onChange={(event: any) => onDisplayNameChanged(event.target.value)}
onBlur={(event: any) => console.log('event', event)}
/>,
];
if (mode === 'new') {
textInputs.push(
<TextInput
id="process-group-identifier"
invalidText="Identifier is required and must be all lowercase characters and hyphens."
invalid={identifierInvalid}
labelText="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(
<TextInput
id="process-group-description"
labelText="Description"
value={processGroup.description}
onChange={(event: any) =>
updateProcessGroup({ description: event.target.value })
}
/>
);
return textInputs;
};
const formButtons = () => {
const buttons = [
<Button kind="secondary" type="submit">
Submit
</Button>,
];
if (mode === 'edit') {
buttons.push(
<ButtonWithConfirmation
description={`Delete Process Group ${processGroup.id}?`}
onConfirmation={deleteProcessGroup}
buttonLabel="Delete"
confirmButtonLabel="Delete"
/>
);
}
return <ButtonSet>{buttons}</ButtonSet>;
};
return (
<Form onSubmit={handleFormSubmission}>
<Stack gap={5}>
{formElements()}
{formButtons()}
</Stack>
</Form>
);
}

View File

@ -9,12 +9,14 @@ type OwnProps = {
onChange: (..._args: any[]) => any;
processModels: ProcessModel[];
selectedItem?: ProcessModel | null;
titleText?: string;
};
export default function ProcessModelSearch({
processModels,
selectedItem,
onChange,
titleText = 'Process model',
}: OwnProps) {
const shouldFilterProcessModel = (options: any) => {
const processModel: ProcessModel = options.item;
@ -38,7 +40,7 @@ export default function ProcessModelSearch({
}}
shouldFilterItem={shouldFilterProcessModel}
placeholder="Choose a process model"
titleText="Process model"
titleText={titleText}
selectedItem={selectedItem}
/>
);

View File

@ -14,6 +14,7 @@ export interface RecentProcessModel {
export interface ProcessGroup {
id: string;
display_name: string;
description?: string | null;
}
export interface ProcessModel {

View File

@ -1,21 +1,18 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams } from 'react-router-dom';
// @ts-ignore
import { Button, Stack } from '@carbon/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessGroupForm from '../components/ProcessGroupForm';
import { ProcessGroup } from '../interfaces';
export default function ProcessGroupEdit() {
const [displayName, setDisplayName] = useState('');
const params = useParams();
const navigate = useNavigate();
const [processGroup, setProcessGroup] = useState(null);
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
useEffect(() => {
const setProcessGroupsFromResult = (result: any) => {
setProcessGroup(result);
setDisplayName(result.display_name);
};
HttpService.makeCallToBackend({
@ -24,69 +21,16 @@ export default function ProcessGroupEdit() {
});
}, [params]);
const navigateToProcessGroup = (_result: any) => {
navigate(`/admin/process-groups/${(processGroup as any).id}`);
};
const navigateToProcessGroups = (_result: any) => {
navigate(`/admin/process-groups`);
};
const updateProcessGroup = (event: any) => {
event.preventDefault();
HttpService.makeCallToBackend({
path: `/process-groups/${(processGroup as any).id}`,
successCallback: navigateToProcessGroup,
httpMethod: 'PUT',
postBody: {
display_name: displayName,
id: (processGroup as any).id,
},
});
};
const deleteProcessGroup = () => {
HttpService.makeCallToBackend({
path: `/process-groups/${(processGroup as any).id}`,
successCallback: navigateToProcessGroups,
httpMethod: 'DELETE',
});
};
const onDisplayNameChanged = (newDisplayName: any) => {
setDisplayName(newDisplayName);
};
if (processGroup) {
return (
<>
<ProcessBreadcrumb processGroupId={(processGroup as any).id} />
<h2>Edit Process Group: {(processGroup as any).id}</h2>
<form onSubmit={updateProcessGroup}>
<label>Display Name:</label>
<input
name="display_name"
type="text"
value={displayName}
onChange={(e) => onDisplayNameChanged(e.target.value)}
/>
<br />
<br />
<Stack direction="horizontal" gap={3}>
<Button type="submit">Submit</Button>
<Button
variant="secondary"
href={`/admin/process-groups/${(processGroup as any).id}`}
>
Cancel
</Button>
<ButtonWithConfirmation
description={`Delete Process Group ${(processGroup as any).id}?`}
onConfirmation={deleteProcessGroup}
buttonLabel="Delete"
/>
</Stack>
</form>
<ProcessGroupForm
mode="edit"
processGroup={processGroup}
setProcessGroup={setProcessGroup}
/>
</>
);
}

View File

@ -1,15 +1,12 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
// @ts-ignore
import { Button, Form, Table } from '@carbon/react';
import { InputGroup } from 'react-bootstrap';
import { Typeahead } from 'react-bootstrap-typeahead';
import { Option } from 'react-bootstrap-typeahead/types/types';
import { Button, Table } from '@carbon/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import PaginationForTable from '../components/PaginationForTable';
import HttpService from '../services/HttpService';
import { getPageInfoFromSearchParams } from '../helpers';
import { CarbonComboBoxSelection, ProcessModel } from '../interfaces';
import { CarbonComboBoxSelection } from '../interfaces';
import ProcessModelSearch from '../components/ProcessModelSearch';
// Example process group json
@ -111,6 +108,7 @@ export default function ProcessGroupList() {
<ProcessModelSearch
onChange={processModelSearchOnChange}
processModels={processModelAvailableItems}
titleText="Process model search"
/>
);
};
@ -119,7 +117,9 @@ export default function ProcessGroupList() {
return (
<>
<ProcessBreadcrumb hotCrumbs={[['Process Groups']]} />
<Button href="/admin/process-groups/new">Add a process group</Button>
<Button kind="secondary" href="/admin/process-groups/new">
Add a process group
</Button>
<br />
<br />
{processModelSearchArea()}

View File

@ -1,71 +1,24 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
import ProcessGroupForm from '../components/ProcessGroupForm';
import { ProcessGroup } from '../interfaces';
export default function ProcessGroupNew() {
const [identifier, setIdentifier] = useState('');
const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = useState(false);
const [displayName, setDisplayName] = useState('');
const navigate = useNavigate();
const navigateToProcessGroup = (_result: any) => {
navigate(`/admin/process-groups/${identifier}`);
};
const addProcessGroup = (event: any) => {
event.preventDefault();
HttpService.makeCallToBackend({
path: `/process-groups`,
successCallback: navigateToProcessGroup,
httpMethod: 'POST',
postBody: {
id: identifier,
display_name: displayName,
},
});
};
const onDisplayNameChanged = (newDisplayName: any) => {
setDisplayName(newDisplayName);
if (!idHasBeenUpdatedByUser) {
setIdentifier(slugifyString(newDisplayName));
}
};
const [processGroup, setProcessGroup] = useState<ProcessGroup>({
id: '',
display_name: '',
description: '',
});
return (
<>
<ProcessBreadcrumb />
<h2>Add Process Group</h2>
<Form onSubmit={addProcessGroup}>
<Form.Group className="mb-3" controlId="display_name">
<Form.Label>Display Name:</Form.Label>
<Form.Control
type="text"
name="display_name"
value={displayName}
onChange={(e) => onDisplayNameChanged(e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="identifier">
<Form.Label>ID:</Form.Label>
<Form.Control
type="text"
name="id"
value={identifier}
onChange={(e) => {
setIdentifier(e.target.value);
setIdHasBeenUpdatedByUser(true);
}}
/>
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
<ProcessGroupForm
mode="new"
processGroup={processGroup}
setProcessGroup={setProcessGroup}
/>
</>
);
}