From 4dc54873e29a455784f601a16c200d32b13c395a Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 25 Oct 2024 15:41:59 +0100 Subject: [PATCH 01/12] Add new core component Autocomplete and use for entity pickers Signed-off-by: Jonathan Roebuck --- packages/core-components/report.api.md | 14 ++ .../Autocomplete/Autocomplete.stories.tsx | 33 ++++ .../Autocomplete/Autocomplete.test.tsx | 117 ++++++++++++ .../components/Autocomplete/Autocomplete.tsx | 171 +++++++++++++++++ .../src/components/Autocomplete/index.tsx | 16 ++ .../core-components/src/components/index.ts | 1 + plugins/catalog-react/report.api.md | 2 +- .../EntityAutocompletePicker.tsx | 60 +++--- .../EntityAutocompletePickerInput.tsx | 43 ----- .../EntityOwnerPicker/EntityOwnerPicker.tsx | 172 ++++++++---------- .../EntityProcessingStatusPicker.tsx | 90 ++++----- 11 files changed, 479 insertions(+), 240 deletions(-) create mode 100644 packages/core-components/src/components/Autocomplete/Autocomplete.stories.tsx create mode 100644 packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx create mode 100644 packages/core-components/src/components/Autocomplete/Autocomplete.tsx create mode 100644 packages/core-components/src/components/Autocomplete/index.tsx delete mode 100644 plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerInput.tsx diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index c3f7cf9a6a..5298ac9816 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -6,6 +6,7 @@ /// import { ApiRef } from '@backstage/core-plugin-api'; +import { AutocompleteProps } from '@material-ui/lab/Autocomplete'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; import { BackstageUserIdentity } from '@backstage/core-plugin-api'; @@ -33,6 +34,7 @@ import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Options } from 'react-markdown'; import { Options as Options_2 } from '@material-table/core'; +import { OutlinedTextFieldProps } from '@material-ui/core/TextField'; import { Overrides } from '@material-ui/core/styles/overrides'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -76,6 +78,18 @@ export type AppIconProps = IconComponentProps & { Fallback?: IconComponent; }; +// Warning: (ae-forgotten-export) The symbol "AutocompleteComponentProps" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function Autocomplete< + T, + Multiple extends boolean | undefined = undefined, + DisableClearable extends boolean | undefined = undefined, + FreeSolo extends boolean | undefined = undefined, +>( + props: AutocompleteComponentProps, +): React_2.JSX.Element; + // @public export const AutoLogout: (props: AutoLogoutProps) => JSX.Element | null; diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.stories.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.stories.tsx new file mode 100644 index 0000000000..7aed709125 --- /dev/null +++ b/packages/core-components/src/components/Autocomplete/Autocomplete.stories.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { AutocompleteComponent as Autocomplete } from './Autocomplete'; + +export default { + title: 'Inputs/Autocomplete', + component: Autocomplete, +}; + +export const Default = (args: any) => { + return ; +}; + +Default.args = { + multiple: true, + label: 'Default', + name: 'default', + options: ['test 1', 'test 2', 'test 3'], +}; diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx new file mode 100644 index 0000000000..fff83eb26d --- /dev/null +++ b/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { AutocompleteComponent as Autocomplete } from './Autocomplete'; + +describe('Autocomplete', () => { + const user = userEvent.setup(); + const mockOptions = ['Option 1', 'Option 2', 'Option 3']; + + it('renders without exploding', () => { + render( + , + ); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('renders the expand icon', () => { + render( + , + ); + const expandIcon = screen.getByTestId('test-autocomplete-expand'); + expect(expandIcon).toBeInTheDocument(); + }); + + it('displays options when clicked', () => { + render( + , + ); + + const input = screen.getByRole('textbox'); + user.click(input); + + mockOptions.forEach(option => { + expect(screen.getByText(option)).toBeInTheDocument(); + }); + }); + + it('supports required input', () => { + render( + , + ); + + const input = screen.getByRole('textbox'); + expect(input).toBeRequired(); + }); + + it('displays helper text when provided', () => { + render( + , + ); + + expect(screen.getByText('Helper text')).toBeInTheDocument(); + }); + + it('renders without label', () => { + render(); + + const input = screen.getByRole('textbox'); + expect(input).toBeInTheDocument(); + }); + + it('displays correct option on selection', () => { + render( + , + ); + + const input = screen.getByRole('textbox'); + user.click(input); + + const optionToSelect = screen.getByText('Option 1'); + user.click(optionToSelect); + + expect(input).toHaveValue('Option 1'); + }); +}); diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.tsx new file mode 100644 index 0000000000..dd0c936c14 --- /dev/null +++ b/packages/core-components/src/components/Autocomplete/Autocomplete.tsx @@ -0,0 +1,171 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Box from '@material-ui/core/Box'; +import Typography from '@material-ui/core/Typography'; +import Paper, { PaperProps } from '@material-ui/core/Paper'; +import Popper, { PopperProps } from '@material-ui/core/Popper'; +import TextField, { OutlinedTextFieldProps } from '@material-ui/core/TextField'; +import Grow from '@material-ui/core/Grow'; +import { + createStyles, + makeStyles, + Theme, + withStyles, +} from '@material-ui/core/styles'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import Autocomplete, { AutocompleteProps } from '@material-ui/lab/Autocomplete'; +import React, { ReactNode } from 'react'; + +const useStyles = makeStyles( + theme => ({ + root: {}, + label: { + position: 'relative', + fontWeight: 'bold', + fontSize: theme.typography.body2.fontSize, + fontFamily: theme.typography.fontFamily, + color: theme.palette.text.primary, + '& > span': { + top: 0, + left: 0, + position: 'absolute', + }, + }, + input: {}, + }), + { name: 'BackstageAutocomplete' }, +); + +const BootstrapAutocomplete = withStyles( + (theme: Theme) => + createStyles({ + root: {}, + paper: { + margin: 0, + }, + hasClearIcon: {}, + hasPopupIcon: {}, + focused: {}, + inputRoot: { + marginTop: 24, + backgroundColor: theme.palette.background.paper, + '$root$hasClearIcon$hasPopupIcon &': { + padding: `${theme.spacing(0.75, 7, 0.75, 1.5)}`, + }, + '$root$focused &': { + outline: 'none', + }, + '$root &:hover > fieldset': { + borderColor: '#ced4da', + }, + '$root$focused & > fieldset': { + borderWidth: 1, + borderColor: theme.palette.primary.main, + }, + }, + popupIndicator: { + padding: 0, + margin: 0, + color: theme.palette.text.primary, + '& [class*="MuiTouchRipple-root"]': { + display: 'none', + }, + }, + endAdornment: { + '$root$hasClearIcon$hasPopupIcon &': { + right: 4, + }, + }, + input: { + '$root$hasClearIcon$hasPopupIcon &': { + height: 32, + fontSize: theme.typography.body1.fontSize, + padding: 0, + }, + }, + }), + { name: 'BackstageAutocompleteBase' }, +)(Autocomplete) as typeof Autocomplete; + +const PopperComponent = (props: PopperProps) => ( + + {({ TransitionProps }) => ( + + {props.children as ReactNode} + + )} + +); + +const PaperComponent = (props: PaperProps) => ( + +); + +export type AutocompleteComponentProps< + T, + Multiple extends boolean | undefined = undefined, + DisableClearable extends boolean | undefined = undefined, + FreeSolo extends boolean | undefined = undefined, +> = { + name: string; + label?: string; + inputProps?: Omit; +} & Omit< + AutocompleteProps, + 'PopperComponent' | 'PaperComponent' | 'renderInput' | 'size' | 'popupIcon' +>; + +/** @public */ +export function AutocompleteComponent< + T, + Multiple extends boolean | undefined = undefined, + DisableClearable extends boolean | undefined = undefined, + FreeSolo extends boolean | undefined = undefined, +>(props: AutocompleteComponentProps) { + const { label, name, inputProps, ...rest } = props; + const classes = useStyles(); + const autocomplete = ( + } + PaperComponent={PaperComponent} + PopperComponent={PopperComponent} + renderInput={params => ( + + )} + /> + ); + + return ( + + {label ? ( + + {label} + {autocomplete} + + ) : ( + autocomplete + )} + + ); +} diff --git a/packages/core-components/src/components/Autocomplete/index.tsx b/packages/core-components/src/components/Autocomplete/index.tsx new file mode 100644 index 0000000000..bf4681db8e --- /dev/null +++ b/packages/core-components/src/components/Autocomplete/index.tsx @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { AutocompleteComponent as Autocomplete } from './Autocomplete'; diff --git a/packages/core-components/src/components/index.ts b/packages/core-components/src/components/index.ts index 828df3ec03..6fcbe2e9a9 100644 --- a/packages/core-components/src/components/index.ts +++ b/packages/core-components/src/components/index.ts @@ -16,6 +16,7 @@ export * from './AlertDisplay'; export * from './AutoLogout'; +export * from './Autocomplete'; export * from './Avatar'; export * from './LinkButton'; export * from './CodeSnippet'; diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index af5102176d..107fbeb0c0 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -205,7 +205,7 @@ export type EntityAutocompletePickerProps< Filter: { new (values: string[]): NonNullable; }; - InputProps?: TextFieldProps; + InputProps?: TextFieldProps['InputProps']; initialSelectedOptions?: string[]; filtersForAvailableValues?: Array; }; diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index fd88a7a31e..d4f978fce5 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -16,21 +16,18 @@ import Box from '@material-ui/core/Box'; import { TextFieldProps } from '@material-ui/core/TextField'; -import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import React, { useEffect, useMemo, useState, ReactNode } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/esm/useAsync'; import { catalogApiRef } from '../../api'; import { EntityAutocompletePickerOption } from './EntityAutocompletePickerOption'; -import { EntityAutocompletePickerInput } from './EntityAutocompletePickerInput'; import { DefaultEntityFilters, useEntityList, } from '../../hooks/useEntityListProvider'; import { EntityFilter } from '../../types'; +import { Autocomplete } from '@backstage/core-components'; import { reduceBackendCatalogFilters } from '../../utils/filters'; /** @public */ @@ -52,7 +49,7 @@ export type EntityAutocompletePickerProps< path: string; showCounts?: boolean; Filter: { new (values: string[]): NonNullable }; - InputProps?: TextFieldProps; + InputProps?: TextFieldProps['InputProps']; initialSelectedOptions?: string[]; filtersForAvailableValues?: Array; }; @@ -82,11 +79,9 @@ export function EntityAutocompletePicker< path, showCounts, Filter, - InputProps, initialSelectedOptions = [], filtersForAvailableValues = ['kind'], } = props; - const classes = useStyles(); const { @@ -152,36 +147,25 @@ export function EntityAutocompletePicker< return ( - - {label} - - PopperComponent={popperProps => ( -
{popperProps.children as ReactNode}
- )} - multiple - disableCloseOnSelect - options={availableOptions} - value={selectedOptions} - onChange={(_event: object, options: string[]) => - setSelectedOptions(options) - } - renderOption={(option, { selected }) => ( - - )} - size="small" - popupIcon={ - - } - renderInput={params => ( - - )} - /> -
+ + multiple + disableCloseOnSelect + label={label} + name={`${String(name)}-picker`} + options={availableOptions} + value={selectedOptions} + onChange={(_event: object, options: string[]) => + setSelectedOptions(options) + } + renderOption={(option, { selected }) => ( + + )} + />
); } diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerInput.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerInput.tsx deleted file mode 100644 index 133024330c..0000000000 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePickerInput.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import TextField, { TextFieldProps } from '@material-ui/core/TextField'; -import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; -import React from 'react'; -import classnames from 'classnames'; - -const useStyles = makeStyles( - (theme: Theme) => - createStyles({ - input: { - backgroundColor: theme.palette.background.paper, - }, - }), - { - name: 'CatalogReactEntityAutocompletePickerInput', - }, -); - -export function EntityAutocompletePickerInput(params: TextFieldProps) { - const classes = useStyles(); - - return ( - - ); -} diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index ecbdae1d63..bf8db5f627 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -22,15 +22,13 @@ import { import Box from '@material-ui/core/Box'; import Checkbox from '@material-ui/core/Checkbox'; import FormControlLabel from '@material-ui/core/FormControlLabel'; -import TextField from '@material-ui/core/TextField'; import Typography from '@material-ui/core/Typography'; import Tooltip from '@material-ui/core/Tooltip'; -import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import { makeStyles } from '@material-ui/core/styles'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import React, { useEffect, useMemo, useState, ReactNode } from 'react'; +import { Autocomplete } from '@backstage/core-components'; +import React, { useEffect, useMemo, useState } from 'react'; import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityOwnerFilter } from '../../filters'; import { useDebouncedEffect } from '@react-hookz/web'; @@ -42,29 +40,20 @@ import { withStyles } from '@material-ui/core/styles'; import { useEntityPresentation } from '../../apis'; import { catalogReactTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { PopperProps } from '@material-ui/core/Popper'; /** @public */ export type CatalogReactEntityOwnerPickerClassKey = 'input'; const useStyles = makeStyles( - (theme: Theme) => - createStyles({ - root: {}, - label: { - textTransform: 'none', - fontWeight: 'bold', - }, - input: { - backgroundColor: theme.palette.background.paper, - }, - fullWidth: { width: '100%' }, - boxLabel: { - width: '100%', - textOverflow: 'ellipsis', - overflow: 'hidden', - }, - }), + { + root: {}, + fullWidth: { width: '100%' }, + boxLabel: { + width: '100%', + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + }, { name: 'CatalogReactEntityOwnerPicker' }, ); @@ -147,7 +136,7 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { [ownersParameter], ); - const [selectedOwners, setSelectedOwners] = useState( + const [selectedOwners, setSelectedOwners] = useState( queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [], ); @@ -186,84 +175,67 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { return ( - - {t('entityOwnerPicker.title')} - { - if (typeof v === 'string') { - return stringifyEntityRef(o) === v; - } - return o === v; - }} - getOptionLabel={o => { - const entity = - typeof o === 'string' - ? cache.getEntity(o) || - parseEntityRef(o, { - defaultKind: 'group', - defaultNamespace: 'default', - }) - : o; - return humanizeEntity(entity, humanizeEntityRef(entity)); - }} - onChange={(_: object, owners) => { - setText(''); - setSelectedOwners( - owners.map(e => { - const entityRef = - typeof e === 'string' ? e : stringifyEntityRef(e); + + label={t('entityOwnerPicker.title')} + multiple + disableCloseOnSelect + loading={loading} + options={availableOwners} + value={selectedOwners as unknown as Entity[]} + getOptionSelected={(o, v) => { + if (typeof v === 'string') { + return stringifyEntityRef(o) === v; + } + return o === v; + }} + getOptionLabel={o => { + const entity = + typeof o === 'string' + ? cache.getEntity(o) || + parseEntityRef(o, { + defaultKind: 'group', + defaultNamespace: 'default', + }) + : o; + return humanizeEntity(entity, humanizeEntityRef(entity)); + }} + onChange={(_: object, owners) => { + setText(''); + setSelectedOwners( + owners.map(e => { + const entityRef = + typeof e === 'string' ? e : stringifyEntityRef(e); - if (typeof e !== 'string') { - cache.setEntity(e); - } - return entityRef; - }), - ); - }} - filterOptions={x => x} - renderOption={(entity, { selected }) => { - return ; - }} - size="small" - popupIcon={} - renderInput={params => ( - { - setText(e.currentTarget.value); - }} - variant="outlined" - /> - )} - ListboxProps={{ - onScroll: (e: React.MouseEvent) => { - const element = e.currentTarget; - const hasReachedEnd = - Math.abs( - element.scrollHeight - - element.clientHeight - - element.scrollTop, - ) < 1; - - if (hasReachedEnd && value?.cursor) { - handleFetch({ items: value.items, cursor: value.cursor }); + if (typeof e !== 'string') { + cache.setEntity(e); } - }, - 'data-testid': 'owner-picker-listbox', - }} - /> - + return entityRef; + }), + ); + }} + filterOptions={x => x} + renderOption={(entity, { selected }) => { + return ; + }} + name="owner-picker" + onInputChange={(_e, inputValue) => { + setText(inputValue); + }} + ListboxProps={{ + onScroll: (e: React.MouseEvent) => { + const element = e.currentTarget; + const hasReachedEnd = + Math.abs( + element.scrollHeight - element.clientHeight - element.scrollTop, + ) < 1; + + if (hasReachedEnd && value?.cursor) { + handleFetch({ items: value.items, cursor: value.cursor }); + } + }, + 'data-testid': 'owner-picker-listbox', + }} + /> ); }; - -function Popper({ children }: PopperProps) { - return
{children as ReactNode}
; -} diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx index a2f0c8f729..02d1ddc238 100644 --- a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx @@ -18,15 +18,12 @@ import { EntityErrorFilter, EntityOrphanFilter } from '../../filters'; import Box from '@material-ui/core/Box'; import Checkbox from '@material-ui/core/Checkbox'; import FormControlLabel from '@material-ui/core/FormControlLabel'; -import TextField from '@material-ui/core/TextField'; -import Typography from '@material-ui/core/Typography'; -import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import { makeStyles } from '@material-ui/core/styles'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import React, { useState, ReactNode } from 'react'; +import React, { useState } from 'react'; import { useEntityList } from '../../hooks'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import { Autocomplete } from '@backstage/core-components'; import { catalogReactTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; @@ -34,17 +31,9 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; const useStyles = makeStyles( - (theme: Theme) => - createStyles({ - root: {}, - input: { - backgroundColor: theme.palette.background.paper, - }, - label: { - textTransform: 'none', - fontWeight: 'bold', - }, - }), + { + root: {}, + }, { name: 'CatalogReactEntityProcessingStatusPickerPicker' }, ); @@ -77,47 +66,32 @@ export const EntityProcessingStatusPicker = () => { return ( - - {t('entityProcessingStatusPicker.title')} - - PopperComponent={popperProps => ( -
{popperProps.children as ReactNode}
- )} - multiple - disableCloseOnSelect - options={availableAdvancedItems} - value={selectedAdvancedItems} - onChange={(_: object, value: string[]) => { - setSelectedAdvancedItems(value); - orphanChange(value.includes('Is Orphan')); - errorChange(value.includes('Has Error')); - }} - renderOption={(option, { selected }) => ( - - } - onClick={event => event.preventDefault()} - label={option} - /> - )} - size="small" - popupIcon={ - - } - renderInput={params => ( - - )} - /> -
+ + label={t('entityProcessingStatusPicker.title')} + multiple + disableCloseOnSelect + options={availableAdvancedItems} + value={selectedAdvancedItems} + onChange={(_: object, value: string[]) => { + setSelectedAdvancedItems(value); + orphanChange(value.includes('Is Orphan')); + errorChange(value.includes('Has Error')); + }} + renderOption={(option, { selected }) => ( + + } + onClick={event => event.preventDefault()} + label={option} + /> + )} + name="processing-status-picker" + />
); }; From b9ad22a0cfffef9d573efa2fa3f652a7909210ce Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Mon, 4 Nov 2024 16:16:17 +0000 Subject: [PATCH 02/12] update styles. make renderInput optional Signed-off-by: Jonathan Roebuck --- .../components/Autocomplete/Autocomplete.tsx | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.tsx index dd0c936c14..f0016be430 100644 --- a/packages/core-components/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/core-components/src/components/Autocomplete/Autocomplete.tsx @@ -27,8 +27,11 @@ import { withStyles, } from '@material-ui/core/styles'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import Autocomplete, { AutocompleteProps } from '@material-ui/lab/Autocomplete'; -import React, { ReactNode } from 'react'; +import Autocomplete, { + AutocompleteProps, + AutocompleteRenderInputParams, +} from '@material-ui/lab/Autocomplete'; +import React, { ReactNode, useCallback } from 'react'; const useStyles = makeStyles( theme => ({ @@ -64,7 +67,8 @@ const BootstrapAutocomplete = withStyles( marginTop: 24, backgroundColor: theme.palette.background.paper, '$root$hasClearIcon$hasPopupIcon &': { - padding: `${theme.spacing(0.75, 7, 0.75, 1.5)}`, + paddingBlock: theme.spacing(1.5625), + paddingInlineStart: theme.spacing(1.5), }, '$root$focused &': { outline: 'none', @@ -80,7 +84,7 @@ const BootstrapAutocomplete = withStyles( popupIndicator: { padding: 0, margin: 0, - color: theme.palette.text.primary, + color: '#616161', '& [class*="MuiTouchRipple-root"]': { display: 'none', }, @@ -92,7 +96,6 @@ const BootstrapAutocomplete = withStyles( }, input: { '$root$hasClearIcon$hasPopupIcon &': { - height: 32, fontSize: theme.typography.body1.fontSize, padding: 0, }, @@ -124,9 +127,15 @@ export type AutocompleteComponentProps< name: string; label?: string; inputProps?: Omit; + renderInput?: AutocompleteProps< + T, + Multiple, + DisableClearable, + FreeSolo + >['renderInput']; } & Omit< AutocompleteProps, - 'PopperComponent' | 'PaperComponent' | 'renderInput' | 'size' | 'popupIcon' + 'PopperComponent' | 'PaperComponent' | 'popupIcon' >; /** @public */ @@ -138,21 +147,25 @@ export function AutocompleteComponent< >(props: AutocompleteComponentProps) { const { label, name, inputProps, ...rest } = props; const classes = useStyles(); + const renderInput = useCallback( + (params: AutocompleteRenderInputParams) => ( + + ), + [], + ); const autocomplete = ( } PaperComponent={PaperComponent} PopperComponent={PopperComponent} - renderInput={params => ( - - )} /> ); From 71a05f932e32844e0d3b3c5aa30c5e6241c9b43d Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Tue, 5 Nov 2024 09:19:20 +0000 Subject: [PATCH 03/12] prevent input classnames breaking change Signed-off-by: Jonathan Roebuck --- packages/core-components/report.api.md | 23 ++++++++++- .../Autocomplete/Autocomplete.test.tsx | 4 +- .../components/Autocomplete/Autocomplete.tsx | 39 ++++++++++--------- .../src/components/Autocomplete/index.tsx | 5 ++- plugins/catalog-react/report.api.md | 2 +- .../EntityAutocompletePicker.tsx | 4 +- 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 5298ac9816..5a9f5440ec 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -78,8 +78,6 @@ export type AppIconProps = IconComponentProps & { Fallback?: IconComponent; }; -// Warning: (ae-forgotten-export) The symbol "AutocompleteComponentProps" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export function Autocomplete< T, @@ -90,6 +88,27 @@ export function Autocomplete< props: AutocompleteComponentProps, ): React_2.JSX.Element; +// @public (undocumented) +export type AutocompleteComponentProps< + T, + Multiple extends boolean | undefined = undefined, + DisableClearable extends boolean | undefined = undefined, + FreeSolo extends boolean | undefined = undefined, +> = Omit< + AutocompleteProps, + 'PopperComponent' | 'PaperComponent' | 'popupIcon' | 'renderInput' +> & { + name: string; + label?: string; + TextFieldProps?: Omit; + renderInput?: AutocompleteProps< + T, + Multiple, + DisableClearable, + FreeSolo + >['renderInput']; +}; + // @public export const AutoLogout: (props: AutoLogoutProps) => JSX.Element | null; diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx index fff83eb26d..445267390a 100644 --- a/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx +++ b/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx @@ -69,7 +69,7 @@ describe('Autocomplete', () => { name="test-autocomplete" options={mockOptions} label="Test Label" - inputProps={{ required: true }} + TextFieldProps={{ required: true }} />, ); @@ -83,7 +83,7 @@ describe('Autocomplete', () => { name="test-autocomplete" options={mockOptions} label="Test Label" - inputProps={{ helperText: 'Helper text' }} + TextFieldProps={{ helperText: 'Helper text' }} />, ); diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.tsx index f0016be430..a34dd8728a 100644 --- a/packages/core-components/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/core-components/src/components/Autocomplete/Autocomplete.tsx @@ -32,10 +32,13 @@ import Autocomplete, { AutocompleteRenderInputParams, } from '@material-ui/lab/Autocomplete'; import React, { ReactNode, useCallback } from 'react'; +import { merge } from 'lodash'; const useStyles = makeStyles( theme => ({ - root: {}, + root: { + margin: theme.spacing(1, 0), + }, label: { position: 'relative', fontWeight: 'bold', @@ -48,7 +51,6 @@ const useStyles = makeStyles( position: 'absolute', }, }, - input: {}, }), { name: 'BackstageAutocomplete' }, ); @@ -67,8 +69,8 @@ const BootstrapAutocomplete = withStyles( marginTop: 24, backgroundColor: theme.palette.background.paper, '$root$hasClearIcon$hasPopupIcon &': { - paddingBlock: theme.spacing(1.5625), - paddingInlineStart: theme.spacing(1.5), + paddingBlock: theme.spacing(0.75), + paddingInlineStart: theme.spacing(0.75), }, '$root$focused &': { outline: 'none', @@ -85,6 +87,9 @@ const BootstrapAutocomplete = withStyles( padding: 0, margin: 0, color: '#616161', + '&:hover': { + backgroundColor: 'unset', + }, '& [class*="MuiTouchRipple-root"]': { display: 'none', }, @@ -97,7 +102,7 @@ const BootstrapAutocomplete = withStyles( input: { '$root$hasClearIcon$hasPopupIcon &': { fontSize: theme.typography.body1.fontSize, - padding: 0, + paddingBlock: theme.spacing(0.8125), }, }, }), @@ -118,25 +123,26 @@ const PaperComponent = (props: PaperProps) => ( ); +/** @public */ export type AutocompleteComponentProps< T, Multiple extends boolean | undefined = undefined, DisableClearable extends boolean | undefined = undefined, FreeSolo extends boolean | undefined = undefined, -> = { +> = Omit< + AutocompleteProps, + 'PopperComponent' | 'PaperComponent' | 'popupIcon' | 'renderInput' +> & { name: string; label?: string; - inputProps?: Omit; + TextFieldProps?: Omit; renderInput?: AutocompleteProps< T, Multiple, DisableClearable, FreeSolo >['renderInput']; -} & Omit< - AutocompleteProps, - 'PopperComponent' | 'PaperComponent' | 'popupIcon' ->; +}; /** @public */ export function AutocompleteComponent< @@ -145,18 +151,13 @@ export function AutocompleteComponent< DisableClearable extends boolean | undefined = undefined, FreeSolo extends boolean | undefined = undefined, >(props: AutocompleteComponentProps) { - const { label, name, inputProps, ...rest } = props; + const { label, name, TextFieldProps, ...rest } = props; const classes = useStyles(); const renderInput = useCallback( (params: AutocompleteRenderInputParams) => ( - + ), - [], + [TextFieldProps], ); const autocomplete = ( ; }; - InputProps?: TextFieldProps['InputProps']; + InputProps?: TextFieldProps; initialSelectedOptions?: string[]; filtersForAvailableValues?: Array; }; diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index d4f978fce5..3f4ec707c6 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -49,7 +49,7 @@ export type EntityAutocompletePickerProps< path: string; showCounts?: boolean; Filter: { new (values: string[]): NonNullable }; - InputProps?: TextFieldProps['InputProps']; + InputProps?: TextFieldProps; initialSelectedOptions?: string[]; filtersForAvailableValues?: Array; }; @@ -79,6 +79,7 @@ export function EntityAutocompletePicker< path, showCounts, Filter, + InputProps, initialSelectedOptions = [], filtersForAvailableValues = ['kind'], } = props; @@ -154,6 +155,7 @@ export function EntityAutocompletePicker< name={`${String(name)}-picker`} options={availableOptions} value={selectedOptions} + TextFieldProps={InputProps} onChange={(_event: object, options: string[]) => setSelectedOptions(options) } From 30021e8dfe1eba27ee29d8a936ad6176af0ca61f Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Tue, 5 Nov 2024 09:47:54 +0000 Subject: [PATCH 04/12] prevent label classnames breaking change Signed-off-by: Jonathan Roebuck --- packages/core-components/report.api.md | 2 ++ .../src/components/Autocomplete/Autocomplete.tsx | 12 +++++++++--- .../EntityOwnerPicker/EntityOwnerPicker.tsx | 4 ++++ .../EntityProcessingStatusPicker.tsx | 4 ++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 5a9f5440ec..b2872a086b 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -53,6 +53,7 @@ import { StyleRules as StyleRules_2 } from '@material-ui/core/styles/withStyles' import { TabProps } from '@material-ui/core/Tab'; import { Theme } from '@material-ui/core/styles'; import { TooltipProps } from '@material-ui/core/Tooltip'; +import { TypographyProps } from '@material-ui/core/Typography'; import { WithStyles } from '@material-ui/core/styles'; // @public @@ -100,6 +101,7 @@ export type AutocompleteComponentProps< > & { name: string; label?: string; + LabelProps?: TypographyProps<'label'>; TextFieldProps?: Omit; renderInput?: AutocompleteProps< T, diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.tsx index a34dd8728a..e01497c5a1 100644 --- a/packages/core-components/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/core-components/src/components/Autocomplete/Autocomplete.tsx @@ -15,7 +15,7 @@ */ import Box from '@material-ui/core/Box'; -import Typography from '@material-ui/core/Typography'; +import Typography, { TypographyProps } from '@material-ui/core/Typography'; import Paper, { PaperProps } from '@material-ui/core/Paper'; import Popper, { PopperProps } from '@material-ui/core/Popper'; import TextField, { OutlinedTextFieldProps } from '@material-ui/core/TextField'; @@ -33,6 +33,7 @@ import Autocomplete, { } from '@material-ui/lab/Autocomplete'; import React, { ReactNode, useCallback } from 'react'; import { merge } from 'lodash'; +import classNames from 'classnames'; const useStyles = makeStyles( theme => ({ @@ -135,6 +136,7 @@ export type AutocompleteComponentProps< > & { name: string; label?: string; + LabelProps?: TypographyProps<'label'>; TextFieldProps?: Omit; renderInput?: AutocompleteProps< T, @@ -151,7 +153,7 @@ export function AutocompleteComponent< DisableClearable extends boolean | undefined = undefined, FreeSolo extends boolean | undefined = undefined, >(props: AutocompleteComponentProps) { - const { label, name, TextFieldProps, ...rest } = props; + const { label, name, LabelProps, TextFieldProps, ...rest } = props; const classes = useStyles(); const renderInput = useCallback( (params: AutocompleteRenderInputParams) => ( @@ -173,7 +175,11 @@ export function AutocompleteComponent< return ( {label ? ( - + {label} {autocomplete} diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index bf8db5f627..e26f231ff4 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -47,6 +47,8 @@ export type CatalogReactEntityOwnerPickerClassKey = 'input'; const useStyles = makeStyles( { root: {}, + label: {}, + input: {}, fullWidth: { width: '100%' }, boxLabel: { width: '100%', @@ -235,6 +237,8 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { }, 'data-testid': 'owner-picker-listbox', }} + LabelProps={{ className: classes.label }} + TextFieldProps={{ className: classes.input }} /> ); diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx index 02d1ddc238..891f6acc9e 100644 --- a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx @@ -33,6 +33,8 @@ export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; const useStyles = makeStyles( { root: {}, + input: {}, + label: {}, }, { name: 'CatalogReactEntityProcessingStatusPickerPicker' }, ); @@ -91,6 +93,8 @@ export const EntityProcessingStatusPicker = () => { /> )} name="processing-status-picker" + LabelProps={{ className: classes.label }} + TextFieldProps={{ className: classes.input }} /> ); From aaf650854bda5c60e6516a54773cd2706e2f4668 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Tue, 5 Nov 2024 10:39:23 +0000 Subject: [PATCH 05/12] add changeset Signed-off-by: Jonathan Roebuck --- .changeset/fluffy-jars-protect.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fluffy-jars-protect.md diff --git a/.changeset/fluffy-jars-protect.md b/.changeset/fluffy-jars-protect.md new file mode 100644 index 0000000000..c4967aded2 --- /dev/null +++ b/.changeset/fluffy-jars-protect.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': minor +'@backstage/plugin-catalog-react': patch +--- + +Uses new Autocomplete component from core-components that aligns with Select component UI for consistent a dropdown UI for all catalog filters From 9e4eb5fc554750ccac8e2f6bd5b07972c777b4c3 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Tue, 5 Nov 2024 17:48:37 +0000 Subject: [PATCH 06/12] fix tests Signed-off-by: Jonathan Roebuck --- .../src/components/Autocomplete/Autocomplete.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx index 445267390a..12269109a4 100644 --- a/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx +++ b/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx @@ -46,7 +46,7 @@ describe('Autocomplete', () => { expect(expandIcon).toBeInTheDocument(); }); - it('displays options when clicked', () => { + it('displays options when clicked', async () => { render( { ); const input = screen.getByRole('textbox'); - user.click(input); + await user.click(input); mockOptions.forEach(option => { expect(screen.getByText(option)).toBeInTheDocument(); @@ -97,7 +97,7 @@ describe('Autocomplete', () => { expect(input).toBeInTheDocument(); }); - it('displays correct option on selection', () => { + it('displays correct option on selection', async () => { render( { ); const input = screen.getByRole('textbox'); - user.click(input); + await user.click(input); const optionToSelect = screen.getByText('Option 1'); - user.click(optionToSelect); + await user.click(optionToSelect); expect(input).toHaveValue('Option 1'); }); From 50ec481ebe219e8e70440be9235a8e0b3e3c1c53 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Mon, 18 Nov 2024 11:30:21 +0000 Subject: [PATCH 07/12] split out core component changeset Signed-off-by: Jonathan Roebuck --- .changeset/eleven-monkeys-cross.md | 5 +++++ .changeset/fluffy-jars-protect.md | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/eleven-monkeys-cross.md diff --git a/.changeset/eleven-monkeys-cross.md b/.changeset/eleven-monkeys-cross.md new file mode 100644 index 0000000000..7a6ae45619 --- /dev/null +++ b/.changeset/eleven-monkeys-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +Introduces a new core component, Autocomplete, which enhances the MUI Autocomplete component with custom input styling, improved popper animation, and better label positioning. This addition will standardize Autocomplete implementations across Backstage and ensure seamless integration with other core components such as Select. diff --git a/.changeset/fluffy-jars-protect.md b/.changeset/fluffy-jars-protect.md index c4967aded2..4fd6b3d194 100644 --- a/.changeset/fluffy-jars-protect.md +++ b/.changeset/fluffy-jars-protect.md @@ -1,6 +1,5 @@ --- -'@backstage/core-components': minor '@backstage/plugin-catalog-react': patch --- -Uses new Autocomplete component from core-components that aligns with Select component UI for consistent a dropdown UI for all catalog filters +Uses new Autocomplete component from core-components that aligns with Select component UI for consistent a dropdown UI for all catalog filters. From b18fe46435971f3ff412405b66a4e8589314978e Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Dec 2024 15:59:50 +0000 Subject: [PATCH 08/12] colocate Autocomplete component in catalog-react Signed-off-by: Jonathan Roebuck --- .../Autocomplete/Autocomplete.stories.tsx | 33 ------------------- .../core-components/src/components/index.ts | 1 - .../CatalogAutocomplete.test.tsx | 18 +++++----- .../CatalogAutocomplete.tsx | 6 ++-- .../components/CatalogAutocomplete}/index.tsx | 6 ++-- .../EntityAutocompletePicker.tsx | 4 +-- .../EntityOwnerPicker/EntityOwnerPicker.tsx | 4 +-- .../EntityProcessingStatusPicker.tsx | 4 +-- 8 files changed, 22 insertions(+), 54 deletions(-) delete mode 100644 packages/core-components/src/components/Autocomplete/Autocomplete.stories.tsx rename packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx => plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.test.tsx (89%) rename packages/core-components/src/components/Autocomplete/Autocomplete.tsx => plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.tsx (97%) rename {packages/core-components/src/components/Autocomplete => plugins/catalog-react/src/components/CatalogAutocomplete}/index.tsx (85%) diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.stories.tsx b/packages/core-components/src/components/Autocomplete/Autocomplete.stories.tsx deleted file mode 100644 index 7aed709125..0000000000 --- a/packages/core-components/src/components/Autocomplete/Autocomplete.stories.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import { AutocompleteComponent as Autocomplete } from './Autocomplete'; - -export default { - title: 'Inputs/Autocomplete', - component: Autocomplete, -}; - -export const Default = (args: any) => { - return ; -}; - -Default.args = { - multiple: true, - label: 'Default', - name: 'default', - options: ['test 1', 'test 2', 'test 3'], -}; diff --git a/packages/core-components/src/components/index.ts b/packages/core-components/src/components/index.ts index 6fcbe2e9a9..828df3ec03 100644 --- a/packages/core-components/src/components/index.ts +++ b/packages/core-components/src/components/index.ts @@ -16,7 +16,6 @@ export * from './AlertDisplay'; export * from './AutoLogout'; -export * from './Autocomplete'; export * from './Avatar'; export * from './LinkButton'; export * from './CodeSnippet'; diff --git a/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx b/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.test.tsx similarity index 89% rename from packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx rename to plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.test.tsx index 12269109a4..51d272f4f1 100644 --- a/packages/core-components/src/components/Autocomplete/Autocomplete.test.tsx +++ b/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { AutocompleteComponent as Autocomplete } from './Autocomplete'; +import { CatalogAutocomplete } from './CatalogAutocomplete'; describe('Autocomplete', () => { const user = userEvent.setup(); @@ -25,7 +25,7 @@ describe('Autocomplete', () => { it('renders without exploding', () => { render( - { it('renders the expand icon', () => { render( - { it('displays options when clicked', async () => { render( - { it('supports required input', () => { render( - { it('displays helper text when provided', () => { render( - { }); it('renders without label', () => { - render(); + render( + , + ); const input = screen.getByRole('textbox'); expect(input).toBeInTheDocument(); @@ -99,7 +101,7 @@ describe('Autocomplete', () => { it('displays correct option on selection', async () => { render( - ( ); /** @public */ -export type AutocompleteComponentProps< +export type CatalogAutocompleteProps< T, Multiple extends boolean | undefined = undefined, DisableClearable extends boolean | undefined = undefined, @@ -147,12 +147,12 @@ export type AutocompleteComponentProps< }; /** @public */ -export function AutocompleteComponent< +export function CatalogAutocomplete< T, Multiple extends boolean | undefined = undefined, DisableClearable extends boolean | undefined = undefined, FreeSolo extends boolean | undefined = undefined, ->(props: AutocompleteComponentProps) { +>(props: CatalogAutocompleteProps) { const { label, name, LabelProps, TextFieldProps, ...rest } = props; const classes = useStyles(); const renderInput = useCallback( diff --git a/packages/core-components/src/components/Autocomplete/index.tsx b/plugins/catalog-react/src/components/CatalogAutocomplete/index.tsx similarity index 85% rename from packages/core-components/src/components/Autocomplete/index.tsx rename to plugins/catalog-react/src/components/CatalogAutocomplete/index.tsx index 404546de1a..7f1552c5bb 100644 --- a/packages/core-components/src/components/Autocomplete/index.tsx +++ b/plugins/catalog-react/src/components/CatalogAutocomplete/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ export { - AutocompleteComponent as Autocomplete, - type AutocompleteComponentProps, -} from './Autocomplete'; + CatalogAutocomplete, + type CatalogAutocompleteProps, +} from './CatalogAutocomplete'; diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index 3f4ec707c6..09b5c1e908 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -27,8 +27,8 @@ import { useEntityList, } from '../../hooks/useEntityListProvider'; import { EntityFilter } from '../../types'; -import { Autocomplete } from '@backstage/core-components'; import { reduceBackendCatalogFilters } from '../../utils/filters'; +import { CatalogAutocomplete } from '../CatalogAutocomplete'; /** @public */ export type AllowedEntityFilters = { @@ -148,7 +148,7 @@ export function EntityAutocompletePicker< return ( - + multiple disableCloseOnSelect label={label} diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index e26f231ff4..b70ec0bcb5 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -27,7 +27,6 @@ import Tooltip from '@material-ui/core/Tooltip'; import { makeStyles } from '@material-ui/core/styles'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; -import { Autocomplete } from '@backstage/core-components'; import React, { useEffect, useMemo, useState } from 'react'; import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityOwnerFilter } from '../../filters'; @@ -40,6 +39,7 @@ import { withStyles } from '@material-ui/core/styles'; import { useEntityPresentation } from '../../apis'; import { catalogReactTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { CatalogAutocomplete } from '../CatalogAutocomplete'; /** @public */ export type CatalogReactEntityOwnerPickerClassKey = 'input'; @@ -177,7 +177,7 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { return ( - + label={t('entityOwnerPicker.title')} multiple disableCloseOnSelect diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx index 891f6acc9e..a1b6e5b562 100644 --- a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx @@ -23,9 +23,9 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import React, { useState } from 'react'; import { useEntityList } from '../../hooks'; -import { Autocomplete } from '@backstage/core-components'; import { catalogReactTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { CatalogAutocomplete } from '../CatalogAutocomplete'; /** @public */ export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; @@ -68,7 +68,7 @@ export const EntityProcessingStatusPicker = () => { return ( - + label={t('entityProcessingStatusPicker.title')} multiple disableCloseOnSelect From 4b264730f49071144129623fd5f62213d4fcd3ab Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Dec 2024 16:09:43 +0000 Subject: [PATCH 09/12] update changesets Signed-off-by: Jonathan Roebuck --- .changeset/eleven-monkeys-cross.md | 5 ----- .changeset/fluffy-jars-protect.md | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .changeset/eleven-monkeys-cross.md diff --git a/.changeset/eleven-monkeys-cross.md b/.changeset/eleven-monkeys-cross.md deleted file mode 100644 index 7a6ae45619..0000000000 --- a/.changeset/eleven-monkeys-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': minor ---- - -Introduces a new core component, Autocomplete, which enhances the MUI Autocomplete component with custom input styling, improved popper animation, and better label positioning. This addition will standardize Autocomplete implementations across Backstage and ensure seamless integration with other core components such as Select. diff --git a/.changeset/fluffy-jars-protect.md b/.changeset/fluffy-jars-protect.md index 4fd6b3d194..89ff39d9af 100644 --- a/.changeset/fluffy-jars-protect.md +++ b/.changeset/fluffy-jars-protect.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-react': patch --- -Uses new Autocomplete component from core-components that aligns with Select component UI for consistent a dropdown UI for all catalog filters. +Creates new CatalogAutocomplete component in catalog-react that aligns with Select component UI for consistent a dropdown UI for all catalog filters. From e8778469ebe7adf04b81bd0138cfbdf90d67d645 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Dec 2024 16:14:04 +0000 Subject: [PATCH 10/12] remove public comments Signed-off-by: Jonathan Roebuck --- .../components/CatalogAutocomplete/CatalogAutocomplete.test.tsx | 2 +- .../src/components/CatalogAutocomplete/CatalogAutocomplete.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.test.tsx b/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.test.tsx index 51d272f4f1..e574919611 100644 --- a/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.test.tsx +++ b/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.test.tsx @@ -19,7 +19,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { CatalogAutocomplete } from './CatalogAutocomplete'; -describe('Autocomplete', () => { +describe('CatalogAutocomplete', () => { const user = userEvent.setup(); const mockOptions = ['Option 1', 'Option 2', 'Option 3']; diff --git a/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.tsx b/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.tsx index 99df8dcad4..af41db8626 100644 --- a/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.tsx +++ b/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.tsx @@ -124,7 +124,6 @@ const PaperComponent = (props: PaperProps) => ( ); -/** @public */ export type CatalogAutocompleteProps< T, Multiple extends boolean | undefined = undefined, @@ -146,7 +145,6 @@ export type CatalogAutocompleteProps< >['renderInput']; }; -/** @public */ export function CatalogAutocomplete< T, Multiple extends boolean | undefined = undefined, From 893e92f0b8f1a9810c1f1ba2a8cc4de6b793da01 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Dec 2024 16:17:24 +0000 Subject: [PATCH 11/12] rebuild api docs Signed-off-by: Jonathan Roebuck --- packages/core-components/report.api.md | 35 -------------------------- 1 file changed, 35 deletions(-) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index b2872a086b..c3f7cf9a6a 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -6,7 +6,6 @@ /// import { ApiRef } from '@backstage/core-plugin-api'; -import { AutocompleteProps } from '@material-ui/lab/Autocomplete'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; import { BackstageUserIdentity } from '@backstage/core-plugin-api'; @@ -34,7 +33,6 @@ import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Options } from 'react-markdown'; import { Options as Options_2 } from '@material-table/core'; -import { OutlinedTextFieldProps } from '@material-ui/core/TextField'; import { Overrides } from '@material-ui/core/styles/overrides'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -53,7 +51,6 @@ import { StyleRules as StyleRules_2 } from '@material-ui/core/styles/withStyles' import { TabProps } from '@material-ui/core/Tab'; import { Theme } from '@material-ui/core/styles'; import { TooltipProps } from '@material-ui/core/Tooltip'; -import { TypographyProps } from '@material-ui/core/Typography'; import { WithStyles } from '@material-ui/core/styles'; // @public @@ -79,38 +76,6 @@ export type AppIconProps = IconComponentProps & { Fallback?: IconComponent; }; -// @public (undocumented) -export function Autocomplete< - T, - Multiple extends boolean | undefined = undefined, - DisableClearable extends boolean | undefined = undefined, - FreeSolo extends boolean | undefined = undefined, ->( - props: AutocompleteComponentProps, -): React_2.JSX.Element; - -// @public (undocumented) -export type AutocompleteComponentProps< - T, - Multiple extends boolean | undefined = undefined, - DisableClearable extends boolean | undefined = undefined, - FreeSolo extends boolean | undefined = undefined, -> = Omit< - AutocompleteProps, - 'PopperComponent' | 'PaperComponent' | 'popupIcon' | 'renderInput' -> & { - name: string; - label?: string; - LabelProps?: TypographyProps<'label'>; - TextFieldProps?: Omit; - renderInput?: AutocompleteProps< - T, - Multiple, - DisableClearable, - FreeSolo - >['renderInput']; -}; - // @public export const AutoLogout: (props: AutoLogoutProps) => JSX.Element | null; From e74ec5a83b601335663977f637d85c62695f1956 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Tue, 7 Jan 2025 09:12:55 +0000 Subject: [PATCH 12/12] add CatalogAutocomplete story Signed-off-by: Jonathan Roebuck --- .../CatalogAutocomplete.stories.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.stories.tsx diff --git a/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.stories.tsx b/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.stories.tsx new file mode 100644 index 0000000000..3dee44d27a --- /dev/null +++ b/plugins/catalog-react/src/components/CatalogAutocomplete/CatalogAutocomplete.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { ComponentType } from 'react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { CatalogAutocomplete } from './CatalogAutocomplete'; + +export default { + title: 'Catalog /CatalogAutocomplete', + decorators: [(Story: ComponentType<{}>) => wrapInTestApp()], +}; + +export const Default = (args: any) => { + return ; +}; + +Default.args = { + multiple: true, + label: 'Default', + name: 'default', + options: ['test 1', 'test 2', 'test 3'], +};