Merge pull request #294 from sartography/feature/typeahead_in_custom_widgets

Feature/typeahead in custom widgets
This commit is contained in:
jasquat 2023-06-02 10:34:00 -04:00 committed by GitHub
commit 6eb0ab0286
61 changed files with 118 additions and 77 deletions

View File

@ -265,17 +265,18 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
"dateparser": dateparser, "dateparser": dateparser,
"datetime": datetime, "datetime": datetime,
"decimal": decimal, "decimal": decimal,
"dict": dict,
"enumerate": enumerate, "enumerate": enumerate,
"filter": filter, "filter": filter,
"format": format, "format": format,
"json": json,
"list": list, "list": list,
"dict": dict,
"map": map, "map": map,
"pytz": pytz, "pytz": pytz,
"set": set,
"sum": sum, "sum": sum,
"time": time, "time": time,
"timedelta": timedelta, "timedelta": timedelta,
"set": set,
} }
# This will overwrite the standard builtins # This will overwrite the standard builtins

View File

@ -1 +1 @@
/src/themes/carbon /src/rjsf/carbon_theme

View File

@ -1,6 +1,4 @@
import React from 'react';
import { FieldHelpProps } from '@rjsf/utils'; import { FieldHelpProps } from '@rjsf/utils';
import FormHelperText from '@mui/material/FormHelperText';
/** The `FieldHelpTemplate` component renders any help desired for a field /** The `FieldHelpTemplate` component renders any help desired for a field
* *

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
// import Box from '@mui/material/Box';
// @ts-ignore // @ts-ignore
import { Button } from '@carbon/react'; import { Button } from '@carbon/react';
import { SubmitButtonProps, getSubmitButtonOptions } from '@rjsf/utils'; import { SubmitButtonProps, getSubmitButtonOptions } from '@rjsf/utils';

View File

@ -0,0 +1,110 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { ComboBox } from '@carbon/react';
import HttpService from '../../../services/HttpService';
interface typeaheadArgs {
id: string;
onChange: any;
options: any;
value: any;
uiSchema?: any;
disabled?: boolean;
readonly?: boolean;
rawErrors?: any;
placeholder?: string;
}
export default function TypeaheadWidget({
id,
onChange,
options: { category, itemFormat },
value,
uiSchema,
disabled,
readonly,
placeholder,
rawErrors = [],
}: typeaheadArgs) {
const lastSearchTerm = useRef('');
const [items, setItems] = useState<any[]>([]);
const [selectedItem, setSelectedItem] = useState<any>(null);
const itemFormatRegex = /[^{}]+(?=})/g;
const itemFormatSubstitutions = itemFormat.match(itemFormatRegex);
const typeaheadSearch = useCallback(
(inputText: string) => {
const pathForCategory = (text: string) => {
return `/connector-proxy/typeahead/${category}?prefix=${text}&limit=100`;
};
if (inputText) {
lastSearchTerm.current = inputText;
// TODO: check cache of prefixes -> results
HttpService.makeCallToBackend({
path: pathForCategory(inputText),
successCallback: (result: any) => {
if (lastSearchTerm.current === inputText) {
setItems(result);
}
},
});
}
},
[category]
);
useEffect(() => {
if (value) {
setSelectedItem(JSON.parse(value));
typeaheadSearch(value);
}
}, [value, typeaheadSearch]);
const itemToString = (item: any) => {
if (!item) {
return null;
}
let str = itemFormat;
itemFormatSubstitutions.forEach((key: string) => {
str = str.replace(`{${key}}`, item[key]);
});
return str;
};
let placeholderText = `Start typing to search...`;
if (placeholder) {
placeholderText = placeholder;
}
let helperText = null;
if (uiSchema && uiSchema['ui:help']) {
helperText = uiSchema['ui:help'];
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
[errorMessageForField] = rawErrors;
}
return (
<ComboBox
onInputChange={typeaheadSearch}
onChange={(event: any) => {
setSelectedItem(event.selectedItem);
onChange(JSON.stringify(event.selectedItem));
}}
id={id}
items={items}
itemToString={itemToString}
placeholder={placeholderText}
selectedItem={selectedItem}
helperText={helperText}
disabled={disabled}
readOnly={readonly}
invalid={invalid}
invalidText={errorMessageForField}
/>
);
}

View File

@ -12,7 +12,7 @@ import {
} from '@carbon/react'; } from '@carbon/react';
import validator from '@rjsf/validator-ajv8'; import validator from '@rjsf/validator-ajv8';
import { FormField, JsonSchemaForm } from '../interfaces'; import { FormField, JsonSchemaForm } from '../interfaces';
import { Form } from '../themes/carbon'; import { Form } from '../rjsf/carbon_theme';
import { import {
modifyProcessIdentifierForPathParam, modifyProcessIdentifierForPathParam,
slugifyString, slugifyString,

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import validator from '@rjsf/validator-ajv8'; import validator from '@rjsf/validator-ajv8';
@ -8,85 +8,18 @@ import {
Tabs, Tabs,
Grid, Grid,
Column, Column,
ComboBox,
Button, Button,
ButtonSet, ButtonSet,
} from '@carbon/react'; } from '@carbon/react';
// eslint-disable-next-line import/no-named-as-default import { Form } from '../rjsf/carbon_theme';
import Form from '../themes/carbon';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
import { modifyProcessIdentifierForPathParam } from '../helpers'; import { modifyProcessIdentifierForPathParam } from '../helpers';
import { EventDefinition, Task } from '../interfaces'; import { EventDefinition, Task } from '../interfaces';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import InstructionsForEndUser from '../components/InstructionsForEndUser'; import InstructionsForEndUser from '../components/InstructionsForEndUser';
import TypeaheadWidget from '../rjsf/custom_widgets/TypeaheadWidget/TypeaheadWidget';
// 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/typeahead/${category}?prefix=${inputText}&limit=100`;
};
const lastSearchTerm = useRef('');
const [items, setItems] = useState<any[]>([]);
const [selectedItem, setSelectedItem] = useState<any>(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 (
<ComboBox
onInputChange={typeaheadSearch}
onChange={(event: any) => {
setSelectedItem(event.selectedItem);
onChange(itemToString(event.selectedItem));
}}
id={id}
items={items}
itemToString={itemToString}
placeholder={`Start typing to search for ${category}...`}
selectedItem={selectedItem}
/>
);
}
export default function TaskShow() { export default function TaskShow() {
const [task, setTask] = useState<Task | null>(null); const [task, setTask] = useState<Task | null>(null);