feat(ui): dynamic fitlers - ui improvements
This commit is contained in:
parent
a6fa45041f
commit
6a100561bf
9 changed files with 722 additions and 243 deletions
|
|
@ -224,7 +224,7 @@ export function AutocompleteModal({
|
|||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
value: string[];
|
||||
params?: any;
|
||||
onApplyValues: (values: string[]) => void;
|
||||
|
|
|
|||
|
|
@ -1,129 +1,132 @@
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { debounce } from 'App/utils';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { searchService } from 'App/services';
|
||||
import { AutocompleteModal, AutoCompleteContainer } from './AutocompleteModal';
|
||||
import { AutoCompleteContainer, AutocompleteModal, Props } from './AutocompleteModal';
|
||||
import { TopValue } from '@/mstore/filterStore';
|
||||
|
||||
type FilterParam = { [key: string]: any };
|
||||
interface FilterParams {
|
||||
id: string;
|
||||
type: string;
|
||||
name?: string;
|
||||
|
||||
// ... other potential properties
|
||||
[key: string]: any; // Keep flexible if needed, but prefer specific types
|
||||
}
|
||||
|
||||
interface OptionType {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function processMetadataValues(input: FilterParams): FilterParams {
|
||||
const result: Partial<FilterParams> = {}; // Use Partial if creating a subset initially
|
||||
const isMetadata = input.type === 'metadata';
|
||||
|
||||
function processKey(input: FilterParam): FilterParam {
|
||||
const result: FilterParam = {};
|
||||
for (const key in input) {
|
||||
if (
|
||||
input.type === 'metadata' &&
|
||||
typeof input[key] === 'string' &&
|
||||
input[key].startsWith('_')
|
||||
) {
|
||||
result[key] = input[key].substring(1);
|
||||
} else {
|
||||
result[key] = input[key];
|
||||
if (Object.prototype.hasOwnProperty.call(input, key)) {
|
||||
const value = input[key];
|
||||
if (isMetadata && typeof value === 'string' && value.startsWith('_')) {
|
||||
result[key] = value.substring(1);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result as FilterParams; // Cast back if confident, or adjust logic
|
||||
}
|
||||
|
||||
interface Props {
|
||||
showOrButton?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
onRemoveValue?: (ind: number) => void;
|
||||
onAddValue?: (ind: number) => void;
|
||||
endpoint?: string;
|
||||
method?: string;
|
||||
params?: any;
|
||||
headerText?: string;
|
||||
placeholder?: string;
|
||||
onSelect: (e: any, item: any, index: number) => void;
|
||||
value: any;
|
||||
icon?: string;
|
||||
hideOrText?: boolean;
|
||||
onApplyValues: (values: string[]) => void;
|
||||
modalProps?: Record<string, any>;
|
||||
isAutoOpen?: boolean;
|
||||
}
|
||||
|
||||
const FilterAutoComplete = observer(
|
||||
({
|
||||
params = {},
|
||||
params, // Expect FilterParams type here
|
||||
values,
|
||||
onClose,
|
||||
onApply,
|
||||
values,
|
||||
placeholder
|
||||
}: {
|
||||
params: any;
|
||||
params: FilterParams;
|
||||
values: string[];
|
||||
onClose: () => void;
|
||||
onApply: (values: string[]) => void;
|
||||
placeholder?: string;
|
||||
}) => {
|
||||
const [options, setOptions] = useState<{ value: string; label: string }[]>(
|
||||
[]
|
||||
);
|
||||
const [initialFocus, setInitialFocus] = useState(false);
|
||||
const [options, setOptions] = useState<OptionType[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { filterStore, projectsStore } = useStore();
|
||||
const _params = processKey(params);
|
||||
const filterKey = `${projectsStore.siteId}_${params.id}`;
|
||||
const topValues = filterStore.topValues[filterKey] || [];
|
||||
|
||||
React.useEffect(() => {
|
||||
const filterKey = `${projectsStore.siteId}_${params.id}`;
|
||||
const topValues: TopValue[] = filterStore.topValues[filterKey] || [];
|
||||
|
||||
// Memoize the mapped top values
|
||||
const mappedTopValues = useMemo(() => {
|
||||
console.log('Recalculating mappedTopValues'); // For debugging memoization
|
||||
return topValues.map((i) => ({ value: i.value, label: i.value }));
|
||||
}, [topValues]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions([]);
|
||||
}, [projectsStore.siteId]);
|
||||
|
||||
const loadTopValues = async () => {
|
||||
setLoading(true);
|
||||
if (projectsStore.siteId) {
|
||||
await filterStore.fetchTopValues(params.id, projectsStore.siteId);
|
||||
const loadTopValues = useCallback(async () => {
|
||||
if (projectsStore.siteId && params.id) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await filterStore.fetchTopValues(params.id, projectsStore.siteId);
|
||||
} catch (error) {
|
||||
console.error('Failed to load top values', error);
|
||||
// Handle error state if needed
|
||||
} finally {
|
||||
setLoading(false); // Ensure loading is set false even on error
|
||||
}
|
||||
} else {
|
||||
setOptions([]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (topValues.length > 0) {
|
||||
const mappedValues = topValues.map((i) => ({
|
||||
value: i.value,
|
||||
label: i.value
|
||||
}));
|
||||
setOptions(mappedValues);
|
||||
}
|
||||
}, [topValues, initialFocus]);
|
||||
}, [filterStore, params.id, projectsStore.siteId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadTopValues();
|
||||
}, [_params.type]);
|
||||
}, [loadTopValues]);
|
||||
|
||||
const loadOptions = async (inputValue: string) => {
|
||||
useEffect(() => {
|
||||
setOptions(mappedTopValues);
|
||||
}, [mappedTopValues]);
|
||||
|
||||
|
||||
const loadOptions = useCallback(async (inputValue: string) => {
|
||||
if (!inputValue.length) {
|
||||
const mappedValues = topValues.map((i) => ({
|
||||
value: i.value,
|
||||
label: i.value
|
||||
}));
|
||||
setOptions(mappedValues);
|
||||
setOptions(mappedTopValues);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await searchService.fetchAutoCompleteValues({
|
||||
type: params.name?.toLowerCase(),
|
||||
const searchType = params.name?.toLowerCase();
|
||||
if (!searchType) {
|
||||
console.warn('Search type (params.name) is missing.');
|
||||
setOptions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const data: { value: string }[] = await searchService.fetchAutoCompleteValues({
|
||||
type: searchType,
|
||||
q: inputValue
|
||||
});
|
||||
const _options =
|
||||
data.map((i: any) => ({ value: i.value, label: i.value })) || [];
|
||||
const _options = data.map((i) => ({ value: i.value, label: i.value })) || [];
|
||||
setOptions(_options);
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
console.error('Failed to fetch autocomplete values:', e);
|
||||
setOptions(mappedTopValues);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [mappedTopValues, params.name, searchService.fetchAutoCompleteValues]);
|
||||
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [
|
||||
params,
|
||||
topValues
|
||||
]);
|
||||
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [loadOptions]);
|
||||
|
||||
const handleInputChange = (newValue: string) => {
|
||||
setInitialFocus(true);
|
||||
debouncedLoadOptions(newValue);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -183,20 +183,27 @@ function FilterItem(props: Props) {
|
|||
const subFilterMarginLeftClass = parentShowsIndex ? 'ml-[1.75rem]' : 'ml-[0.75rem]';
|
||||
const subFilterPaddingLeftClass = parentShowsIndex ? 'pl-11' : 'pl-7';
|
||||
|
||||
const categoryPart = filter?.subCategory ? filter.subCategory : filter?.category;
|
||||
const namePart = filter?.displayName || filter?.name;
|
||||
const hasCategory = Boolean(categoryPart);
|
||||
const hasName = Boolean(namePart);
|
||||
const showSeparator = hasCategory && hasName;
|
||||
const defaultText = 'Select Filter';
|
||||
|
||||
return (
|
||||
<div className={cn('w-full', isDragging ? 'opacity-50' : '')}>
|
||||
<div className="flex items-start w-full gap-x-2"> {/* Use items-start */}
|
||||
|
||||
{!isSubItem && !hideIndex && filterIndex !== undefined && filterIndex >= 0 && (
|
||||
<div
|
||||
className="flex-shrink-0 w-6 h-6 mt-[7px] text-xs flex items-center justify-center rounded-full bg-gray-lightest text-gray-600 font-medium"> {/* Align index top */}
|
||||
className="flex-shrink-0 w-6 h-6 mt-[2px] text-xs flex items-center justify-center rounded-full bg-gray-lightest text-gray-600 font-medium"> {/* Align index top */}
|
||||
<span>{filterIndex + 1}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSubItem && (
|
||||
<div
|
||||
className="flex-shrink-0 w-14 text-right text-sm text-neutral-500/90 pr-2 pt-[5px]"> {/* Align where/and top */}
|
||||
className="flex-shrink-0 w-14 text-right text-sm text-neutral-500/90 pr-2">
|
||||
{subFilterIndex === 0 && (
|
||||
<Typography.Text className="text-inherit">
|
||||
where
|
||||
|
|
@ -220,109 +227,120 @@ function FilterItem(props: Props) {
|
|||
|
||||
{/* Main content area */}
|
||||
<div
|
||||
className="flex flex-grow flex-wrap gap-x-1 items-center"> {/* Use baseline inside here */}
|
||||
className="flex flex-grow flex-wrap gap-x-1 items-center">
|
||||
<FilterSelection
|
||||
filters={filterSelections}
|
||||
onFilterClick={replaceFilter}
|
||||
disabled={disableDelete || readonly}
|
||||
loading={isSubItem ? false : eventFiltersLoading}
|
||||
>
|
||||
<Space
|
||||
className={cn(
|
||||
'rounded-lg px-2 cursor-pointer bg-white border border-gray-light text-ellipsis hover:border-main',
|
||||
'transition-colors duration-100 flex-shrink-0 max-w-xs h-[26px] items-center gap-1', // Fixed height, ensure items-center
|
||||
{ 'opacity-70 pointer-events-none': disableDelete || readonly }
|
||||
)}
|
||||
// style={{ lineHeight: '1rem' }}
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
// disabled={isDisabled}
|
||||
// onClick={onClick} // Pass onClick handler
|
||||
style={{
|
||||
maxWidth: '20rem',
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<div className="text-gray-600 flex-shrink-0">
|
||||
{filter && getIconForFilter(filter)}
|
||||
</div>
|
||||
{(filter?.subCategory || filter?.category) && (
|
||||
<div className="text-neutral-500/90 capitalize text-sm truncate">
|
||||
{`${filter?.subCategory ? filter.subCategory : filter?.category}`}
|
||||
</div>
|
||||
)}
|
||||
{(filter?.subCategory || filter?.category) && (filter.displayName || filter.name) &&
|
||||
<span className="text-neutral-400 mx-1">•</span>
|
||||
}
|
||||
<div
|
||||
className="text-sm text-black truncate"
|
||||
>
|
||||
{filter.displayName || filter.name || 'Select Filter'}
|
||||
</div>
|
||||
</Space>
|
||||
<Space size={4} align="center">
|
||||
{/* Icon */}
|
||||
{filter && (
|
||||
<span className="text-gray-600 flex-shrink-0">
|
||||
{getIconForFilter(filter)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Category/SubCategory */}
|
||||
{hasCategory && (
|
||||
<span className="text-neutral-500/90 capitalize text-sm truncate">
|
||||
{categoryPart}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{showSeparator && (
|
||||
<span className="text-neutral-400">•</span>
|
||||
)}
|
||||
|
||||
<span className="text-sm text-black truncate">
|
||||
{hasName ? namePart : (hasCategory ? '' : defaultText)} {/* Show name or placeholder */}
|
||||
</span>
|
||||
</Space>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center flex-wrap gap-x-2 gap-y-1', // Use baseline inside here
|
||||
isReversed ? 'flex-row-reverse' : 'flex-row'
|
||||
)}
|
||||
>
|
||||
{filter.hasSource && (
|
||||
<>
|
||||
<FilterOperator
|
||||
options={filter.sourceOperatorOptions}
|
||||
onChange={handleSourceOperatorChange}
|
||||
value={filter.sourceOperator}
|
||||
isDisabled={filter.operatorDisabled || readonly}
|
||||
/>
|
||||
<FilterSource filter={filter} onUpdate={onUpdate} />
|
||||
</>
|
||||
)}
|
||||
{/*<div*/}
|
||||
{/* className={cn(*/}
|
||||
{/* 'flex items-center flex-wrap gap-x-2 gap-y-1', // Use baseline inside here*/}
|
||||
{/* isReversed ? 'flex-row-reverse' : 'flex-row'*/}
|
||||
{/* )}*/}
|
||||
{/*>*/}
|
||||
{filter.hasSource && (
|
||||
<>
|
||||
<FilterOperator
|
||||
options={filter.sourceOperatorOptions}
|
||||
onChange={handleSourceOperatorChange}
|
||||
value={filter.sourceOperator}
|
||||
isDisabled={filter.operatorDisabled || readonly}
|
||||
name="operator"
|
||||
/>
|
||||
<FilterSource filter={filter} onUpdate={onUpdate} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{operatorOptions.length > 0 && filter.type && (
|
||||
<>
|
||||
<FilterOperator
|
||||
options={operatorOptions}
|
||||
onChange={handleOperatorChange}
|
||||
value={filter.operator}
|
||||
isDisabled={filter.operatorDisabled || readonly}
|
||||
/>
|
||||
{canShowValues &&
|
||||
(readonly ? (
|
||||
<div
|
||||
className="rounded bg-gray-lightest text-gray-dark px-2 py-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis border border-gray-light max-w-xs"
|
||||
title={filter.value.join(', ')}
|
||||
>
|
||||
{filter.value
|
||||
.map((val: string) =>
|
||||
filter.options?.find((i: any) => i.value === val)?.label ?? val
|
||||
)
|
||||
.join(', ')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex"> {/* Wrap FilterValue */}
|
||||
<FilterValue isConditional={isConditional} filter={filter} onUpdate={onUpdate} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{operatorOptions.length > 0 && filter.type && (
|
||||
<>
|
||||
<FilterOperator
|
||||
options={operatorOptions}
|
||||
onChange={handleOperatorChange}
|
||||
value={filter.operator}
|
||||
isDisabled={filter.operatorDisabled || readonly}
|
||||
name="operator"
|
||||
/>
|
||||
{canShowValues &&
|
||||
(readonly ? (
|
||||
<div
|
||||
className="rounded bg-gray-lightest text-gray-dark px-2 py-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis border border-gray-light max-w-xs"
|
||||
title={filter.value.join(', ')}
|
||||
>
|
||||
{filter.value
|
||||
.map((val: string) =>
|
||||
filter.options?.find((i: any) => i.value === val)?.label ?? val
|
||||
)
|
||||
.join(', ')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex"> {/* Wrap FilterValue */}
|
||||
<FilterValue isConditional={isConditional} filter={filter} onUpdate={onUpdate} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{filter.isEvent && !isSubItem && (
|
||||
<FilterSelection
|
||||
filters={eventFilterOptions}
|
||||
onFilterClick={addSubFilter}
|
||||
disabled={disableDelete || readonly || eventFiltersLoading}
|
||||
loading={eventFiltersLoading}
|
||||
>
|
||||
<Tooltip title="Add filter condition" mouseEnterDelay={1}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FunnelPlus size={14} className="text-gray-600" />}
|
||||
size="small"
|
||||
className="h-[26px] w-[26px] flex items-center justify-center" // Fixed size button
|
||||
/>
|
||||
</Tooltip>
|
||||
</FilterSelection>
|
||||
)}
|
||||
</div>
|
||||
{filter.isEvent && !isSubItem && (
|
||||
<FilterSelection
|
||||
filters={eventFilterOptions}
|
||||
onFilterClick={addSubFilter}
|
||||
disabled={disableDelete || readonly || eventFiltersLoading}
|
||||
loading={eventFiltersLoading}
|
||||
>
|
||||
<Tooltip title="Add filter condition" mouseEnterDelay={1}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FunnelPlus size={14} className="text-gray-600" />}
|
||||
size="small"
|
||||
className="flex items-center justify-center" // Fixed size button
|
||||
/>
|
||||
</Tooltip>
|
||||
</FilterSelection>
|
||||
)}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{!readonly && !hideDelete && (
|
||||
<div className="flex flex-shrink-0 gap-1 items-center self-start mt-[1px]"> {/* Align top */}
|
||||
<div className="flex flex-shrink-0 gap-1 items-center self-start"> {/* Align top */}
|
||||
<Tooltip title={isSubItem ? 'Remove filter condition' : 'Remove filter'} mouseEnterDelay={1}>
|
||||
<Button
|
||||
type="text"
|
||||
|
|
@ -330,7 +348,7 @@ function FilterItem(props: Props) {
|
|||
disabled={disableDelete}
|
||||
onClick={onRemoveFilter}
|
||||
size="small"
|
||||
className="h-[26px] w-[26px] flex items-center justify-center" // Fixed size button
|
||||
className="flex items-center justify-center" // Fixed size button
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -341,7 +359,7 @@ function FilterItem(props: Props) {
|
|||
{filteredSubFilters.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'relative w-full mt-1' // Relative parent for border
|
||||
'relative w-full mt-3 mb-2 flex flex-col gap-2' // Relative parent for border
|
||||
)}
|
||||
>
|
||||
{/* Dashed line */}
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
}, []);
|
||||
|
||||
return filters.length ? (
|
||||
<div className={cn('flex flex-col', className)} style={style}>
|
||||
<div className={cn('flex flex-col gap-2', className)} style={style}>
|
||||
{filters.map((filterItem: any, filterIndex: number) => (
|
||||
<div
|
||||
key={`filter-${filterItem.key || filterIndex}`}
|
||||
|
|
@ -176,7 +176,7 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
>
|
||||
{isDraggable && filters.length > 1 && (
|
||||
<div
|
||||
className="cursor-grab text-neutral-500 hover:text-neutral-700 pt-[10px] flex-shrink-0" // Align handle visually
|
||||
className="cursor-grab text-neutral-500 hover:text-neutral-700 pt-[4px] flex-shrink-0" // Align handle visually
|
||||
// Draggable is set on parent div
|
||||
style={{ cursor: draggedInd !== null ? 'grabbing' : 'grab' }}
|
||||
title="Drag to reorder"
|
||||
|
|
|
|||
|
|
@ -1,38 +1,100 @@
|
|||
import React from 'react';
|
||||
import Select from 'Shared/Select';
|
||||
import { Dropdown, Menu, Button } from 'antd'; // Import Dropdown, Menu, Button
|
||||
import { DownOutlined } from '@ant-design/icons'; // Optional: Icon for the button
|
||||
|
||||
interface OptionType {
|
||||
label: React.ReactNode; // Label can be text or other React elements
|
||||
value: string | number; // Value is typically string or number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onChange: (e: any, { name, value }: any) => void;
|
||||
className?: string;
|
||||
options?: any;
|
||||
value?: string;
|
||||
name: string;
|
||||
options: OptionType[];
|
||||
value?: string | number; // Should match the type of OptionType.value
|
||||
onChange: (
|
||||
event: unknown, // Keep original signature for compatibility upstream
|
||||
payload: { name: string; value: string | number | undefined }
|
||||
) => void;
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
allowClear?: boolean; // Prop name from original component
|
||||
popupClassName?: string; // Use this for the dropdown overlay class
|
||||
}
|
||||
|
||||
// Define a special key for the clear action
|
||||
const CLEAR_VALUE_KEY = '__antd_clear_value__';
|
||||
|
||||
function FilterOperator(props: Props) {
|
||||
const {
|
||||
name,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
isDisabled = false,
|
||||
className = '',
|
||||
placeholder = 'Select', // Default placeholder
|
||||
allowClear = false, // Default from original component
|
||||
popupClassName = 'shadow-lg border border-gray-200 rounded-md w-fit' // Default popup class
|
||||
} = props;
|
||||
|
||||
// Find the label of the currently selected option
|
||||
const selectedOption = options.find(option => option.value === value);
|
||||
const displayLabel = selectedOption ? selectedOption.label : placeholder;
|
||||
|
||||
// Handler for menu item clicks
|
||||
const handleMenuClick = (e: { key: string }) => {
|
||||
let selectedValue: string | number | undefined;
|
||||
|
||||
if (e.key === CLEAR_VALUE_KEY) {
|
||||
// Handle the clear action
|
||||
selectedValue = undefined;
|
||||
} else {
|
||||
// Find the option corresponding to the key (which we set as the value)
|
||||
// Antd Menu keys are strings, so convert value to string for comparison/lookup if needed
|
||||
const clickedOption = options.find(option => String(option.value) === e.key);
|
||||
selectedValue = clickedOption?.value;
|
||||
}
|
||||
|
||||
// Call the original onChange prop with the expected structure
|
||||
onChange(null, { name: name, value: selectedValue });
|
||||
};
|
||||
|
||||
// Construct the menu items
|
||||
const menu = (
|
||||
<Menu onClick={handleMenuClick} selectedKeys={value !== undefined ? [String(value)] : []}>
|
||||
{/* Add Clear Option if allowClear is true and a value is selected */}
|
||||
{allowClear && value !== undefined && (
|
||||
<>
|
||||
<Menu.Item key={CLEAR_VALUE_KEY} danger>
|
||||
Clear Selection {/* Or use a specific label */}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
{/* Map options to Menu.Item */}
|
||||
{options.map(option => (
|
||||
<Menu.Item key={String(option.value)}>
|
||||
{option.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-2">
|
||||
<Select
|
||||
name="operator"
|
||||
options={options || []}
|
||||
styles={{ height: 26 }}
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder="Select"
|
||||
isDisabled={isDisabled}
|
||||
value={value ? options?.find((i: any) => i.value === value) : null}
|
||||
onChange={({ value }: any) =>
|
||||
onChange(null, { name: 'operator', value: value.value })
|
||||
}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<Dropdown
|
||||
overlay={menu}
|
||||
trigger={['click']} // Open dropdown on click
|
||||
disabled={isDisabled}
|
||||
overlayClassName={popupClassName} // Apply the custom class to the overlay
|
||||
>
|
||||
{/* The Button acts as the trigger */}
|
||||
<Button type="default" size="small" disabled={isDisabled} className="w-fit text-sm">
|
||||
{displayLabel}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ const FilterSelection: React.FC<FilterSelectionProps> = observer(({
|
|||
|
||||
return (
|
||||
// Add a class to the wrapper if needed, e.g., for opacity when loading
|
||||
<div className={cn('relative flex-shrink-0')}>
|
||||
// <div className={cn('relative flex-shrink-0')}>
|
||||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
|
|
@ -86,7 +86,7 @@ const FilterSelection: React.FC<FilterSelectionProps> = observer(({
|
|||
>
|
||||
{triggerElement}
|
||||
</Popover>
|
||||
</div>
|
||||
// </div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import FilterDuration from '../FilterDuration';
|
|||
import FilterValueDropdown from '../FilterValueDropdown';
|
||||
import FilterAutoCompleteLocal from '../FilterAutoCompleteLocal';
|
||||
import FilterAutoComplete from '../FilterAutoComplete';
|
||||
import ValueAutoComplete from 'Shared/Filters/FilterValue/ValueAutoComplete';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
|
|
@ -154,23 +155,40 @@ function FilterValue(props: Props) {
|
|||
/>
|
||||
);
|
||||
case FilterType.STRING:
|
||||
// return <BaseFilterLocalAutoComplete placeholder={filter.placeholder} />;
|
||||
return <FilterAutoComplete
|
||||
value={value}
|
||||
return <ValueAutoComplete
|
||||
initialValues={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
// showCloseButton={showCloseButton}
|
||||
// showOrButton={showOrButton}
|
||||
onApplyValues={onApplyValues}
|
||||
onRemoveValue={(index) => onRemoveValue(index)}
|
||||
method="GET"
|
||||
endpoint="/PROJECT_ID/events/search"
|
||||
// onRemoveValue={(index) => onRemoveValue(index)}
|
||||
// method="GET"
|
||||
// endpoint="/PROJECT_ID/events/search"
|
||||
params={getParams(filter.key)}
|
||||
headerText=""
|
||||
placeholder={filter.placeholder}
|
||||
onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
icon={filter.icon}
|
||||
modalProps={{ placeholder: 'Search' }}
|
||||
// headerText=""
|
||||
// placeholder={filter.placeholder}
|
||||
commaQuery={true}
|
||||
// onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
// icon={filter.icon}
|
||||
// modalProps={{ placeholder: 'Search' }}
|
||||
/>;
|
||||
// return <BaseFilterLocalAutoComplete placeholder={filter.placeholder} />;
|
||||
// return <FilterAutoComplete
|
||||
// value={value}
|
||||
// isAutoOpen={isAutoOpen}
|
||||
// showCloseButton={showCloseButton}
|
||||
// showOrButton={showOrButton}
|
||||
// onApplyValues={onApplyValues}
|
||||
// onRemoveValue={(index) => onRemoveValue(index)}
|
||||
// method="GET"
|
||||
// endpoint="/PROJECT_ID/events/search"
|
||||
// params={getParams(filter.key)}
|
||||
// headerText=""
|
||||
// placeholder={filter.placeholder}
|
||||
// onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
// icon={filter.icon}
|
||||
// modalProps={{ placeholder: 'Search' }}
|
||||
// />;
|
||||
case FilterType.DROPDOWN:
|
||||
return <BaseDropDown />;
|
||||
case FilterType.ISSUE:
|
||||
|
|
@ -218,14 +236,14 @@ function FilterValue(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="ignore-outside"
|
||||
className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
|
||||
'grid-cols-2': filter.hasSource
|
||||
})}
|
||||
<
|
||||
// id="ignore-outside"
|
||||
// className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
|
||||
// 'grid-cols-2': filter.hasSource
|
||||
// })}
|
||||
>
|
||||
{renderValueFiled(filter.value)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,395 @@
|
|||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { debounce } from 'App/utils';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { searchService } from 'App/services';
|
||||
import { Button, Checkbox, Input, Tooltip, Popover, Spin, Typography, List, Divider, Space } from 'antd';
|
||||
import { RedoOutlined, CloseCircleFilled } from '@ant-design/icons';
|
||||
import cn from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TopValue } from '@/mstore/filterStore';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface FilterParams {
|
||||
id: string;
|
||||
type: string;
|
||||
name?: string;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface OptionType {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Removed custom TruncatedText, will use Typography.Text ellipsis
|
||||
|
||||
interface Props {
|
||||
initialValues: string[];
|
||||
params: FilterParams;
|
||||
onApplyValues: (values: string[]) => void;
|
||||
placeholder?: string;
|
||||
mapValues?: (value: string) => string;
|
||||
isAutoOpen?: boolean;
|
||||
commaQuery?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const ValueAutoComplete = observer(
|
||||
({
|
||||
initialValues,
|
||||
params,
|
||||
onApplyValues,
|
||||
placeholder = 'Select value(s)',
|
||||
mapValues,
|
||||
isAutoOpen = false,
|
||||
commaQuery = false,
|
||||
isDisabled = false
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { filterStore, projectsStore } = useStore();
|
||||
const [showValueModal, setShowValueModal] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [options, setOptions] = useState<OptionType[]>([]);
|
||||
const [loadingTopValues, setLoadingTopValues] = useState(false);
|
||||
const [loadingSearch, setLoadingSearch] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const filterKey = useMemo(() => {
|
||||
if (!projectsStore.siteId || !params.id) return null;
|
||||
return `${projectsStore.siteId}_${params.id}`;
|
||||
}, [projectsStore.siteId, params.id]);
|
||||
|
||||
const topValues: TopValue[] = filterKey ? filterStore.topValues[filterKey] || [] : [];
|
||||
|
||||
const mappedTopValues = useMemo(() => {
|
||||
return topValues.map((i) => ({ value: i.value, label: i.value }));
|
||||
}, [topValues]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedValues(initialValues.filter((i) => i && i.length > 0));
|
||||
}, [initialValues]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (filterKey && !filterStore.topValues[filterKey]) {
|
||||
setLoadingTopValues(true);
|
||||
filterStore.fetchTopValues(params.id, projectsStore.siteId)
|
||||
.catch(error => console.error('Failed to load top values', error))
|
||||
.finally(() => setLoadingTopValues(false));
|
||||
}
|
||||
}, [filterKey, params.id, projectsStore.siteId, filterStore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAutoOpen && !isDisabled) {
|
||||
setShowValueModal(true);
|
||||
}
|
||||
}, [isAutoOpen, isDisabled]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (showValueModal) {
|
||||
setSelectedValues(initialValues.filter((i) => i && i.length > 0));
|
||||
setQuery('');
|
||||
setOptions(mappedTopValues.length > 0 ? mappedTopValues : []);
|
||||
setLoadingSearch(false);
|
||||
}
|
||||
}, [showValueModal, initialValues, mappedTopValues]);
|
||||
|
||||
const loadOptions = useCallback(async (inputValue: string) => {
|
||||
const trimmedQuery = inputValue.trim();
|
||||
if (!trimmedQuery.length) {
|
||||
setOptions(mappedTopValues);
|
||||
setLoadingSearch(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingSearch(true);
|
||||
try {
|
||||
const searchType = params.name?.toLowerCase();
|
||||
if (!searchType) {
|
||||
console.warn('Search type (params.name) is missing.');
|
||||
setOptions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const data: { value: string }[] = await searchService.fetchAutoCompleteValues({
|
||||
type: searchType,
|
||||
q: trimmedQuery
|
||||
});
|
||||
const _options = data.map((i) => ({ value: i.value, label: i.value })) || [];
|
||||
setOptions(_options);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch autocomplete values:', e);
|
||||
setOptions(mappedTopValues);
|
||||
} finally {
|
||||
setLoadingSearch(false);
|
||||
}
|
||||
}, [mappedTopValues, params.name, searchService]);
|
||||
|
||||
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [loadOptions]);
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
setQuery(value);
|
||||
debouncedLoadOptions(value);
|
||||
};
|
||||
|
||||
const onSelectOption = (item: OptionType) => {
|
||||
const currentlySelected = selectedValues.includes(item.value);
|
||||
if (!currentlySelected) {
|
||||
setSelectedValues([...selectedValues, item.value]);
|
||||
} else {
|
||||
setSelectedValues(selectedValues.filter((i) => i !== item.value));
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (item: OptionType) => selectedValues.includes(item.value);
|
||||
|
||||
const applySelectedValues = () => {
|
||||
onApplyValues(selectedValues);
|
||||
setShowValueModal(false);
|
||||
};
|
||||
|
||||
const applyQuery = () => {
|
||||
const vals = commaQuery ? query.split(',').map((i) => i.trim()).filter(Boolean) : [query.trim()].filter(Boolean);
|
||||
if (vals.length > 0) {
|
||||
const merged = Array.from(new Set([...selectedValues, ...vals]));
|
||||
onApplyValues(merged);
|
||||
setShowValueModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedValues([]);
|
||||
};
|
||||
|
||||
const sortedOptions = useMemo(() => {
|
||||
const currentOptionsWithValue = [...options];
|
||||
selectedValues.forEach((val) => {
|
||||
if (!currentOptionsWithValue.find((i) => i.value === val)) {
|
||||
currentOptionsWithValue.unshift({ value: val, label: val });
|
||||
}
|
||||
});
|
||||
|
||||
currentOptionsWithValue.sort((a, b) => {
|
||||
const aIsSelected = selectedValues.includes(a.value);
|
||||
const bIsSelected = selectedValues.includes(b.value);
|
||||
|
||||
if (aIsSelected && !bIsSelected) return -1;
|
||||
if (!aIsSelected && bIsSelected) return 1;
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
|
||||
return currentOptionsWithValue;
|
||||
}, [options, selectedValues]);
|
||||
|
||||
const queryBlocks = commaQuery ? query.split(',').map(s => s.trim()).filter(Boolean) : [query.trim()].filter(Boolean);
|
||||
const blocksAmount = queryBlocks.length;
|
||||
const queryStr = useMemo(() => {
|
||||
return queryBlocks.map(block => `"${block}"`).join(blocksAmount > 1 ? ', ' : '');
|
||||
}, [queryBlocks]);
|
||||
|
||||
|
||||
const onClearClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onApplyValues([]);
|
||||
setShowValueModal(false);
|
||||
};
|
||||
|
||||
const handleOpenChange = (visible: boolean) => {
|
||||
if (isDisabled) return;
|
||||
setShowValueModal(visible);
|
||||
};
|
||||
|
||||
const isEmpty = initialValues.length === 0;
|
||||
|
||||
const popoverContent = (
|
||||
<div
|
||||
style={{ width: 360 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Input.Search
|
||||
value={query}
|
||||
loading={loadingSearch}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="mb-2" // Antd margin class
|
||||
autoFocus
|
||||
allowClear
|
||||
/>
|
||||
<Spin spinning={loadingTopValues && query.length === 0}>
|
||||
<List
|
||||
size="small"
|
||||
locale={{
|
||||
emptyText: t(
|
||||
(loadingSearch || loadingTopValues) ? 'Loading...' :
|
||||
(query.length > 0 ? 'No results found' : 'No options available')
|
||||
)
|
||||
}}
|
||||
dataSource={sortedOptions}
|
||||
renderItem={item => (
|
||||
<List.Item
|
||||
key={item.value}
|
||||
onClick={() => onSelectOption(item)}
|
||||
className={cn(
|
||||
'cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2',
|
||||
{ 'bg-active-blue-faded': isSelected(item) }
|
||||
)}
|
||||
role="option"
|
||||
aria-selected={isSelected(item)}
|
||||
style={{ borderBottom: 'none', padding: '4px 8px' }} // Adjust padding
|
||||
>
|
||||
<Space>
|
||||
<Checkbox checked={isSelected(item)} readOnly tabIndex={-1} />
|
||||
<Text ellipsis={{ tooltip: { title: item.label, placement: 'topLeft', mouseEnterDelay: 0.5 } }}
|
||||
style={{ maxWidth: 'calc(360px - 80px)' }}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
marginBottom: (query.trim().length > 0 && !loadingSearch) ? 8 : 0
|
||||
}}
|
||||
/>
|
||||
{(query.trim().length > 0 && !loadingSearch && sortedOptions.length > 0) ? (
|
||||
<>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Button
|
||||
type="link"
|
||||
onClick={applyQuery}
|
||||
style={{ paddingLeft: 8, whiteSpace: 'normal', height: 'auto', lineHeight: 'inherit' }}
|
||||
>
|
||||
{t('Apply search')}: {queryStr}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</Spin>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<div className="flex justify-between items-center">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={applySelectedValues}
|
||||
disabled={selectedValues.length === 0 && initialValues.length === 0}
|
||||
>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
<Tooltip title={t('Clear all selection')}>
|
||||
<Button
|
||||
onClick={clearSelection}
|
||||
type="text"
|
||||
disabled={selectedValues.length === 0}
|
||||
icon={<RedoOutlined />}
|
||||
aria-label={t('Clear all selection')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={popoverContent}
|
||||
trigger="click"
|
||||
open={showValueModal && !isDisabled}
|
||||
onOpenChange={handleOpenChange}
|
||||
placement="bottomLeft"
|
||||
arrow={false}
|
||||
getPopupContainer={triggerNode => triggerNode || document.body} // Ensure it attaches correctly
|
||||
>
|
||||
{/* className={cn(*/}
|
||||
{/* 'rounded-lg px-2 cursor-pointer bg-white border border-gray-light text-ellipsis hover:border-main',*/}
|
||||
{/* 'transition-colors duration-100 flex-shrink-0 max-w-xs h-[26px] items-center gap-1', // Fixed height, ensure items-center*/}
|
||||
{/* { 'opacity-70 pointer-events-none': disableDelete || readonly }*/}
|
||||
{/*)}*/}
|
||||
<Button // Main trigger container using Ant Design classes
|
||||
// className={cn(
|
||||
// // 'ant-input', // Mimic Ant Input appearance
|
||||
// 'relative rounded-xl px-2 cursor-pointer bg-white border border-gray-light text-ellipsis hover:border-main',
|
||||
// 'transition-colors duration-100 flex-shrink-0 max-w-xs h-[24px] items-center gap-1 pr-8',
|
||||
// {
|
||||
// // 'cursor-pointer hover:border-primary': !isDisabled, // Use theme primary color for hover
|
||||
// 'cursor-not-allowed bg-disabled border-disabled': isDisabled, // Ant disabled styles
|
||||
// // 'border-primary shadow-outline-primary': showValueModal && !isDisabled, // Ant focus styles
|
||||
// // 'border': true, // Base border
|
||||
// // 'rounded': true // Base rounded corners
|
||||
// }
|
||||
// )}
|
||||
size="small"
|
||||
// style={{ height: 26, lineHeight: 'normal' }} // Adjust styling
|
||||
ref={triggerRef}
|
||||
disabled={isDisabled}
|
||||
onMouseEnter={() => !isDisabled && setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
role={isDisabled ? undefined : 'button'}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={showValueModal}
|
||||
aria-disabled={isDisabled}
|
||||
>
|
||||
<Space size={4} wrap className="w-full overflow-hidden">
|
||||
{!isEmpty ? (
|
||||
<>
|
||||
<Text ellipsis={{
|
||||
tooltip: {
|
||||
title: mapValues ? mapValues(initialValues[0]) : initialValues[0],
|
||||
placement: 'topLeft',
|
||||
mouseEnterDelay: 0.5
|
||||
}
|
||||
}} style={{ maxWidth: '8rem' }}>
|
||||
{mapValues ? mapValues(initialValues[0]) : initialValues[0]}
|
||||
</Text>
|
||||
{initialValues.length > 1 && (
|
||||
<>
|
||||
<Text type="secondary" className="flex-shrink-0">{t('or')}</Text>
|
||||
<Text ellipsis={{
|
||||
tooltip: {
|
||||
title: mapValues ? mapValues(initialValues[1]) : initialValues[1],
|
||||
placement: 'topLeft',
|
||||
mouseEnterDelay: 0.5
|
||||
}
|
||||
}} style={{ maxWidth: '8rem' }}>
|
||||
{mapValues ? mapValues(initialValues[1]) : initialValues[1]}
|
||||
</Text>
|
||||
{initialValues.length > 2 && (
|
||||
<Text type="secondary" className="flex-shrink-0">
|
||||
{`+ ${initialValues.length - 2}`}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Text type={isDisabled ? 'secondary' : undefined} className={cn({ 'text-disabled': isDisabled })}>
|
||||
{placeholder}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
{!isEmpty && hovered && !isDisabled && (
|
||||
// Using Button for clear for better accessibility/styling consistency
|
||||
<Button
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2"
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CloseCircleFilled className="text-neutral-400 hover:text-neutral-600" />}
|
||||
onClick={onClearClick}
|
||||
aria-label={t('Clear selection')}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onTouchStart={(e) => e.stopPropagation()}
|
||||
style={{ height: '100%', border: 'none', background: 'transparent', zIndex: 1 }} // Ensure clickable area
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ValueAutoComplete;
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
import { makePersistable } from 'mobx-persist-store';
|
||||
import { filterService } from 'App/services';
|
||||
import { Filter, Operator, COMMON_FILTERS, getOperatorsByType } from './types/filterConstants';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { projectStore } from '@/mstore/index';
|
||||
|
||||
interface TopValue {
|
||||
export interface TopValue {
|
||||
rowCount?: number;
|
||||
rowPercentage?: number;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface TopValues {
|
||||
export interface TopValues {
|
||||
[key: string]: TopValue[];
|
||||
}
|
||||
|
||||
|
|
@ -31,17 +30,6 @@ export default class FilterStore {
|
|||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
||||
// Set up persistence with 10-minute expiration
|
||||
/*void makePersistable(this, {
|
||||
name: 'FilterStore',
|
||||
// properties: ['filters', 'commonFilters'],
|
||||
properties: ['filters'],
|
||||
storage: window.localStorage,
|
||||
expireIn: 10 * 60 * 1000, // 10 minutes in milliseconds
|
||||
removeOnExpiration: true
|
||||
});*/
|
||||
|
||||
// Initialize common static filters
|
||||
this.initCommonFilters();
|
||||
}
|
||||
|
||||
|
|
@ -156,34 +144,29 @@ export default class FilterStore {
|
|||
return this.getAllFilters(projectStore.activeSiteId + '');
|
||||
};
|
||||
|
||||
// getEventFilters = (eventName: string): Filter[] => {
|
||||
// const filters = await filterService.fetchProperties(eventName)
|
||||
// return filters;
|
||||
// // const filters = this.getAllFilters(projectStore.activeSiteId + '');
|
||||
// // return filters.filter(i => !i.isEvent); // TODO fetch from the API for this event and cache them
|
||||
// };
|
||||
|
||||
getEventFilters = async (eventName: string): Promise<Filter[]> => {
|
||||
if (this.filterCache[eventName]) {
|
||||
return this.filterCache[eventName];
|
||||
const cacheKey = `${projectStore.activeSiteId}_${eventName}`;
|
||||
console.log('cacheKey store', cacheKey);
|
||||
if (this.filterCache[cacheKey]) {
|
||||
return this.filterCache[cacheKey];
|
||||
}
|
||||
|
||||
if (await this.pendingFetches[eventName]) {
|
||||
return this.pendingFetches[eventName];
|
||||
if (await this.pendingFetches[cacheKey]) {
|
||||
return this.pendingFetches[cacheKey];
|
||||
}
|
||||
|
||||
try {
|
||||
this.pendingFetches[eventName] = this.fetchAndProcessPropertyFilters(eventName);
|
||||
const filters = await this.pendingFetches[eventName];
|
||||
this.pendingFetches[cacheKey] = this.fetchAndProcessPropertyFilters(eventName);
|
||||
const filters = await this.pendingFetches[cacheKey];
|
||||
|
||||
runInAction(() => {
|
||||
this.filterCache[eventName] = filters;
|
||||
this.filterCache[cacheKey] = filters;
|
||||
});
|
||||
|
||||
delete this.pendingFetches[eventName];
|
||||
delete this.pendingFetches[cacheKey];
|
||||
return filters;
|
||||
} catch (error) {
|
||||
delete this.pendingFetches[eventName];
|
||||
delete this.pendingFetches[cacheKey];
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue