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..67749042cb 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, { useState } from 'react'; // Import useState
import { useForm, Controller } from 'react-hook-form';
import useAsync from 'react-use/lib/useAsync';
import useAsyncFn from 'react-use/lib/useAsyncFn';
@@ -74,6 +75,15 @@ export const PlaylistEditDialog = ({
}: PlaylistEditDialogProps) => {
const classes = useStyles();
const identityApi = useApi(identityApiRef);
+ const playlistApi = useApi(playlistApiRef);
+ const playListApiData = playlistApi.getAllPlaylists();
+
+ const [editingOtherFields, setEditingOtherFields] = useState(false);
+
+ const fetchAndProcessData = async () => {
+ const playlistArray = await playListApiData;
+ return playlistArray;
+ };
const { loading: loadingOwnership, value: ownershipRefs } =
useAsync(async () => {
@@ -81,6 +91,18 @@ export const PlaylistEditDialog = ({
return ownershipEntityRefs;
}, []);
+ const nameIsUnique = async (name: string) => {
+ const playlistArray = await fetchAndProcessData();
+ if (
+ editingOtherFields ||
+ (await playlistArray.some(
+ (playlistData: { name: string }) => playlistData.name === name,
+ ))
+ )
+ return 'A playlist with this name already exists';
+ return true;
+ };
+
const defaultValues = {
...playlist,
public: playlist.public.toString(),
@@ -103,6 +125,7 @@ export const PlaylistEditDialog = ({
if (!saving.loading) {
onClose();
reset(defaultValues);
+ setEditingOtherFields(false);
}
};
@@ -117,13 +140,17 @@ export const PlaylistEditDialog = ({
(
setEditingOtherFields(true)}
/>
)}
/>