diff --git a/.changeset/swift-frogs-drop.md b/.changeset/swift-frogs-drop.md
new file mode 100644
index 0000000000..3edff21ca2
--- /dev/null
+++ b/.changeset/swift-frogs-drop.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-playlist': patch
+---
+
+Limit the use of the same playlist name when adding a playlist
diff --git a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.test.tsx b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.test.tsx
index 9f30d7608c..b01b5acbf2 100644
--- a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.test.tsx
+++ b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.test.tsx
@@ -18,11 +18,32 @@ import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { fireEvent, getByRole, waitFor } from '@testing-library/react';
import { act } from '@testing-library/react-hooks';
+import { PlaylistApi, playlistApiRef } from '../../api';
import React from 'react';
import { PlaylistEditDialog } from './PlaylistEditDialog';
describe('', () => {
+ const samplePlaylists = [
+ {
+ 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,
+ },
+ ];
it('handle saving with an edited playlist', async () => {
const identityApi: Partial = {
getBackstageIdentity: async () => ({
@@ -33,8 +54,18 @@ describe('', () => {
};
const mockOnSave = jest.fn().mockImplementation(async () => {});
+ const playlistApi: Partial = {
+ getAllPlaylists: jest
+ .fn()
+ .mockImplementation(async () => samplePlaylists),
+ };
const rendered = await renderInTestApp(
-
+
,
);
@@ -83,4 +114,53 @@ describe('', () => {
});
});
});
+
+ it('displays duplicate validation message for playlist name', async () => {
+ const identityApi: Partial = {
+ getBackstageIdentity: async () => ({
+ type: 'user',
+ userEntityRef: 'user:default/me',
+ ownershipEntityRefs: ['group:default/test-owner', 'user:default/me'],
+ }),
+ };
+
+ const mockOnSave = jest.fn().mockImplementation(async () => {});
+ const playlistApi: Partial = {
+ getAllPlaylists: jest
+ .fn()
+ .mockImplementation(async () => [...samplePlaylists]),
+ };
+
+ const rendered = await renderInTestApp(
+
+
+ ,
+ );
+
+ act(() => {
+ fireEvent.input(
+ getByRole(rendered.getByTestId('edit-dialog-name-input'), 'textbox'),
+ {
+ target: {
+ value: 'playlist-1',
+ },
+ },
+ );
+
+ fireEvent.click(rendered.getByTestId('edit-dialog-save-button'));
+ });
+
+ await waitFor(() => {
+ expect(
+ rendered.getByText('A playlist with this name already exists'),
+ ).toBeInTheDocument();
+ });
+
+ expect(mockOnSave).not.toHaveBeenCalled();
+ });
});
diff --git a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx
index a213c7c51c..11f972afbe 100644
--- a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx
+++ b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx
@@ -18,6 +18,7 @@ import { parseEntityRef } from '@backstage/catalog-model';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
import { humanizeEntityRef } from '@backstage/plugin-catalog-react';
import { PlaylistMetadata } from '@backstage/plugin-playlist-common';
+import { playlistApiRef } from '../../api';
import {
Button,
CircularProgress,
@@ -35,7 +36,7 @@ import {
Select,
TextField,
} from '@material-ui/core';
-import React from 'react';
+import React, { useRef } from 'react';
import { useForm, Controller } from 'react-hook-form';
import useAsync from 'react-use/lib/useAsync';
import useAsyncFn from 'react-use/lib/useAsyncFn';
@@ -74,13 +75,27 @@ export const PlaylistEditDialog = ({
}: PlaylistEditDialogProps) => {
const classes = useStyles();
const identityApi = useApi(identityApiRef);
-
+ const playlistApi = useApi(playlistApiRef);
+ const playlistPromise = useRef(playlistApi.getAllPlaylists({}));
const { loading: loadingOwnership, value: ownershipRefs } =
useAsync(async () => {
const { ownershipEntityRefs } = await identityApi.getBackstageIdentity();
return ownershipEntityRefs;
}, []);
+ const nameIsUnique = async (name: string) => {
+ const playlists = await playlistPromise.current;
+
+ if (name !== playlist.name) {
+ return (
+ !playlists.some(p => p.name === name) ||
+ 'A playlist with this name already exists'
+ );
+ }
+
+ return true;
+ };
+
const defaultValues = {
...playlist,
public: playlist.public.toString(),
@@ -117,13 +132,17 @@ export const PlaylistEditDialog = ({
nameIsUnique(value),
+ }}
render={({ field }) => (
{
const playlistApi = useApi(playlistApiRef);
const navigate = useNavigate();
const rootRoute = useRouteRef(rootRouteRef);
+
const [openEditDialog, setOpenEditDialog] = useState(false);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
@@ -99,12 +100,17 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => {
try {
await playlistApi.updatePlaylist({ ...update, id: playlist.id });
setOpenEditDialog(false);
- onUpdate();
+ let message = `Updated playlist '${playlist.name}'`;
+ if (update.name !== playlist.name) {
+ message = `Updated playlist name '${playlist.name}' to '${update.name}'`;
+ }
+
alertApi.post({
- message: `Updated playlist '${playlist.name}'`,
+ message,
severity: 'success',
display: 'transient',
});
+ onUpdate();
} catch (e) {
errorApi.post(e);
}
@@ -116,15 +122,16 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => {
try {
await playlistApi.deletePlaylist(playlist.id);
navigate(rootRoute());
+ const message = `Deleted playlist '${playlist.name}'`;
alertApi.post({
- message: `Deleted playlist '${playlist.name}'`,
+ message,
severity: 'success',
display: 'transient',
});
} catch (e) {
errorApi.post(e);
}
- }, [playlistApi]);
+ }, [playlistApi, alertApi]);
const singularTitle = useTitle({
pluralize: false,