feat: add overrideable FavoriteToggle to core-components
Signed-off-by: Carl-Erik Bergström <cbergstrom@spotify.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-home': patch
|
||||
---
|
||||
|
||||
Add `FavoriteToggle` in `core-components` to standardise favorite marking
|
||||
@@ -19,6 +19,7 @@ import { default as CSS_2 } from 'csstype';
|
||||
import { CSSProperties } from 'react';
|
||||
import { ElementType } from 'react';
|
||||
import { ErrorInfo } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { Icons } from '@material-table/core';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
@@ -401,6 +402,32 @@ export type ErrorPanelProps = {
|
||||
title?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function FavoriteToggle({
|
||||
id,
|
||||
title,
|
||||
isFavorite: value,
|
||||
onToggle: onChange,
|
||||
...iconButtonProps
|
||||
}: FavoriteToggleProps): React_2.JSX.Element;
|
||||
|
||||
// @public
|
||||
export function FavoriteToggleIcon({
|
||||
isFavorite,
|
||||
}: {
|
||||
isFavorite: boolean;
|
||||
}): React_2.JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "FavoriteToggleProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type FavoriteToggleProps = ComponentProps<typeof IconButton> & {
|
||||
id: string;
|
||||
title: string;
|
||||
isFavorite: boolean;
|
||||
onToggle: (value: boolean) => void;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type FeatureCalloutCircleClassKey =
|
||||
| '@keyframes pulsateSlightly'
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 { FavoriteToggle } from './FavoriteToggle';
|
||||
import {
|
||||
UnifiedThemeProvider,
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export default {
|
||||
title: 'Core/FavoriteToggle',
|
||||
component: FavoriteToggle,
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const [isFavorite, setFavorite] = React.useState(false);
|
||||
return (
|
||||
<FavoriteToggle
|
||||
id="favorite-toggle"
|
||||
title="Add entity to favorites"
|
||||
isFavorite={isFavorite}
|
||||
onToggle={setFavorite}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const theme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.dark,
|
||||
}),
|
||||
components: {
|
||||
BackstageFavoriteToggleIcon: {
|
||||
styleOverrides: {
|
||||
icon: () => ({ color: 'aqua' }),
|
||||
iconBorder: () => ({ color: 'white' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const WithThemeOverride = () => {
|
||||
const [isFavorite, setFavorite] = React.useState(false);
|
||||
return (
|
||||
<UnifiedThemeProvider theme={theme}>
|
||||
<FavoriteToggle
|
||||
id="favorite-toggle"
|
||||
title="Add entity to favorites"
|
||||
isFavorite={isFavorite}
|
||||
onToggle={setFavorite}
|
||||
/>
|
||||
</UnifiedThemeProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { FavoriteToggle, FavoriteToggleProps } from './FavoriteToggle';
|
||||
import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('<FavoriteToggle />', () => {
|
||||
const onToggle = jest.fn();
|
||||
|
||||
const props: FavoriteToggleProps = {
|
||||
title: 'Favorite this thing',
|
||||
id: 'some-thing-favorite',
|
||||
onToggle,
|
||||
isFavorite: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders with valid props', async () => {
|
||||
const { getByRole } = render(<FavoriteToggle {...props} />);
|
||||
|
||||
expect(getByRole('button', { name: props.title })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return inverted value on toggle', async () => {
|
||||
const { getByRole } = render(<FavoriteToggle {...props} />);
|
||||
|
||||
await userEvent.click(getByRole('button', { name: props.title }));
|
||||
expect(onToggle).toHaveBeenCalledWith(!props.isFavorite);
|
||||
});
|
||||
|
||||
it('should show accessible tooltip', async () => {
|
||||
const { findByRole, getByRole } = render(<FavoriteToggle {...props} />);
|
||||
|
||||
await userEvent.hover(getByRole('button', { name: props.title }));
|
||||
|
||||
expect(await findByRole('tooltip')).toHaveTextContent(props.title);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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, { ComponentProps } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { Theme, makeStyles } from '@material-ui/core/styles';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
|
||||
const useStyles = makeStyles<Theme>(
|
||||
theme => ({
|
||||
icon: {
|
||||
color: '#f3ba37',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
iconBorder: {
|
||||
color: theme.palette.text.primary,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
{ name: 'BackstageFavoriteToggleIcon' },
|
||||
);
|
||||
|
||||
// @public (undocumented)
|
||||
export type FavoriteToggleIconClassKey = 'icon' | 'iconBorder';
|
||||
|
||||
// @public (undocumented)
|
||||
export type FavoriteToggleProps = ComponentProps<typeof IconButton> & {
|
||||
id: string;
|
||||
title: string;
|
||||
isFavorite: boolean;
|
||||
onToggle: (value: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Icon used in FavoriteToggle component.
|
||||
*
|
||||
* Can be used independently, useful when used as {@link @material-table/core#MaterialTableProps.actions} in {@link @material-table/core#MaterialTable}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function FavoriteToggleIcon({ isFavorite }: { isFavorite: boolean }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return isFavorite ? (
|
||||
<Star className={classes.icon} />
|
||||
) : (
|
||||
<StarBorder className={classes.iconBorder} />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle encapsulating logic for marking something as favorite,
|
||||
* primarily used in various instances of entity lists and cards but can be used elsewhere.
|
||||
*
|
||||
* This component can only be used in as a controlled toggle and does not keep internal state.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function FavoriteToggle({
|
||||
id,
|
||||
title,
|
||||
isFavorite: value,
|
||||
onToggle: onChange,
|
||||
...iconButtonProps
|
||||
}: FavoriteToggleProps) {
|
||||
return (
|
||||
<Tooltip id={id} title={title}>
|
||||
<IconButton
|
||||
aria-label={title}
|
||||
id={id}
|
||||
onClick={() => onChange(!value)}
|
||||
{...iconButtonProps}
|
||||
>
|
||||
<FavoriteToggleIcon isFavorite={value} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { FavoriteToggle, FavoriteToggleIcon } from './FavoriteToggle';
|
||||
export type { FavoriteToggleProps } from './FavoriteToggle';
|
||||
@@ -25,6 +25,7 @@ export * from './DependencyGraph';
|
||||
export * from './DismissableBanner';
|
||||
export * from './EmptyState';
|
||||
export * from './ErrorPanel';
|
||||
export * from './FavoriteToggle';
|
||||
export * from './ResponseErrorPanel';
|
||||
export * from './FeatureDiscovery';
|
||||
export * from './HeaderIconLinkRow';
|
||||
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
BoldHeaderClassKey,
|
||||
CardTabClassKey,
|
||||
} from './layout';
|
||||
import { FavoriteToggleIconClassKey } from './components/FavoriteToggle/FavoriteToggle';
|
||||
|
||||
type BackstageComponentsNameToClassKey = {
|
||||
BackstageAvatar: AvatarClassKey;
|
||||
@@ -163,6 +164,7 @@ type BackstageComponentsNameToClassKey = {
|
||||
BackstageTabbedCard: TabbedCardClassKey;
|
||||
BackstageTabbedCardBoldHeader: BoldHeaderClassKey;
|
||||
BackstageCardTab: CardTabClassKey;
|
||||
BackstageFavoriteToggleIcon: FavoriteToggleIconClassKey;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -16,26 +16,17 @@
|
||||
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { useStarredEntity } from '../../hooks/useStarredEntity';
|
||||
import { catalogReactTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { FavoriteToggle } from '@backstage/core-components';
|
||||
|
||||
/** @public */
|
||||
export type FavoriteEntityProps = ComponentProps<typeof IconButton> & {
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
const YellowStar = withStyles({
|
||||
root: {
|
||||
color: '#f3ba37',
|
||||
},
|
||||
})(Star);
|
||||
|
||||
/**
|
||||
* IconButton for showing if a current entity is starred and adding/removing it from the favorite entities
|
||||
* @param props - MaterialUI IconButton props extended by required `entity` prop
|
||||
@@ -56,16 +47,12 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => {
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={title}
|
||||
<FavoriteToggle
|
||||
title={title}
|
||||
id={id}
|
||||
color="inherit"
|
||||
isFavorite={isStarredEntity}
|
||||
onToggle={toggleStarredEntity}
|
||||
{...props}
|
||||
onClick={() => toggleStarredEntity()}
|
||||
>
|
||||
<Tooltip id={id} title={title}>
|
||||
{isStarredEntity ? <YellowStar /> : <StarBorder />}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,12 +35,9 @@ import {
|
||||
useStarredEntities,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { visuallyHidden } from '@mui/utils';
|
||||
import Edit from '@material-ui/icons/Edit';
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import { capitalize } from 'lodash';
|
||||
import pluralize from 'pluralize';
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
@@ -50,6 +47,7 @@ import { PaginatedCatalogTable } from './PaginatedCatalogTable';
|
||||
import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogTranslationRef } from '../../alpha/translation';
|
||||
import { FavoriteToggleIcon } from '@backstage/core-components';
|
||||
|
||||
/**
|
||||
* Props for {@link CatalogTable}.
|
||||
@@ -64,12 +62,6 @@ export interface CatalogTableProps {
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
const YellowStar = withStyles({
|
||||
root: {
|
||||
color: '#f3ba37',
|
||||
},
|
||||
})(Star);
|
||||
|
||||
const refCompare = (a: Entity, b: Entity) => {
|
||||
const toRef = (entity: Entity) =>
|
||||
entity.metadata.title ||
|
||||
@@ -160,12 +152,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
|
||||
return {
|
||||
cellStyle: { paddingLeft: '1em' },
|
||||
icon: () => (
|
||||
<>
|
||||
<Typography style={visuallyHidden}>{title}</Typography>
|
||||
{isStarred ? <YellowStar /> : <StarBorder />}
|
||||
</>
|
||||
),
|
||||
icon: () => <FavoriteToggleIcon isFavorite={isStarred} />,
|
||||
tooltip: title,
|
||||
onClick: () => toggleStarredEntity(entity),
|
||||
};
|
||||
|
||||
@@ -17,14 +17,12 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { entityRouteParams } from '@backstage/plugin-catalog-react';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import { FavoriteToggle } from '@backstage/core-components';
|
||||
|
||||
type EntityListItemProps = {
|
||||
entity: Entity;
|
||||
@@ -40,15 +38,12 @@ export const StarredEntityListItem = ({
|
||||
return (
|
||||
<ListItem key={stringifyEntityRef(entity)}>
|
||||
<ListItemIcon>
|
||||
<Tooltip title="Remove from starred">
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="unstar"
|
||||
onClick={() => onToggleStarredEntity(entity)}
|
||||
>
|
||||
<StarIcon style={{ color: '#f3ba37' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<FavoriteToggle
|
||||
id={`remove-favorite-${entity.metadata.uid}`}
|
||||
title="Remove entity from favorites"
|
||||
isFavorite
|
||||
onToggle={() => onToggleStarredEntity(entity)}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<Link to={catalogEntityRoute(entityRouteParams(entity))}>
|
||||
<ListItemText primary={entity.metadata.title ?? entity.metadata.name} />
|
||||
|
||||
@@ -17,15 +17,7 @@
|
||||
import React from 'react';
|
||||
import ShareIcon from '@material-ui/icons/Share';
|
||||
import { DocsTableRow } from './types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
|
||||
const YellowStar = withStyles({
|
||||
root: {
|
||||
color: '#f3ba37',
|
||||
},
|
||||
})(Star);
|
||||
import { FavoriteToggleIcon } from '@backstage/core-components';
|
||||
|
||||
/**
|
||||
* Not directly exported, but through DocsTable.actions and EntityListDocsTable.actions
|
||||
@@ -52,7 +44,7 @@ export const actionFactories = {
|
||||
const isStarred = isStarredEntity(entity);
|
||||
return {
|
||||
cellStyle: { paddingLeft: '1em' },
|
||||
icon: () => (isStarred ? <YellowStar /> : <StarBorder />),
|
||||
icon: () => <FavoriteToggleIcon isFavorite={isStarred} />,
|
||||
tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites',
|
||||
onClick: () => toggleStarredEntity(entity),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user