diff --git a/.gitignore b/.gitignore index 24a0ada5c..22f7178f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ pyrightconfig.json .idea/ t +*~ .dccache *~ \ No newline at end of file diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 373f18319..7b97781e6 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -2089,6 +2089,37 @@ paths: schema: $ref: "#/components/schemas/Secret" + /connector-proxy/type-ahead/{category}: + parameters: + - name: category + in: path + required: true + description: The category for the type-ahead search + schema: + type: string + - name: prefix + in: query + required: true + description: The prefix to search for + schema: + type: string + - name: limit + in: query + required: true + description: The maximum number of search results + schema: + type: integer + get: + operationId: spiffworkflow_backend.routes.connector_proxy_controller.type_ahead + summary: Return type ahead search results + tags: + - Type Ahead + responses: + "200": + description: We return type ahead search results + #content: + # - application/json + components: securitySchemes: jwt: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py index 4ba0efd99..dec4c444a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py @@ -41,6 +41,10 @@ SPIFFWORKFLOW_BACKEND_URL = environ.get("SPIFFWORKFLOW_BACKEND_URL", default="ht SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_URL = environ.get( "SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_URL", default="http://localhost:7004" ) +SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_TYPE_AHEAD_URL = environ.get( + "SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_TYPE_AHEAD_URL", + default="https://emehvlxpwodjawtgi7ctkbvpse0vmaow.lambda-url.us-east-1.on.aws", +) # Open ID server # use "http://localhost:7000/openid" for running with simple openid diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/connector_proxy_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/connector_proxy_controller.py new file mode 100644 index 000000000..45c0bd28e --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/connector_proxy_controller.py @@ -0,0 +1,25 @@ +from typing import Any + +import flask.wrappers +import requests +from flask import current_app +from flask.wrappers import Response + + +def connector_proxy_type_ahead_url() -> Any: + """Returns the connector proxy type ahead url.""" + return current_app.config["SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_TYPE_AHEAD_URL"] + + +def type_ahead(category: str, prefix: str, limit: int) -> flask.wrappers.Response: + url = f"{connector_proxy_type_ahead_url()}/v1/type-ahead/{category}?prefix={prefix}&limit={limit}" + + proxy_response = requests.get(url) + status = proxy_response.status_code + if status // 100 == 2: + response = proxy_response.text + else: + # supress pop up errors on the client + status = 200 + response = "[]" + return Response(response, status=status, mimetype="application/json") diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index d343d1a1f..863ee5f3d 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import validator from '@rjsf/validator-ajv8'; @@ -8,6 +8,7 @@ import { Tabs, Grid, Column, + ComboBox, Button, ButtonSet, // @ts-ignore @@ -22,6 +23,73 @@ import { modifyProcessIdentifierForPathParam } from '../helpers'; import { ProcessInstanceTask } from '../interfaces'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; +// TODO: move this somewhere else +function TypeAheadWidget({ + id, + onChange, + options: { category, itemFormat }, +}: { + id: string; + onChange: any; + options: any; +}) { + const pathForCategory = (inputText: string) => { + return `/connector-proxy/type-ahead/${category}?prefix=${inputText}&limit=100`; + }; + + const lastSearchTerm = useRef(''); + const [items, setItems] = useState([]); + const [selectedItem, setSelectedItem] = useState(null); + const itemFormatRegex = /[^{}]+(?=})/g; + const itemFormatSubstitutions = itemFormat.match(itemFormatRegex); + + const itemToString = (item: any) => { + if (!item) { + return null; + } + + let str = itemFormat; + itemFormatSubstitutions.forEach((key: string) => { + str = str.replace(`{${key}}`, item[key]); + }); + return str; + }; + + const handleTypeAheadResult = (result: any, inputText: string) => { + if (lastSearchTerm.current === inputText) { + setItems(result); + } + }; + + const typeAheadSearch = (inputText: string) => { + if (inputText) { + lastSearchTerm.current = inputText; + // TODO: check cache of prefixes -> results + HttpService.makeCallToBackend({ + path: pathForCategory(inputText), + successCallback: (result: any) => + handleTypeAheadResult(result, inputText), + }); + } + }; + + return ( + { + setSelectedItem(event.selectedItem); + onChange(itemToString(event.selectedItem)); + }} + id={id} + items={items} + itemToString={itemToString} + placeholder={`Start typing to search for ${category}...`} + titleText={`Type ahead search for ${category}`} + selectedItem={selectedItem} + /> + ); +} + class UnexpectedHumanTaskType extends Error { constructor(message: string) { super(message); @@ -294,6 +362,8 @@ export default function TaskShow() { return getFieldsWithDateValidations(jsonSchema, formData, errors); }; + const widgets = { typeAhead: TypeAheadWidget }; + return ( @@ -303,6 +373,7 @@ export default function TaskShow() { onSubmit={handleFormSubmit} schema={jsonSchema} uiSchema={formUiSchema} + widgets={widgets} validator={validator} onChange={updateFormData} customValidate={customValidate}