Skip to content

Commit

Permalink
Add multiselect menu with typeahead option
Browse files Browse the repository at this point in the history
  • Loading branch information
ashley-o0o committed Aug 29, 2024
1 parent 3174a74 commit fc8c646
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 21 deletions.
86 changes: 78 additions & 8 deletions frontend/src/components/MultiSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,23 @@ type MultiSelectionProps = {
groupedValues?: GroupSelectionOptions[];
setValue: (itemSelection: SelectionOptions[]) => void;
toggleId?: string;
inputId?: string;
ariaLabel: string;
placeholder?: string;
isDisabled?: boolean;
selectionRequired?: boolean;
noSelectedOptionsMessage?: string;
toggleTestId?: string;
/** Flag to indicate if the typeahead select allows new items */
isCreatable?: boolean;
/** Flag to indicate if create option should be at top of typeahead */
isCreateOptionOnTop?: boolean;
/** Message to display to create a new option */
createOptionMessage?: string | ((newValue: string) => string);
};

const defaultCreateOptionMessage = (newValue: string) => `Create "${newValue}"`;

export const MultiSelection: React.FC<MultiSelectionProps> = ({
value = [],
groupedValues = [],
Expand All @@ -53,9 +62,13 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
ariaLabel = 'Options menu',
id,
toggleId,
inputId,
toggleTestId,
selectionRequired,
noSelectedOptionsMessage = 'One or more options must be selected',
isCreatable = false,
isCreateOptionOnTop = false,
createOptionMessage = defaultCreateOptionMessage,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState<string>('');
Expand Down Expand Up @@ -97,15 +110,50 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
[groupOptions, inputValue, value],
);

const allOptions = React.useMemo(() => {
const allValues = React.useMemo(() => {
const options = [];
groupedValues.forEach((group) => options.push(...group.values));
options.push(...value);

return options;
}, [groupedValues, value]);

const visibleOptions = [...groupOptions, ...selectOptions];
const createOption = React.useMemo(() => {
const inputValueTrim = inputValue.trim();

if (
isCreatable &&
inputValueTrim &&
!allValues.find((o) => String(o.name).toLowerCase() === inputValueTrim.toLowerCase())
) {
return {
id: inputValueTrim,
name:
typeof createOptionMessage === 'string'
? createOptionMessage
: createOptionMessage(inputValueTrim),
selected: false,
};
}
return undefined;
}, [inputValue, isCreatable, isCreateOptionOnTop, createOptionMessage]);

Check failure on line 138 in frontend/src/components/MultiSelection.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

React Hook React.useMemo has a missing dependency: 'allValues'. Either include it or remove the dependency array

const allOptions = React.useMemo(() => {
let options = [];

Check failure on line 141 in frontend/src/components/MultiSelection.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

'options' is never reassigned. Use 'const' instead
groupedValues.forEach((group) => options.push(...group.values));
options.push(...value);
if (createOption) {
options.push(createOption);
}
return options;
}, [allValues, createOption]);

Check failure on line 148 in frontend/src/components/MultiSelection.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

React Hook React.useMemo has missing dependencies: 'groupedValues' and 'value'. Either include them or remove the dependency array

const visibleOptions = React.useMemo(() => {
let options = [...groupOptions, ...selectOptions];
if (createOption) {
options = isCreateOptionOnTop ? [createOption, ...options] : [...options, createOption];
}
return options;
}, [groupOptions, selectOptions, createOption]);

Check failure on line 156 in frontend/src/components/MultiSelection.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

React Hook React.useMemo has a missing dependency: 'isCreateOptionOnTop'. Either include it or remove the dependency array

const selected = React.useMemo(() => allOptions.filter((v) => v.selected), [allOptions]);

Expand Down Expand Up @@ -156,6 +204,7 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
case 'Enter':
if (isOpen && focusedItem) {
onSelect(focusedItem);
setInputValue('');
}
if (!isOpen) {
setIsOpen(true);
Expand Down Expand Up @@ -188,6 +237,7 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
option.id === menuItem.id ? { ...option, selected: !option.selected } : option,
),
);
setInputValue('');
}
textInputRef.current?.focus();
};
Expand All @@ -209,6 +259,7 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
>
<TextInputGroup isPlain>
<TextInputGroupMain
inputId={inputId}
value={inputValue}
onClick={onToggleClick}
onChange={onTextInputChange}
Expand Down Expand Up @@ -267,7 +318,14 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
onOpenChange={() => setOpen(false)}
toggle={toggle}
>
{visibleOptions.length === 0 && inputValue ? (
{createOption && isCreateOptionOnTop && groupOptions.length > 0 ? (
<SelectList isAriaMultiselectable>
<SelectOption value={createOption.id} isFocused={focusedItemIndex === 0}>
{createOption.name}
</SelectOption>
</SelectList>
) : null}
{!createOption && visibleOptions.length === 0 && inputValue ? (
<SelectList isAriaMultiselectable>
<SelectOption isDisabled>No results found</SelectOption>
</SelectList>
Expand All @@ -279,7 +337,7 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
{g.values.map((option) => (
<SelectOption
key={option.name}
isFocused={focusedItemIndex === option.index}
isFocused={focusedItemIndex === option.index + (isCreateOptionOnTop ? 1 : 0)}
id={`select-multi-typeahead-${option.name.replace(' ', '-')}`}
value={option.id}
ref={null}
Expand All @@ -293,12 +351,16 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
{index < selectGroups.length - 1 || selectOptions.length ? <Divider /> : null}
</>
))}
{selectOptions.length ? (
{selectOptions.length ||
(createOption && (!isCreateOptionOnTop || groupOptions.length === 0)) ? (
<SelectList isAriaMultiselectable>
{createOption && isCreateOptionOnTop && groupOptions.length === 0 ? (
<SelectOption value={createOption.id}>{createOption.name}</SelectOption>
) : null}
{selectOptions.map((option) => (
<SelectOption
key={option.name}
isFocused={focusedItemIndex === option.index}
isFocused={focusedItemIndex === option.index + (isCreateOptionOnTop ? 1 : 0)}
id={`select-multi-typeahead-${option.name.replace(' ', '-')}`}
value={option.id}
ref={null}
Expand All @@ -307,6 +369,14 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
{option.name}
</SelectOption>
))}
{createOption && !isCreateOptionOnTop ? (
<SelectOption
value={createOption.id}
isFocused={focusedItemIndex === visibleOptions.length - 1}
>
{createOption.name}
</SelectOption>
) : null}
</SelectList>
) : null}
</Select>
Expand All @@ -319,4 +389,4 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({
)}
</>
);
};
};

Check failure on line 392 in frontend/src/components/MultiSelection.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

Insert `⏎`
30 changes: 24 additions & 6 deletions frontend/src/pages/connectionTypes/ConnectionTypesTableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as React from 'react';
import { useNavigate } from 'react-router';
import { ActionsColumn, Td, Tr } from '@patternfly/react-table';
import { Label, Switch, Timestamp, TimestampTooltipVariant } from '@patternfly/react-core';
import {
Label,
LabelGroup,
Switch,
Timestamp,
TimestampTooltipVariant,
} from '@patternfly/react-core';
import { ConnectionTypeConfigMapObj } from '~/concepts/connectionTypes/types';
import { relativeTime } from '~/utilities/time';
import { updateConnectionTypeEnabled } from '~/services/connectionTypesService';
Expand All @@ -14,6 +20,7 @@ import {
ownedByDSC,
} from '~/concepts/k8s/utils';
import { connectionTypeColumns } from '~/pages/connectionTypes/columns';
import CategoryLabel from '~/concepts/connectionTypes/CategoryLabel';

type ConnectionTypesTableRowProps = {
obj: ConnectionTypeConfigMapObj;
Expand Down Expand Up @@ -72,29 +79,40 @@ const ConnectionTypesTableRow: React.FC<ConnectionTypesTableRowProps> = ({

return (
<Tr>
<Td dataLabel={connectionTypeColumns[0].label} width={50}>
<Td dataLabel={connectionTypeColumns[0].label} width={30}>
<TableRowTitleDescription
title={getDisplayNameFromK8sResource(obj)}
description={getDescriptionFromK8sResource(obj)}
/>
</Td>
<Td dataLabel={connectionTypeColumns[1].label} data-testid="connection-type-creator">
<Td dataLabel={connectionTypeColumns[1].label}>
{obj.data?.category?.length ? (
<LabelGroup>
{obj.data.category.map((category) => (
<CategoryLabel key={category} category={category} />
))}
</LabelGroup>
) : (
'-'
)}
</Td>
<Td dataLabel={connectionTypeColumns[2].label} data-testid="connection-type-creator">
{ownedByDSC(obj) ? (
<Label data-testid="connection-type-user-label">{creator}</Label>
) : (
creator
)}
</Td>
<Td
dataLabel={connectionTypeColumns[2].label}
dataLabel={connectionTypeColumns[3].label}
data-testid="connection-type-created"
modifier="nowrap"
>
<Timestamp date={createdDate} tooltip={{ variant: TimestampTooltipVariant.default }}>
{createdDate ? relativeTime(Date.now(), createdDate.getTime()) : 'Unknown'}
</Timestamp>
</Td>
<Td dataLabel={connectionTypeColumns[3].label}>
<Td dataLabel={connectionTypeColumns[4].label}>
<Switch
isChecked={isEnabled}
aria-label="toggle enabled"
Expand Down Expand Up @@ -125,4 +143,4 @@ const ConnectionTypesTableRow: React.FC<ConnectionTypesTableRowProps> = ({
);
};

export default ConnectionTypesTableRow;
export default ConnectionTypesTableRow;

Check failure on line 146 in frontend/src/pages/connectionTypes/ConnectionTypesTableRow.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

Insert `⏎`
7 changes: 6 additions & 1 deletion frontend/src/pages/connectionTypes/columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export const connectionTypeColumns: SortableData<ConnectionTypeConfigMapObj>[] =
field: 'name',
sortable: sorter,
},
{
label: 'Category',
field: 'category',
sortable: false,
},
{
label: 'Creator',
field: 'creator',
Expand All @@ -70,4 +75,4 @@ export const connectionTypeColumns: SortableData<ConnectionTypeConfigMapObj>[] =
},
},
},
];
];

Check failure on line 78 in frontend/src/pages/connectionTypes/columns.ts

View workflow job for this annotation

GitHub Actions / Tests (18.x)

Insert `⏎`
2 changes: 2 additions & 0 deletions frontend/src/pages/connectionTypes/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const initialFilterData: Record<ConnectionTypesOptions, string | undefine
[ConnectionTypesOptions.keyword]: '',
[ConnectionTypesOptions.createdBy]: '',
};

export const categoryOptions = ['Object storage', 'Database', 'Model registry', 'URI'];

Check failure on line 18 in frontend/src/pages/connectionTypes/const.ts

View workflow job for this annotation

GitHub Actions / Tests (18.x)

Insert `⏎`
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
import { translateDisplayNameForK8s } from '~/concepts/k8s/utils';
import ApplicationsPage from '~/pages/ApplicationsPage';
import { NameDescType } from '~/pages/projects/types';
import { MultiSelection, SelectionOptions } from '~/components/MultiSelection';
import { categoryOptions } from '~/pages/connectionTypes/const';
import CreateConnectionTypeFooter from './ManageConnectionTypeFooter';
import ManageConnectionTypeFieldsTable from './ManageConnectionTypeFieldsTable';
import ManageConnectionTypeBreadcrumbs from './ManageConnectionTypeBreadcrumbs';
Expand Down Expand Up @@ -62,7 +64,16 @@ const ManageConnectionTypePage: React.FC<Props> = ({ prefill, isEdit, onSave })
const [connectionEnabled, setConnectionEnabled] = React.useState<boolean>(prefillEnabled);
const [connectionFields, setConnectionFields] =
React.useState<ConnectionTypeField[]>(prefillFields);
const [category] = React.useState<string[]>(prefillCategory);
const [category, setCategory] = React.useState<string[]>(prefillCategory);

const categoryItems = React.useMemo<SelectionOptions[]>(
() =>
category
.filter((c) => !categoryOptions.includes(c))
.concat(categoryOptions)
.map((c) => ({ id: c, name: c, selected: category.includes(c) })),
[category],
);

const connectionTypeObj = React.useMemo(
() =>
Expand All @@ -85,8 +96,8 @@ const ManageConnectionTypePage: React.FC<Props> = ({ prefill, isEdit, onSave })

const isValid = React.useMemo(() => {
const trimmedName = connectionNameDesc.name.trim();
return Boolean(trimmedName) && !isEnvVarConflict;
}, [connectionNameDesc.name, isEnvVarConflict]);
return Boolean(trimmedName) && !isEnvVarConflict && category.length > 0;
}, [connectionNameDesc.name, isEnvVarConflict, category]);

const onCancel = () => {
navigate('/connectionTypes');
Expand All @@ -102,7 +113,7 @@ const ManageConnectionTypePage: React.FC<Props> = ({ prefill, isEdit, onSave })
title={isEdit ? 'Edit connection type' : 'Create connection type'}
loaded
empty={false}
errorMessage="Unable load to connection types"
errorMessage="Unable to load connection types"
breadcrumb={<ManageConnectionTypeBreadcrumbs />}
headerAction={
isDrawerExpanded ? undefined : (
Expand Down Expand Up @@ -139,7 +150,20 @@ const ManageConnectionTypePage: React.FC<Props> = ({ prefill, isEdit, onSave })
setData={setConnectionNameDesc}
autoFocusName
/>
<FormGroup label="Enable">
<FormGroup label="Category" fieldId="connection-type-category" isRequired>
<MultiSelection
inputId="connection-type-category"
toggleTestId="connection-type-category"
ariaLabel="Category"
placeholder="Select a category"
isCreatable
value={categoryItems}
setValue={(value) => {
setCategory(value.filter((v) => v.selected).map((v) => String(v.id)));
}}
/>
</FormGroup>
<FormGroup label="Enable" fieldId="connection-type-enable">
<Checkbox
label="Enable users in your organization to use this connection type when adding connections."
id="connection-type-enable"
Expand Down Expand Up @@ -189,4 +213,4 @@ const ManageConnectionTypePage: React.FC<Props> = ({ prefill, isEdit, onSave })
);
};

export default ManageConnectionTypePage;
export default ManageConnectionTypePage;

Check failure on line 216 in frontend/src/pages/connectionTypes/manage/ManageConnectionTypePage.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

Insert `⏎`

0 comments on commit fc8c646

Please sign in to comment.