diff --git a/.changeset/perfect-deers-add.md b/.changeset/perfect-deers-add.md new file mode 100644 index 0000000000..c932351a19 --- /dev/null +++ b/.changeset/perfect-deers-add.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist': patch +--- + +Added config properties to change dynamically the group noun for all the components in the UI diff --git a/plugins/playlist/README.md b/plugins/playlist/README.md index 6ae3fb84ac..e35f94b9e5 100644 --- a/plugins/playlist/README.md +++ b/plugins/playlist/README.md @@ -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 diff --git a/plugins/playlist/config.d.ts b/plugins/playlist/config.d.ts new file mode 100644 index 0000000000..d949cfe73a --- /dev/null +++ b/plugins/playlist/config.d.ts @@ -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; + }; +} diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index ccba981f05..a962e82290 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -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" } diff --git a/plugins/playlist/src/components/CreatePlaylistButton/CreatePlaylistButton.tsx b/plugins/playlist/src/components/CreatePlaylistButton/CreatePlaylistButton.tsx index c064f28591..7669a48c26 100644 --- a/plugins/playlist/src/components/CreatePlaylistButton/CreatePlaylistButton.tsx +++ b/plugins/playlist/src/components/CreatePlaylistButton/CreatePlaylistButton.tsx @@ -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 ? ( setOpenDialog(true)} > @@ -74,7 +80,7 @@ export const CreatePlaylistButton = () => { color="primary" onClick={() => setOpenDialog(true)} > - Create Playlist + Create {singularTitle} )} { [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) && } - Add to Playlist + Add to {singularTitle} { {error && ( - + )} {playlists && entity && ( @@ -205,7 +229,9 @@ export const EntityPlaylistDialog = (props: EntityPlaylistDialogProps) => { - + )} {playlists diff --git a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx index 27cc13fede..a213c7c51c 100644 --- a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx +++ b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx @@ -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 ( @@ -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" /> )} diff --git a/plugins/playlist/src/components/PlaylistIndexPage/PlaylistIndexPage.tsx b/plugins/playlist/src/components/PlaylistIndexPage/PlaylistIndexPage.tsx index 0ee829018a..f25df19ee7 100644 --- a/plugins/playlist/src/components/PlaylistIndexPage/PlaylistIndexPage.tsx +++ b/plugins/playlist/src/components/PlaylistIndexPage/PlaylistIndexPage.tsx @@ -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 = () => ( - - - - - - - - - - - - - - - - - - - - - -); +export const PlaylistIndexPage = () => { + const pluralTitle = useTitle({ + pluralize: true, + lowerCase: false, + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/playlist/src/components/PlaylistList/PlaylistList.test.tsx b/plugins/playlist/src/components/PlaylistList/PlaylistList.test.tsx index 27030ec1c7..976db99983 100644 --- a/plugins/playlist/src/components/PlaylistList/PlaylistList.test.tsx +++ b/plugins/playlist/src/components/PlaylistList/PlaylistList.test.tsx @@ -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; + jest.mock('../PlaylistCard', () => ({ PlaylistCard: ({ playlist }: { playlist: Playlist }) => (
{playlist.name}
@@ -30,9 +36,11 @@ jest.mock('../PlaylistCard', () => ({ describe('', () => { it('renders error on error', () => { const rendered = render( - - - , + + + + + , ); expect(rendered.getByText('Test Error')).toBeInTheDocument(); @@ -40,9 +48,11 @@ describe('', () => { it('handles no playlists', () => { const rendered = render( - - - , + + + + + , ); expect( @@ -52,32 +62,34 @@ describe('', () => { it('renders playlists', () => { const rendered = render( - - - , + + + + + , ); expect(rendered.getByText('playlist-1')).toBeInTheDocument(); diff --git a/plugins/playlist/src/components/PlaylistList/PlaylistList.tsx b/plugins/playlist/src/components/PlaylistList/PlaylistList.tsx index fcc3971542..2f815f07c6 100644 --- a/plugins/playlist/src/components/PlaylistList/PlaylistList.tsx +++ b/plugins/playlist/src/components/PlaylistList/PlaylistList.tsx @@ -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 && } {error && ( - + {error.message} )} diff --git a/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.tsx b/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.tsx index f499f59a61..3d8efe3bc5 100644 --- a/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.tsx +++ b/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.tsx @@ -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), }, diff --git a/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.tsx b/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.tsx index 7f4457062d..621f94f75c 100644 --- a/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.tsx +++ b/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.tsx @@ -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 (
{ onClick: () => setOpenEditDialog(true), }, { - label: 'Delete Playlist', + label: `Delete ${singularTitle}`, icon: , disabled: !deleteAllowed, onClick: () => setOpenDeleteDialog(true), diff --git a/plugins/playlist/src/hooks/index.ts b/plugins/playlist/src/hooks/index.ts index 316121b515..bb164d961c 100644 --- a/plugins/playlist/src/hooks/index.ts +++ b/plugins/playlist/src/hooks/index.ts @@ -15,3 +15,4 @@ */ export * from './usePlaylistList'; +export * from './useTitle'; diff --git a/plugins/playlist/src/hooks/useTitle.ts b/plugins/playlist/src/hooks/useTitle.ts new file mode 100644 index 0000000000..5f46e027d7 --- /dev/null +++ b/plugins/playlist/src/hooks/useTitle.ts @@ -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; +} diff --git a/yarn.lock b/yarn.lock index 589db99d4e..bd44c43230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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