Merge pull request #16955 from antoniobergas/feat/playlist-dynamic-group-noun

[Playlist] feat: add dynamic group noun through config
This commit is contained in:
Ben Lambert
2023-04-04 10:08:13 +02:00
committed by GitHub
15 changed files with 227 additions and 74 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-playlist': patch
---
Added config properties to change dynamically the group noun for all the components in the UI
+9
View File
@@ -131,6 +131,15 @@ const defaultEntityPage = (
Note: the above only shows an example for the `defaultEntityPage` for a full example of this you can look at [this EntityPage](../../packages/app/src/components/catalog/EntityPage.tsx)
## Custom Title
You can define a custom title to be shown in all the components of this plugin to replace the default term "playlist" in the UI. To do this you just need to add some config in your **app-config.yaml**, here's an example:
```yaml
playlist:
title: Collection
```
## Features
### View All Playlists
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2023 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 interface Config {
playlist?: {
/**
* (Optional) The title that will shown in the UI; Defaults to`Playlist` if undefined.
* @visibility frontend
*/
title?: string | undefined;
};
}
+5 -2
View File
@@ -42,6 +42,7 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"lodash": "^4.17.21",
"pluralize": "^8.0.0",
"qs": "^6.9.4",
"react-hook-form": "^7.13.0",
"react-use": "^17.2.4"
@@ -68,6 +69,8 @@
"swr": "^2.0.0"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -29,6 +29,7 @@ import { useNavigate } from 'react-router-dom';
import { playlistApiRef } from '../../api';
import { playlistRouteRef } from '../../routes';
import { PlaylistEditDialog } from '../PlaylistEditDialog';
import { useTitle } from '../../hooks';
export const CreatePlaylistButton = () => {
const navigate = useNavigate();
@@ -55,13 +56,18 @@ export const CreatePlaylistButton = () => {
[errorApi, navigate, playlistApi, playlistRoute],
);
const singularTitle = useTitle({
pluralize: false,
lowerCase: false,
});
return (
<>
{isXSScreen ? (
<IconButton
disabled={!allowed}
color="primary"
title="Create Playlist"
title={`Create ${singularTitle}`}
size="small"
onClick={() => setOpenDialog(true)}
>
@@ -74,7 +80,7 @@ export const CreatePlaylistButton = () => {
color="primary"
onClick={() => setOpenDialog(true)}
>
Create Playlist
Create {singularTitle}
</Button>
)}
<PlaylistEditDialog
@@ -51,6 +51,7 @@ import { useNavigate } from 'react-router-dom';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import { playlistApiRef } from '../../api';
import { useTitle } from '../../hooks';
import { playlistRouteRef } from '../../routes';
import { PlaylistEditDialog } from '../PlaylistEditDialog';
@@ -104,6 +105,19 @@ export const EntityPlaylistDialog = (props: EntityPlaylistDialogProps) => {
[playlistApi],
);
const singularTitle = useTitle({
pluralize: true,
lowerCase: false,
});
const singularTitleLowerCase = useTitle({
pluralize: false,
lowerCase: true,
});
const pluralTitleLowerCase = useTitle({
pluralize: true,
lowerCase: true,
});
useEffect(() => {
if (open) {
loadPlaylists();
@@ -120,12 +134,19 @@ export const EntityPlaylistDialog = (props: EntityPlaylistDialogProps) => {
navigate(playlistRoute({ playlistId }));
} catch (e) {
alertApi.post({
message: `Failed to add entity to playlist: ${e}`,
message: `Failed to add entity to ${singularTitleLowerCase}: ${e}`,
severity: 'error',
});
}
},
[alertApi, entity, navigate, playlistApi, playlistRoute],
[
alertApi,
entity,
navigate,
playlistApi,
playlistRoute,
singularTitleLowerCase,
],
);
const [{ loading: addEntityLoading }, addToPlaylist] = useAsyncFn(
@@ -141,12 +162,12 @@ export const EntityPlaylistDialog = (props: EntityPlaylistDialogProps) => {
});
} catch (e) {
alertApi.post({
message: `Failed to add entity to playlist: ${e}`,
message: `Failed to add entity to ${singularTitleLowerCase}: ${e}`,
severity: 'error',
});
}
},
[alertApi, closeDialog, entity, playlistApi],
[alertApi, closeDialog, entity, playlistApi, singularTitleLowerCase],
);
return (
@@ -160,7 +181,7 @@ export const EntityPlaylistDialog = (props: EntityPlaylistDialogProps) => {
>
{(loading || addEntityLoading) && <LinearProgress />}
<DialogTitle className={classes.dialogTitle}>
Add to Playlist
Add to {singularTitle}
<TextField
fullWidth
data-testid="entity-playlist-dialog-search"
@@ -191,7 +212,10 @@ export const EntityPlaylistDialog = (props: EntityPlaylistDialogProps) => {
</DialogTitle>
<DialogContent className={classes.dialogContent}>
{error && (
<ResponseErrorPanel title="Error loading playlists" error={error} />
<ResponseErrorPanel
title={`Error loading ${pluralTitleLowerCase}`}
error={error}
/>
)}
{playlists && entity && (
<List>
@@ -205,7 +229,9 @@ export const EntityPlaylistDialog = (props: EntityPlaylistDialogProps) => {
<ListItemIcon>
<PlaylistAddIcon />
</ListItemIcon>
<ListItemText primary="Create new playlist" />
<ListItemText
primary={`Create new ${singularTitleLowerCase}`}
/>
</ListItem>
)}
{playlists
@@ -39,6 +39,7 @@ import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import useAsync from 'react-use/lib/useAsync';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import { useTitle } from '../../hooks';
const useStyles = makeStyles({
buttonWrapper: {
@@ -105,6 +106,11 @@ export const PlaylistEditDialog = ({
}
};
const titleSingularLowerCase = useTitle({
pluralize: false,
lowerCase: false,
});
return (
<Dialog fullWidth maxWidth="xs" onClose={closeDialog} open={open}>
<DialogContent>
@@ -121,7 +127,7 @@ export const PlaylistEditDialog = ({
fullWidth
label="Name"
margin="dense"
placeholder="Give your playlist name"
placeholder={`Give your ${titleSingularLowerCase} a name`}
required
type="text"
/>
@@ -139,7 +145,7 @@ export const PlaylistEditDialog = ({
label="Description"
margin="dense"
multiline
placeholder="Describe your playlist"
placeholder={`Describe your ${titleSingularLowerCase}`}
type="text"
/>
)}
@@ -23,7 +23,7 @@ import {
} from '@backstage/core-components';
import { CatalogFilterLayout } from '@backstage/plugin-catalog-react';
import { PlaylistListProvider } from '../../hooks';
import { PlaylistListProvider, useTitle } from '../../hooks';
import { CreatePlaylistButton } from '../CreatePlaylistButton';
import { PersonalListPicker } from '../PersonalListPicker';
import { PlaylistList } from '../PlaylistList';
@@ -31,26 +31,33 @@ import { PlaylistOwnerPicker } from '../PlaylistOwnerPicker';
import { PlaylistSearchBar } from '../PlaylistSearchBar';
import { PlaylistSortPicker } from '../PlaylistSortPicker';
export const PlaylistIndexPage = () => (
<PageWithHeader themeId="home" title="Playlists">
<PlaylistListProvider>
<Content>
<ContentHeader title="">
<PlaylistSortPicker />
<CreatePlaylistButton />
<SupportButton />
</ContentHeader>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<PlaylistSearchBar />
<PersonalListPicker />
<PlaylistOwnerPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<PlaylistList />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</Content>
</PlaylistListProvider>
</PageWithHeader>
);
export const PlaylistIndexPage = () => {
const pluralTitle = useTitle({
pluralize: true,
lowerCase: false,
});
return (
<PageWithHeader themeId="home" title={pluralTitle}>
<PlaylistListProvider>
<Content>
<ContentHeader title="">
<PlaylistSortPicker />
<CreatePlaylistButton />
<SupportButton />
</ContentHeader>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<PlaylistSearchBar />
<PersonalListPicker />
<PlaylistOwnerPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<PlaylistList />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</Content>
</PlaylistListProvider>
</PageWithHeader>
);
};
@@ -14,13 +14,19 @@
* limitations under the License.
*/
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
import { Playlist } from '@backstage/plugin-playlist-common';
import { TestApiProvider } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import React from 'react';
import { MockPlaylistListProvider } from '../../testUtils';
import { PlaylistList } from './PlaylistList';
const mockConfigApi = {
getOptionalString: () => undefined,
} as Partial<ConfigApi>;
jest.mock('../PlaylistCard', () => ({
PlaylistCard: ({ playlist }: { playlist: Playlist }) => (
<div>{playlist.name}</div>
@@ -30,9 +36,11 @@ jest.mock('../PlaylistCard', () => ({
describe('<PlaylistList/>', () => {
it('renders error on error', () => {
const rendered = render(
<MockPlaylistListProvider value={{ error: new Error('Test Error') }}>
<PlaylistList />
</MockPlaylistListProvider>,
<TestApiProvider apis={[[configApiRef, mockConfigApi]]}>
<MockPlaylistListProvider value={{ error: new Error('Test Error') }}>
<PlaylistList />
</MockPlaylistListProvider>
</TestApiProvider>,
);
expect(rendered.getByText('Test Error')).toBeInTheDocument();
@@ -40,9 +48,11 @@ describe('<PlaylistList/>', () => {
it('handles no playlists', () => {
const rendered = render(
<MockPlaylistListProvider value={{ playlists: [] }}>
<PlaylistList />
</MockPlaylistListProvider>,
<TestApiProvider apis={[[configApiRef, mockConfigApi]]}>
<MockPlaylistListProvider value={{ playlists: [] }}>
<PlaylistList />
</MockPlaylistListProvider>
</TestApiProvider>,
);
expect(
@@ -52,32 +62,34 @@ describe('<PlaylistList/>', () => {
it('renders playlists', () => {
const rendered = render(
<MockPlaylistListProvider
value={{
playlists: [
{
id: 'id1',
name: 'playlist-1',
owner: 'group:default/some-owner',
public: true,
entities: 1,
followers: 2,
isFollowing: false,
},
{
id: 'id2',
name: 'playlist-2',
owner: 'group:default/another-owner',
public: true,
entities: 2,
followers: 1,
isFollowing: true,
},
],
}}
>
<PlaylistList />
</MockPlaylistListProvider>,
<TestApiProvider apis={[[configApiRef, mockConfigApi]]}>
<MockPlaylistListProvider
value={{
playlists: [
{
id: 'id1',
name: 'playlist-1',
owner: 'group:default/some-owner',
public: true,
entities: 1,
followers: 2,
isFollowing: false,
},
{
id: 'id2',
name: 'playlist-2',
owner: 'group:default/another-owner',
public: true,
entities: 2,
followers: 1,
isFollowing: true,
},
],
}}
>
<PlaylistList />
</MockPlaylistListProvider>
</TestApiProvider>,
);
expect(rendered.getByText('playlist-1')).toBeInTheDocument();
@@ -23,18 +23,24 @@ import {
} from '@backstage/core-components';
import { Typography } from '@material-ui/core';
import { usePlaylistList } from '../../hooks';
import { useTitle, usePlaylistList } from '../../hooks';
import { PlaylistCard } from '../PlaylistCard';
export const PlaylistList = () => {
const { loading, error, playlists } = usePlaylistList();
const pluralTitleLowerCase = useTitle({
pluralize: true,
lowerCase: true,
});
return (
<>
{loading && <Progress />}
{error && (
<WarningPanel title="Oops! Something went wrong loading playlists">
<WarningPanel
title={`Oops! Something went wrong loading ${pluralTitleLowerCase}`}
>
{error.message}
</WarningPanel>
)}
@@ -32,6 +32,7 @@ import React, { forwardRef, useCallback, useEffect, useState } from 'react';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import { playlistApiRef } from '../../api';
import { useTitle } from '../../hooks';
import { AddEntitiesDrawer } from './AddEntitiesDrawer';
export const PlaylistEntitiesTable = ({
@@ -84,16 +85,21 @@ export const PlaylistEntitiesTable = ({
[errorApi, loadEntities, playlistApi, playlistId],
);
const singularTitleLowerCase = useTitle({
pluralize: false,
lowerCase: true,
});
const actions = editAllowed
? [
{
icon: DeleteIcon,
tooltip: 'Remove from playlist',
tooltip: `Remove from ${singularTitleLowerCase}`,
onClick: removeEntity,
},
{
icon: AddBoxIcon,
tooltip: 'Add entities to playlist',
tooltip: `Add entities to ${singularTitleLowerCase}`,
isFreeAction: true,
onClick: () => setOpenAddEntitiesDrawer(true),
},
@@ -45,6 +45,7 @@ import useAsyncFn from 'react-use/lib/useAsyncFn';
import { playlistApiRef } from '../../api';
import { rootRouteRef } from '../../routes';
import { PlaylistEditDialog } from '../PlaylistEditDialog';
import { useTitle } from '../../hooks';
const useStyles = makeStyles({
buttonWrapper: {
@@ -109,6 +110,11 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => {
}
}, [playlistApi]);
const singularTitle = useTitle({
pluralize: false,
lowerCase: false,
});
return (
<Header
type={!playlist.public ? 'private' : undefined}
@@ -143,7 +149,7 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => {
onClick: () => setOpenEditDialog(true),
},
{
label: 'Delete Playlist',
label: `Delete ${singularTitle}`,
icon: <DeleteIcon />,
disabled: !deleteAllowed,
onClick: () => setOpenDeleteDialog(true),
+1
View File
@@ -15,3 +15,4 @@
*/
export * from './usePlaylistList';
export * from './useTitle';
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2023 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 { configApiRef, useApi } from '@backstage/core-plugin-api';
import pluralize from 'pluralize';
interface UseTitleOptions {
pluralize?: boolean;
lowerCase?: boolean;
}
export function useTitle(opts: UseTitleOptions) {
const configApi = useApi(configApiRef);
let title = configApi.getOptionalString('playlist.title') ?? 'Playlist';
if (opts.pluralize) {
title = pluralize(title);
}
return opts.lowerCase ? title.toLocaleLowerCase('en-US') : title;
}
+1
View File
@@ -7777,6 +7777,7 @@ __metadata:
cross-fetch: ^3.1.5
lodash: ^4.17.21
msw: ^1.0.0
pluralize: ^8.0.0
qs: ^6.9.4
react-hook-form: ^7.13.0
react-use: ^17.2.4