From 7fffa650b067df1a978164a8922499faccb37809 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 3 Nov 2022 15:55:50 -0400 Subject: [PATCH] some more updates for group forms w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 2 - .../models/process_group.py | 7 + .../routes/process_api_blueprint.py | 23 +-- .../services/process_model_service.py | 3 +- .../integration/test_process_api.py | 13 +- .../src/components/ButtonWithConfirmation.tsx | 44 +++-- .../src/components/ProcessGroupForm.tsx | 178 ++++++++++++++++++ .../src/components/ProcessModelSearch.tsx | 4 +- spiffworkflow-frontend/src/interfaces.ts | 1 + .../src/routes/ProcessGroupEdit.tsx | 74 +------- .../src/routes/ProcessGroupList.tsx | 12 +- .../src/routes/ProcessGroupNew.tsx | 71 ++----- 12 files changed, 266 insertions(+), 166 deletions(-) create mode 100644 spiffworkflow-frontend/src/components/ProcessGroupForm.tsx diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index de10f598c..2ca57999e 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -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 diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py index 0b100ed45..549ab0082 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_group.py @@ -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.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index fd20341c7..63c717d17 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -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( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py index 57d842292..7789493f3 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py @@ -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, diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 9a923b97b..6fe38f403 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -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, diff --git a/spiffworkflow-frontend/src/components/ButtonWithConfirmation.tsx b/spiffworkflow-frontend/src/components/ButtonWithConfirmation.tsx index 663bae1e2..f8e8bf77d 100644 --- a/spiffworkflow-frontend/src/components/ButtonWithConfirmation.tsx +++ b/spiffworkflow-frontend/src/components/ButtonWithConfirmation.tsx @@ -41,23 +41,35 @@ export default function ButtonWithConfirmation({ const confirmationDialog = () => { return ( - - {title} - - {modalBodyElement()} - - - - - + open={showConfirmationPrompt} + danger + modalHeading={description} + modalLabel={title} + primaryButtonText={confirmButtonLabel} + secondaryButtonText="Cancel" + onSecondarySubmit={handleConfirmationPromptCancel} + onRequestSubmit={handleConfirmation} + /> ); + // return ( + // + // + // {title} + // + // {modalBodyElement()} + // + // + // + // + // + // ); }; return ( diff --git a/spiffworkflow-frontend/src/components/ProcessGroupForm.tsx b/spiffworkflow-frontend/src/components/ProcessGroupForm.tsx new file mode 100644 index 000000000..21cabc93e --- /dev/null +++ b/spiffworkflow-frontend/src/components/ProcessGroupForm.tsx @@ -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(false); + const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = + useState(false); + const [displayNameInvalid, setDisplayNameInvalid] = useState(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 = [ + onDisplayNameChanged(event.target.value)} + onBlur={(event: any) => console.log('event', event)} + />, + ]; + + if (mode === 'new') { + textInputs.push( + { + updateProcessGroup({ id: event.target.value }); + // was invalid, and now valid + if (identifierInvalid && hasValidIdentifier(event.target.value)) { + setIdentifierInvalid(false); + } + setIdHasBeenUpdatedByUser(true); + }} + /> + ); + } + + textInputs.push( + + updateProcessGroup({ description: event.target.value }) + } + /> + ); + + return textInputs; + }; + + const formButtons = () => { + const buttons = [ + , + ]; + if (mode === 'edit') { + buttons.push( + + ); + } + return {buttons}; + }; + + return ( +
+ + {formElements()} + {formButtons()} + +
+ ); +} diff --git a/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx b/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx index 679831a53..1f138f6ea 100644 --- a/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx +++ b/spiffworkflow-frontend/src/components/ProcessModelSearch.tsx @@ -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} /> ); diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 08c8e4fa3..14ead3ba5 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -14,6 +14,7 @@ export interface RecentProcessModel { export interface ProcessGroup { id: string; display_name: string; + description?: string | null; } export interface ProcessModel { diff --git a/spiffworkflow-frontend/src/routes/ProcessGroupEdit.tsx b/spiffworkflow-frontend/src/routes/ProcessGroupEdit.tsx index 05f75fb1f..d624309cd 100644 --- a/spiffworkflow-frontend/src/routes/ProcessGroupEdit.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessGroupEdit.tsx @@ -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(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 ( <>

Edit Process Group: {(processGroup as any).id}

-
- - onDisplayNameChanged(e.target.value)} - /> -
-
- - - - - -
+ ); } diff --git a/spiffworkflow-frontend/src/routes/ProcessGroupList.tsx b/spiffworkflow-frontend/src/routes/ProcessGroupList.tsx index f9881deff..bb19e9393 100644 --- a/spiffworkflow-frontend/src/routes/ProcessGroupList.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessGroupList.tsx @@ -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() { ); }; @@ -119,7 +117,9 @@ export default function ProcessGroupList() { return ( <> - +

{processModelSearchArea()} diff --git a/spiffworkflow-frontend/src/routes/ProcessGroupNew.tsx b/spiffworkflow-frontend/src/routes/ProcessGroupNew.tsx index 2f3f3a357..d4d8b0387 100644 --- a/spiffworkflow-frontend/src/routes/ProcessGroupNew.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessGroupNew.tsx @@ -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({ + id: '', + display_name: '', + description: '', + }); return ( <>

Add Process Group

-
- - Display Name: - onDisplayNameChanged(e.target.value)} - /> - - - ID: - { - setIdentifier(e.target.value); - setIdHasBeenUpdatedByUser(true); - }} - /> - - -
+ ); }