import { Check, ChevronsUpDown, X } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { CollectionItem } from "@vertesia/common";
import {
Button, cn, ErrorBox, useDebounce, useFetch,
Popover, PopoverContent, PopoverTrigger,
Command, CommandEmpty, CommandGroup, CommandItem, CommandInput
} from "@vertesia/ui/core";
import { useUserSession } from "@vertesia/ui/session";
import { useUITranslation } from '../../../i18n/index.js';
/**
* A component to select a collection from a list of collections.
* It fetches the collections from the store and displays them in a dropdown.
* @param props - The properties for the component.
* @returns A dropdown to select a collection.
**/
interface SelectCollectionProps {
value?: string | string[];
onChange: (collectionId: string | string[] | undefined, collection?: CollectionItem | CollectionItem[]) => void;
disabled?: boolean;
placeholder?: string;
searchPlaceholder?: string;
filterOut?: string[]; // collection IDs to filter out from the list
allowDynamic?: boolean;
multiple?: boolean;
}
export function SelectCollection({ onChange, value, disabled = false, placeholder, searchPlaceholder, filterOut, allowDynamic = true, multiple = false }: SelectCollectionProps) {
const { client } = useUserSession();
const { t } = useUITranslation();
const resolvedPlaceholder = placeholder ?? t('store.selectACollection');
const resolvedSearchPlaceholder = searchPlaceholder ?? t('store.searchCollections');
const [searchQuery, setSearchQuery] = useState('');
const [isSearching, setIsSearching] = useState(false);
const [useServerSearch, setUseServerSearch] = useState(false);
// Debounce the search query to avoid excessive API calls (only used for server-side search)
const debouncedSearchQuery = useDebounce(searchQuery, 300);
// Memoize the search function to prevent unnecessary re-renders
const searchCollections = useCallback(async (query: string) => {
setIsSearching(true);
const trimmedQuery = query.trim();
const collections = await client.store.collections.search({
dynamic: allowDynamic ? undefined : false,
name: useServerSearch ? (trimmedQuery || undefined) : undefined
});
setIsSearching(false);
// Check if we hit the maximum limit (1000 collections) - if so, enable server-side search
if (!useServerSearch && collections.length >= 1000) {
setUseServerSearch(true);
}
// Filter out collections if filterOut is provided
if (filterOut && filterOut.length > 0) {
return collections.filter(col => !filterOut.includes(col.id));
}
return collections;
}, [client, allowDynamic, filterOut, useServerSearch]);
// Fetch collections based on search mode
const { data: collections, error } = useFetch(
() => searchCollections(useServerSearch ? debouncedSearchQuery : ''),
[useServerSearch ? debouncedSearchQuery : '', searchCollections]
);
// Memoize the selected collection(s)
const selectedCollection = useMemo(() => {
if (!collections) return multiple ? [] : undefined;
if (multiple && Array.isArray(value)) {
return collections.filter((collection: CollectionItem) => value.includes(collection.id));
} else if (!multiple && typeof value === 'string') {
return collections.find((collection: CollectionItem) => collection.id === value);
}
return multiple ? [] : undefined;
}, [collections, value, multiple]);
// Handle collection selection
const handleSelect = useCallback((collection: CollectionItem) => {
if (multiple) {
const currentValues = Array.isArray(value) ? value : [];
const isSelected = currentValues.includes(collection.id);
if (isSelected) {
// Remove from selection
const newValues = currentValues.filter(id => id !== collection.id);
const newCollections = collections?.filter(c => newValues.includes(c.id)) || [];
onChange(newValues, newCollections);
} else {
// Add to selection
const newValues = [...currentValues, collection.id];
const newCollections = collections?.filter(c => newValues.includes(c.id)) || [];
onChange(newValues, newCollections);
}
} else {
onChange(collection.id, collection);
}
}, [onChange, value, collections, multiple]);
// Handle clear selection
const handleClear = useCallback(() => {
onChange(undefined, undefined);
}, [onChange]);
// Handle search input change
const handleSearchChange = useCallback((query: string) => {
setSearchQuery(query);
}, []);
const hasSearchQuery = searchQuery.trim().length > 0;
// Client-side filtering when not using server search
const filteredCollections = useMemo(() => {
if (!collections) return [];
// If using server search, collections are already filtered by the server
if (useServerSearch) return collections;
// Otherwise, do client-side filtering
if (!hasSearchQuery) return collections;
const queryLower = searchQuery.toLowerCase();
return collections.filter(col => col.name.toLowerCase().includes(queryLower));
}, [collections, useServerSearch, hasSearchQuery, searchQuery]);
const showClearOption = multiple
? Array.isArray(selectedCollection) && selectedCollection.length > 0
: !!selectedCollection;
const renderTrailingIcon = () => {
if (showClearOption) {
return (
{
e.stopPropagation();
handleClear();
}}
>
);
}
return ;
};
// Show error state
if (error) {
return (
{error.message}
);
}
// Get display text for the button
const getDisplayText = () => {
if (multiple && Array.isArray(selectedCollection) && selectedCollection.length > 0) {
if (selectedCollection.length === 1) {
return selectedCollection[0].name;
}
return t('store.collectionsSelected', { count: selectedCollection.length });
} else if (!multiple && selectedCollection && !Array.isArray(selectedCollection)) {
return selectedCollection.name;
}
return resolvedPlaceholder;
};
return (
{
isSearching
? t('store.searching')
: hasSearchQuery
? t('store.noCollectionsFound')
: t('store.noCollectionsAvailable')
}
{
showClearOption && !hasSearchQuery && (
Remove collection selection(s)
)
}
{
hasSearchQuery && (
setSearchQuery("")}
className="text-muted"
>
{t('store.clearSelection')}
)
}
{
filteredCollections.map((collection: CollectionItem) => {
const isSelected = multiple && Array.isArray(value)
? value.includes(collection.id)
: value === collection.id;
return (
handleSelect(collection)}
className="flex items-center justify-between"
>
{collection.name}
{isSelected && (
)}
);
})
}
);
}