mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-02-20 07:37:26 -05:00
[MOB-68] Search (#2434)
* wip: search plugin query & save * wip * wip mob search * mob search - explorer filtering & support name, extension search * Mobile search * cleanup * code improvements & type to search all locations * Update Search.tsx * ts * code doc and type tweak * lint * move onSuccess
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import { useRef } from 'react';
|
||||
import { ColorValue, Pressable, Text, View } from 'react-native';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
@@ -19,11 +19,9 @@ type DrawerTagItemProps = {
|
||||
const DrawerTagItem: React.FC<DrawerTagItemProps> = (props) => {
|
||||
const { tagName, tagColor, onPress } = props;
|
||||
return (
|
||||
<Pressable onPress={onPress} testID="drawer-tag">
|
||||
<Pressable style={tw`flex-1`} onPress={onPress} testID="drawer-tag">
|
||||
<View
|
||||
style={twStyle(
|
||||
'h-auto flex-row items-center gap-2 rounded-md border border-app-inputborder/50 bg-app-darkBox p-2'
|
||||
)}
|
||||
style={tw`flex-row items-center gap-2 rounded-md border border-app-inputborder/50 bg-app-darkBox p-2`}
|
||||
>
|
||||
<View style={twStyle('h-4 w-4 rounded-full', { backgroundColor: tagColor })} />
|
||||
<Text style={twStyle('text-xs font-medium text-ink')} numberOfLines={1}>
|
||||
@@ -50,7 +48,7 @@ const DrawerTags = () => {
|
||||
>
|
||||
<View style={tw`mt-2 flex-row justify-between gap-1`}>
|
||||
<TagColumn tags={tagData} dataAmount={[0, 2]} />
|
||||
<TagColumn tags={tagData} dataAmount={[2, 4]} />
|
||||
{tagData?.length > 2 && <TagColumn tags={tagData} dataAmount={[2, 4]} />}
|
||||
</View>
|
||||
<View style={tw`mt-2 flex-row flex-wrap gap-1`}>
|
||||
{/* Add Tag */}
|
||||
@@ -92,8 +90,10 @@ interface TagColumnProps {
|
||||
const TagColumn = ({ tags, dataAmount }: TagColumnProps) => {
|
||||
const navigation = useNavigation<DrawerNavigationHelpers>();
|
||||
return (
|
||||
<View style={tw`w-[49%] flex-col gap-1`}>
|
||||
{tags?.slice(dataAmount[0], dataAmount[1]).map((tag: any) => (
|
||||
<View style={twStyle(`gap-1`,
|
||||
tags && tags.length > 2 ? 'w-[49%] flex-col' : 'flex-1 flex-row'
|
||||
)}>
|
||||
{tags?.slice(dataAmount[0], dataAmount[1]).map((tag: Tag) => (
|
||||
<DrawerTagItem
|
||||
key={tag.id}
|
||||
tagName={tag.name!}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { SearchData, isPath, type ExplorerItem } from '@sd/client';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
import { ActivityIndicator, Pressable } from 'react-native';
|
||||
import { isPath, SearchData, type ExplorerItem } from '@sd/client';
|
||||
import Layout from '~/constants/Layout';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
import { useExplorerStore } from '~/stores/explorerStore';
|
||||
import { useActionsModalStore } from '~/stores/modalStore';
|
||||
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import ScreenContainer from '../layout/ScreenContainer';
|
||||
import FileItem from './FileItem';
|
||||
import FileRow from './FileRow';
|
||||
@@ -21,21 +21,29 @@ type ExplorerProps = {
|
||||
loadMore: () => void;
|
||||
query: UseInfiniteQueryResult<SearchData<ExplorerItem>>;
|
||||
count?: number;
|
||||
};
|
||||
empty?: never;
|
||||
isEmpty?: never;
|
||||
}
|
||||
|
||||
const Explorer = (props: ExplorerProps) => {
|
||||
type Props = |
|
||||
ExplorerProps
|
||||
| ({
|
||||
// isEmpty and empty are mutually exclusive
|
||||
emptyComponent: React.ReactElement; // component to show when FlashList has no data
|
||||
isEmpty: boolean; // if true - show empty component
|
||||
} & Omit<ExplorerProps, 'empty' | 'isEmpty'>);
|
||||
|
||||
const Explorer = (props: Props) => {
|
||||
const navigation = useNavigation<BrowseStackScreenProps<'Location'>['navigation']>();
|
||||
|
||||
const store = useExplorerStore();
|
||||
|
||||
const { modalRef, setData } = useActionsModalStore();
|
||||
|
||||
function handlePress(data: ExplorerItem) {
|
||||
if (isPath(data) && data.item.is_dir && data.item.location_id !== null) {
|
||||
navigation.push('Location', {
|
||||
id: data.item.location_id,
|
||||
path: `${data.item.materialized_path}${data.item.name}/`
|
||||
});
|
||||
navigation.push('Location', {
|
||||
id: data.item.location_id,
|
||||
path: `${data.item.materialized_path}${data.item.name}/`
|
||||
});
|
||||
} else {
|
||||
setData(data);
|
||||
modalRef.current?.present();
|
||||
@@ -45,38 +53,44 @@ const Explorer = (props: ExplorerProps) => {
|
||||
return (
|
||||
<ScreenContainer tabHeight={props.tabHeight} scrollview={false} style={'gap-0 py-0'}>
|
||||
<Menu />
|
||||
{/* Flashlist not supporting empty centering: https://github.com/Shopify/flash-list/discussions/517
|
||||
So needs to be done this way */}
|
||||
{/* Items */}
|
||||
<FlashList
|
||||
key={store.layoutMode}
|
||||
numColumns={store.layoutMode === 'grid' ? store.gridNumColumns : 1}
|
||||
data={props.items ?? []}
|
||||
keyExtractor={(item) =>
|
||||
item.type === 'NonIndexedPath'
|
||||
? item.item.path
|
||||
: item.type === 'SpacedropPeer'
|
||||
? item.item.name
|
||||
: item.item.id.toString()
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable onPress={() => handlePress(item)}>
|
||||
{store.layoutMode === 'grid' ? (
|
||||
<FileItem data={item} />
|
||||
) : (
|
||||
<FileRow data={item} />
|
||||
{props.isEmpty ? (
|
||||
props.emptyComponent
|
||||
) :
|
||||
<FlashList
|
||||
key={store.layoutMode}
|
||||
numColumns={store.layoutMode === 'grid' ? store.gridNumColumns : 1}
|
||||
data={props.items ?? []}
|
||||
keyExtractor={(item) =>
|
||||
item.type === 'NonIndexedPath'
|
||||
? item.item.path
|
||||
: item.type === 'SpacedropPeer'
|
||||
? item.item.name
|
||||
: item.item.id.toString()
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable onPress={() => handlePress(item)}>
|
||||
{store.layoutMode === 'grid' ? (
|
||||
<FileItem data={item} />
|
||||
) : (
|
||||
<FileRow data={item} />
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
contentContainerStyle={tw`px-2 py-5`}
|
||||
extraData={store.layoutMode}
|
||||
estimatedItemSize={
|
||||
store.layoutMode === 'grid'
|
||||
? Layout.window.width / store.gridNumColumns
|
||||
: store.listItemSize
|
||||
}
|
||||
onEndReached={() => props.loadMore?.()}
|
||||
onEndReachedThreshold={0.6}
|
||||
ListFooterComponent={props.query.isFetchingNextPage ? <ActivityIndicator /> : null}
|
||||
/>
|
||||
contentContainerStyle={tw`px-2 py-5`}
|
||||
extraData={store.layoutMode}
|
||||
estimatedItemSize={
|
||||
store.layoutMode === 'grid'
|
||||
? Layout.window.width / store.gridNumColumns
|
||||
: store.listItemSize
|
||||
}
|
||||
onEndReached={() => props.loadMore?.()}
|
||||
onEndReachedThreshold={0.6}
|
||||
ListFooterComponent={props.query.isFetchingNextPage ? <ActivityIndicator /> : null}
|
||||
/>
|
||||
}
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Location, arraysEqual, byteSize, useOnlineLocations } from '@sd/client';
|
||||
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
|
||||
import { useRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { arraysEqual, byteSize, Location, useOnlineLocations } from '@sd/client';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||
|
||||
@@ -63,7 +63,7 @@ const ListLocation = ({ location }: ListLocationProps) => {
|
||||
</View>
|
||||
<View style={tw`flex-row items-center gap-3`}>
|
||||
<View
|
||||
style={tw`rounded-md border border-app-lightborder bg-app-highlight p-1.5`}
|
||||
style={tw`rounded-md border border-app-box bg-app p-1.5`}
|
||||
>
|
||||
<Text
|
||||
style={tw`text-left text-xs font-medium text-ink-dull`}
|
||||
|
||||
56
apps/mobile/src/components/modal/search/SaveSearchModal.tsx
Normal file
56
apps/mobile/src/components/modal/search/SaveSearchModal.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { Modal, ModalRef } from '~/components/layout/Modal';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
import { ModalInput } from '~/components/primitive/Input';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { useSearchStore } from '~/stores/searchStore';
|
||||
|
||||
const SaveSearchModal = forwardRef<ModalRef>((_, ref) => {
|
||||
const [searchName, setSearchName] = useState('');
|
||||
const navigation = useNavigation();
|
||||
const searchStore = useSearchStore();
|
||||
const saveSearch = useLibraryMutation('search.saved.create', {
|
||||
onSuccess: () => {
|
||||
searchStore.applyFilters();
|
||||
navigation.navigate('SearchStack', {
|
||||
screen: 'Search'
|
||||
});
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Modal snapPoints={['22']} title="Save search" ref={ref}>
|
||||
<View style={tw`p-4`}>
|
||||
<ModalInput
|
||||
autoFocus
|
||||
value={searchName}
|
||||
onChangeText={(text) => setSearchName(text)}
|
||||
placeholder="Search Name..."
|
||||
/>
|
||||
<Button
|
||||
disabled={searchName.length === 0}
|
||||
style={tw`mt-2`}
|
||||
variant="accent"
|
||||
onPress={async () => {
|
||||
await saveSearch.mutateAsync(
|
||||
{
|
||||
name: searchName,
|
||||
filters: JSON.stringify(searchStore.mergedFilters),
|
||||
description: null,
|
||||
icon: null,
|
||||
search: null
|
||||
}
|
||||
);
|
||||
setSearchName('');
|
||||
}}
|
||||
>
|
||||
<Text style={tw`font-medium text-ink`}>Save</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default SaveSearchModal;
|
||||
@@ -20,20 +20,22 @@ import {
|
||||
KindItem,
|
||||
SearchFilters,
|
||||
TagItem,
|
||||
getSearchStore,
|
||||
useSearchStore
|
||||
} from '~/stores/searchStore';
|
||||
|
||||
const FiltersBar = () => {
|
||||
const { filters, appliedFilters } = useSearchStore();
|
||||
const searchStore = useSearchStore();
|
||||
const navigation = useNavigation<SearchStackScreenProps<'Filters'>['navigation']>();
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
const appliedFiltersLength = Object.keys(searchStore.appliedFilters).length;
|
||||
|
||||
// Scroll to start when there are less than 2 filters.
|
||||
useEffect(() => {
|
||||
if (Object.entries(appliedFilters).length < 2) {
|
||||
flatListRef.current?.scrollToOffset({ animated: true, offset: 0 });
|
||||
// If there are applied filters, update the searchStore filters
|
||||
if (appliedFiltersLength > 0) {
|
||||
Object.assign(getSearchStore().filters, searchStore.appliedFilters);
|
||||
}
|
||||
}, [appliedFilters]);
|
||||
}, [appliedFiltersLength, searchStore.appliedFilters]);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -52,8 +54,13 @@ const FiltersBar = () => {
|
||||
ref={flatListRef}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
horizontal
|
||||
data={Object.entries(appliedFilters)}
|
||||
extraData={filters}
|
||||
onContentSizeChange={() => {
|
||||
if (flatListRef.current && appliedFiltersLength < 2) {
|
||||
flatListRef.current.scrollToOffset({ animated: true, offset: 0 });
|
||||
}
|
||||
}}
|
||||
data={Object.entries(searchStore.appliedFilters)}
|
||||
extraData={searchStore.filters}
|
||||
keyExtractor={(item) => item[0]}
|
||||
renderItem={({ item }) => (
|
||||
<FilterItem filter={item[0] as SearchFilters} value={item[1]} />
|
||||
@@ -75,6 +82,10 @@ const FilterItem = ({ filter, value }: FilterItemProps) => {
|
||||
const boxStyle = tw`w-auto flex-row items-center gap-1.5 border border-app-cardborder bg-app-card p-2`;
|
||||
const filterCapital = filter.charAt(0).toUpperCase() + filter.slice(1);
|
||||
const searchStore = useSearchStore();
|
||||
|
||||
// if the filter value is false or empty, return null i.e "Hidden"
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<View style={tw`flex-row gap-0.5`}>
|
||||
<View style={twStyle(boxStyle, 'rounded-bl-md rounded-tl-md')}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnimatePresence } from 'moti';
|
||||
import { AnimatePresence, MotiView } from 'moti';
|
||||
import { MotiPressable } from 'moti/interactions';
|
||||
import {
|
||||
CircleDashed,
|
||||
@@ -13,7 +13,7 @@ import { Text, View } from 'react-native';
|
||||
import Card from '~/components/layout/Card';
|
||||
import SectionTitle from '~/components/layout/SectionTitle';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { getSearchStore, SearchFilters, useSearchStore } from '~/stores/searchStore';
|
||||
import { SearchFilters, getSearchStore, useSearchStore } from '~/stores/searchStore';
|
||||
|
||||
import Extension from './Extension';
|
||||
import Kind from './Kind';
|
||||
@@ -51,12 +51,15 @@ const FiltersList = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<SearchFilters[]>(
|
||||
Object.keys(searchStore.appliedFilters) as SearchFilters[]
|
||||
);
|
||||
const appliedFiltersLength = Object.keys(searchStore.appliedFilters).length;
|
||||
|
||||
|
||||
// If any filters are applied - we need to update the store
|
||||
// so the UI can reflect the applied filters
|
||||
useEffect(() => {
|
||||
Object.assign(getSearchStore().filters, getSearchStore().appliedFilters);
|
||||
}, []);
|
||||
//if there are selected filters but not applied reset them
|
||||
if (appliedFiltersLength === 0) {
|
||||
getSearchStore().resetFilters();
|
||||
}
|
||||
}, [appliedFiltersLength]);
|
||||
|
||||
const selectedHandler = useCallback(
|
||||
(option: Capitalize<SearchFilters>) => {
|
||||
@@ -80,13 +83,16 @@ const FiltersList = () => {
|
||||
searchStore.resetFilter(searchFiltersLowercase);
|
||||
}
|
||||
},
|
||||
[selectedOptions, searchStore]
|
||||
);
|
||||
[selectedOptions, searchStore])
|
||||
|
||||
return (
|
||||
<View style={tw`gap-10`}>
|
||||
<SavedSearches />
|
||||
<View>
|
||||
<MotiView
|
||||
from={{ opacity: 0, translateY: 20 }}
|
||||
animate={{ opacity: 1, translateY: 0 }}
|
||||
transition={{ type: 'timing', duration: 300 }}
|
||||
>
|
||||
<SectionTitle
|
||||
style={tw`px-6 pb-3`}
|
||||
title="What are you searching for?"
|
||||
@@ -139,7 +145,7 @@ const FiltersList = () => {
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</MotiView>
|
||||
{/* conditionally render the selected options - this approach makes sure the animation is right
|
||||
by not relying on the index position of the object */}
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Location, useLibraryQuery } from '@sd/client';
|
||||
import { MotiView } from 'moti';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FlatList, Pressable, Text, View } from 'react-native';
|
||||
import { LinearTransition } from 'react-native-reanimated';
|
||||
import { Location, useLibraryQuery } from '@sd/client';
|
||||
import { Icon } from '~/components/icons/Icon';
|
||||
import Card from '~/components/layout/Card';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
@@ -77,6 +77,7 @@ const LocationFilter = memo(({ data }: Props) => {
|
||||
name: data.name as string
|
||||
});
|
||||
}, [data.id, data.name, searchStore]);
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress}>
|
||||
<Card
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Plus } from 'phosphor-react-native';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Platform, Text, View } from 'react-native';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import SaveSearchModal from '~/components/modal/search/SaveSearchModal';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { SearchStackScreenProps } from '~/navigation/SearchStack';
|
||||
@@ -10,6 +12,8 @@ import { getSearchStore, useSearchStore } from '~/stores/searchStore';
|
||||
const SaveAdd = () => {
|
||||
const searchStore = useSearchStore();
|
||||
const navigation = useNavigation<SearchStackScreenProps<'Search'>['navigation']>();
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const filtersApplied = Object.keys(searchStore.appliedFilters).length > 0;
|
||||
const buttonDisable = !filtersApplied && searchStore.disableActionButtons;
|
||||
const isAndroid = Platform.OS === 'android';
|
||||
@@ -38,6 +42,7 @@ const SaveAdd = () => {
|
||||
opacity: buttonDisable ? 0.5 : 1
|
||||
})}
|
||||
variant="dashed"
|
||||
onPress={() => modalRef.current?.present()}
|
||||
>
|
||||
<Plus weight="bold" size={12} color={tw.color('text-ink-dull')} />
|
||||
<Text style={tw`font-medium text-ink-dull`}>Save search</Text>
|
||||
@@ -58,6 +63,7 @@ const SaveAdd = () => {
|
||||
{filtersApplied ? 'Update filters' : 'Add filters'}
|
||||
</Text>
|
||||
</Button>
|
||||
<SaveSearchModal ref={modalRef} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
SavedSearch as ISavedSearch,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
import { MotiView } from 'moti';
|
||||
import { MotiPressable } from 'moti/interactions';
|
||||
import { FlatList, Text, View } from 'react-native';
|
||||
import { X } from 'phosphor-react-native';
|
||||
import { FlatList, Pressable, Text, View } from 'react-native';
|
||||
import { Icon } from '~/components/icons/Icon';
|
||||
import Card from '~/components/layout/Card';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
import Fade from '~/components/layout/Fade';
|
||||
import SectionTitle from '~/components/layout/SectionTitle';
|
||||
import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper';
|
||||
import DottedDivider from '~/components/primitive/DottedDivider';
|
||||
import { useSavedSearch } from '~/hooks/useSavedSearch';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { getSearchStore } from '~/stores/searchStore';
|
||||
|
||||
const SavedSearches = () => {
|
||||
const { data: savedSearches } = useLibraryQuery(['search.saved.list']);
|
||||
return (
|
||||
<Fade color="black" width={30} height="100%">
|
||||
<MotiView
|
||||
@@ -22,10 +34,16 @@ const SavedSearches = () => {
|
||||
title="Saved searches"
|
||||
sub="Tap a saved search for searching quickly"
|
||||
/>
|
||||
<VirtualizedListWrapper contentContainerStyle={tw`px-6`} horizontal>
|
||||
<VirtualizedListWrapper contentContainerStyle={tw`w-full px-6`} horizontal>
|
||||
<FlatList
|
||||
data={Array.from({ length: 6 })}
|
||||
renderItem={() => <SavedSearch />}
|
||||
data={savedSearches}
|
||||
ListEmptyComponent={() => {
|
||||
return (
|
||||
<Empty
|
||||
icon="Folder" description="No saved searches" style={tw`w-full`} />
|
||||
);
|
||||
}}
|
||||
renderItem={({ item }) => <SavedSearch search={item} />}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
numColumns={Math.ceil(6 / 2)}
|
||||
scrollEnabled={false}
|
||||
@@ -41,16 +59,37 @@ const SavedSearches = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const SavedSearch = () => {
|
||||
interface Props {
|
||||
search: ISavedSearch;
|
||||
}
|
||||
|
||||
const SavedSearch = ({ search }: Props) => {
|
||||
const navigation = useNavigation();
|
||||
const dataForSearch = useSavedSearch(search);
|
||||
const rspc = useRspcLibraryContext();
|
||||
const deleteSearch = useLibraryMutation('search.saved.delete', {
|
||||
onSuccess: () => rspc.queryClient.invalidateQueries(['search.saved.list'])
|
||||
});
|
||||
return (
|
||||
<MotiPressable
|
||||
from={{ opacity: 0, translateY: 20 }}
|
||||
animate={{ opacity: 1, translateY: 0 }}
|
||||
transition={{ type: 'timing', duration: 300 }}
|
||||
onPress={() => {
|
||||
getSearchStore().appliedFilters = dataForSearch;
|
||||
navigation.navigate('SearchStack', {
|
||||
screen: 'Search'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Card style={tw`mr-2 w-auto flex-row gap-2 p-2.5`}>
|
||||
<Card style={tw`mr-2 w-auto flex-row items-center gap-2 p-2.5`}>
|
||||
<Pressable
|
||||
onPress={async () => await deleteSearch.mutateAsync(search.id)}
|
||||
>
|
||||
<X size={14} color={tw.color('text-ink-dull')} />
|
||||
</Pressable>
|
||||
<Icon name="Folder" size={20} />
|
||||
<Text style={tw`text-sm font-medium text-ink`}>Saved search</Text>
|
||||
<Text style={tw`text-sm font-medium text-ink`}>{search.name}</Text>
|
||||
</Card>
|
||||
</MotiPressable>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import { MotiView } from 'moti';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { LinearTransition } from 'react-native-reanimated';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import Card from '~/components/layout/Card';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
import Fade from '~/components/layout/Fade';
|
||||
@@ -63,10 +63,7 @@ interface Props {
|
||||
const TagFilter = memo(({ tag }: Props) => {
|
||||
const searchStore = useSearchStore();
|
||||
const isSelected = useMemo(
|
||||
() =>
|
||||
searchStore.filters.tags.some(
|
||||
(filter) => filter.id === tag.id && filter.color === tag.color
|
||||
),
|
||||
() => searchStore.filters.tags.some((filter) => filter.id === tag.id),
|
||||
[searchStore.filters.tags, tag]
|
||||
);
|
||||
const onPress = useCallback(() => {
|
||||
@@ -74,7 +71,8 @@ const TagFilter = memo(({ tag }: Props) => {
|
||||
id: tag.id,
|
||||
color: tag.color!
|
||||
});
|
||||
}, [searchStore, tag.id, tag.color]);
|
||||
}, [searchStore, tag]);
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress}>
|
||||
<Card
|
||||
|
||||
86
apps/mobile/src/hooks/useFiltersSearch.ts
Normal file
86
apps/mobile/src/hooks/useFiltersSearch.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { SearchFilterArgs } from '@sd/client';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Filters, SearchFilters, getSearchStore, useSearchStore } from '~/stores/searchStore';
|
||||
|
||||
/**
|
||||
* This hook merges the selected filters from Filters page in order
|
||||
* to make query calls for saved searches and setups filters for the search
|
||||
* the data structure has been designed to match the desktop app
|
||||
*/
|
||||
|
||||
export function useFiltersSearch() {
|
||||
const searchStore = useSearchStore();;
|
||||
|
||||
|
||||
const filterFactory = (key: SearchFilters, value: Filters[keyof Filters]) => {
|
||||
|
||||
//hidden is the only boolean filter - so we can return it directly
|
||||
//Rest of the filters are arrays, so we map them to the correct format
|
||||
const filterValue = Array.isArray(value) ? value.map((v: any) => {
|
||||
return v.id ? v.id : v;
|
||||
}) : value;
|
||||
|
||||
//switch case for each filter
|
||||
//This makes it easier to add new filters in the future and setup
|
||||
//the correct object of each filter accordingly and easily
|
||||
|
||||
switch (key) {
|
||||
case 'locations':
|
||||
return { filePath: { locations: { in: filterValue } } };
|
||||
case 'name':
|
||||
return Array.isArray(filterValue) && filterValue.map((v: string) => {
|
||||
return { filePath: { [key]: { contains: v } } };
|
||||
})
|
||||
case 'hidden':
|
||||
return { filePath: { hidden: filterValue } };
|
||||
case 'extension':
|
||||
return Array.isArray(filterValue) && filterValue.map((v: string) => {
|
||||
return { filePath: { [key]: { in: [v] } } };
|
||||
})
|
||||
case 'tags':
|
||||
return { object: { tags: { in: filterValue } } };
|
||||
case 'kind':
|
||||
return { object: { kind: { in: filterValue } } };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mergedFilters = useMemo(() => {
|
||||
|
||||
const filters = [] as SearchFilterArgs[];
|
||||
|
||||
for (const key in searchStore.filters) {
|
||||
|
||||
const filterKey = key as SearchFilters;
|
||||
//due to an issue with Valtio and Hermes Engine - need to do getSearchStore()
|
||||
//https://github.com/pmndrs/valtio/issues/765
|
||||
const filterValue = getSearchStore().filters[filterKey];
|
||||
|
||||
// no need to add empty filters
|
||||
if (Array.isArray(filterValue)) {
|
||||
const realValues = filterValue.filter((v) => v !== '');
|
||||
if (realValues.length === 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// create the filter object
|
||||
const filter = filterFactory(filterKey, filterValue);
|
||||
|
||||
// add the filter to the mergedFilters
|
||||
filters.push(filter as SearchFilterArgs);
|
||||
|
||||
}
|
||||
|
||||
// makes sure the array is not 2D
|
||||
return filters.flat();
|
||||
|
||||
}, [searchStore.filters]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getSearchStore().mergedFilters = mergedFilters;
|
||||
}, [searchStore.filters]);
|
||||
};
|
||||
134
apps/mobile/src/hooks/useSavedSearch.ts
Normal file
134
apps/mobile/src/hooks/useSavedSearch.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { SavedSearch, SearchFilterArgs, useLibraryQuery } from '@sd/client';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { kinds } from '~/components/search/filters/Kind';
|
||||
import { Filters, SearchFilters } from '~/stores/searchStore';
|
||||
|
||||
/**
|
||||
* This hook takes in the JSON of a Saved Search
|
||||
* and returns the data of its filters for rendering in the UI
|
||||
*/
|
||||
|
||||
export function useSavedSearch(search: SavedSearch) {
|
||||
const parseFilters = JSON.parse(search.filters as string);
|
||||
|
||||
// returns an array of keys of the filters being used in the Saved Search
|
||||
//i.e locations, tags, kind, etc...
|
||||
const filterKeys: SearchFilters[] = parseFilters.reduce((acc: SearchFilters[], curr: keyof SearchFilterArgs) => {
|
||||
const objectOrFilePath = Object.keys(curr)[0] as 'filePath' | 'object';
|
||||
const key = Object.keys(curr[objectOrFilePath])[0] as SearchFilters;
|
||||
if (!acc.includes(key)) {
|
||||
acc.push(key as SearchFilters);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// this util function extracts the data of a filter from the Saved Search
|
||||
const extractDataFromSavedSearch = (key: SearchFilters, filterTag: 'contains' | 'in', type: 'filePath' | 'object') => {
|
||||
// Iterate through each item in the data array
|
||||
for (const item of parseFilters) {
|
||||
// Check if 'filePath' | 'object' exists and contains a the key
|
||||
if (item[type] && key in item[type]) {
|
||||
// Return the data of the filters
|
||||
return item.filePath[key][filterTag];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const locations = useLibraryQuery(['locations.list'], {
|
||||
keepPreviousData: true,
|
||||
enabled: filterKeys.includes('locations'),
|
||||
});
|
||||
const tags = useLibraryQuery(['tags.list'], {
|
||||
keepPreviousData: true,
|
||||
enabled: filterKeys.includes('tags'),
|
||||
});
|
||||
|
||||
// Filters like locations, tags, and kind require data to be rendered as a Filter
|
||||
// We prepare the data in the same format as the "filters" object in the "SearchStore"
|
||||
// it is then 'matched' with the data from the "Saved Search"
|
||||
|
||||
const prepFilters = useCallback(() => {
|
||||
const data = {} as Record<SearchFilters, any>;
|
||||
filterKeys.forEach((key: SearchFilters) => {
|
||||
switch (key) {
|
||||
case 'locations':
|
||||
data.locations = locations.data?.map((location) => {
|
||||
return {
|
||||
id: location.id,
|
||||
name: location.name
|
||||
};
|
||||
});
|
||||
break;
|
||||
case 'tags':
|
||||
data.tags = tags.data?.map((tag) => {
|
||||
return {
|
||||
id: tag.id,
|
||||
color: tag.color
|
||||
};
|
||||
});
|
||||
break;
|
||||
case 'kind':
|
||||
data.kind = kinds.map((kind) => {
|
||||
return {
|
||||
name: kind.name,
|
||||
id: kind.value,
|
||||
icon: kind.icon
|
||||
};
|
||||
});
|
||||
break;
|
||||
case 'name':
|
||||
data.name = extractDataFromSavedSearch(key, 'contains', 'filePath');
|
||||
break;
|
||||
case 'extension':
|
||||
data.extension = extractDataFromSavedSearch(key, 'contains', 'filePath');
|
||||
break;
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}, [locations, tags]);
|
||||
|
||||
const filters: Partial<Filters> = useMemo(() => {
|
||||
return parseFilters.reduce((acc: Record<SearchFilters, {}>, curr: keyof SearchFilterArgs) => {
|
||||
|
||||
const objectOrFilePath = Object.keys(curr)[0] as 'filePath' | 'object';
|
||||
const key = Object.keys(curr[objectOrFilePath])[0] as SearchFilters; //locations, tags, kind, etc...
|
||||
|
||||
// this function extracts the data from the result of the "filters" object in the Saved Search
|
||||
// and matches it with the values of the filters
|
||||
const extractData = (key: SearchFilters) => {
|
||||
const values: {
|
||||
contains?: string;
|
||||
in?: number[];
|
||||
} = curr[objectOrFilePath][key];
|
||||
const type = Object.keys(values)[0];
|
||||
|
||||
switch (type) {
|
||||
case 'contains':
|
||||
// some filters have a name property and some are just strings
|
||||
return prepFilters()[key].filter((item: any) => {
|
||||
return item.name ? item.name === values[type] :
|
||||
item
|
||||
});
|
||||
case 'in':
|
||||
return prepFilters()[key].filter((item: any) => values[type]?.includes(item.id));
|
||||
default:
|
||||
return values;
|
||||
}
|
||||
};
|
||||
|
||||
// the data being setup for the filters so it can be rendered
|
||||
if (!acc[key]) {
|
||||
acc[key] = extractData(key);
|
||||
//don't include false values i.e if the "Hidden" filter is false
|
||||
if (acc[key] === false) {
|
||||
delete acc[key];
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
}, [parseFilters]);
|
||||
|
||||
return filters;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createNativeStackNavigator, NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import DynamicHeader from '~/components/header/DynamicHeader';
|
||||
import Header from '~/components/header/Header';
|
||||
import LocationScreen from '~/screens/browse/Location';
|
||||
import FiltersScreen from '~/screens/search/Filters';
|
||||
import SearchScreen from '~/screens/search/Search';
|
||||
|
||||
@@ -25,6 +27,14 @@ export default function SearchStack() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/** This screen is already in BrowseStack - but added here as it offers the UX needed */}
|
||||
<Stack.Screen
|
||||
name="Location"
|
||||
component={LocationScreen}
|
||||
options={({route: optionsRoute}) => ({
|
||||
header: (route) => <DynamicHeader optionsRoute={optionsRoute} headerRoute={route} kind="location" />
|
||||
})}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +42,7 @@ export default function SearchStack() {
|
||||
export type SearchStackParamList = {
|
||||
Search: undefined;
|
||||
Filters: undefined;
|
||||
Location: { id: number; path: string };
|
||||
};
|
||||
|
||||
export type SearchStackScreenProps<Screen extends keyof SearchStackParamList> =
|
||||
|
||||
@@ -146,7 +146,7 @@ export default function TabNavigator() {
|
||||
listeners={() => ({
|
||||
focus: () => {
|
||||
setActiveIndex(index);
|
||||
}
|
||||
},
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -16,7 +16,9 @@ const Stack = createNativeStackNavigator<BrowseStackParamList>();
|
||||
|
||||
export default function BrowseStack() {
|
||||
return (
|
||||
<Stack.Navigator initialRouteName="Browse">
|
||||
<Stack.Navigator
|
||||
initialRouteName="Browse"
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Browse"
|
||||
component={BrowseScreen}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLibraryQuery, usePathsExplorerQuery } from '@sd/client';
|
||||
import { useEffect } from 'react';
|
||||
import Explorer from '~/components/explorer/Explorer';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
import { getExplorerStore } from '~/stores/explorerStore';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { Plus } from 'phosphor-react-native';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { FlatList, Pressable, View } from 'react-native';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { Plus } from 'phosphor-react-native';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
@@ -24,15 +24,15 @@ export default function TagsScreen({ viewStyle = 'list' }: Props) {
|
||||
|
||||
const { search } = useSearchStore();
|
||||
const tags = useLibraryQuery(['tags.list']);
|
||||
const tagData = tags.data || [];
|
||||
const tagsData = tags.data;
|
||||
const [debouncedSearch] = useDebounce(search, 200);
|
||||
|
||||
const filteredTags = useMemo(
|
||||
() =>
|
||||
tagData?.filter((location) =>
|
||||
tagsData?.filter((location) =>
|
||||
location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
|
||||
) ?? [],
|
||||
[debouncedSearch, tagData]
|
||||
[debouncedSearch, tagsData]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -76,7 +76,7 @@ export default function TagsScreen({ viewStyle = 'list' }: Props) {
|
||||
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
|
||||
contentContainerStyle={twStyle(
|
||||
`py-6`,
|
||||
tagData.length === 0 && 'h-full items-center justify-center'
|
||||
tagsData?.length === 0 && 'h-full items-center justify-center'
|
||||
)}
|
||||
/>
|
||||
<CreateTagModal ref={modalRef} />
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { SearchFilterArgs, useLibraryQuery, usePathsExplorerQuery } from '@sd/client';
|
||||
import { ArrowLeft, DotsThreeOutline, FunnelSimple, MagnifyingGlass } from 'phosphor-react-native';
|
||||
import { Suspense, useDeferredValue, useMemo, useState } from 'react';
|
||||
import { ActivityIndicator, Platform, Pressable, TextInput, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { SearchFilterArgs, useObjectsExplorerQuery } from '@sd/client';
|
||||
import Explorer from '~/components/explorer/Explorer';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
import FiltersBar from '~/components/search/filters/FiltersBar';
|
||||
import { useFiltersSearch } from '~/hooks/useFiltersSearch';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { SearchStackScreenProps } from '~/navigation/SearchStack';
|
||||
import { getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
|
||||
@@ -15,11 +18,12 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const searchStore = useSearchStore();
|
||||
const explorerStore = useExplorerStore();
|
||||
const appliedFiltersLength = useMemo(
|
||||
() => Object.keys(searchStore.appliedFilters).length,
|
||||
[searchStore.appliedFilters]
|
||||
);
|
||||
const isFocused = useIsFocused();
|
||||
const appliedFiltersLength = Object.keys(searchStore.appliedFilters).length;
|
||||
const isAndroid = Platform.OS === 'android';
|
||||
const locations = useLibraryQuery(['locations.list'], {
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const deferredSearch = useDeferredValue(search);
|
||||
@@ -32,20 +36,32 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
|
||||
if (name) filters.push({ filePath: { name: { contains: name } } });
|
||||
if (ext) filters.push({ filePath: { extension: { in: [ext] } } });
|
||||
|
||||
return filters;
|
||||
}, [deferredSearch]);
|
||||
if (name || ext) {
|
||||
// Add locations filter to search all locations
|
||||
if (locations.data && locations.data.length > 0) filters.push({ filePath: { locations: { in:
|
||||
locations.data?.map((location) => location.id) } } });
|
||||
}
|
||||
|
||||
const objects = useObjectsExplorerQuery({
|
||||
return searchStore.mergedFilters.concat(filters);
|
||||
}, [deferredSearch, searchStore.mergedFilters, locations.data]);
|
||||
|
||||
const objects = usePathsExplorerQuery({
|
||||
arg: {
|
||||
take: 30,
|
||||
filters
|
||||
},
|
||||
order: null,
|
||||
enabled: isFocused && filters.length > 1, // only fetch when screen is focused & filters are applied
|
||||
suspense: true,
|
||||
enabled: !!deferredSearch,
|
||||
order: null,
|
||||
onSuccess: () => getExplorerStore().resetNewThumbnails()
|
||||
});
|
||||
|
||||
// Check if there are no objects or no search
|
||||
const noObjects = objects.items?.length === 0 || !objects.items;
|
||||
const noSearch = deferredSearch.length === 0 && appliedFiltersLength === 0;
|
||||
|
||||
useFiltersSearch();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={twStyle('flex-1 bg-app-header', {
|
||||
@@ -119,12 +135,26 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
{appliedFiltersLength > 0 && <FiltersBar />}
|
||||
{appliedFiltersLength > 0 && <FiltersBar/>}
|
||||
</View>
|
||||
{/* Content */}
|
||||
<View style={tw`flex-1`}>
|
||||
<Suspense fallback={<ActivityIndicator />}>
|
||||
<Explorer {...objects} tabHeight={false} />
|
||||
<Explorer
|
||||
{...objects}
|
||||
isEmpty={noObjects}
|
||||
emptyComponent={
|
||||
<Empty
|
||||
icon={noSearch ? 'Search' : 'FolderNoSpace'}
|
||||
style={twStyle('flex-1 items-center justify-center border-0', {
|
||||
marginBottom: headerHeight
|
||||
})}
|
||||
textSize="text-md"
|
||||
iconSize={100}
|
||||
description={noSearch ? 'Add filters or type to search for files' : 'No files found'}
|
||||
/>
|
||||
}
|
||||
tabHeight={false} />
|
||||
</Suspense>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SearchFilterArgs } from '@sd/client';
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
import { IconName } from '~/components/icons/Icon';
|
||||
|
||||
@@ -19,29 +20,20 @@ export interface KindItem {
|
||||
icon: IconName;
|
||||
}
|
||||
|
||||
export interface Filters {
|
||||
locations: FilterItem[];
|
||||
tags: TagItem[];
|
||||
name: string[];
|
||||
extension: string[];
|
||||
hidden: boolean;
|
||||
kind: KindItem[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
search: string;
|
||||
filters: {
|
||||
locations: FilterItem[];
|
||||
tags: TagItem[];
|
||||
name: string[];
|
||||
extension: string[];
|
||||
hidden: boolean;
|
||||
kind: KindItem[];
|
||||
};
|
||||
appliedFilters: Partial<
|
||||
Record<
|
||||
SearchFilters,
|
||||
{
|
||||
locations: FilterItem[];
|
||||
tags: TagItem[];
|
||||
name: string[];
|
||||
extension: string[];
|
||||
hidden: boolean;
|
||||
kind: KindItem[];
|
||||
}
|
||||
>
|
||||
>;
|
||||
filters: Filters;
|
||||
appliedFilters: Partial<Filters>;
|
||||
mergedFilters: SearchFilterArgs[],
|
||||
disableActionButtons: boolean;
|
||||
}
|
||||
|
||||
@@ -56,6 +48,7 @@ const initialState: State = {
|
||||
kind: []
|
||||
},
|
||||
appliedFilters: {},
|
||||
mergedFilters: [],
|
||||
disableActionButtons: true
|
||||
};
|
||||
|
||||
@@ -88,6 +81,7 @@ const searchStore = proxy<
|
||||
applyFilters: () => void;
|
||||
setSearch: (search: string) => void;
|
||||
resetFilter: <K extends keyof State['filters']>(filter: K, apply?: boolean) => void;
|
||||
resetFilters: () => void;
|
||||
setInput: (index: number, value: string, key: 'name' | 'extension') => void;
|
||||
addInput: (key: 'name' | 'extension') => void;
|
||||
removeInput: (index: number, key: 'name' | 'extension') => void;
|
||||
@@ -120,8 +114,9 @@ const searchStore = proxy<
|
||||
searchStore.appliedFilters = Object.entries(searchStore.filters).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0 && value[0] !== '') {
|
||||
acc[key as SearchFilters] = value.filter((v) => v !== ''); // Remove empty values i.e empty inputs
|
||||
const realValues = value.filter((v) => v !== '');
|
||||
if (realValues.length > 0) {
|
||||
acc[key as SearchFilters] = realValues;
|
||||
}
|
||||
} else if (typeof value === 'boolean') {
|
||||
// Only apply the hidden filter if it's true
|
||||
@@ -144,7 +139,9 @@ const searchStore = proxy<
|
||||
//instead of a useEffect or subscription - we can call applyFilters directly
|
||||
if (apply) searchStore.applyFilters();
|
||||
},
|
||||
|
||||
resetFilters: () => {
|
||||
searchStore.filters = { ...initialState.filters };
|
||||
},
|
||||
setInput: (index, value, key) => {
|
||||
const newValues = [...searchStore.filters[key]];
|
||||
newValues[index] = value;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { FunnelSimple, Icon, Plus } from '@phosphor-icons/react';
|
||||
import { IconTypes } from '@sd/assets/util';
|
||||
import clsx from 'clsx';
|
||||
import { memo, PropsWithChildren, useDeferredValue, useMemo, useState } from 'react';
|
||||
import { useFeatureFlag, useLibraryMutation } from '@sd/client';
|
||||
import {
|
||||
Button,
|
||||
@@ -13,9 +11,11 @@ import {
|
||||
tw,
|
||||
usePopover
|
||||
} from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { memo, PropsWithChildren, useDeferredValue, useMemo, useState } from 'react';
|
||||
import { useIsDark, useKeybind } from '~/hooks';
|
||||
|
||||
import { AppliedFilters, FilterContainer, InteractiveSection } from './AppliedFilters';
|
||||
import { AppliedFilters, InteractiveSection } from './AppliedFilters';
|
||||
import { useSearchContext } from './context';
|
||||
import { filterRegistry, SearchFilterCRUD, useToggleOptionSelected } from './Filters';
|
||||
import {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { usePathsOffsetInfiniteQuery } from './usePathsOffsetInfiniteQuery';
|
||||
export function usePathsExplorerQuery(props: {
|
||||
arg: FilePathSearchArgs;
|
||||
order: FilePathOrder | null;
|
||||
enabled?: boolean;
|
||||
suspense?: boolean;
|
||||
/** This callback will fire any time the query successfully fetches new data. (NOTE: This will be removed on the next major version (react-query)) */
|
||||
onSuccess?: () => void;
|
||||
}) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AlphaClient } from '@oscartbeaumont-sd/rspc-client/v2';
|
||||
import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react';
|
||||
import { createContext, PropsWithChildren, useContext, useMemo } from 'react';
|
||||
|
||||
import { LibraryConfigWrapped, Procedures } from '../core';
|
||||
import { valtioPersist } from '../lib';
|
||||
|
||||
Reference in New Issue
Block a user