Skip to content

Commit

Permalink
JNG-5706 implement typeahead
Browse files Browse the repository at this point in the history
  • Loading branch information
noherczeg committed Apr 30, 2024
1 parent 9bfd3f2 commit ab5f565
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 0 deletions.
26 changes: 26 additions & 0 deletions docs/pages/01_ui_react.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,32 @@ export class DefaultApplicationCustomizer implements ApplicationCustomizer {
}
----

=== Implementing typeahead queries for text inputs

If a text input is modeled with typeahead turned on, an API is generated which we can inplement to return results.

*src/custom/application-customizer.tsx:*
[source,typescriptjsx]
----
import type { BundleContext } from '@pandino/pandino-api';
import type { ApplicationCustomizer } from './interfaces';
import { GOD_GOD_GALAXIES_ACCESS_VIEW_PAGE_ACTIONS_HOOK_INTERFACE_KEY, ViewGalaxyViewActionsHook } from '~/pages/God/God/Galaxies/AccessViewPage/customization';
export class DefaultApplicationCustomizer implements ApplicationCustomizer {
async customize(context: BundleContext): Promise<void> {
context.registerService<ViewGalaxyViewActionsHook>(GOD_GOD_GALAXIES_ACCESS_VIEW_PAGE_ACTIONS_HOOK_INTERFACE_KEY, () => {
return {
getNameOptions: async (text) => {
console.log(text);
return [text + '__a', text + '__b'];
},
};
});
}
}
----


=== Implementing custom columns

When the "Custom Implementation" option is checked in the Designer for a column in a table, we get access to an API where
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ public static boolean containerHasTrinaryLogicCombo(PageContainer container) {
return !collectElementsOfType(container, new ArrayList<>(), TrinaryLogicCombo.class).isEmpty();
}

public static boolean containerHasTypeAhead(PageContainer container) {
var elements = collectElementsOfType(container, new ArrayList<>(), TextInput.class);
return elements.stream().anyMatch(TextInput::isIsTypeAheadField);
}

public static boolean containerHasDivider(PageContainer container) {
return !collectElementsOfType(container, new ArrayList<>(), Divider.class).isEmpty();
}
Expand Down Expand Up @@ -288,6 +293,16 @@ public static List<Input> getEnumsForContainer(PageContainer container) {
.collect(Collectors.toList());
}

public static List<Input> getTextInputsWithTypeAhead(PageContainer container) {
Set<VisualElement> elements = new LinkedHashSet<>();
collectVisualElementsMatchingCondition(container, e -> e instanceof TextInput, elements);
return elements.stream()
.filter(e -> ((TextInput) e).isIsTypeAheadField())
.map(e -> ((Input) e))
.sorted(Comparator.comparing(NamedElement::getFQName))
.collect(Collectors.toList());
}

public static List<EnumerationType> getEnumDataTypesForContainer(PageContainer container) {
return getEnumsForContainer(container).stream()
.map(e -> (EnumerationType) e.getAttributeType().getDataType())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
{{> fragment.header.hbs }}

import Autocomplete from '@mui/material/Autocomplete';
import InputAdornment from '@mui/material/InputAdornment';
import TextField from '@mui/material/TextField';
import { debounce } from '@mui/material/utils';
import { clsx } from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { debounceInputs } from '~/config';

interface TextWithTypeAheadProps {
name: string;
id: string;
required?: boolean;
label?: string;
ownerData: any;
error?: boolean;
helperText?: string;
readOnly?: boolean;
disabled?: boolean;
editMode?: boolean;
icon?: ReactNode;
onAutoCompleteSearch?: (searchText: string) => Promise<string[]>;
onChange: (target?: string | null) => void;
onBlur?: (event: any) => void;
maxLength?: number;
}

export const TextWithTypeAhead = ({
name,
id,
required,
label,
ownerData,
error = false,
helperText,
readOnly = false,
disabled = false,
editMode = true,
icon,
onAutoCompleteSearch,
onChange,
onBlur,
maxLength,
}: TextWithTypeAheadProps) => {
const [options, setOptions] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [value, setValue] = useState<any>(ownerData[name] || null);

useEffect(() => {
setValue(ownerData[name] || null);
}, [ownerData[name]]);

const handleSearch = async (searchText: string) => {
try {
if (onAutoCompleteSearch) {
setLoading(true);
const data = await onAutoCompleteSearch(searchText);
setOptions(data);
}
} catch (error) {
// Handle error
} finally {
setLoading(false);
}
};

const propagateChange = useMemo(
() =>
debounce((val: string) => {
handleSearch(val);
}, debounceInputs),
[],
);

const onInputChange = useMemo(
() => (event: any, val: string, reason: string) => {
if (value !== val && reason !== 'reset') {
onChange(val);
propagateChange(val);
}
},
[ownerData],
);

const effectiveReadOnly = readOnly || !onAutoCompleteSearch;

return (
<Autocomplete
freeSolo={true}
id={id}
disabled={!effectiveReadOnly && disabled}
readOnly={effectiveReadOnly}
onOpen={ () => {
if (!readOnly) {
setOptions([]); // always start with a clean slate
handleSearch(ownerData[name] || '');
}
} }
isOptionEqualToValue={(option: any, value: any) => option === value}
getOptionLabel={(option) => option || ''}
options={options}
value={ownerData[name] || null}
clearOnBlur={false}
disableClearable={true}
renderInput={(params) => (
<TextField
{...params}
name={name}
id={id}
required={required}
label={label}
error={error}
helperText={helperText}
className={clsx({
'JUDO-viewMode': !editMode,
'JUDO-required': required,
})}
onFocus={ (event) => {
event.target.select();
} }
onBlur={onBlur}
InputProps={ {
...params.InputProps,
readOnly: readOnly,
startAdornment: icon && (
<InputAdornment position="start" style={ { marginTop: 0 } }>
{icon}
</InputAdornment>
),
} }
inputProps={ {
...params.inputProps,
maxLength: maxLength,
} }
/>
)}
onInputChange={onInputChange}
onChange={ (event, target) => {
setValue(target);
handleSearch(target);
} }
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './AggregationInput';
export * from './AssociationButton';
export * from './BinaryInput';
export * from './NumericInput';
export * from './TextWithTypeAhead';
export * from './TrinaryLogicCombobox';
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
{{# each (getEnumsForContainer container) as |ve| }}
filter{{ firstToUpper ve.attributeType.name }}Options?: (data: {{ classDataName container.dataElement '' }}, options: EnumOption<keyof typeof {{ restParamName ve.attributeType.dataType }}>[]) => EnumOption<keyof typeof {{ restParamName ve.attributeType.dataType }}>[];
{{/ each }}
{{# each (getTextInputsWithTypeAhead container) as |ve| }}
get{{ firstToUpper ve.attributeType.name }}Options?: (searchText: string, data: {{ classDataName container.dataElement '' }}, editMode: boolean, validation: Map<keyof {{ classDataName container.dataElement '' }}, string>) => Promise<string[]>;
{{/ each }}
{{# if container.view }}
getMask?: () => string;
{{/ if }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,43 @@
actions={actions}
>
{{/ if }}
{{# if child.isTypeAheadField }}
<TextWithTypeAhead
required={actions?.is{{ firstToUpper child.attributeType.name }}Required ? actions.is{{ firstToUpper child.attributeType.name }}Required(data, editMode) : ({{# if child.requiredBy }}data.{{ child.requiredBy.name }} ||{{/ if }} {{ boolValue child.attributeType.isRequired }})}
name="{{ child.attributeType.name }}"
id="{{ getXMIID child }}"
label={ t('{{ getTranslationKeyForVisualElement child }}', { defaultValue: '{{ child.label }}' }) as string }
ownerData={data}
disabled={actions?.is{{ firstToUpper child.attributeType.name }}Disabled ? actions.is{{ firstToUpper child.attributeType.name }}Disabled(data, editMode, isLoading) : ({{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} isLoading)}
error={ !!validation.get('{{ child.attributeType.name }}') }
helperText={ validation.get('{{ child.attributeType.name }}') }
{{# if child.icon }}
icon={<MdiIcon path="{{ child.icon.iconName }}" />}
{{/ if }}
readOnly={ {{ boolValue child.attributeType.isReadOnly }} || !isFormUpdateable() }
editMode={editMode}
{{# if child.onBlur }}
onBlur={ () => {
if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) {
actions.on{{ firstToUpper child.attributeType.name }}BlurAction(data, storeDiff, editMode, submit);
}
} }
{{/ if }}
onChange={ (value: any) => {
const realValue = value?.length === 0 ? null : value;
storeDiff('{{ child.attributeType.name }}', realValue);
} }
{{# if child.attributeType.dataType.maxLength }}
maxLength={ {{ child.attributeType.dataType.maxLength }} }
{{/ if }}
onAutoCompleteSearch={ async (searchText: string) => {
if (actions?.get{{ firstToUpper child.attributeType.name }}Options) {
return await actions.get{{ firstToUpper child.attributeType.name }}Options(searchText, data, editMode, validation);
}
return Promise.resolve([]);
} }
/>
{{ else }}
<TextField
required={actions?.is{{ firstToUpper child.attributeType.name }}Required ? actions.is{{ firstToUpper child.attributeType.name }}Required(data, editMode) : ({{# if child.requiredBy }}data.{{ child.requiredBy.name }} ||{{/ if }} {{ boolValue child.attributeType.isRequired }})}
name="{{ child.attributeType.name }}"
Expand Down Expand Up @@ -55,6 +92,7 @@
} }
{{/ if }}
/>
{{/ if }}
{{# if child.customImplementation }}
</ComponentProxy>
{{/ if }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import {
{{# if (containerHasTrinaryLogicCombo container) }}
TrinaryLogicCombobox,
{{/ if }}
{{# if (containerHasTypeAhead container) }}
TextWithTypeAhead,
{{/ if }}
} from '~/components/widgets';
import { useConfirmationBeforeChange } from '~/hooks';
{{# if container.form }}
Expand Down
4 changes: 4 additions & 0 deletions judo-ui-react/src/main/resources/ui-react.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@ templates:
pathExpression: "'src/components/widgets/TrinaryLogicCombobox.tsx'"
templateName: actor/src/components/widgets/TrinaryLogicCombobox.tsx.hbs

- name: actor/src/components/widgets/TextWithTypeAhead.tsx
pathExpression: "'src/components/widgets/TextWithTypeAhead.tsx'"
templateName: actor/src/components/widgets/TextWithTypeAhead.tsx.hbs

# Actor - src - components-api

- name: actor/src/components-api/components/Action.ts
Expand Down

0 comments on commit ab5f565

Please sign in to comment.