diff --git a/interface/app/$libraryId/search/Filters/FilterRegistry.tsx b/interface/app/$libraryId/search/Filters/FilterRegistry.tsx index bfbf7aac1..59b809e2a 100644 --- a/interface/app/$libraryId/search/Filters/FilterRegistry.tsx +++ b/interface/app/$libraryId/search/Filters/FilterRegistry.tsx @@ -1,48 +1,18 @@ -// Import icons -import { Folder } from '@phosphor-icons/react'; -import { useLibraryQuery } from '@sd/client'; - import { RenderSearchFilter } from '.'; -import i18n from '../../../I18n'; -import { FilterOptionList } from './components/FilterOptionList'; -import { createInOrNotInFilter } from './factories/createInOrNotInFilter'; +import { filePathDateCreated } from './registry/DateFilters'; +import { kindFilter } from './registry/KindFilter'; +import { locationFilter } from './registry/LocationFilter'; +import { tagsFilter } from './registry/TagsFilter'; +import { extensionFilter, nameFilter } from './registry/TextFilters'; -// Range Filters -export const filterRegistry = [ - createInOrNotInFilter({ - name: i18n.t('location'), - translationKey: 'location', - icon: Folder, - create: (locations) => ({ filePath: { locations } }), - extract: (arg) => { - if ('filePath' in arg && 'locations' in arg.filePath) return arg.filePath.locations; - }, - argsToFilterOptions(values, options) { - return values - .map((value) => { - const option = options.get(this.name)?.find((o) => o.value === value); - if (!option) return; - return { - ...option, - type: this.name - }; - }) - .filter(Boolean) as any; - }, - useOptions: () => { - const query = useLibraryQuery(['locations.list'], { keepPreviousData: true }); - const locations = query.data; - - return (locations ?? []).map((location) => ({ - name: location.name!, - value: location.id, - icon: 'Folder' - })); - }, - Render: ({ filter, options, search }) => ( - - ) - }) -] as const satisfies ReadonlyArray>; +export const filterRegistry: ReadonlyArray> = [ + // Put filters here + locationFilter, + filePathDateCreated, + tagsFilter, + kindFilter, + nameFilter, + extensionFilter +] as const; export type FilterType = (typeof filterRegistry)[number]['name']; diff --git a/interface/app/$libraryId/search/Filters.tsx b/interface/app/$libraryId/search/Filters/FiltersOld.tsx similarity index 98% rename from interface/app/$libraryId/search/Filters.tsx rename to interface/app/$libraryId/search/Filters/FiltersOld.tsx index 6cbad09fc..77229ecd0 100644 --- a/interface/app/$libraryId/search/Filters.tsx +++ b/interface/app/$libraryId/search/Filters/FiltersOld.tsx @@ -24,11 +24,11 @@ // import { SearchOptionItem, SearchOptionSubMenu } from '.'; // import { translateKindName } from '../Explorer/util'; -// import { FilterTypeCondition, filterTypeCondition } from './Filters'; +// import { FilterTypeCondition, filterTypeCondition } from './FiltersOld'; // import { AllKeys, FilterOption, getKey } from './store'; // import { UseSearch } from './useSearch'; -// export interface SearchFilter< +// interface SearchFilter< // TConditions extends FilterTypeCondition[keyof FilterTypeCondition] = any // > { // name: string; @@ -37,7 +37,7 @@ // translationKey?: string; // } -// export interface SearchFilterCRUD< +// interface SearchFilterCRUD< // TConditions extends FilterTypeCondition[keyof FilterTypeCondition] = any, // TConditions represents the available conditions for a specific filter, it defaults to any condition from the FilterTypeCondition // T = any // T is the type of the data that is being filtered. This can be any type. // > extends SearchFilter { @@ -77,7 +77,7 @@ // merge: (left: T, right: T) => T; // } -// export interface RenderSearchFilter< +// interface RenderSearchFilter< // TConditions extends FilterTypeCondition[keyof FilterTypeCondition] = any, // T = any // > extends SearchFilterCRUD { @@ -91,7 +91,7 @@ // useOptions: (props: { search: string }) => FilterOption[]; // } -// export function useToggleOptionSelected({ search }: { search: UseSearch }) { +// function useToggleOptionSelected({ search }: { search: UseSearch }) { // return ({ // filter, // option, @@ -581,7 +581,7 @@ // }); // } -// export const filterRegistry = [ +// const filterRegistry = [ // createGenericRangeFilter( // i18n.t('date_created_range'), // 'date_created_range', @@ -887,4 +887,4 @@ // // }) // ] as const satisfies ReadonlyArray>; -// export type FilterType = (typeof filterRegistry)[number]['name']; +// type FilterType = (typeof filterRegistry)[number]['name']; diff --git a/interface/app/$libraryId/search/Filters/components/AppliedFilters.tsx b/interface/app/$libraryId/search/Filters/components/AppliedFilters.tsx index 660fe8167..1894627e3 100644 --- a/interface/app/$libraryId/search/Filters/components/AppliedFilters.tsx +++ b/interface/app/$libraryId/search/Filters/components/AppliedFilters.tsx @@ -7,8 +7,8 @@ import { useLocale } from '~/hooks'; import { useSearchContext } from '../..'; import HorizontalScroll from '../../../overview/Layout/HorizontalScroll'; import { filterRegistry } from '../../Filters/index'; -import { useSearchStore } from '../../store'; import { RenderIcon } from '../../util'; +import { useFilterOptionStore } from '../store'; export const FilterContainer = tw.div`flex flex-row items-center rounded bg-app-box overflow-hidden shrink-0 h-6`; @@ -75,7 +75,7 @@ export const AppliedFilters = () => { }; export function FilterArg({ arg, onDelete }: { arg: SearchFilterArgs; onDelete?: () => void }) { - const searchStore = useSearchStore(); + const filterStore = useFilterOptionStore(); const { t } = useLocale(); const filter = filterRegistry.find((f) => f.extract(arg)); @@ -83,7 +83,7 @@ export function FilterArg({ arg, onDelete }: { arg: SearchFilterArgs; onDelete?: const activeOptions = filter.argsToFilterOptions( filter.extract(arg)! as any, - searchStore.filterOptions + filterStore.filterOptions ); function isFilterDescriptionDisplayed() { diff --git a/interface/app/$libraryId/search/Filters/components/FilterOptionBoolean.tsx b/interface/app/$libraryId/search/Filters/components/FilterOptionBoolean.tsx index 37bfd487a..979a66864 100644 --- a/interface/app/$libraryId/search/Filters/components/FilterOptionBoolean.tsx +++ b/interface/app/$libraryId/search/Filters/components/FilterOptionBoolean.tsx @@ -1,7 +1,7 @@ import { SearchFilterCRUD } from '..'; import { SearchOptionItem } from '../../SearchOptions'; -import { getKey } from '../../store'; import { UseSearch } from '../../useSearch'; +import { getKey } from '../store'; export const FilterOptionBoolean = ({ filter, diff --git a/interface/app/$libraryId/search/Filters/components/FilterOptionDateRange.tsx b/interface/app/$libraryId/search/Filters/components/FilterOptionDateRange.tsx new file mode 100644 index 000000000..8ebb23c3e --- /dev/null +++ b/interface/app/$libraryId/search/Filters/components/FilterOptionDateRange.tsx @@ -0,0 +1,45 @@ +import { Range } from '@sd/client'; + +import { SearchFilterCRUD } from '..'; +import { SearchOptionItem } from '../../SearchOptions'; +import { UseSearch } from '../../useSearch'; +import { getKey } from '../store'; + +export const FilterOptionDateRange = ({ + filter, + search +}: { + filter: SearchFilterCRUD; + search: UseSearch; +}) => { + const { allFiltersKeys } = search; + + const key = getKey({ + type: filter.name, + name: filter.name, + value: { start: new Date(), end: new Date() } // Example default range + }); + + return ( + { + search.setFilters?.((filters = []) => { + const index = filters.findIndex((f) => filter.extract(f) !== undefined); + + if (index !== -1) { + filters.splice(index, 1); + } else { + const arg = filter.create({ start: new Date(), end: new Date() }); // Example default range + filters.push(arg); + } + + return filters; + }); + }} + > + {filter.name} + + ); +}; diff --git a/interface/app/$libraryId/search/Filters/components/FilterOptionList.tsx b/interface/app/$libraryId/search/Filters/components/FilterOptionList.tsx index d7c125297..21e46d2a5 100644 --- a/interface/app/$libraryId/search/Filters/components/FilterOptionList.tsx +++ b/interface/app/$libraryId/search/Filters/components/FilterOptionList.tsx @@ -1,8 +1,8 @@ import { SearchFilterCRUD } from '..'; import { SearchOptionItem, SearchOptionSubMenu } from '../../SearchOptions'; -import { FilterOption, getKey } from '../../store'; import { UseSearch } from '../../useSearch'; import { useToggleOptionSelected } from '../hooks/useToggleOptionSelected'; +import { FilterOption, getKey } from '../store'; export const FilterOptionList = ({ filter, diff --git a/interface/app/$libraryId/search/Filters/components/FilterOptionRange.tsx b/interface/app/$libraryId/search/Filters/components/FilterOptionRange.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/interface/app/$libraryId/search/Filters/components/FilterOptionText.tsx b/interface/app/$libraryId/search/Filters/components/FilterOptionText.tsx index 660965e21..6b35e494b 100644 --- a/interface/app/$libraryId/search/Filters/components/FilterOptionText.tsx +++ b/interface/app/$libraryId/search/Filters/components/FilterOptionText.tsx @@ -4,8 +4,8 @@ import { useLocale } from '~/hooks'; import { SearchFilterCRUD } from '..'; import { SearchOptionSubMenu } from '../../SearchOptions'; -import { getKey } from '../../store'; import { UseSearch } from '../../useSearch'; +import { getKey } from '../store'; export const FilterOptionText = ({ filter, diff --git a/interface/app/$libraryId/search/Filters/factories/createRangeFilter.ts b/interface/app/$libraryId/search/Filters/factories/createDateRangeFilter.ts similarity index 97% rename from interface/app/$libraryId/search/Filters/factories/createRangeFilter.ts rename to interface/app/$libraryId/search/Filters/factories/createDateRangeFilter.ts index 8b2963527..0d74f0356 100644 --- a/interface/app/$libraryId/search/Filters/factories/createRangeFilter.ts +++ b/interface/app/$libraryId/search/Filters/factories/createDateRangeFilter.ts @@ -9,7 +9,7 @@ import { createFilter, CreateFilterFunction, filterTypeCondition, FilterTypeCond * @param filter - The initial filter configuration, including the create method, argsToFilterOptions, and other specific behaviors. * @returns A filter object that supports CRUD operations for range conditions. */ -export function createRangeFilter( +export function createDateRangeFilter( filter: CreateFilterFunction> ): ReturnType>> { return { diff --git a/interface/app/$libraryId/search/Filters/hooks/useToggleOptionSelected.tsx b/interface/app/$libraryId/search/Filters/hooks/useToggleOptionSelected.tsx index 85e20528d..65764af14 100644 --- a/interface/app/$libraryId/search/Filters/hooks/useToggleOptionSelected.tsx +++ b/interface/app/$libraryId/search/Filters/hooks/useToggleOptionSelected.tsx @@ -1,5 +1,5 @@ import { SearchFilterCRUD } from '..'; -import { FilterOption } from '../../store'; +import { FilterOption } from '../'; import { UseSearch } from '../../useSearch'; export function useToggleOptionSelected({ search }: { search: UseSearch }) { diff --git a/interface/app/$libraryId/search/Filters/index.tsx b/interface/app/$libraryId/search/Filters/index.tsx index 17209ffbb..f456899b2 100644 --- a/interface/app/$libraryId/search/Filters/index.tsx +++ b/interface/app/$libraryId/search/Filters/index.tsx @@ -1,8 +1,8 @@ /** - * This module defines an abstraction layer for search filters, reducing redundancy and improving scalability. + * This module defines an abstraction layer for search filters. * * Instead of duplicating logic for every type of filter, we use generic factory patterns to create filters dynamically. - * The core idea is to define reusable "conditions" for each filter type (e.g., `TextMatch`, `Range`, `InOrNotIn`) and + * The core idea is to define reusable "conditions" for each filter type (e.g., `TextMatch`, `DateRange`, `InOrNotIn`) and * allow filters to be created via factory functions. The interface for CRUD operations remains the same across all filters, * but the condition logic varies depending on the type of filter. * @@ -21,15 +21,15 @@ import { Icon } from '@phosphor-icons/react'; import { SearchFilterArgs } from '@sd/client'; +import i18n from '~/app/I18n'; -import i18n from '../../../I18n'; -import { AllKeys, FilterOption } from '../store'; import { UseSearch } from '../useSearch'; +import { AllKeys, type FilterOption } from './store'; import { OmitCommonFilterProperties } from './typeGuards'; -export { filterRegistry, FilterType } from './FilterRegistry'; +export { filterRegistry, type FilterType } from './FilterRegistry'; -export { FilterOption }; +export type { FilterOption }; export { useToggleOptionSelected } from './hooks/useToggleOptionSelected'; diff --git a/interface/app/$libraryId/search/Filters/registry/DateFilters.tsx b/interface/app/$libraryId/search/Filters/registry/DateFilters.tsx new file mode 100644 index 000000000..c80d515ee --- /dev/null +++ b/interface/app/$libraryId/search/Filters/registry/DateFilters.tsx @@ -0,0 +1,58 @@ +import type {} from '@sd/client'; // required for type inference of createDateRangeFilter + +import { Calendar } from '@phosphor-icons/react'; +import i18n from '~/app/I18n'; + +import { FilterOption } from '..'; +import { FilterOptionList } from '../components/FilterOptionList'; +import { createDateRangeFilter } from '../factories/createDateRangeFilter'; + +export const useCommonDateOptions = (): FilterOption[] => { + return [ + { + name: i18n.t('Last 7 Days'), + value: { from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() }, + icon: Calendar + }, + { + name: i18n.t('Last 30 Days'), + value: { from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() }, + icon: Calendar + }, + { + name: i18n.t('This Year'), + value: { from: new Date(new Date().getFullYear(), 0, 1).toISOString() }, + icon: Calendar + } + ]; +}; + +export const filePathDateCreated = createDateRangeFilter({ + name: i18n.t('Date Created'), + translationKey: 'dateCreated', + icon: Calendar, + create: (dateRange) => ({ filePath: { createdAt: dateRange } }), + extract: (arg) => { + if ('filePath' in arg && 'createdAt' in arg.filePath) return arg.filePath.createdAt; + }, + argsToFilterOptions: (dateRange) => { + return dateRange.map((value) => ({ + name: value, + value: value + })); + }, + useOptions: (): FilterOption[] => useCommonDateOptions(), + Render: ({ filter, options, search }) => ( + + ) +}); + +// export const filePathDateModified = createDateRangeFilter({}); +// export const filePathDateAccessed = createDateRangeFilter({}); +// export const objectDateAccessed = createDateRangeFilter({}); + +// export const dateFilters = [ +// filePathDateCreated, +// filePathDateModified, +// filePathDateAccessed +// ] as const; diff --git a/interface/app/$libraryId/search/Filters/registry/KindFilter.tsx b/interface/app/$libraryId/search/Filters/registry/KindFilter.tsx new file mode 100644 index 000000000..b42bd936b --- /dev/null +++ b/interface/app/$libraryId/search/Filters/registry/KindFilter.tsx @@ -0,0 +1,43 @@ +import { Cube } from '@phosphor-icons/react'; +import { ObjectKind } from '@sd/client'; // Assuming ObjectKind is an enum or set of constants +import i18n from '~/app/I18n'; + +import { FilterOptionList } from '../components/FilterOptionList'; +import { createInOrNotInFilter } from '../factories/createInOrNotInFilter'; + +export const kindFilter = createInOrNotInFilter({ + name: i18n.t('kind'), + translationKey: 'kind', + icon: Cube, + extract: (arg) => { + if ('object' in arg && 'kind' in arg.object) return arg.object.kind; + }, + create: (kind) => ({ object: { kind } }), + argsToFilterOptions(values, options) { + return values + .map((value) => { + const option = options.get(this.name)?.find((o) => o.value === value); + if (!option) return; + + return { + ...option, + type: this.name + }; + }) + .filter(Boolean) as any; + }, + useOptions: () => + Object.keys(ObjectKind) + .filter((key) => !isNaN(Number(key)) && ObjectKind[Number(key)] !== undefined) + .map((key) => { + const kind = ObjectKind[Number(key)] as string; + return { + name: i18n.t(kind), // Assuming translations for kinds + value: Number(key), + icon: Cube // You can customize this based on the kind if needed + }; + }), + Render: ({ filter, options, search }) => ( + + ) +}); diff --git a/interface/app/$libraryId/search/Filters/registry/LocationFilter.tsx b/interface/app/$libraryId/search/Filters/registry/LocationFilter.tsx new file mode 100644 index 000000000..f6bad1539 --- /dev/null +++ b/interface/app/$libraryId/search/Filters/registry/LocationFilter.tsx @@ -0,0 +1,42 @@ +// Import icons +import { Folder } from '@phosphor-icons/react'; +import { useLibraryQuery } from '@sd/client'; +import i18n from '~/app/I18n'; + +import { FilterOptionList } from '../components/FilterOptionList'; +import { createInOrNotInFilter } from '../factories/createInOrNotInFilter'; + +export const locationFilter = createInOrNotInFilter({ + name: i18n.t('location'), + translationKey: 'location', + icon: Folder, + create: (locations) => ({ filePath: { locations } }), + extract: (arg) => { + if ('filePath' in arg && 'locations' in arg.filePath) return arg.filePath.locations; + }, + argsToFilterOptions(values, options) { + return values + .map((value) => { + const option = options.get(this.name)?.find((o) => o.value === value); + if (!option) return; + return { + ...option, + type: this.name + }; + }) + .filter(Boolean) as any; + }, + useOptions: () => { + const query = useLibraryQuery(['locations.list'], { keepPreviousData: true }); + const locations = query.data; + + return (locations ?? []).map((location) => ({ + name: location.name!, + value: location.id, + icon: 'Folder' + })); + }, + Render: ({ filter, options, search }) => ( + + ) +}); diff --git a/interface/app/$libraryId/search/Filters/registry/TagsFilter.tsx b/interface/app/$libraryId/search/Filters/registry/TagsFilter.tsx new file mode 100644 index 000000000..7cffad8e5 --- /dev/null +++ b/interface/app/$libraryId/search/Filters/registry/TagsFilter.tsx @@ -0,0 +1,51 @@ +import { CircleDashed } from '@phosphor-icons/react'; +import { useLibraryQuery } from '@sd/client'; +import i18n from '~/app/I18n'; + +import { FilterOptionList } from '../components/FilterOptionList'; +import { createInOrNotInFilter } from '../factories/createInOrNotInFilter'; + +export const tagsFilter = createInOrNotInFilter({ + name: i18n.t('tags'), + translationKey: 'tag', + icon: CircleDashed, + extract: (arg) => { + if ('object' in arg && 'tags' in arg.object) return arg.object.tags; + }, + create: (tags) => ({ object: { tags } }), + argsToFilterOptions(values, options) { + return values + .map((value) => { + const option = options.get(this.name)?.find((o) => o.value === value); + if (!option) return; + return { + ...option, + type: this.name + }; + }) + .filter(Boolean) as any; + }, + useOptions: () => { + const query = useLibraryQuery(['tags.list'], { keepPreviousData: true }); + const tags = query.data; + + return (tags ?? []).map((tag) => ({ + name: tag.name!, + value: tag.id, + icon: tag.color || 'CircleDashed' + })); + }, + Render: ({ filter, options, search }) => ( + ( +
+ +

{i18n.t('no_tags')}

+
+ )} + filter={filter} + options={options} + search={search} + /> + ) +}); diff --git a/interface/app/$libraryId/search/Filters/registry/TextFilters.tsx b/interface/app/$libraryId/search/Filters/registry/TextFilters.tsx new file mode 100644 index 000000000..eeeacc7c0 --- /dev/null +++ b/interface/app/$libraryId/search/Filters/registry/TextFilters.tsx @@ -0,0 +1,34 @@ +import type {} from '@sd/client'; // required for type inference of createDateRangeFilter + +import { Textbox } from '@phosphor-icons/react'; +import i18n from '~/app/I18n'; + +import { FilterOptionText } from '../components/FilterOptionText'; +import { createInOrNotInFilter } from '../factories/createInOrNotInFilter'; +import { createTextMatchFilter } from '../factories/createTextMatchFilter'; + +// Name Filter +export const nameFilter = createTextMatchFilter({ + name: i18n.t('name'), + translationKey: 'name', + icon: Textbox, + extract: (arg) => { + if ('filePath' in arg && 'name' in arg.filePath) return arg.filePath.name; + }, + create: (name) => ({ filePath: { name } }), + useOptions: ({ search }) => [{ name: search, value: search, icon: Textbox }], + Render: ({ filter, search }) => +}); + +// Extension Filter +export const extensionFilter = createInOrNotInFilter({ + name: i18n.t('extension'), + translationKey: 'extension', + icon: Textbox, + extract: (arg) => { + if ('filePath' in arg && 'extension' in arg.filePath) return arg.filePath.extension; + }, + create: (extension) => ({ filePath: { extension } }), + useOptions: ({ search }) => [{ name: search, value: search, icon: Textbox }], + Render: ({ filter, search }) => +}); diff --git a/interface/app/$libraryId/search/Filters/store.ts b/interface/app/$libraryId/search/Filters/store.ts new file mode 100644 index 000000000..29312fa0c --- /dev/null +++ b/interface/app/$libraryId/search/Filters/store.ts @@ -0,0 +1,97 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { Icon } from '@phosphor-icons/react'; +import { useEffect, useMemo } from 'react'; +import { proxy, ref, useSnapshot } from 'valtio'; +import { proxyMap } from 'valtio/utils'; +import { Range, SearchFilterArgs } from '@sd/client'; + +import { FilterType, RenderSearchFilter } from '.'; +import { filterRegistry } from './FilterRegistry'; + +// Define filter option interface +export interface FilterOption { + value: string | Range | any; + name: string; + icon?: string | Icon; +} + +export interface FilterOptionWithType extends FilterOption { + type: FilterType; +} + +const filterOptionStore = proxy({ + filterOptions: ref(new Map()), + registeredFilters: proxyMap() as Map +}); + +// Generate a unique key for a filter option +export const getKey = (filter: FilterOptionWithType) => + `${filter.type}-${filter.name}-${filter.value}`; + +// Hook to register filter options into the local store +export const useRegisterFilterOptions = ( + filter: RenderSearchFilter, + options: (FilterOption & { type: FilterType })[] +) => { + const optionsAsKeys = useMemo(() => options.map(getKey), [options]); + + useEffect(() => { + filterOptionStore.filterOptions.set(filter.name, options); + filterOptionStore.filterOptions = ref(new Map(filterOptionStore.filterOptions)); + }, [optionsAsKeys]); + + useEffect(() => { + const keys = options.map((option) => { + const key = getKey(option); + if (!filterOptionStore.registeredFilters.has(key)) { + filterOptionStore.registeredFilters.set(key, option); + return key; + } + }); + + return () => { + keys.forEach((key) => { + if (key) filterOptionStore.registeredFilters.delete(key); + }); + }; + }, [optionsAsKeys]); +}; + +// Function to retrieve registered filters based on a query +export const useSearchRegisteredFilters = (query: string) => { + const { registeredFilters } = useFilterOptionStore(); + + return useMemo(() => { + if (!query) return []; + // Filter the registered filters by matching the query string + return [...registeredFilters.entries()] + .filter(([key, _]) => key.toLowerCase().includes(query.toLowerCase())) + .map(([key, filter]) => ({ ...filter, key })); + }, [registeredFilters, query]); +}; + +// Get snapshot of the filter option store +export const useFilterOptionStore = () => useSnapshot(filterOptionStore); + +// Function to reset filter options (if needed) +export const resetFilterOptionStore = () => { + filterOptionStore.filterOptions.clear(); + filterOptionStore.registeredFilters.clear(); +}; + +// Helper to convert arguments to filter options +export function argsToFilterOptions( + args: SearchFilterArgs[], + options: Map +) { + return args.flatMap((fixedArg) => { + const filter = filterRegistry.find((f) => f.extract(fixedArg)); + if (!filter) return []; + + return filter + .argsToFilterOptions(filter.extract(fixedArg) as any, options) + .map((arg) => ({ arg, filter })); + }); +} + +export type AllKeys = T extends any ? keyof T : never; diff --git a/interface/app/$libraryId/search/SearchOptions.tsx b/interface/app/$libraryId/search/SearchOptions.tsx index 991de7943..9629dd594 100644 --- a/interface/app/$libraryId/search/SearchOptions.tsx +++ b/interface/app/$libraryId/search/SearchOptions.tsx @@ -23,11 +23,11 @@ import { useSearchContext } from './context'; import { AppliedFilters, InteractiveSection } from './Filters/components/AppliedFilters'; import { filterRegistry, SearchFilterCRUD, useToggleOptionSelected } from './Filters/index'; import { - getSearchStore, - useRegisterSearchFilterOptions, - useSearchRegisteredFilters, - useSearchStore -} from './store'; + useFilterOptionStore, + useRegisterFilterOptions, + useSearchRegisteredFilters +} from './Filters/store'; +import { getSearchStore, useSearchStore } from './store'; import { UseSearch } from './useSearch'; import { RenderIcon } from './util'; @@ -209,7 +209,7 @@ const SearchResults = memo( function AddFilterButton() { const search = useSearchContext(); - const searchState = useSearchStore(); + const filterStore = useFilterOptionStore(); const [searchQuery, setSearch] = useState(''); @@ -261,7 +261,7 @@ function AddFilterButton() { )) @@ -373,7 +373,7 @@ function RegisterSearchFilterOptions(props: { }) { const options = props.filter.useOptions({ search: props.searchQuery }); - useRegisterSearchFilterOptions( + useRegisterFilterOptions( props.filter, useMemo( () => options.map((o) => ({ ...o, type: props.filter.name })), diff --git a/interface/app/$libraryId/search/store.tsx b/interface/app/$libraryId/search/store.tsx index 0d933fbc3..3ce79da6e 100644 --- a/interface/app/$libraryId/search/store.tsx +++ b/interface/app/$libraryId/search/store.tsx @@ -1,99 +1,27 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { Icon } from '@phosphor-icons/react'; -import { useEffect, useMemo } from 'react'; -import { proxy, ref, useSnapshot } from 'valtio'; -import { proxyMap } from 'valtio/utils'; -import { SearchFilterArgs } from '@sd/client'; - -import { filterRegistry, FilterType, RenderSearchFilter } from './Filters/index'; +import { proxy, useSnapshot } from 'valtio'; export type SearchType = 'paths' | 'objects'; -export interface FilterOption { - value: string | any; - name: string; - icon?: string | Icon; // "Folder" or "#efefef" -} - -export interface FilterOptionWithType extends FilterOption { - type: FilterType; -} - -export type AllKeys = T extends any ? keyof T : never; - const searchStore = proxy({ interactingWithSearchOptions: false, searchType: 'paths' as SearchType, - filterOptions: ref(new Map()), - // we register filters so we can search them - registeredFilters: proxyMap() as Map + searchQuery: '' // Search query to track user input + // Any other search-specific state can go here }); -// this makes the filter unique and easily searchable using .includes -export const getKey = (filter: FilterOptionWithType) => - `${filter.type}-${filter.name}-${filter.value}`; - -// this hook allows us to register filters to the search store -// and returns the filters with the correct type -export const useRegisterSearchFilterOptions = ( - filter: RenderSearchFilter, - options: (FilterOption & { type: FilterType })[] -) => { - const optionsAsKeys = useMemo(() => options.map(getKey), [options]); - - useEffect(() => { - searchStore.filterOptions.set(filter.name, options); - searchStore.filterOptions = ref(new Map(searchStore.filterOptions)); - }, [optionsAsKeys]); - - useEffect(() => { - const keys = options.map((option) => { - const key = getKey(option); - - if (!searchStore.registeredFilters.has(key)) { - searchStore.registeredFilters.set(key, option); - - return key; - } - }); - - return () => - keys.forEach((key) => { - if (key) searchStore.registeredFilters.delete(key); - }); - }, [optionsAsKeys]); -}; - -export function argsToFilterOptions( - args: SearchFilterArgs[], - options: Map -) { - return args.flatMap((fixedArg) => { - const filter = filterRegistry.find((f) => f.extract(fixedArg)); - if (!filter) return []; - - return filter - .argsToFilterOptions(filter.extract(fixedArg) as any, options) - .map((arg) => ({ arg, filter })); - }); -} - -export const useSearchRegisteredFilters = (query: string) => { - const { registeredFilters } = useSearchStore(); - - return useMemo( - () => - !query - ? [] - : [...registeredFilters.entries()] - .filter(([key, _]) => key.toLowerCase().includes(query.toLowerCase())) - .map(([key, filter]) => ({ ...filter, key })), - [registeredFilters, query] - ); -}; - -export const resetSearchStore = () => {}; - +// Hook to interact with the search store export const useSearchStore = () => useSnapshot(searchStore); +// Function to set the search query +export const setSearchQuery = (query: string) => { + searchStore.searchQuery = query; +}; + +// Function to reset search state (if needed) +export const resetSearchStore = () => { + searchStore.interactingWithSearchOptions = false; + searchStore.searchQuery = ''; +}; + +// Function to retrieve the search store directly export const getSearchStore = () => searchStore; diff --git a/interface/app/$libraryId/search/useSearch.ts b/interface/app/$libraryId/search/useSearch.ts index 1982d8baa..a72e2f5e8 100644 --- a/interface/app/$libraryId/search/useSearch.ts +++ b/interface/app/$libraryId/search/useSearch.ts @@ -4,7 +4,7 @@ import { useSearchParams as useRawSearchParams } from 'react-router-dom'; import { useDebouncedValue } from 'rooks'; import { SearchFilterArgs } from '@sd/client'; -import { argsToFilterOptions, getKey, useSearchStore } from './store'; +import { argsToFilterOptions, getKey, useFilterOptionStore } from './Filters/store'; export type SearchTarget = 'paths' | 'objects'; @@ -120,11 +120,11 @@ export function useSearch(props: UseSearchProps const [searchBarFocused, setSearchBarFocused] = useState(false); - const searchState = useSearchStore(); + const filterStore = useFilterOptionStore(); const filtersAsOptions = useMemo( - () => argsToFilterOptions(filters ?? [], searchState.filterOptions), - [filters, searchState.filterOptions] + () => argsToFilterOptions(filters ?? [], filterStore.filterOptions), + [filters, filterStore.filterOptions] ); const filtersKeys: Set = useMemo(() => { @@ -140,7 +140,6 @@ export function useSearch(props: UseSearchProps }, [filtersAsOptions]); // Merging of filters that should be ORed - const mergedFilters = useMemo( () => filters?.map((arg, removalIndex) => ({ arg, removalIndex })), [filters] @@ -166,8 +165,8 @@ export function useSearch(props: UseSearchProps ); const allFiltersAsOptions = useMemo( - () => argsToFilterOptions(allFilters, searchState.filterOptions), - [searchState.filterOptions, allFilters] + () => argsToFilterOptions(allFilters, filterStore.filterOptions), + [filterStore.filterOptions, allFilters] ); const allFiltersKeys: Set = useMemo(() => {