mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
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:
@@ -1,13 +1,8 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { 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 { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
|
||||||
import { Playlist } from '/@/shared/types/domain-types';
|
import { Playlist } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
interface EditPlaylistActionProps {
|
interface EditPlaylistActionProps {
|
||||||
@@ -17,38 +12,16 @@ interface EditPlaylistActionProps {
|
|||||||
|
|
||||||
export const EditPlaylistAction = ({ disabled, items }: EditPlaylistActionProps) => {
|
export const EditPlaylistAction = ({ disabled, items }: EditPlaylistActionProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const server = useCurrentServer();
|
|
||||||
|
|
||||||
const handleEditPlaylist = useCallback(async () => {
|
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];
|
const playlist = items[0];
|
||||||
|
|
||||||
try {
|
openUpdatePlaylistModal({
|
||||||
// Fetch the full playlist detail
|
playlist,
|
||||||
const playlistDetail = await queryClient.fetchQuery({
|
});
|
||||||
queryFn: ({ signal }) =>
|
}, [items]);
|
||||||
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]);
|
|
||||||
|
|
||||||
if (items.length === 0 || items.length > 1) return null;
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
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 { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||||
import { queryClient } from '/@/renderer/lib/react-query';
|
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer, useCurrentServerId, usePermissions } from '/@/renderer/store';
|
||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { ModalButton } from '/@/shared/components/modal/model-shared';
|
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 { toast } from '/@/shared/components/toast/toast';
|
||||||
import { useForm } from '/@/shared/hooks/use-form';
|
import { useForm } from '/@/shared/hooks/use-form';
|
||||||
import {
|
import {
|
||||||
PlaylistDetailResponse,
|
Playlist,
|
||||||
ServerListItem,
|
|
||||||
ServerType,
|
ServerType,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
UpdatePlaylistBody,
|
UpdatePlaylistBody,
|
||||||
UpdatePlaylistQuery,
|
UpdatePlaylistQuery,
|
||||||
User,
|
|
||||||
UserListQuery,
|
|
||||||
UserListSort,
|
UserListSort,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
|
|
||||||
interface UpdatePlaylistFormProps {
|
export const UpdatePlaylistContextModal = ({
|
||||||
|
id,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<{
|
||||||
body: Partial<UpdatePlaylistBody>;
|
body: Partial<UpdatePlaylistBody>;
|
||||||
onCancel: () => void;
|
|
||||||
query: UpdatePlaylistQuery;
|
query: UpdatePlaylistQuery;
|
||||||
users?: User[];
|
}>) => {
|
||||||
}
|
|
||||||
|
|
||||||
export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlaylistFormProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mutation = useUpdatePlaylist({});
|
const mutation = useUpdatePlaylist({});
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
const { body, query } = innerProps;
|
||||||
const userList = users?.map((user) => ({
|
|
||||||
label: user.name,
|
|
||||||
value: user.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const form = useForm<UpdatePlaylistBody>({
|
const form = useForm<UpdatePlaylistBody>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -75,14 +67,15 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
|
|||||||
toast.success({
|
toast.success({
|
||||||
message: t('form.editPlaylist.success', { postProcess: 'sentenceCase' }),
|
message: t('form.editPlaylist.success', { postProcess: 'sentenceCase' }),
|
||||||
});
|
});
|
||||||
onCancel();
|
closeModal(id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
|
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;
|
const isSubmitDisabled = !form.values.name || mutation.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -97,7 +90,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
|
|||||||
required
|
required
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>
|
/>
|
||||||
{server?.type === ServerType.NAVIDROME && (
|
{isCommentDisplayed && (
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('form.createPlaylist.input', {
|
label={t('form.createPlaylist.input', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
@@ -106,16 +99,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
|
|||||||
{...form.getInputProps('comment')}
|
{...form.getInputProps('comment')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOwnerDisplayed && (
|
{isOwnerDisplayed && <OwnerSelect form={form} />}
|
||||||
<Select
|
|
||||||
data={userList || []}
|
|
||||||
{...form.getInputProps('ownerId')}
|
|
||||||
label={t('form.createPlaylist.input', {
|
|
||||||
context: 'owner',
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isPublicDisplayed && (
|
{isPublicDisplayed && (
|
||||||
<>
|
<>
|
||||||
{server?.type === ServerType.JELLYFIN && (
|
{server?.type === ServerType.JELLYFIN && (
|
||||||
@@ -135,7 +119,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton>
|
<ModalButton onClick={() => closeModal(id)}>{t('common.cancel')}</ModalButton>
|
||||||
<ModalButton
|
<ModalButton
|
||||||
disabled={isSubmitDisabled}
|
disabled={isSubmitDisabled}
|
||||||
loading={mutation.isPending}
|
loading={mutation.isPending}
|
||||||
@@ -150,55 +134,57 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openUpdatePlaylistModal = async (args: {
|
const OwnerSelect = ({ form }: { form: ReturnType<typeof useForm<UpdatePlaylistBody>> }) => {
|
||||||
playlist: PlaylistDetailResponse;
|
const serverId = useCurrentServerId();
|
||||||
server: ServerListItem;
|
const permissions = usePermissions();
|
||||||
}) => {
|
|
||||||
const { playlist, server } = args;
|
|
||||||
|
|
||||||
const query: UserListQuery = {
|
const usersQuery = useQuery(
|
||||||
sortBy: UserListSort.NAME,
|
sharedQueries.users({
|
||||||
sortOrder: SortOrder.ASC,
|
options: { enabled: permissions.playlists.editOwner },
|
||||||
startIndex: 0,
|
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 =
|
if (!permissions.playlists.editOwner) {
|
||||||
server?.type === ServerType.NAVIDROME
|
return null;
|
||||||
? 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;
|
|
||||||
|
|
||||||
openModal({
|
return (
|
||||||
children: (
|
<Select
|
||||||
<UpdatePlaylistForm
|
data={usersQuery.isLoading ? [] : userList}
|
||||||
body={{
|
disabled={usersQuery.isLoading}
|
||||||
comment: playlist?.description || undefined,
|
{...form.getInputProps('ownerId')}
|
||||||
genres: playlist?.genres,
|
label={t('form.createPlaylist.input', {
|
||||||
name: playlist?.name,
|
context: 'owner',
|
||||||
ownerId: playlist?.ownerId || undefined,
|
postProcess: 'titleCase',
|
||||||
public: playlist?.public || false,
|
})}
|
||||||
queryBuilderRules: playlist?.rules || undefined,
|
/>
|
||||||
sync: playlist?.sync || undefined,
|
);
|
||||||
}}
|
};
|
||||||
onCancel={closeAllModals}
|
|
||||||
query={{ id: playlist?.id }}
|
export const openUpdatePlaylistModal = async (args: { playlist: Playlist }) => {
|
||||||
users={users?.items}
|
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,
|
title: i18n.t('form.editPlaylist.title', { postProcess: 'titleCase' }) as string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { HashRouter, Route, Routes } from 'react-router';
|
|||||||
import { ShuffleAllContextModal } from '/@/renderer/features/player/components/shuffle-all-modal';
|
import { ShuffleAllContextModal } from '/@/renderer/features/player/components/shuffle-all-modal';
|
||||||
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists/components/add-to-playlist-context-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 { 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 { SettingsContextModal } from '/@/renderer/features/settings/components/settings-modal';
|
||||||
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
|
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
|
||||||
import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
|
import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
|
||||||
@@ -90,6 +91,7 @@ export const AppRouter = () => {
|
|||||||
settings: SettingsContextModal,
|
settings: SettingsContextModal,
|
||||||
shareItem: ShareItemContextModal,
|
shareItem: ShareItemContextModal,
|
||||||
shuffleAll: ShuffleAllContextModal,
|
shuffleAll: ShuffleAllContextModal,
|
||||||
|
updatePlaylist: UpdatePlaylistContextModal,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RouterErrorBoundary>
|
<RouterErrorBoundary>
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export const usePermissions = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
playlists: {
|
playlists: {
|
||||||
|
editOwner: isAdmin,
|
||||||
editPublic: isAdmin,
|
editPublic: isAdmin,
|
||||||
},
|
},
|
||||||
radio: {
|
radio: {
|
||||||
|
|||||||
Reference in New Issue
Block a user