mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
refactor search into individual sections by itemtype, add infinite loader
This commit is contained in:
@@ -114,6 +114,7 @@
|
|||||||
"no": "no",
|
"no": "no",
|
||||||
"none": "none",
|
"none": "none",
|
||||||
"noResultsFromQuery": "the query returned no results",
|
"noResultsFromQuery": "the query returned no results",
|
||||||
|
"numberOfResults": "{{numberOfResults}} results",
|
||||||
"noFilters": "no filters configured",
|
"noFilters": "no filters configured",
|
||||||
"note": "note",
|
"note": "note",
|
||||||
"ok": "ok",
|
"ok": "ok",
|
||||||
|
|||||||
@@ -347,6 +347,11 @@ export const queryKeys: Record<
|
|||||||
list: (serverId: string) => [serverId, 'roles'] as const,
|
list: (serverId: string) => [serverId, 'roles'] as const,
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
|
infiniteList: (
|
||||||
|
serverId: string,
|
||||||
|
type: 'albumArtists' | 'albums' | 'songs',
|
||||||
|
searchTerm: string,
|
||||||
|
) => [serverId, 'search', 'infiniteList', type, searchTerm] as const,
|
||||||
list: (serverId: string, query?: SearchQuery) => {
|
list: (serverId: string, query?: SearchQuery) => {
|
||||||
if (query) return [serverId, 'search', 'list', query] as const;
|
if (query) return [serverId, 'search', 'list', query] as const;
|
||||||
return [serverId, 'search', 'list'] as const;
|
return [serverId, 'search', 'list'] as const;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { queryOptions } from '@tanstack/react-query';
|
import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
import { SearchQuery } from '/@/shared/types/domain-types';
|
import { SearchQuery, SearchResponse } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
const SEARCH_PAGE_SIZE = 4;
|
||||||
|
|
||||||
export const searchQueries = {
|
export const searchQueries = {
|
||||||
search: (args: QueryHookArgs<SearchQuery>) => {
|
search: (args: QueryHookArgs<SearchQuery>) => {
|
||||||
@@ -18,4 +20,103 @@ export const searchQueries = {
|
|||||||
...args.options,
|
...args.options,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
searchAlbumArtistsInfinite: (args: {
|
||||||
|
enabled?: boolean;
|
||||||
|
searchTerm: string;
|
||||||
|
serverId: string | undefined;
|
||||||
|
}) => {
|
||||||
|
const { enabled = true, searchTerm, serverId } = args;
|
||||||
|
return infiniteQueryOptions({
|
||||||
|
enabled: Boolean(serverId && searchTerm && enabled),
|
||||||
|
getNextPageParam: (lastPage: SearchResponse, allPages: SearchResponse[]) => {
|
||||||
|
const len = lastPage.albumArtists.length;
|
||||||
|
if (len < SEARCH_PAGE_SIZE) return undefined;
|
||||||
|
return allPages.length * SEARCH_PAGE_SIZE;
|
||||||
|
},
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: ({ pageParam, signal }) => {
|
||||||
|
if (!serverId) throw new Error('serverId required');
|
||||||
|
const startIndex = (pageParam ?? 0) as number;
|
||||||
|
return api.controller.search({
|
||||||
|
apiClientProps: { serverId, signal },
|
||||||
|
query: {
|
||||||
|
albumArtistLimit: SEARCH_PAGE_SIZE,
|
||||||
|
albumArtistStartIndex: startIndex,
|
||||||
|
albumLimit: 0,
|
||||||
|
albumStartIndex: 0,
|
||||||
|
query: searchTerm,
|
||||||
|
songLimit: 0,
|
||||||
|
songStartIndex: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.search.infiniteList(serverId ?? '', 'albumArtists', searchTerm),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
searchAlbumsInfinite: (args: {
|
||||||
|
enabled?: boolean;
|
||||||
|
searchTerm: string;
|
||||||
|
serverId: string | undefined;
|
||||||
|
}) => {
|
||||||
|
const { enabled = true, searchTerm, serverId } = args;
|
||||||
|
return infiniteQueryOptions({
|
||||||
|
enabled: Boolean(serverId && searchTerm && enabled),
|
||||||
|
getNextPageParam: (lastPage: SearchResponse, allPages: SearchResponse[]) => {
|
||||||
|
const len = lastPage.albums.length;
|
||||||
|
if (len < SEARCH_PAGE_SIZE) return undefined;
|
||||||
|
return allPages.length * SEARCH_PAGE_SIZE;
|
||||||
|
},
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: ({ pageParam, signal }) => {
|
||||||
|
if (!serverId) throw new Error('serverId required');
|
||||||
|
const startIndex = (pageParam ?? 0) as number;
|
||||||
|
return api.controller.search({
|
||||||
|
apiClientProps: { serverId, signal },
|
||||||
|
query: {
|
||||||
|
albumArtistLimit: 0,
|
||||||
|
albumArtistStartIndex: 0,
|
||||||
|
albumLimit: SEARCH_PAGE_SIZE,
|
||||||
|
albumStartIndex: startIndex,
|
||||||
|
query: searchTerm,
|
||||||
|
songLimit: 0,
|
||||||
|
songStartIndex: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.search.infiniteList(serverId ?? '', 'albums', searchTerm),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
searchSongsInfinite: (args: {
|
||||||
|
enabled?: boolean;
|
||||||
|
searchTerm: string;
|
||||||
|
serverId: string | undefined;
|
||||||
|
}) => {
|
||||||
|
const { enabled = true, searchTerm, serverId } = args;
|
||||||
|
return infiniteQueryOptions({
|
||||||
|
enabled: Boolean(serverId && searchTerm && enabled),
|
||||||
|
getNextPageParam: (lastPage: SearchResponse, allPages: SearchResponse[]) => {
|
||||||
|
const len = lastPage.songs.length;
|
||||||
|
if (len < SEARCH_PAGE_SIZE) return undefined;
|
||||||
|
return allPages.length * SEARCH_PAGE_SIZE;
|
||||||
|
},
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: ({ pageParam, signal }) => {
|
||||||
|
if (!serverId) throw new Error('serverId required');
|
||||||
|
const startIndex = (pageParam ?? 0) as number;
|
||||||
|
return api.controller.search({
|
||||||
|
apiClientProps: { serverId, signal },
|
||||||
|
query: {
|
||||||
|
albumArtistLimit: 0,
|
||||||
|
albumArtistStartIndex: 0,
|
||||||
|
albumLimit: 0,
|
||||||
|
albumStartIndex: 0,
|
||||||
|
query: searchTerm,
|
||||||
|
songLimit: SEARCH_PAGE_SIZE,
|
||||||
|
songStartIndex: startIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.search.infiniteList(serverId ?? '', 'songs', searchTerm),
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
.root {
|
.root {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--theme-spacing-xs);
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: var(--theme-spacing-xs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -38,5 +35,4 @@
|
|||||||
.items {
|
.items {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ReactNode, useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import styles from './collapsible-command-group.module.css';
|
import styles from './collapsible-command-group.module.css';
|
||||||
|
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Paper } from '/@/shared/components/paper/paper';
|
import { Paper } from '/@/shared/components/paper/paper';
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ interface CollapsibleCommandGroupProps {
|
|||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
heading: string;
|
heading: string;
|
||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CollapsibleCommandGroup({
|
export function CollapsibleCommandGroup({
|
||||||
@@ -19,6 +21,7 @@ export function CollapsibleCommandGroup({
|
|||||||
expanded: controlledExpanded,
|
expanded: controlledExpanded,
|
||||||
heading,
|
heading,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
subtitle,
|
||||||
}: CollapsibleCommandGroupProps) {
|
}: CollapsibleCommandGroupProps) {
|
||||||
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
||||||
|
|
||||||
@@ -54,7 +57,10 @@ export function CollapsibleCommandGroup({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Icon className={styles.chevron} icon={expanded ? 'dropdown' : 'arrowRightS'} />
|
<Icon className={styles.chevron} icon={expanded ? 'dropdown' : 'arrowRightS'} />
|
||||||
<span>{heading}</span>
|
<Group justify="space-between" w="100%">
|
||||||
|
<span>{heading}</span>
|
||||||
|
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||||
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
{expanded && <div className={styles.items}>{children}</div>}
|
{expanded && <div className={styles.items}>{children}</div>}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { generatePath, useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
import { searchQueries } from '/@/renderer/features/search/api/search-api';
|
|
||||||
import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group';
|
|
||||||
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
|
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
|
||||||
import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable';
|
|
||||||
import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands';
|
import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands';
|
||||||
import { HomeCommands } from '/@/renderer/features/search/components/home-commands';
|
import { HomeCommands } from '/@/renderer/features/search/components/home-commands';
|
||||||
import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item';
|
import { SearchAlbumArtistsSection } from '/@/renderer/features/search/components/search-album-artists-section';
|
||||||
|
import { SearchAlbumsSection } from '/@/renderer/features/search/components/search-albums-section';
|
||||||
|
import { SearchSongsSection } from '/@/renderer/features/search/components/search-songs-section';
|
||||||
import { ServerCommands } from '/@/renderer/features/search/components/server-commands';
|
import { ServerCommands } from '/@/renderer/features/search/components/server-commands';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { useAppStore } from '/@/renderer/store';
|
||||||
import { useAppStore, useCurrentServer } from '/@/renderer/store';
|
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Breadcrumb } from '/@/shared/components/breadcrumb/breadcrumb';
|
import { Breadcrumb } from '/@/shared/components/breadcrumb/breadcrumb';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
@@ -21,11 +16,10 @@ import { Group } from '/@/shared/components/group/group';
|
|||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Kbd } from '/@/shared/components/kbd/kbd';
|
import { Kbd } from '/@/shared/components/kbd/kbd';
|
||||||
import { Modal } from '/@/shared/components/modal/modal';
|
import { Modal } from '/@/shared/components/modal/modal';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
||||||
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
interface CommandPaletteProps {
|
interface CommandPaletteProps {
|
||||||
modalProps: (typeof useDisclosure)['arguments'];
|
modalProps: (typeof useDisclosure)['arguments'];
|
||||||
@@ -37,23 +31,110 @@ const SEARCH_SECTION_IDS = {
|
|||||||
tracks: 'tracks',
|
tracks: 'tracks',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
interface CommandPaletteSearchProps {
|
||||||
const navigate = useNavigate();
|
children?: React.ReactNode;
|
||||||
const server = useCurrentServer();
|
isHome: boolean;
|
||||||
|
onSelectResult: () => void;
|
||||||
|
query: string;
|
||||||
|
searchInputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
setQuery: (query: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandPaletteSearch({
|
||||||
|
children,
|
||||||
|
isHome,
|
||||||
|
onSelectResult,
|
||||||
|
query,
|
||||||
|
searchInputRef,
|
||||||
|
setQuery,
|
||||||
|
}: CommandPaletteSearchProps) {
|
||||||
|
const [debouncedQuery] = useDebouncedValue(query, 400);
|
||||||
const searchSectionsExpanded = useAppStore(
|
const searchSectionsExpanded = useAppStore(
|
||||||
(state) => state.commandPaletteSearchSectionsExpanded,
|
(state) => state.commandPaletteSearchSectionsExpanded,
|
||||||
);
|
);
|
||||||
const setSearchSectionExpanded = useAppStore(
|
const setSearchSectionExpanded = useAppStore(
|
||||||
(state) => state.actions.setCommandPaletteSearchSectionExpanded,
|
(state) => state.actions.setCommandPaletteSearchSectionExpanded,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
data-autofocus
|
||||||
|
leftSection={<Icon icon="search" />}
|
||||||
|
onChange={(e) => setQuery(e.currentTarget.value)}
|
||||||
|
ref={searchInputRef}
|
||||||
|
rightSection={
|
||||||
|
query && (
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => {
|
||||||
|
setQuery('');
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
}}
|
||||||
|
variant="transparent"
|
||||||
|
>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</ActionIcon>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
value={query}
|
||||||
|
/>
|
||||||
|
<Divider my="sm" />
|
||||||
|
<Command.List>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<SearchAlbumsSection
|
||||||
|
debouncedQuery={debouncedQuery ?? ''}
|
||||||
|
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.albums] ?? true}
|
||||||
|
isHome={isHome}
|
||||||
|
onSelectResult={onSelectResult}
|
||||||
|
onToggle={() =>
|
||||||
|
setSearchSectionExpanded(
|
||||||
|
SEARCH_SECTION_IDS.albums,
|
||||||
|
!(searchSectionsExpanded[SEARCH_SECTION_IDS.albums] ?? true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
query={query}
|
||||||
|
/>
|
||||||
|
<SearchAlbumArtistsSection
|
||||||
|
debouncedQuery={debouncedQuery ?? ''}
|
||||||
|
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.artists] ?? true}
|
||||||
|
isHome={isHome}
|
||||||
|
onSelectResult={onSelectResult}
|
||||||
|
onToggle={() =>
|
||||||
|
setSearchSectionExpanded(
|
||||||
|
SEARCH_SECTION_IDS.artists,
|
||||||
|
!(searchSectionsExpanded[SEARCH_SECTION_IDS.artists] ?? true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
query={query}
|
||||||
|
/>
|
||||||
|
<SearchSongsSection
|
||||||
|
debouncedQuery={debouncedQuery ?? ''}
|
||||||
|
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.tracks] ?? true}
|
||||||
|
isHome={isHome}
|
||||||
|
onSelectResult={onSelectResult}
|
||||||
|
onToggle={() =>
|
||||||
|
setSearchSectionExpanded(
|
||||||
|
SEARCH_SECTION_IDS.tracks,
|
||||||
|
!(searchSectionsExpanded[SEARCH_SECTION_IDS.tracks] ?? true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
query={query}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
{children}
|
||||||
|
</Command.List>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [debouncedQuery] = useDebouncedValue(query, 400);
|
|
||||||
const [pages, setPages] = useState<CommandPalettePages[]>([CommandPalettePages.HOME]);
|
const [pages, setPages] = useState<CommandPalettePages[]>([CommandPalettePages.HOME]);
|
||||||
const activePage = pages[pages.length - 1];
|
const activePage = pages[pages.length - 1];
|
||||||
const isHome = activePage === CommandPalettePages.HOME;
|
const isHome = activePage === CommandPalettePages.HOME;
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const popPage = useCallback(() => {
|
const popPage = useCallback(() => {
|
||||||
setPages((pages) => {
|
setPages((pages) => {
|
||||||
@@ -63,25 +144,10 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data, isLoading } = useQuery(
|
const handleSelectResult = useCallback(() => {
|
||||||
searchQueries.search({
|
modalProps.handlers.close();
|
||||||
options: { enabled: isHome && debouncedQuery !== '' && query !== '' },
|
setQuery('');
|
||||||
query: {
|
}, [modalProps.handlers]);
|
||||||
albumArtistLimit: 4,
|
|
||||||
albumArtistStartIndex: 0,
|
|
||||||
albumLimit: 4,
|
|
||||||
albumStartIndex: 0,
|
|
||||||
query: debouncedQuery,
|
|
||||||
songLimit: 4,
|
|
||||||
songStartIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const showAlbumGroup = isHome && Boolean(query && data && data?.albums?.length > 0);
|
|
||||||
const showArtistGroup = isHome && Boolean(query && data && data?.albumArtists?.length > 0);
|
|
||||||
const showTrackGroup = isHome && Boolean(query && data && data?.songs?.length > 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -120,8 +186,6 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
}}
|
}}
|
||||||
label="Global Command Menu"
|
label="Global Command Menu"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// Focus the search input when navigating with arrow keys
|
|
||||||
// to prevent the focus from staying on the command-item ActionIcon
|
|
||||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
searchInputRef.current?.focus();
|
searchInputRef.current?.focus();
|
||||||
}
|
}
|
||||||
@@ -129,171 +193,13 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
onValueChange={setValue}
|
onValueChange={setValue}
|
||||||
value={value}
|
value={value}
|
||||||
>
|
>
|
||||||
<TextInput
|
<CommandPaletteSearch
|
||||||
data-autofocus
|
isHome={isHome}
|
||||||
leftSection={<Icon icon="search" />}
|
onSelectResult={handleSelectResult}
|
||||||
onChange={(e) => setQuery(e.currentTarget.value)}
|
query={query}
|
||||||
ref={searchInputRef}
|
searchInputRef={searchInputRef}
|
||||||
rightSection={
|
setQuery={setQuery}
|
||||||
isLoading ? (
|
>
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
query && (
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => {
|
|
||||||
setQuery('');
|
|
||||||
searchInputRef.current?.focus();
|
|
||||||
}}
|
|
||||||
variant="transparent"
|
|
||||||
>
|
|
||||||
<Icon icon="x" />
|
|
||||||
</ActionIcon>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
value={query}
|
|
||||||
/>
|
|
||||||
<Divider my="sm" />
|
|
||||||
<Command.List>
|
|
||||||
<Command.Empty>
|
|
||||||
{t('common.noResultsFromQuery', { postProcess: 'sentenceCase' })}
|
|
||||||
</Command.Empty>
|
|
||||||
{showAlbumGroup && (
|
|
||||||
<CollapsibleCommandGroup
|
|
||||||
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.albums] ?? true}
|
|
||||||
heading="Albums"
|
|
||||||
onToggle={() =>
|
|
||||||
setSearchSectionExpanded(
|
|
||||||
SEARCH_SECTION_IDS.albums,
|
|
||||||
!(searchSectionsExpanded[SEARCH_SECTION_IDS.albums] ?? true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{data?.albums?.map((album) => (
|
|
||||||
<CommandItemSelectable
|
|
||||||
key={`search-album-${album.id}`}
|
|
||||||
onSelect={() => {
|
|
||||||
navigate(
|
|
||||||
generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
|
||||||
albumId: album.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
modalProps.handlers.close();
|
|
||||||
setQuery('');
|
|
||||||
}}
|
|
||||||
value={`search-${album.id}`}
|
|
||||||
>
|
|
||||||
{({ isHighlighted }) => (
|
|
||||||
<LibraryCommandItem
|
|
||||||
explicitStatus={album.explicitStatus}
|
|
||||||
id={album.id}
|
|
||||||
imageId={album.imageId}
|
|
||||||
imageUrl={album.imageUrl}
|
|
||||||
isHighlighted={isHighlighted}
|
|
||||||
itemType={LibraryItem.ALBUM}
|
|
||||||
subtitle={album.albumArtists
|
|
||||||
.map((artist) => artist.name)
|
|
||||||
.join(', ')}
|
|
||||||
title={album.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</CommandItemSelectable>
|
|
||||||
))}
|
|
||||||
</CollapsibleCommandGroup>
|
|
||||||
)}
|
|
||||||
{showArtistGroup && (
|
|
||||||
<CollapsibleCommandGroup
|
|
||||||
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.artists] ?? true}
|
|
||||||
heading="Artists"
|
|
||||||
onToggle={() =>
|
|
||||||
setSearchSectionExpanded(
|
|
||||||
SEARCH_SECTION_IDS.artists,
|
|
||||||
!(searchSectionsExpanded[SEARCH_SECTION_IDS.artists] ?? true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{data?.albumArtists.map((artist) => (
|
|
||||||
<CommandItemSelectable
|
|
||||||
key={`artist-${artist.id}`}
|
|
||||||
onSelect={() => {
|
|
||||||
navigate(
|
|
||||||
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
|
||||||
albumArtistId: artist.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
modalProps.handlers.close();
|
|
||||||
setQuery('');
|
|
||||||
}}
|
|
||||||
value={`search-${artist.id}`}
|
|
||||||
>
|
|
||||||
{({ isHighlighted }) => (
|
|
||||||
<LibraryCommandItem
|
|
||||||
disabled={artist?.albumCount === 0}
|
|
||||||
id={artist.id}
|
|
||||||
imageId={artist.imageId}
|
|
||||||
imageUrl={artist.imageUrl}
|
|
||||||
isHighlighted={isHighlighted}
|
|
||||||
itemType={LibraryItem.ALBUM_ARTIST}
|
|
||||||
subtitle={
|
|
||||||
artist?.albumCount !== undefined &&
|
|
||||||
artist?.albumCount !== null
|
|
||||||
? t('entity.albumWithCount', {
|
|
||||||
count: artist.albumCount,
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
title={artist.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</CommandItemSelectable>
|
|
||||||
))}
|
|
||||||
</CollapsibleCommandGroup>
|
|
||||||
)}
|
|
||||||
{showTrackGroup && (
|
|
||||||
<CollapsibleCommandGroup
|
|
||||||
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.tracks] ?? true}
|
|
||||||
heading="Tracks"
|
|
||||||
onToggle={() =>
|
|
||||||
setSearchSectionExpanded(
|
|
||||||
SEARCH_SECTION_IDS.tracks,
|
|
||||||
!(searchSectionsExpanded[SEARCH_SECTION_IDS.tracks] ?? true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{data?.songs.map((song) => (
|
|
||||||
<CommandItemSelectable
|
|
||||||
key={`artist-${song.id}`}
|
|
||||||
onSelect={() => {
|
|
||||||
navigate(
|
|
||||||
generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
|
||||||
albumId: song.albumId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
modalProps.handlers.close();
|
|
||||||
setQuery('');
|
|
||||||
}}
|
|
||||||
value={`search-${song.id}`}
|
|
||||||
>
|
|
||||||
{({ isHighlighted }) => (
|
|
||||||
<LibraryCommandItem
|
|
||||||
explicitStatus={song.explicitStatus}
|
|
||||||
id={song.id}
|
|
||||||
imageId={song.imageId}
|
|
||||||
imageUrl={song.imageUrl}
|
|
||||||
isHighlighted={isHighlighted}
|
|
||||||
itemType={LibraryItem.SONG}
|
|
||||||
song={song}
|
|
||||||
subtitle={song.artists
|
|
||||||
.map((artist) => artist.name)
|
|
||||||
.join(', ')}
|
|
||||||
title={song.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</CommandItemSelectable>
|
|
||||||
))}
|
|
||||||
</CollapsibleCommandGroup>
|
|
||||||
)}
|
|
||||||
{activePage === CommandPalettePages.HOME && (
|
{activePage === CommandPalettePages.HOME && (
|
||||||
<HomeCommands
|
<HomeCommands
|
||||||
handleClose={modalProps.handlers.close}
|
handleClose={modalProps.handlers.close}
|
||||||
@@ -317,7 +223,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
setQuery={setQuery}
|
setQuery={setQuery}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Command.List>
|
</CommandPaletteSearch>
|
||||||
</Command>
|
</Command>
|
||||||
<Divider my="sm" />
|
<Divider my="sm" />
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { searchQueries } from '/@/renderer/features/search/api/search-api';
|
||||||
|
import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group';
|
||||||
|
import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable';
|
||||||
|
import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { Box } from '/@/shared/components/box/box';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
interface SearchAlbumArtistsSectionProps {
|
||||||
|
debouncedQuery: string;
|
||||||
|
expanded: boolean;
|
||||||
|
isHome: boolean;
|
||||||
|
onSelectResult: () => void;
|
||||||
|
onToggle: () => void;
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchAlbumArtistsSection({
|
||||||
|
debouncedQuery,
|
||||||
|
expanded,
|
||||||
|
isHome,
|
||||||
|
onSelectResult,
|
||||||
|
onToggle,
|
||||||
|
query,
|
||||||
|
}: SearchAlbumArtistsSectionProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data, fetchNextPage, hasNextPage, isFetched, isFetchingNextPage, isLoading } =
|
||||||
|
useInfiniteQuery(
|
||||||
|
searchQueries.searchAlbumArtistsInfinite({
|
||||||
|
enabled: isHome && debouncedQuery !== '' && query !== '',
|
||||||
|
searchTerm: debouncedQuery,
|
||||||
|
serverId: server?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const artists = data?.pages.flatMap((p) => p.albumArtists) ?? [];
|
||||||
|
const showSection = isHome;
|
||||||
|
const numberOfResults = hasNextPage ? `${artists.length}+` : artists.length;
|
||||||
|
|
||||||
|
if (!showSection) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleCommandGroup
|
||||||
|
expanded={expanded}
|
||||||
|
heading="Artists"
|
||||||
|
onToggle={onToggle}
|
||||||
|
subtitle={isFetched ? t('common.numberOfResults', { numberOfResults }) : undefined}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Box p="md">
|
||||||
|
<Spinner container />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{artists.map((artist) => (
|
||||||
|
<CommandItemSelectable
|
||||||
|
key={`search-artist-${artist.id}`}
|
||||||
|
onSelect={() => {
|
||||||
|
navigate(
|
||||||
|
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||||
|
albumArtistId: artist.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
onSelectResult();
|
||||||
|
}}
|
||||||
|
value={`search-artist-${artist.id}`}
|
||||||
|
>
|
||||||
|
{({ isHighlighted }) => (
|
||||||
|
<LibraryCommandItem
|
||||||
|
disabled={artist?.albumCount === 0}
|
||||||
|
id={artist.id}
|
||||||
|
imageId={artist.imageId}
|
||||||
|
imageUrl={artist.imageUrl}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
|
subtitle={
|
||||||
|
artist?.albumCount !== undefined &&
|
||||||
|
artist?.albumCount !== null
|
||||||
|
? t('entity.albumWithCount', {
|
||||||
|
count: artist.albumCount,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
title={artist.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommandItemSelectable>
|
||||||
|
))}
|
||||||
|
{hasNextPage && (
|
||||||
|
<CommandItemSelectable
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
onSelect={() => fetchNextPage()}
|
||||||
|
value="search-artists-load-more"
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<Text>
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<Text size="sm">
|
||||||
|
{t('action.viewMore', { postProcess: 'titleCase' })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</CommandItemSelectable>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleCommandGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { searchQueries } from '/@/renderer/features/search/api/search-api';
|
||||||
|
import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group';
|
||||||
|
import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable';
|
||||||
|
import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { Box } from '/@/shared/components/box/box';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
interface SearchAlbumsSectionProps {
|
||||||
|
debouncedQuery: string;
|
||||||
|
expanded: boolean;
|
||||||
|
isHome: boolean;
|
||||||
|
onSelectResult: () => void;
|
||||||
|
onToggle: () => void;
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchAlbumsSection({
|
||||||
|
debouncedQuery,
|
||||||
|
expanded,
|
||||||
|
isHome,
|
||||||
|
onSelectResult,
|
||||||
|
onToggle,
|
||||||
|
query,
|
||||||
|
}: SearchAlbumsSectionProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data, fetchNextPage, hasNextPage, isFetched, isFetchingNextPage, isLoading } =
|
||||||
|
useInfiniteQuery(
|
||||||
|
searchQueries.searchAlbumsInfinite({
|
||||||
|
enabled: isHome && debouncedQuery !== '' && query !== '',
|
||||||
|
searchTerm: debouncedQuery,
|
||||||
|
serverId: server?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const albums = data?.pages.flatMap((p) => p.albums) ?? [];
|
||||||
|
const showSection = isHome;
|
||||||
|
const numberOfResults = hasNextPage ? `${albums.length}+` : albums.length;
|
||||||
|
|
||||||
|
if (!showSection) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleCommandGroup
|
||||||
|
expanded={expanded}
|
||||||
|
heading="Albums"
|
||||||
|
onToggle={onToggle}
|
||||||
|
subtitle={isFetched ? t('common.numberOfResults', { numberOfResults }) : undefined}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Box p="md">
|
||||||
|
<Spinner container />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{albums.map((album) => (
|
||||||
|
<CommandItemSelectable
|
||||||
|
key={`search-album-${album.id}`}
|
||||||
|
onSelect={() => {
|
||||||
|
navigate(
|
||||||
|
generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||||
|
albumId: album.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
onSelectResult();
|
||||||
|
}}
|
||||||
|
value={`search-album-${album.id}`}
|
||||||
|
>
|
||||||
|
{({ isHighlighted }) => (
|
||||||
|
<LibraryCommandItem
|
||||||
|
explicitStatus={album.explicitStatus}
|
||||||
|
id={album.id}
|
||||||
|
imageId={album.imageId}
|
||||||
|
imageUrl={album.imageUrl}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
subtitle={album.albumArtists
|
||||||
|
.map((artist) => artist.name)
|
||||||
|
.join(', ')}
|
||||||
|
title={album.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommandItemSelectable>
|
||||||
|
))}
|
||||||
|
{hasNextPage && (
|
||||||
|
<CommandItemSelectable
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
onSelect={() => fetchNextPage()}
|
||||||
|
value="search-albums-load-more"
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<Text size="sm">
|
||||||
|
{t('action.viewMore', { postProcess: 'titleCase' })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CommandItemSelectable>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleCommandGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { searchQueries } from '/@/renderer/features/search/api/search-api';
|
||||||
|
import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group';
|
||||||
|
import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable';
|
||||||
|
import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { Box } from '/@/shared/components/box/box';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
interface SearchSongsSectionProps {
|
||||||
|
debouncedQuery: string;
|
||||||
|
expanded: boolean;
|
||||||
|
isHome: boolean;
|
||||||
|
onSelectResult: () => void;
|
||||||
|
onToggle: () => void;
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchSongsSection({
|
||||||
|
debouncedQuery,
|
||||||
|
expanded,
|
||||||
|
isHome,
|
||||||
|
onSelectResult,
|
||||||
|
onToggle,
|
||||||
|
query,
|
||||||
|
}: SearchSongsSectionProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data, fetchNextPage, hasNextPage, isFetched, isFetchingNextPage, isLoading } =
|
||||||
|
useInfiniteQuery(
|
||||||
|
searchQueries.searchSongsInfinite({
|
||||||
|
enabled: isHome && debouncedQuery !== '' && query !== '',
|
||||||
|
searchTerm: debouncedQuery,
|
||||||
|
serverId: server?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const songs = data?.pages.flatMap((p) => p.songs) ?? [];
|
||||||
|
const showSection = isHome;
|
||||||
|
const numberOfResults = hasNextPage ? `${songs.length}+` : songs.length;
|
||||||
|
|
||||||
|
if (!showSection) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleCommandGroup
|
||||||
|
expanded={expanded}
|
||||||
|
heading="Tracks"
|
||||||
|
onToggle={onToggle}
|
||||||
|
subtitle={isFetched ? t('common.numberOfResults', { numberOfResults }) : undefined}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Box p="md">
|
||||||
|
<Spinner container />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{songs.map((song) => (
|
||||||
|
<CommandItemSelectable
|
||||||
|
key={`search-song-${song.id}`}
|
||||||
|
onSelect={() => {
|
||||||
|
navigate(
|
||||||
|
generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||||
|
albumId: song.albumId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
onSelectResult();
|
||||||
|
}}
|
||||||
|
value={`search-song-${song.id}`}
|
||||||
|
>
|
||||||
|
{({ isHighlighted }) => (
|
||||||
|
<LibraryCommandItem
|
||||||
|
explicitStatus={song.explicitStatus}
|
||||||
|
id={song.id}
|
||||||
|
imageId={song.imageId}
|
||||||
|
imageUrl={song.imageUrl}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
|
itemType={LibraryItem.SONG}
|
||||||
|
song={song}
|
||||||
|
subtitle={song.artists.map((artist) => artist.name).join(', ')}
|
||||||
|
title={song.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommandItemSelectable>
|
||||||
|
))}
|
||||||
|
{hasNextPage && (
|
||||||
|
<CommandItemSelectable
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
onSelect={() => fetchNextPage()}
|
||||||
|
value="search-songs-load-more"
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<Text size="sm">
|
||||||
|
{t('action.viewMore', { postProcess: 'titleCase' })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CommandItemSelectable>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleCommandGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user