mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-01 20:03:51 -04:00
progress on implementation
This commit is contained in:
@@ -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<number>({
|
||||
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 }) => (
|
||||
<FilterOptionList filter={filter} options={options} search={search} />
|
||||
)
|
||||
})
|
||||
] as const satisfies ReadonlyArray<RenderSearchFilter<any>>;
|
||||
export const filterRegistry: ReadonlyArray<RenderSearchFilter<any>> = [
|
||||
// Put filters here
|
||||
locationFilter,
|
||||
filePathDateCreated,
|
||||
tagsFilter,
|
||||
kindFilter,
|
||||
nameFilter,
|
||||
extensionFilter
|
||||
] as const;
|
||||
|
||||
export type FilterType = (typeof filterRegistry)[number]['name'];
|
||||
|
||||
@@ -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<TConditions> {
|
||||
@@ -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<TConditions, T> {
|
||||
@@ -91,7 +91,7 @@
|
||||
// useOptions: (props: { search: string }) => FilterOption[];
|
||||
// }
|
||||
|
||||
// export function useToggleOptionSelected({ search }: { search: UseSearch<any> }) {
|
||||
// function useToggleOptionSelected({ search }: { search: UseSearch<any> }) {
|
||||
// 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<RenderSearchFilter<any>>;
|
||||
|
||||
// export type FilterType = (typeof filterRegistry)[number]['name'];
|
||||
// type FilterType = (typeof filterRegistry)[number]['name'];
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<any>;
|
||||
}) => {
|
||||
const { allFiltersKeys } = search;
|
||||
|
||||
const key = getKey({
|
||||
type: filter.name,
|
||||
name: filter.name,
|
||||
value: { start: new Date(), end: new Date() } // Example default range
|
||||
});
|
||||
|
||||
return (
|
||||
<SearchOptionItem
|
||||
icon={filter.icon}
|
||||
selected={allFiltersKeys?.has(key)}
|
||||
setSelected={() => {
|
||||
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}
|
||||
</SearchOptionItem>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<T>(
|
||||
export function createDateRangeFilter<T extends string | number>(
|
||||
filter: CreateFilterFunction<FilterTypeCondition['dateRange'], Range<T>>
|
||||
): ReturnType<typeof createFilter<FilterTypeCondition['dateRange'], Range<T>>> {
|
||||
return {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SearchFilterCRUD } from '..';
|
||||
import { FilterOption } from '../../store';
|
||||
import { FilterOption } from '../';
|
||||
import { UseSearch } from '../../useSearch';
|
||||
|
||||
export function useToggleOptionSelected({ search }: { search: UseSearch<any> }) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<string>({
|
||||
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 }) => (
|
||||
<FilterOptionList filter={filter} options={options} search={search} />
|
||||
)
|
||||
});
|
||||
|
||||
// export const filePathDateModified = createDateRangeFilter({});
|
||||
// export const filePathDateAccessed = createDateRangeFilter({});
|
||||
// export const objectDateAccessed = createDateRangeFilter({});
|
||||
|
||||
// export const dateFilters = [
|
||||
// filePathDateCreated,
|
||||
// filePathDateModified,
|
||||
// filePathDateAccessed
|
||||
// ] as const;
|
||||
@@ -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<number>({
|
||||
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 }) => (
|
||||
<FilterOptionList filter={filter} options={options} search={search} />
|
||||
)
|
||||
});
|
||||
@@ -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<number>({
|
||||
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 }) => (
|
||||
<FilterOptionList filter={filter} options={options} search={search} />
|
||||
)
|
||||
});
|
||||
@@ -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<number>({
|
||||
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 }) => (
|
||||
<FilterOptionList
|
||||
empty={() => (
|
||||
<div className="flex flex-col items-center justify-center gap-2 p-2">
|
||||
<span className="icon-tag size-4" />
|
||||
<p className="w-4/5 text-center text-xs text-ink-dull">{i18n.t('no_tags')}</p>
|
||||
</div>
|
||||
)}
|
||||
filter={filter}
|
||||
options={options}
|
||||
search={search}
|
||||
/>
|
||||
)
|
||||
});
|
||||
@@ -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 }) => <FilterOptionText filter={filter} search={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 }) => <FilterOptionText filter={filter} search={search} />
|
||||
});
|
||||
97
interface/app/$libraryId/search/Filters/store.ts
Normal file
97
interface/app/$libraryId/search/Filters/store.ts
Normal file
@@ -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<T = any> {
|
||||
value: string | Range<T> | any;
|
||||
name: string;
|
||||
icon?: string | Icon;
|
||||
}
|
||||
|
||||
export interface FilterOptionWithType extends FilterOption {
|
||||
type: FilterType;
|
||||
}
|
||||
|
||||
const filterOptionStore = proxy({
|
||||
filterOptions: ref(new Map<string, FilterOptionWithType[]>()),
|
||||
registeredFilters: proxyMap() as Map<string, FilterOptionWithType>
|
||||
});
|
||||
|
||||
// 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<string, FilterOption[]>
|
||||
) {
|
||||
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> = T extends any ? keyof T : never;
|
||||
@@ -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() {
|
||||
<filter.Render
|
||||
key={filter.name}
|
||||
filter={filter as any}
|
||||
options={searchState.filterOptions.get(filter.name)!}
|
||||
options={filterStore.filterOptions.get(filter.name)!}
|
||||
search={search}
|
||||
/>
|
||||
))
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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> = T extends any ? keyof T : never;
|
||||
|
||||
const searchStore = proxy({
|
||||
interactingWithSearchOptions: false,
|
||||
searchType: 'paths' as SearchType,
|
||||
filterOptions: ref(new Map<string, FilterOptionWithType[]>()),
|
||||
// we register filters so we can search them
|
||||
registeredFilters: proxyMap() as Map<string, FilterOptionWithType>
|
||||
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<string, FilterOption[]>
|
||||
) {
|
||||
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;
|
||||
|
||||
@@ -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<TSource extends UseSearchSource>(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<string> = useMemo(() => {
|
||||
@@ -140,7 +140,6 @@ export function useSearch<TSource extends UseSearchSource>(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<TSource extends UseSearchSource>(props: UseSearchProps
|
||||
);
|
||||
|
||||
const allFiltersAsOptions = useMemo(
|
||||
() => argsToFilterOptions(allFilters, searchState.filterOptions),
|
||||
[searchState.filterOptions, allFilters]
|
||||
() => argsToFilterOptions(allFilters, filterStore.filterOptions),
|
||||
[filterStore.filterOptions, allFilters]
|
||||
);
|
||||
|
||||
const allFiltersKeys: Set<string> = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user