Introduce a useStarredEntity hook to check if a single entity is starred
Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Introduce a `useStarredEntity` hook to check if a single entity is starred.
|
||||
It provides a more efficient implementation compared to the `useStarredEntities` hook, because the rendering is only triggered if the selected entity is starred, not if _any_ entity is starred.
|
||||
@@ -894,6 +894,14 @@ export function useStarredEntities(): {
|
||||
isStarredEntity: (entityOrRef: Entity | EntityName | string) => boolean;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "useStarredEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export function useStarredEntity(entityOrRef: Entity | EntityName | string): {
|
||||
toggleStarredEntity: () => void;
|
||||
isStarredEntity: boolean;
|
||||
};
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/types.d.ts:6:49 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntities';
|
||||
import { IconButton, Tooltip, withStyles } from '@material-ui/core';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { IconButton, Tooltip, withStyles } from '@material-ui/core';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { useStarredEntity } from '../../hooks/useStarredEntity';
|
||||
|
||||
type Props = ComponentProps<typeof IconButton> & { entity: Entity };
|
||||
|
||||
@@ -40,16 +40,17 @@ export const favoriteEntityIcon = (isStarred: boolean) =>
|
||||
* @param props MaterialUI IconButton props extended by required `entity` prop
|
||||
*/
|
||||
export const FavoriteEntity = (props: Props) => {
|
||||
const { toggleStarredEntity, isStarredEntity } = useStarredEntities();
|
||||
const isStarred = isStarredEntity(props.entity);
|
||||
const { toggleStarredEntity, isStarredEntity } = useStarredEntity(
|
||||
props.entity,
|
||||
);
|
||||
return (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
{...props}
|
||||
onClick={() => toggleStarredEntity(props.entity)}
|
||||
onClick={() => toggleStarredEntity()}
|
||||
>
|
||||
<Tooltip title={favoriteEntityTooltip(isStarred)}>
|
||||
{favoriteEntityIcon(isStarred)}
|
||||
<Tooltip title={favoriteEntityTooltip(isStarredEntity)}>
|
||||
{favoriteEntityIcon(isStarredEntity)}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
@@ -36,4 +36,5 @@ export { useEntityKinds } from './useEntityKinds';
|
||||
export { useOwnUser } from './useOwnUser';
|
||||
export { useRelatedEntities } from './useRelatedEntities';
|
||||
export { useStarredEntities } from './useStarredEntities';
|
||||
export { useStarredEntity } from './useStarredEntity';
|
||||
export { useEntityOwnership } from './useEntityOwnership';
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import Observable from 'zen-observable';
|
||||
import { StarredEntitiesApi, starredEntitiesApiRef } from '../apis';
|
||||
import { useStarredEntity } from './useStarredEntity';
|
||||
|
||||
describe('useStarredEntity', () => {
|
||||
const mockStarredEntitiesApi: jest.Mocked<StarredEntitiesApi> = {
|
||||
toggleStarred: jest.fn(),
|
||||
starredEntitie$: jest.fn(),
|
||||
};
|
||||
let wrapper: React.ComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = ({ children }: PropsWithChildren<{}>) => (
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.with(starredEntitiesApiRef, mockStarredEntitiesApi)}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe.each`
|
||||
title | entityOrRef
|
||||
${'entity reference'} | ${'component:default/mock'}
|
||||
${'entity name'} | ${{ kind: 'component', namespace: 'default', name: 'mock' } as EntityName}
|
||||
${'entity'} | ${{ apiVersion: '1', kind: 'Component', metadata: { name: 'mock' } } as Entity}
|
||||
`('with $title', ({ entityOrRef }) => {
|
||||
describe('toggleStarredEntity', () => {
|
||||
it('should toggle starred entity', () => {
|
||||
mockStarredEntitiesApi.starredEntitie$.mockReturnValue(Observable.of());
|
||||
mockStarredEntitiesApi.toggleStarred.mockResolvedValue();
|
||||
|
||||
const { result } = renderHook(() => useStarredEntity(entityOrRef), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
result.current.toggleStarredEntity();
|
||||
|
||||
expect(mockStarredEntitiesApi.toggleStarred).toBeCalledTimes(1);
|
||||
expect(mockStarredEntitiesApi.toggleStarred).toBeCalledWith(
|
||||
'component:default/mock',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStarredEntity', () => {
|
||||
it('should return not starred entity', () => {
|
||||
mockStarredEntitiesApi.starredEntitie$.mockReturnValue(Observable.of());
|
||||
mockStarredEntitiesApi.toggleStarred.mockResolvedValue();
|
||||
|
||||
const { result } = renderHook(() => useStarredEntity(entityOrRef), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.isStarredEntity).toBe(false);
|
||||
});
|
||||
|
||||
it('should return starred entity', async () => {
|
||||
mockStarredEntitiesApi.starredEntitie$.mockReturnValue(
|
||||
Observable.of(new Set(['component:default/mock'])),
|
||||
);
|
||||
mockStarredEntitiesApi.toggleStarred.mockResolvedValue();
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useStarredEntity(entityOrRef),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
// the initial value will always be false because the observable triggers async
|
||||
expect(result.current.isStarredEntity).toBe(false);
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.isStarredEntity).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2021 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 {
|
||||
Entity,
|
||||
EntityName,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { starredEntitiesApiRef } from '../apis';
|
||||
|
||||
function getEntityRef(entityOrRef: Entity | EntityName | string): string {
|
||||
return typeof entityOrRef === 'string'
|
||||
? entityOrRef
|
||||
: stringifyEntityRef(entityOrRef);
|
||||
}
|
||||
|
||||
export function useStarredEntity(entityOrRef: Entity | EntityName | string): {
|
||||
toggleStarredEntity: () => void;
|
||||
isStarredEntity: boolean;
|
||||
} {
|
||||
const starredEntitiesApi = useApi(starredEntitiesApiRef);
|
||||
|
||||
const [isStarredEntity, setIsStarredEntity] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = starredEntitiesApi.starredEntitie$().subscribe({
|
||||
next(starredEntities: Set<string>) {
|
||||
setIsStarredEntity(starredEntities.has(getEntityRef(entityOrRef)));
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [entityOrRef, starredEntitiesApi]);
|
||||
|
||||
const toggleStarredEntity = useCallback(
|
||||
() => starredEntitiesApi.toggleStarred(getEntityRef(entityOrRef)).then(),
|
||||
[entityOrRef, starredEntitiesApi],
|
||||
);
|
||||
|
||||
return {
|
||||
toggleStarredEntity,
|
||||
isStarredEntity,
|
||||
};
|
||||
}
|
||||
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps, useMemo } from 'react';
|
||||
import { useStarredEntities } from '@backstage/plugin-catalog-react';
|
||||
import { IconButton, makeStyles, Tooltip, withStyles } from '@material-ui/core';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useStarredEntity } from '@backstage/plugin-catalog-react';
|
||||
import { IconButton, makeStyles, Tooltip, withStyles } from '@material-ui/core';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import React, { ComponentProps } from 'react';
|
||||
|
||||
type Props = ComponentProps<typeof IconButton> & { entity: Entity };
|
||||
|
||||
@@ -56,20 +56,18 @@ export const favouriteTemplateIcon = (isStarred: boolean) =>
|
||||
*/
|
||||
export const FavouriteTemplate = (props: Props) => {
|
||||
const classes = useStyles();
|
||||
const { toggleStarredEntity, isStarredEntity } = useStarredEntities();
|
||||
const isStarred = useMemo(
|
||||
() => isStarredEntity(props.entity),
|
||||
[isStarredEntity, props.entity],
|
||||
const { toggleStarredEntity, isStarredEntity } = useStarredEntity(
|
||||
props.entity,
|
||||
);
|
||||
return (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
className={classes.starButton}
|
||||
{...props}
|
||||
onClick={() => toggleStarredEntity(props.entity)}
|
||||
onClick={() => toggleStarredEntity()}
|
||||
>
|
||||
<Tooltip title={favouriteTemplateTooltip(isStarred)}>
|
||||
{favouriteTemplateIcon(isStarred)}
|
||||
<Tooltip title={favouriteTemplateTooltip(isStarredEntity)}>
|
||||
{favouriteTemplateIcon(isStarredEntity)}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user