optimize playlist edit modal (#1234)

- remove user list fetch if not admin
- move to context modal to allow dynamic content
This commit is contained in:
jeffvli
2025-12-14 04:43:48 -08:00
parent b19e752314
commit 752b191ad7
4 changed files with 73 additions and 111 deletions
@@ -1,13 +1,8 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer } from '/@/renderer/store';
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
import { toast } from '/@/shared/components/toast/toast';
import { Playlist } from '/@/shared/types/domain-types';
interface EditPlaylistActionProps {
@@ -17,38 +12,16 @@ interface EditPlaylistActionProps {
export const EditPlaylistAction = ({ disabled, items }: EditPlaylistActionProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
const handleEditPlaylist = useCallback(async () => {
if (items.length === 0 || !server) return;
if (items.length === 0) return;
// Only allow editing a single playlist at a time
const playlist = items[0];
try {
// Fetch the full playlist detail
const playlistDetail = await queryClient.fetchQuery({
queryFn: ({ signal }) =>
api.controller.getPlaylistDetail({
apiClientProps: { serverId: server.id, signal },
query: { id: playlist.id },
}),
queryKey: queryKeys.playlists.detail(server.id, playlist.id, { id: playlist.id }),
});
if (playlistDetail) {
await openUpdatePlaylistModal({
playlist: playlistDetail,
server,
});
}
} catch (err: any) {
toast.error({
message: err.message,
title: t('error.genericError', { postProcess: 'sentenceCase' }),
});
}
}, [items, server, t]);
openUpdatePlaylistModal({
playlist,
});
}, [items]);
if (items.length === 0 || items.length > 1) return null;
@@ -1,12 +1,12 @@
import { closeAllModals, openModal } from '@mantine/modals';
import { closeModal, ContextModalProps, openContextModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { t } from 'i18next';
import { useTranslation } from 'react-i18next';
import i18n from '/@/i18n/i18n';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer } from '/@/renderer/store';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { useCurrentServer, useCurrentServerId, usePermissions } from '/@/renderer/store';
import { hasFeature } from '/@/shared/api/utils';
import { Group } from '/@/shared/components/group/group';
import { ModalButton } from '/@/shared/components/modal/model-shared';
@@ -17,34 +17,26 @@ import { TextInput } from '/@/shared/components/text-input/text-input';
import { toast } from '/@/shared/components/toast/toast';
import { useForm } from '/@/shared/hooks/use-form';
import {
PlaylistDetailResponse,
ServerListItem,
Playlist,
ServerType,
SortOrder,
UpdatePlaylistBody,
UpdatePlaylistQuery,
User,
UserListQuery,
UserListSort,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
interface UpdatePlaylistFormProps {
export const UpdatePlaylistContextModal = ({
id,
innerProps,
}: ContextModalProps<{
body: Partial<UpdatePlaylistBody>;
onCancel: () => void;
query: UpdatePlaylistQuery;
users?: User[];
}
export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlaylistFormProps) => {
}>) => {
const { t } = useTranslation();
const mutation = useUpdatePlaylist({});
const server = useCurrentServer();
const userList = users?.map((user) => ({
label: user.name,
value: user.id,
}));
const { body, query } = innerProps;
const form = useForm<UpdatePlaylistBody>({
initialValues: {
@@ -75,14 +67,15 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
toast.success({
message: t('form.editPlaylist.success', { postProcess: 'sentenceCase' }),
});
onCancel();
closeModal(id);
},
},
);
});
const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
const isOwnerDisplayed = server?.type === ServerType.NAVIDROME && userList;
const isOwnerDisplayed = server?.type === ServerType.NAVIDROME;
const isCommentDisplayed = server?.type === ServerType.NAVIDROME;
const isSubmitDisabled = !form.values.name || mutation.isPending;
return (
@@ -97,7 +90,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
required
{...form.getInputProps('name')}
/>
{server?.type === ServerType.NAVIDROME && (
{isCommentDisplayed && (
<TextInput
label={t('form.createPlaylist.input', {
context: 'description',
@@ -106,16 +99,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
{...form.getInputProps('comment')}
/>
)}
{isOwnerDisplayed && (
<Select
data={userList || []}
{...form.getInputProps('ownerId')}
label={t('form.createPlaylist.input', {
context: 'owner',
postProcess: 'titleCase',
})}
/>
)}
{isOwnerDisplayed && <OwnerSelect form={form} />}
{isPublicDisplayed && (
<>
{server?.type === ServerType.JELLYFIN && (
@@ -135,7 +119,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
</>
)}
<Group justify="flex-end">
<ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton>
<ModalButton onClick={() => closeModal(id)}>{t('common.cancel')}</ModalButton>
<ModalButton
disabled={isSubmitDisabled}
loading={mutation.isPending}
@@ -150,55 +134,57 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
);
};
export const openUpdatePlaylistModal = async (args: {
playlist: PlaylistDetailResponse;
server: ServerListItem;
}) => {
const { playlist, server } = args;
const OwnerSelect = ({ form }: { form: ReturnType<typeof useForm<UpdatePlaylistBody>> }) => {
const serverId = useCurrentServerId();
const permissions = usePermissions();
const query: UserListQuery = {
sortBy: UserListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
};
const usersQuery = useQuery(
sharedQueries.users({
options: { enabled: permissions.playlists.editOwner },
query: { sortBy: UserListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0 },
serverId,
}),
);
if (!server) return;
const userList = usersQuery.data?.items?.map((user) => ({
label: user.name,
value: user.id,
}));
const users =
server?.type === ServerType.NAVIDROME
? await queryClient
.fetchQuery({
queryFn: ({ signal }) =>
api.controller.getUserList({
apiClientProps: { serverId: server?.id || '', signal },
query,
}),
queryKey: queryKeys.users.list(server?.id || '', query),
})
.catch((error) => {
// This eror most likely happens if the user is not an admin
console.error(error);
return null;
})
: null;
if (!permissions.playlists.editOwner) {
return null;
}
openModal({
children: (
<UpdatePlaylistForm
body={{
comment: playlist?.description || undefined,
genres: playlist?.genres,
name: playlist?.name,
ownerId: playlist?.ownerId || undefined,
public: playlist?.public || false,
queryBuilderRules: playlist?.rules || undefined,
sync: playlist?.sync || undefined,
}}
onCancel={closeAllModals}
query={{ id: playlist?.id }}
users={users?.items}
/>
),
return (
<Select
data={usersQuery.isLoading ? [] : userList}
disabled={usersQuery.isLoading}
{...form.getInputProps('ownerId')}
label={t('form.createPlaylist.input', {
context: 'owner',
postProcess: 'titleCase',
})}
/>
);
};
export const openUpdatePlaylistModal = async (args: { playlist: Playlist }) => {
const { playlist } = args;
openContextModal({
innerProps: {
body: {
comment: playlist?.description || undefined,
genres: playlist?.genres,
name: playlist?.name,
ownerId: playlist?.ownerId || undefined,
public: playlist?.public || false,
queryBuilderRules: playlist?.rules || undefined,
sync: playlist?.sync || undefined,
},
query: { id: playlist?.id },
},
modalKey: 'updatePlaylist',
title: i18n.t('form.editPlaylist.title', { postProcess: 'titleCase' }) as string,
});
};
+2
View File
@@ -4,6 +4,7 @@ import { HashRouter, Route, Routes } from 'react-router';
import { ShuffleAllContextModal } from '/@/renderer/features/player/components/shuffle-all-modal';
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists/components/add-to-playlist-context-modal';
import { SaveAndReplaceContextModal } from '/@/renderer/features/playlists/components/save-and-replace-context-modal';
import { UpdatePlaylistContextModal } from '/@/renderer/features/playlists/components/update-playlist-form';
import { SettingsContextModal } from '/@/renderer/features/settings/components/settings-modal';
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
@@ -90,6 +91,7 @@ export const AppRouter = () => {
settings: SettingsContextModal,
shareItem: ShareItemContextModal,
shuffleAll: ShuffleAllContextModal,
updatePlaylist: UpdatePlaylistContextModal,
}}
>
<RouterErrorBoundary>
+1
View File
@@ -161,6 +161,7 @@ export const usePermissions = () => {
return {
playlists: {
editOwner: isAdmin,
editPublic: isAdmin,
},
radio: {