mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
directly replace playlist rules on save and replace
This commit is contained in:
+30
-9
@@ -44,7 +44,13 @@ import { Modal } from '/@/shared/components/modal/modal';
|
|||||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||||
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import {
|
||||||
|
LibraryItem,
|
||||||
|
Playlist,
|
||||||
|
SongListSort,
|
||||||
|
SortOrder,
|
||||||
|
UpdatePlaylistBody,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistDetailSongListHeaderFiltersProps {
|
interface PlaylistDetailSongListHeaderFiltersProps {
|
||||||
@@ -193,7 +199,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
<MoreButton onClick={handleMore} />
|
<MoreButton onClick={handleMore} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
{isViewEditMode && <SaveAndReplaceButton mode={mode} />}
|
{isViewEditMode && <SaveAndReplaceButton mode={mode} playlist={detailQuery.data} />}
|
||||||
{isViewEditMode && (
|
{isViewEditMode && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setMode?.(mode === 'edit' ? 'view' : 'edit')}
|
onClick={() => setMode?.(mode === 'edit' ? 'view' : 'edit')}
|
||||||
@@ -242,24 +248,39 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openSaveAndReplaceModal = (playlistId: string, listData: unknown[]) => {
|
export const openSaveAndReplaceModal = (playlistId: string, updateBody: UpdatePlaylistBody) => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: { listData, playlistId },
|
innerProps: { playlistId, updateBody },
|
||||||
modalKey: 'saveAndReplace',
|
modalKey: 'saveAndReplace',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
title: i18n.t('common.saveAndReplace', { postProcess: 'titleCase' }) as string,
|
title: i18n.t('common.saveAndReplace', { postProcess: 'titleCase' }) as string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const SaveAndReplaceButton = ({ mode }: { mode: 'edit' | 'view' | undefined }) => {
|
const SaveAndReplaceButton = ({
|
||||||
|
mode,
|
||||||
|
playlist,
|
||||||
|
}: {
|
||||||
|
mode: 'edit' | 'view' | undefined;
|
||||||
|
playlist: Playlist | undefined;
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const { listData } = useListContext();
|
|
||||||
|
|
||||||
const handleOpenModal = useCallback(() => {
|
const handleOpenModal = useCallback(() => {
|
||||||
if (!playlistId || !listData) return;
|
if (!playlistId || !playlist) return;
|
||||||
openSaveAndReplaceModal(playlistId, listData);
|
|
||||||
}, [playlistId, listData]);
|
const updateBody: UpdatePlaylistBody = {
|
||||||
|
comment: playlist.description ?? '',
|
||||||
|
name: playlist.name,
|
||||||
|
ownerId: playlist.ownerId ?? '',
|
||||||
|
public: playlist.public ?? false,
|
||||||
|
queryBuilderRules: playlist.rules ?? undefined,
|
||||||
|
sync: playlist.sync ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
openSaveAndReplaceModal(playlistId, updateBody);
|
||||||
|
}, [playlistId, playlist]);
|
||||||
|
|
||||||
if (mode === 'view') {
|
if (mode === 'view') {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
PlaylistQueryBuilder,
|
PlaylistQueryBuilder,
|
||||||
PlaylistQueryBuilderRef,
|
PlaylistQueryBuilderRef,
|
||||||
} from '/@/renderer/features/playlists/components/playlist-query-builder';
|
} from '/@/renderer/features/playlists/components/playlist-query-builder';
|
||||||
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||||
import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils';
|
import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils';
|
||||||
import { JsonPreview } from '/@/renderer/features/shared/components/json-preview';
|
import { JsonPreview } from '/@/renderer/features/shared/components/json-preview';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
import { Box } from '/@/shared/components/box/box';
|
||||||
@@ -25,7 +25,6 @@ import { toast } from '/@/shared/components/toast/toast';
|
|||||||
import { SongListSort } from '/@/shared/types/domain-types';
|
import { SongListSort } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export interface PlaylistQueryEditorProps {
|
export interface PlaylistQueryEditorProps {
|
||||||
createPlaylistMutation: ReturnType<typeof useCreatePlaylist>;
|
|
||||||
detailQuery: ReturnType<typeof useQuery<any>>;
|
detailQuery: ReturnType<typeof useQuery<any>>;
|
||||||
handleSave: (
|
handleSave: (
|
||||||
filter: Record<string, any>,
|
filter: Record<string, any>,
|
||||||
@@ -39,6 +38,7 @@ export interface PlaylistQueryEditorProps {
|
|||||||
onToggleExpand: () => void;
|
onToggleExpand: () => void;
|
||||||
playlistId: string;
|
playlistId: string;
|
||||||
queryBuilderRef: React.RefObject<null | PlaylistQueryBuilderRef>;
|
queryBuilderRef: React.RefObject<null | PlaylistQueryBuilderRef>;
|
||||||
|
updatePlaylistMutation: ReturnType<typeof useUpdatePlaylist>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppliedJsonState = {
|
type AppliedJsonState = {
|
||||||
@@ -77,7 +77,6 @@ const parseRulesJsonToSaveArgs = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PlaylistQueryEditor = ({
|
export const PlaylistQueryEditor = ({
|
||||||
createPlaylistMutation,
|
|
||||||
detailQuery,
|
detailQuery,
|
||||||
handleSave,
|
handleSave,
|
||||||
handleSaveAs,
|
handleSaveAs,
|
||||||
@@ -85,6 +84,7 @@ export const PlaylistQueryEditor = ({
|
|||||||
onToggleExpand,
|
onToggleExpand,
|
||||||
playlistId,
|
playlistId,
|
||||||
queryBuilderRef,
|
queryBuilderRef,
|
||||||
|
updatePlaylistMutation,
|
||||||
}: PlaylistQueryEditorProps) => {
|
}: PlaylistQueryEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -322,7 +322,7 @@ export const PlaylistQueryEditor = ({
|
|||||||
<Button
|
<Button
|
||||||
disabled={!isQueryBuilderExpanded}
|
disabled={!isQueryBuilderExpanded}
|
||||||
leftSection={<Icon icon="save" />}
|
leftSection={<Icon icon="save" />}
|
||||||
loading={createPlaylistMutation?.isPending}
|
loading={updatePlaylistMutation?.isPending}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isQueryBuilderExpanded) return;
|
if (!isQueryBuilderExpanded) return;
|
||||||
const payload = getFiltersForSave();
|
const payload = getFiltersForSave();
|
||||||
|
|||||||
@@ -1,40 +1,22 @@
|
|||||||
import { closeAllModals, ContextModalProps } from '@mantine/modals';
|
import { closeAllModals, ContextModalProps } from '@mantine/modals';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useReplacePlaylist } from '/@/renderer/features/playlists/mutations/replace-playlist-mutation';
|
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||||
import { useCurrentServerId } from '/@/renderer/store';
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { Song } from '/@/shared/types/domain-types';
|
import { UpdatePlaylistBody } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const SaveAndReplaceContextModal = ({
|
export const SaveAndReplaceContextModal = ({
|
||||||
innerProps,
|
innerProps,
|
||||||
}: ContextModalProps<{ listData: unknown[]; playlistId: string }>) => {
|
}: ContextModalProps<{ playlistId: string; updateBody: UpdatePlaylistBody }>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { listData, playlistId } = innerProps;
|
const { playlistId, updateBody } = innerProps;
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
const replacePlaylistMutation = useReplacePlaylist({});
|
const updatePlaylistMutation = useUpdatePlaylist({});
|
||||||
|
|
||||||
// Get current songs from list data
|
|
||||||
const currentSongIds = useMemo(() => {
|
|
||||||
if (!listData || !Array.isArray(listData)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return listData
|
|
||||||
.filter((item): item is Song => {
|
|
||||||
return (
|
|
||||||
typeof item === 'object' &&
|
|
||||||
item !== null &&
|
|
||||||
'id' in item &&
|
|
||||||
typeof (item as any).id === 'string'
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((song) => song.id);
|
|
||||||
}, [listData]);
|
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
if (!serverId || !playlistId) {
|
if (!serverId || !playlistId) {
|
||||||
@@ -42,24 +24,11 @@ export const SaveAndReplaceContextModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSongIds.length === 0) {
|
updatePlaylistMutation.mutate(
|
||||||
console.error('currentSongIds is empty');
|
|
||||||
toast.error({
|
|
||||||
message: t('error.genericError', { postProcess: 'sentenceCase' }),
|
|
||||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
replacePlaylistMutation.mutate(
|
|
||||||
{
|
{
|
||||||
apiClientProps: { serverId },
|
apiClientProps: { serverId },
|
||||||
body: {
|
body: updateBody,
|
||||||
songId: currentSongIds,
|
query: { id: playlistId },
|
||||||
},
|
|
||||||
query: {
|
|
||||||
id: playlistId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -81,10 +50,10 @@ export const SaveAndReplaceContextModal = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [t, currentSongIds, serverId, playlistId, replacePlaylistMutation]);
|
}, [t, serverId, playlistId, updateBody, updatePlaylistMutation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmModal loading={replacePlaylistMutation.isPending} onConfirm={handleConfirm}>
|
<ConfirmModal loading={updatePlaylistMutation.isPending} onConfirm={handleConfirm}>
|
||||||
<Text>{t('form.editPlaylist.editNote', { postProcess: 'sentenceCase' })}</Text>
|
<Text>{t('form.editPlaylist.editNote', { postProcess: 'sentenceCase' })}</Text>
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { useRecentPlaylists } from '/@/renderer/features/playlists/hooks/use-recent-playlists';
|
|
||||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
|
||||||
import { useCurrentServerId } from '/@/renderer/store';
|
|
||||||
import { ReplacePlaylistArgs, ReplacePlaylistResponse } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
export const useReplacePlaylist = (args: MutationHookArgs) => {
|
|
||||||
const { options } = args || {};
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const serverId = useCurrentServerId();
|
|
||||||
|
|
||||||
const { addRecentPlaylist } = useRecentPlaylists(serverId);
|
|
||||||
|
|
||||||
return useMutation<ReplacePlaylistResponse, AxiosError, ReplacePlaylistArgs, null>({
|
|
||||||
mutationFn: (args) => {
|
|
||||||
return api.controller.replacePlaylist({
|
|
||||||
...args,
|
|
||||||
apiClientProps: { serverId: args.apiClientProps.serverId },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: (_data, variables, context) => {
|
|
||||||
const { apiClientProps } = variables;
|
|
||||||
const serverId = apiClientProps.serverId;
|
|
||||||
|
|
||||||
if (!serverId) return;
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
exact: false,
|
|
||||||
queryKey: queryKeys.playlists.list(serverId),
|
|
||||||
});
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: queryKeys.playlists.detail(serverId, variables.query.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: queryKeys.playlists.songList(serverId, variables.query.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
addRecentPlaylist(variables.query.id);
|
|
||||||
|
|
||||||
options?.onSuccess?.(_data, variables, context);
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -31,6 +31,9 @@ export const useUpdatePlaylist = (args: MutationHookArgs) => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: queryKeys.playlists.detail(serverId, query.id),
|
queryKey: queryKeys.playlists.detail(serverId, query.id),
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.playlists.songList(serverId, query.id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import { PlaylistQueryBuilderRef } from '/@/renderer/features/playlists/componen
|
|||||||
import { PlaylistQueryEditor } from '/@/renderer/features/playlists/components/playlist-query-editor';
|
import { PlaylistQueryEditor } from '/@/renderer/features/playlists/components/playlist-query-editor';
|
||||||
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
|
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
|
||||||
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
||||||
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
|
||||||
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
||||||
|
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
|
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
|
||||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
@@ -80,8 +80,8 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
...playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
...playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
||||||
placeholderData: location.state?.item,
|
placeholderData: location.state?.item,
|
||||||
});
|
});
|
||||||
const createPlaylistMutation = useCreatePlaylist({});
|
|
||||||
const deletePlaylistMutation = useDeletePlaylist({});
|
const deletePlaylistMutation = useDeletePlaylist({});
|
||||||
|
const updatePlaylistMutation = useUpdatePlaylist({});
|
||||||
|
|
||||||
const handleSave = (
|
const handleSave = (
|
||||||
filter: Record<string, any>,
|
filter: Record<string, any>,
|
||||||
@@ -89,8 +89,6 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
) => {
|
) => {
|
||||||
if (!detailQuery?.data) return;
|
if (!detailQuery?.data) return;
|
||||||
|
|
||||||
// New syntax: sortBy is now a single string with comma-separated fields and +/- prefix
|
|
||||||
// e.g., "+album,-year" means sort by album ascending, then year descending
|
|
||||||
const sortValue =
|
const sortValue =
|
||||||
extraFilters.sortBy && extraFilters.sortBy.length > 0
|
extraFilters.sortBy && extraFilters.sortBy.length > 0
|
||||||
? extraFilters.sortBy[0]
|
? extraFilters.sortBy[0]
|
||||||
@@ -99,11 +97,10 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
const rules = {
|
const rules = {
|
||||||
...filter,
|
...filter,
|
||||||
limit: extraFilters.limit || undefined,
|
limit: extraFilters.limit || undefined,
|
||||||
// order field is now optional - sort direction is embedded in sort field
|
|
||||||
sort: sortValue,
|
sort: sortValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
createPlaylistMutation.mutate(
|
updatePlaylistMutation.mutate(
|
||||||
{
|
{
|
||||||
apiClientProps: { serverId: detailQuery?.data?._serverId },
|
apiClientProps: { serverId: detailQuery?.data?._serverId },
|
||||||
body: {
|
body: {
|
||||||
@@ -114,22 +111,11 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
queryBuilderRules: rules,
|
queryBuilderRules: rules,
|
||||||
sync: detailQuery?.data?.sync || false,
|
sync: detailQuery?.data?.sync || false,
|
||||||
},
|
},
|
||||||
|
query: { id: playlistId },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
onSuccess: () => {
|
||||||
toast.success({ message: 'Playlist has been saved' });
|
toast.success({ message: 'Playlist has been saved' });
|
||||||
navigate(
|
|
||||||
generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, {
|
|
||||||
playlistId: data?.id || '',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
replace: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
deletePlaylistMutation.mutate({
|
|
||||||
apiClientProps: { serverId: detailQuery?.data?._serverId },
|
|
||||||
query: { id: playlistId },
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -297,7 +283,6 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
</ListWithSidebarContainer>
|
</ListWithSidebarContainer>
|
||||||
{(isSmartPlaylist || showQueryBuilder) && (
|
{(isSmartPlaylist || showQueryBuilder) && (
|
||||||
<PlaylistQueryEditor
|
<PlaylistQueryEditor
|
||||||
createPlaylistMutation={createPlaylistMutation}
|
|
||||||
detailQuery={detailQuery}
|
detailQuery={detailQuery}
|
||||||
handleSave={handleSave}
|
handleSave={handleSave}
|
||||||
handleSaveAs={handleSaveAs}
|
handleSaveAs={handleSaveAs}
|
||||||
@@ -305,6 +290,7 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
onToggleExpand={handleToggleExpand}
|
onToggleExpand={handleToggleExpand}
|
||||||
playlistId={playlistId}
|
playlistId={playlistId}
|
||||||
queryBuilderRef={queryBuilderRef}
|
queryBuilderRef={queryBuilderRef}
|
||||||
|
updatePlaylistMutation={updatePlaylistMutation}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
|
|||||||
Reference in New Issue
Block a user