Refactor filtering system with improved autocomplete and event handling
- Add property type icons and improved filter categorization - Implement virtualized filter selection with VList - Update autocomplete to use new property-based endpoints - Add support for event vs property filter distinction - Improve top values fetching with proper caching - Add cards/try endpoint routing in API client - Various code style and formatting improvements Refactor filtering system with improved autocomplete and event handling
This commit is contained in:
parent
0418fafb92
commit
4a54830cad
17 changed files with 1201 additions and 685 deletions
|
|
@ -103,8 +103,8 @@ export default class APIClient {
|
|||
// Always fetch the latest JWT from the store
|
||||
const jwt = this.getJwt();
|
||||
const headers = new Headers({
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
if (reqHeaders) {
|
||||
|
|
@ -121,7 +121,7 @@ export default class APIClient {
|
|||
const init: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
body: params ? JSON.stringify(params) : undefined
|
||||
body: params ? JSON.stringify(params) : undefined,
|
||||
};
|
||||
|
||||
if (method === 'GET') {
|
||||
|
|
@ -185,20 +185,28 @@ export default class APIClient {
|
|||
delete init.body;
|
||||
}
|
||||
|
||||
if ((
|
||||
path.includes('login')
|
||||
|| path.includes('refresh')
|
||||
|| path.includes('logout')
|
||||
|| path.includes('reset')
|
||||
) && window.env.NODE_ENV !== 'development'
|
||||
if (
|
||||
(path.includes('login') ||
|
||||
path.includes('refresh') ||
|
||||
path.includes('logout') ||
|
||||
path.includes('reset')) &&
|
||||
window.env.NODE_ENV !== 'development'
|
||||
) {
|
||||
init.credentials = 'include';
|
||||
} else {
|
||||
delete init.credentials;
|
||||
}
|
||||
|
||||
const noChalice = path.includes('/kai') || path.includes('v1/integrations') || path.includes('/spot') && !path.includes('/login');
|
||||
const noChalice =
|
||||
path.includes('/kai') ||
|
||||
path.includes('v1/integrations') ||
|
||||
(path.includes('/spot') && !path.includes('/login'));
|
||||
let edp = window.env.API_EDP || window.location.origin + '/api';
|
||||
|
||||
if (path.includes('/cards/try')) {
|
||||
// TODO - Remove this condition
|
||||
edp = 'http://localhost:8080/v1/analytics';
|
||||
}
|
||||
if (noChalice && !edp.includes('api.openreplay.com')) {
|
||||
edp = edp.replace('/api', '');
|
||||
}
|
||||
|
|
@ -227,8 +235,7 @@ export default class APIClient {
|
|||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.errors?.[0] || errorMsg;
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Card, Space, Button, Alert, Form, Select, Tooltip } from 'antd';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { projectStore, useStore } from 'App/mstore';
|
||||
import { eventKeys } from 'Types/filter/newFilter';
|
||||
import {
|
||||
HEATMAP,
|
||||
|
|
@ -204,12 +204,17 @@ const FilterSection = observer(
|
|||
|
||||
const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
||||
const { t } = useTranslation();
|
||||
const metricValueOptions = [
|
||||
{ value: 'location', label: t('Pages') },
|
||||
{ value: 'click', label: t('Clicks') },
|
||||
{ value: 'input', label: t('Input') },
|
||||
{ value: 'custom', label: t('Custom Events') },
|
||||
];
|
||||
// const metricValueOptions = [
|
||||
// { value: 'location', label: t('Pages') },
|
||||
// { value: 'click', label: t('Clicks') },
|
||||
// { value: 'input', label: t('Input') },
|
||||
// { value: 'custom', label: t('Custom Events') },
|
||||
// ];
|
||||
//
|
||||
const { filterStore } = useStore();
|
||||
const metricValueOptions = useMemo(() => {
|
||||
return filterStore.getEventOptions(projectStore?.activeSiteId + '');
|
||||
}, []);
|
||||
|
||||
const onPointChange = (value: any) => {
|
||||
writeOption({ name: 'startType', value: { value } });
|
||||
|
|
@ -261,12 +266,12 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
|||
<FilterItem
|
||||
hideDelete
|
||||
filter={metric.startPoint}
|
||||
allowedFilterKeys={[
|
||||
FilterKey.LOCATION,
|
||||
FilterKey.CLICK,
|
||||
FilterKey.INPUT,
|
||||
FilterKey.CUSTOM,
|
||||
]}
|
||||
// allowedFilterKeys={[
|
||||
// FilterKey.LOCATION,
|
||||
// FilterKey.CLICK,
|
||||
// FilterKey.INPUT,
|
||||
// FilterKey.CUSTOM,
|
||||
// ]}
|
||||
onUpdate={(val) => metric.updateStartPoint(val)}
|
||||
onRemoveFilter={() => {}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,19 @@
|
|||
import React, { useState, useEffect, useCallback, useMemo, 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 { AutoCompleteContainer, AutocompleteModal, Props } from './AutocompleteModal';
|
||||
import {
|
||||
AutoCompleteContainer,
|
||||
AutocompleteModal,
|
||||
Props,
|
||||
} from './AutocompleteModal';
|
||||
import { TopValue } from '@/mstore/filterStore';
|
||||
|
||||
interface FilterParams {
|
||||
|
|
@ -37,15 +47,14 @@ function processMetadataValues(input: FilterParams): FilterParams {
|
|||
return result as FilterParams; // Cast back if confident, or adjust logic
|
||||
}
|
||||
|
||||
|
||||
const FilterAutoComplete = observer(
|
||||
({
|
||||
params, // Expect FilterParams type here
|
||||
values,
|
||||
onClose,
|
||||
onApply,
|
||||
placeholder
|
||||
}: {
|
||||
params, // Expect FilterParams type here
|
||||
values,
|
||||
onClose,
|
||||
onApply,
|
||||
placeholder,
|
||||
}: {
|
||||
params: FilterParams;
|
||||
values: string[];
|
||||
onClose: () => void;
|
||||
|
|
@ -73,7 +82,10 @@ const FilterAutoComplete = observer(
|
|||
if (projectsStore.siteId && params.id) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await filterStore.fetchTopValues(params.id, projectsStore.siteId);
|
||||
await filterStore.fetchTopValues({
|
||||
id: params.id,
|
||||
siteId: projectsStore.siteId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load top values', error);
|
||||
// Handle error state if needed
|
||||
|
|
@ -93,38 +105,43 @@ const FilterAutoComplete = observer(
|
|||
setOptions(mappedTopValues);
|
||||
}, [mappedTopValues]);
|
||||
|
||||
|
||||
const loadOptions = useCallback(async (inputValue: string) => {
|
||||
if (!inputValue.length) {
|
||||
setOptions(mappedTopValues);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const searchType = params.name?.toLowerCase();
|
||||
if (!searchType) {
|
||||
console.warn('Search type (params.name) is missing.');
|
||||
setOptions([]);
|
||||
const loadOptions = useCallback(
|
||||
async (inputValue: string) => {
|
||||
if (!inputValue.length) {
|
||||
setOptions(mappedTopValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const data: { value: string }[] = await searchService.fetchAutoCompleteValues({
|
||||
type: searchType,
|
||||
q: inputValue
|
||||
});
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [mappedTopValues, params.name, searchService.fetchAutoCompleteValues]);
|
||||
setLoading(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: inputValue,
|
||||
});
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[mappedTopValues, params.name, searchService.fetchAutoCompleteValues],
|
||||
);
|
||||
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [loadOptions]);
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [
|
||||
loadOptions,
|
||||
]);
|
||||
|
||||
const handleInputChange = (newValue: string) => {
|
||||
debouncedLoadOptions(newValue);
|
||||
|
|
@ -149,7 +166,7 @@ const FilterAutoComplete = observer(
|
|||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function AutoCompleteController(props: Props) {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ function FilterItem(props: Props) {
|
|||
onPropertyOrderChange,
|
||||
parentEventFilterOptions,
|
||||
isDragging,
|
||||
isFirst = false // Default to false
|
||||
isFirst = false, // Default to false
|
||||
} = props;
|
||||
|
||||
const [eventFilterOptions, setEventFilterOptions] = useState<Filter[]>([]);
|
||||
|
|
@ -58,7 +58,9 @@ function FilterItem(props: Props) {
|
|||
|
||||
const { filterStore } = useStore();
|
||||
const allFilters = filterStore.getCurrentProjectFilters();
|
||||
const eventSelections = allFilters.filter((i) => i.isEvent === filter.isEvent);
|
||||
const eventSelections = allFilters.filter(
|
||||
(i) => i.isEvent === filter.isEvent,
|
||||
);
|
||||
|
||||
const filterSelections = useMemo(() => {
|
||||
if (isSubItem) {
|
||||
|
|
@ -67,7 +69,7 @@ function FilterItem(props: Props) {
|
|||
return eventSelections;
|
||||
}, [isSubItem, parentEventFilterOptions, eventSelections]);
|
||||
|
||||
const operatorOptions = getOperatorsByType(filter.type);
|
||||
const operatorOptions = getOperatorsByType(filter.dataType);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true; // Mounted flag
|
||||
|
|
@ -81,10 +83,18 @@ function FilterItem(props: Props) {
|
|||
// Only set loading if not already loading for this specific fetch
|
||||
if (isMounted) setEventFiltersLoading(true);
|
||||
|
||||
const options = await filterStore.getEventFilters(fetchName);
|
||||
const options = await filterStore.getEventFilters(
|
||||
fetchName,
|
||||
filter.autoCaptured,
|
||||
);
|
||||
|
||||
// Check mount status AND if the relevant dependencies are still the same
|
||||
if (isMounted && filter.name === fetchName && !isSubItem && filter.isEvent) {
|
||||
if (
|
||||
isMounted &&
|
||||
filter.name === fetchName &&
|
||||
!isSubItem &&
|
||||
filter.isEvent
|
||||
) {
|
||||
// Avoid setting state if options haven't actually changed (optional optimization)
|
||||
// This requires comparing options, which might be complex/costly.
|
||||
// Sticking to setting state is usually fine if dependencies are stable.
|
||||
|
|
@ -92,11 +102,21 @@ function FilterItem(props: Props) {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load event filters:', error);
|
||||
if (isMounted && filter.name === fetchName && !isSubItem && filter.isEvent) {
|
||||
if (
|
||||
isMounted &&
|
||||
filter.name === fetchName &&
|
||||
!isSubItem &&
|
||||
filter.isEvent
|
||||
) {
|
||||
setEventFilterOptions([]);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted && filter.name === fetchName && !isSubItem && filter.isEvent) {
|
||||
if (
|
||||
isMounted &&
|
||||
filter.name === fetchName &&
|
||||
!isSubItem &&
|
||||
filter.isEvent
|
||||
) {
|
||||
setEventFiltersLoading(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +144,13 @@ function FilterItem(props: Props) {
|
|||
// that determine *if* and *what* to fetch.
|
||||
}, [filter.name, filter.isEvent, isSubItem, filterStore]); //
|
||||
|
||||
console.log('filter...', filter);
|
||||
|
||||
const isUserEvent = useMemo(
|
||||
() => filter.isEvent && !filter.autoCaptured,
|
||||
[filter.isEvent, filter.autoCaptured],
|
||||
);
|
||||
|
||||
const canShowValues = useMemo(
|
||||
() =>
|
||||
!(
|
||||
|
|
@ -131,10 +158,13 @@ function FilterItem(props: Props) {
|
|||
filter.operator === 'onAny' ||
|
||||
filter.operator === 'isUndefined'
|
||||
),
|
||||
[filter.operator]
|
||||
[filter.operator],
|
||||
);
|
||||
|
||||
const isReversed = useMemo(() => filter.key === FilterKey.TAGGED_ELEMENT, [filter.key]);
|
||||
const isReversed = useMemo(
|
||||
() => filter.key === FilterKey.TAGGED_ELEMENT,
|
||||
[filter.key],
|
||||
);
|
||||
|
||||
const replaceFilter = useCallback(
|
||||
(selectedFilter: any) => {
|
||||
|
|
@ -144,56 +174,59 @@ function FilterItem(props: Props) {
|
|||
filters: selectedFilter.filters
|
||||
? selectedFilter.filters.map((i: any) => ({ ...i, value: [''] }))
|
||||
: [],
|
||||
operator: selectedFilter.operator // Ensure operator is carried over or reset if needed
|
||||
operator: selectedFilter.operator, // Ensure operator is carried over or reset if needed
|
||||
});
|
||||
},
|
||||
[onUpdate]
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
const handleOperatorChange = useCallback(
|
||||
(e: any, { value }: any) => {
|
||||
onUpdate({ ...filter, operator: value });
|
||||
},
|
||||
[filter, onUpdate]
|
||||
[filter, onUpdate],
|
||||
);
|
||||
|
||||
const handleSourceOperatorChange = useCallback(
|
||||
(e: any, { value }: any) => {
|
||||
onUpdate({ ...filter, sourceOperator: value });
|
||||
},
|
||||
[filter, onUpdate]
|
||||
[filter, onUpdate],
|
||||
);
|
||||
|
||||
const handleUpdateSubFilter = useCallback(
|
||||
(subFilter: any, index: number) => {
|
||||
onUpdate({
|
||||
...filter,
|
||||
filters: filter.filters.map((i: any, idx: number) => (idx === index ? subFilter : i))
|
||||
filters: filter.filters.map((i: any, idx: number) =>
|
||||
idx === index ? subFilter : i,
|
||||
),
|
||||
});
|
||||
},
|
||||
[filter, onUpdate]
|
||||
[filter, onUpdate],
|
||||
);
|
||||
|
||||
const handleRemoveSubFilter = useCallback(
|
||||
(index: number) => {
|
||||
onUpdate({
|
||||
...filter,
|
||||
filters: filter.filters.filter((_: any, idx: number) => idx !== index)
|
||||
filters: filter.filters.filter((_: any, idx: number) => idx !== index),
|
||||
});
|
||||
},
|
||||
[filter, onUpdate]
|
||||
[filter, onUpdate],
|
||||
);
|
||||
|
||||
const filteredSubFilters = useMemo(
|
||||
() =>
|
||||
filter.filters
|
||||
? filter.filters.filter(
|
||||
(i: any) =>
|
||||
(i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) ||
|
||||
saveRequestPayloads
|
||||
)
|
||||
(i: any) =>
|
||||
(i.key !== FilterKey.FETCH_REQUEST_BODY &&
|
||||
i.key !== FilterKey.FETCH_RESPONSE_BODY) ||
|
||||
saveRequestPayloads,
|
||||
)
|
||||
: [],
|
||||
[filter.filters, saveRequestPayloads]
|
||||
[filter.filters, saveRequestPayloads],
|
||||
);
|
||||
|
||||
const addSubFilter = useCallback(
|
||||
|
|
@ -201,21 +234,25 @@ function FilterItem(props: Props) {
|
|||
const newSubFilter = {
|
||||
...selectedFilter,
|
||||
value: selectedFilter.value || [''],
|
||||
operator: selectedFilter.operator || 'is'
|
||||
operator: selectedFilter.operator || 'is',
|
||||
};
|
||||
onUpdate({
|
||||
...filter,
|
||||
filters: [...(filter.filters || []), newSubFilter]
|
||||
filters: [...(filter.filters || []), newSubFilter],
|
||||
});
|
||||
},
|
||||
[filter, onUpdate]
|
||||
[filter, onUpdate],
|
||||
);
|
||||
|
||||
const parentShowsIndex = !hideIndex;
|
||||
const subFilterMarginLeftClass = parentShowsIndex ? 'ml-[1.75rem]' : 'ml-[0.75rem]';
|
||||
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 categoryPart = filter?.subCategory
|
||||
? filter.subCategory
|
||||
: filter?.category;
|
||||
const namePart = filter?.displayName || filter?.name;
|
||||
const hasCategory = Boolean(categoryPart);
|
||||
const hasName = Boolean(namePart);
|
||||
|
|
@ -224,30 +261,34 @@ function FilterItem(props: Props) {
|
|||
|
||||
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-[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>
|
||||
)}
|
||||
|
||||
<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-[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-neutral-500/90 pr-2">
|
||||
<div className="flex-shrink-0 w-14 text-right text-neutral-500/90 pr-2">
|
||||
{subFilterIndex === 0 && (
|
||||
<Typography.Text className="text-inherit">
|
||||
where
|
||||
</Typography.Text>
|
||||
<Typography.Text className="text-inherit">where</Typography.Text>
|
||||
)}
|
||||
{subFilterIndex !== 0 && propertyOrder && onPropertyOrderChange && (
|
||||
<Typography.Text
|
||||
className={cn(
|
||||
'text-inherit',
|
||||
!readonly && 'cursor-pointer hover:text-main transition-colors'
|
||||
!readonly &&
|
||||
'cursor-pointer hover:text-main transition-colors',
|
||||
)}
|
||||
onClick={() =>
|
||||
!readonly && onPropertyOrderChange(propertyOrder === 'and' ? 'or' : 'and')
|
||||
!readonly &&
|
||||
onPropertyOrderChange(propertyOrder === 'and' ? 'or' : 'and')
|
||||
}
|
||||
>
|
||||
{propertyOrder}
|
||||
|
|
@ -255,10 +296,8 @@ function FilterItem(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content area */}
|
||||
<div
|
||||
className="flex flex-grow flex-wrap gap-x-2 items-center">
|
||||
<div className="flex flex-grow flex-wrap gap-x-2 items-center">
|
||||
<FilterSelection
|
||||
filters={filterSelections}
|
||||
onFilterClick={replaceFilter}
|
||||
|
|
@ -272,7 +311,7 @@ function FilterItem(props: Props) {
|
|||
// onClick={onClick} // Pass onClick handler
|
||||
style={{
|
||||
maxWidth: '20rem',
|
||||
flexShrink: 0
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Space size={4} align="center">
|
||||
|
|
@ -280,22 +319,21 @@ function FilterItem(props: Props) {
|
|||
{filter && (
|
||||
<span className="text-gray-600 flex-shrink-0">
|
||||
{getIconForFilter(filter)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Category/SubCategory */}
|
||||
{hasCategory && (
|
||||
<span className="text-neutral-500/90 capitalize truncate">
|
||||
{categoryPart}
|
||||
</span>
|
||||
{categoryPart}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{showSeparator && (
|
||||
<span className="text-neutral-400">•</span>
|
||||
)}
|
||||
{showSeparator && <span className="text-neutral-400">•</span>}
|
||||
|
||||
<span className="text-black truncate">
|
||||
{hasName ? namePart : (hasCategory ? '' : defaultText)} {/* Show name or placeholder */}
|
||||
{hasName ? namePart : hasCategory ? '' : defaultText}{' '}
|
||||
{/* Show name or placeholder */}
|
||||
</span>
|
||||
</Space>
|
||||
</Button>
|
||||
|
|
@ -320,7 +358,7 @@ function FilterItem(props: Props) {
|
|||
</>
|
||||
)}
|
||||
|
||||
{operatorOptions.length > 0 && filter.type && (
|
||||
{operatorOptions.length > 0 && filter.dataType && !isUserEvent && (
|
||||
<>
|
||||
<FilterOperator
|
||||
options={operatorOptions}
|
||||
|
|
@ -336,14 +374,22 @@ function FilterItem(props: Props) {
|
|||
title={filter.value.join(', ')}
|
||||
>
|
||||
{filter.value
|
||||
.map((val: string) =>
|
||||
filter.options?.find((i: any) => i.value === val)?.label ?? val
|
||||
.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 className="inline-flex">
|
||||
{' '}
|
||||
{/* Wrap FilterValue */}
|
||||
<FilterValue
|
||||
isConditional={isConditional}
|
||||
filter={filter}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
|
@ -368,11 +414,15 @@ function FilterItem(props: Props) {
|
|||
)}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{!readonly && !hideDelete && (
|
||||
<div className="flex flex-shrink-0 gap-1 items-center self-start"> {/* Align top */}
|
||||
<Tooltip title={isSubItem ? 'Remove filter condition' : 'Remove filter'} mouseEnterDelay={1}>
|
||||
<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"
|
||||
icon={<CircleMinus size={14} />}
|
||||
|
|
@ -388,17 +438,16 @@ function FilterItem(props: Props) {
|
|||
|
||||
{/* Sub-Filter Rendering */}
|
||||
{filteredSubFilters.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'relative w-full mt-3 mb-2 flex flex-col gap-2'
|
||||
)}
|
||||
>
|
||||
<div className={cn('relative w-full mt-3 mb-2 flex flex-col gap-2')}>
|
||||
{/* Dashed line */}
|
||||
<div className={cn(
|
||||
'absolute top-0 bottom-0 left-1 w-px',
|
||||
'border-l border-dashed border-gray-300',
|
||||
subFilterMarginLeftClass
|
||||
)} style={{ height: 'calc(100% - 4px)' }} />
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-0 bottom-0 left-1 w-px',
|
||||
'border-l border-dashed border-gray-300',
|
||||
subFilterMarginLeftClass,
|
||||
)}
|
||||
style={{ height: 'calc(100% - 4px)' }}
|
||||
/>
|
||||
|
||||
{filteredSubFilters.map((subFilter: any, index: number) => (
|
||||
<div
|
||||
|
|
@ -408,7 +457,9 @@ function FilterItem(props: Props) {
|
|||
<FilterItem
|
||||
filter={subFilter}
|
||||
subFilterIndex={index}
|
||||
onUpdate={(updatedSubFilter) => handleUpdateSubFilter(updatedSubFilter, index)}
|
||||
onUpdate={(updatedSubFilter) =>
|
||||
handleUpdateSubFilter(updatedSubFilter, index)
|
||||
}
|
||||
onRemoveFilter={() => handleRemoveSubFilter(index)}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={disableDelete}
|
||||
|
|
@ -419,7 +470,9 @@ function FilterItem(props: Props) {
|
|||
isSubItem={true}
|
||||
propertyOrder={filter.propertyOrder || 'and'}
|
||||
onPropertyOrderChange={onPropertyOrderChange}
|
||||
parentEventFilterOptions={isSubItem ? parentEventFilterOptions : eventFilterOptions}
|
||||
parentEventFilterOptions={
|
||||
isSubItem ? parentEventFilterOptions : eventFilterOptions
|
||||
}
|
||||
isFirst={index === 0}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Filter } from '@/mstore/types/filterConstants';
|
|||
|
||||
interface UnifiedFilterListProps {
|
||||
title: string;
|
||||
filters: any[];
|
||||
filters: Filter[];
|
||||
header?: React.ReactNode;
|
||||
filterSelection?: React.ReactNode;
|
||||
handleRemove: (key: string) => void;
|
||||
|
|
@ -42,30 +42,39 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
saveRequestPayloads = false,
|
||||
supportsEmpty = true,
|
||||
style,
|
||||
className
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const [hoveredItem, setHoveredItem] = useState<{ i: number | null; position: string | null }>({
|
||||
const [hoveredItem, setHoveredItem] = useState<{
|
||||
i: number | null;
|
||||
position: string | null;
|
||||
}>({
|
||||
i: null,
|
||||
position: null
|
||||
position: null,
|
||||
});
|
||||
const [draggedInd, setDraggedItem] = useState<number | null>(null);
|
||||
|
||||
const cannotDelete = !supportsEmpty && filters.length <= 1;
|
||||
|
||||
const updateFilter = useCallback((key: string, updatedFilter: any) => {
|
||||
handleUpdate(key, updatedFilter);
|
||||
}, [handleUpdate]);
|
||||
const updateFilter = useCallback(
|
||||
(key: string, updatedFilter: any) => {
|
||||
handleUpdate(key, updatedFilter);
|
||||
},
|
||||
[handleUpdate],
|
||||
);
|
||||
|
||||
const removeFilter = useCallback((key: string) => {
|
||||
handleRemove(key);
|
||||
}, [handleRemove]);
|
||||
const removeFilter = useCallback(
|
||||
(key: string) => {
|
||||
handleRemove(key);
|
||||
},
|
||||
[handleRemove],
|
||||
);
|
||||
|
||||
const calculateNewPosition = useCallback(
|
||||
(hoverIndex: number, hoverPosition: string) => {
|
||||
return hoverPosition === 'bottom' ? hoverIndex + 1 : hoverIndex;
|
||||
},
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
|
|
@ -88,21 +97,23 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
setTimeout(() => document.body.removeChild(clone), 0);
|
||||
}
|
||||
},
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDragOver = useCallback((event: React.DragEvent, i: number) => {
|
||||
event.preventDefault();
|
||||
// Prevent re-calculating hover position if already hovering over the same item
|
||||
if (hoveredItem.i === i) return;
|
||||
|
||||
const target = event.currentTarget.getBoundingClientRect();
|
||||
const hoverMiddleY = (target.bottom - target.top) / 2;
|
||||
const hoverClientY = event.clientY - target.top;
|
||||
const position = hoverClientY < hoverMiddleY ? 'top' : 'bottom';
|
||||
setHoveredItem({ position, i });
|
||||
}, [hoveredItem.i]); // Depend on hoveredItem.i to avoid unnecessary updates
|
||||
const handleDragOver = useCallback(
|
||||
(event: React.DragEvent, i: number) => {
|
||||
event.preventDefault();
|
||||
// Prevent re-calculating hover position if already hovering over the same item
|
||||
if (hoveredItem.i === i) return;
|
||||
|
||||
const target = event.currentTarget.getBoundingClientRect();
|
||||
const hoverMiddleY = (target.bottom - target.top) / 2;
|
||||
const hoverClientY = event.clientY - target.top;
|
||||
const position = hoverClientY < hoverMiddleY ? 'top' : 'bottom';
|
||||
setHoveredItem({ position, i });
|
||||
},
|
||||
[hoveredItem.i],
|
||||
); // Depend on hoveredItem.i to avoid unnecessary updates
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
|
|
@ -125,17 +136,20 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
newPosition--;
|
||||
}
|
||||
|
||||
if (dragInd !== newPosition && !(dragInd === hoverIndex && hoverPosition === 'top') && !(dragInd === hoverIndex - 1 && hoverPosition === 'bottom')) {
|
||||
if (
|
||||
dragInd !== newPosition &&
|
||||
!(dragInd === hoverIndex && hoverPosition === 'top') &&
|
||||
!(dragInd === hoverIndex - 1 && hoverPosition === 'bottom')
|
||||
) {
|
||||
handleMove(dragInd, newPosition);
|
||||
}
|
||||
|
||||
setHoveredItem({ i: null, position: null });
|
||||
setDraggedItem(null);
|
||||
},
|
||||
[handleMove, hoveredItem.i, hoveredItem.position, calculateNewPosition]
|
||||
[handleMove, hoveredItem.i, hoveredItem.position, calculateNewPosition],
|
||||
);
|
||||
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
setHoveredItem({ i: null, position: null });
|
||||
setDraggedItem(null);
|
||||
|
|
@ -147,21 +161,42 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
|
||||
return filters.length ? (
|
||||
<div className={cn('flex flex-col', className)} style={style}>
|
||||
{filters.map((filterItem: any, filterIndex: number) => (
|
||||
{filters.map((filterItem: Filter, filterIndex: number) => (
|
||||
<div
|
||||
key={`filter-${filterItem.key || filterIndex}`}
|
||||
className={cn('flex gap-2 py-2 items-start hover:bg-active-blue -mx-5 px-5 pe-3 transition-colors duration-100 relative', { // Lighter hover, keep relative
|
||||
'opacity-50': draggedInd === filterIndex,
|
||||
'border-t-2 border-dashed border-teal': hoveredItem.i === filterIndex && hoveredItem.position === 'top',
|
||||
'border-b-2 border-dashed border-teal': hoveredItem.i === filterIndex && hoveredItem.position === 'bottom',
|
||||
'-mt-0.5': hoveredItem.i === filterIndex && hoveredItem.position === 'top',
|
||||
'-mb-0.5': hoveredItem.i === filterIndex && hoveredItem.position === 'bottom'
|
||||
})}
|
||||
id={`filter-${filterItem.key || filterIndex}`}
|
||||
key={`filter-${filterItem.id || filterIndex}`}
|
||||
className={cn(
|
||||
'flex gap-2 py-2 items-start hover:bg-active-blue -mx-5 px-5 pe-3 transition-colors duration-100 relative',
|
||||
{
|
||||
// Lighter hover, keep relative
|
||||
'opacity-50': draggedInd === filterIndex,
|
||||
'border-t-2 border-dashed border-teal':
|
||||
hoveredItem.i === filterIndex && hoveredItem.position === 'top',
|
||||
'border-b-2 border-dashed border-teal':
|
||||
hoveredItem.i === filterIndex &&
|
||||
hoveredItem.position === 'bottom',
|
||||
'-mt-0.5':
|
||||
hoveredItem.i === filterIndex && hoveredItem.position === 'top',
|
||||
'-mb-0.5':
|
||||
hoveredItem.i === filterIndex &&
|
||||
hoveredItem.position === 'bottom',
|
||||
},
|
||||
)}
|
||||
id={`filter-${filterItem.id || filterIndex}`}
|
||||
draggable={isDraggable && filters.length > 1} // Only draggable if enabled and more than one item
|
||||
onDragStart={isDraggable && filters.length > 1 ? (e) => handleDragStart(e, filterIndex, `filter-${filterItem.key || filterIndex}`) : undefined}
|
||||
onDragStart={
|
||||
isDraggable && filters.length > 1
|
||||
? (e) =>
|
||||
handleDragStart(
|
||||
e,
|
||||
filterIndex,
|
||||
`filter-${filterItem.id || filterIndex}`,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onDragEnd={isDraggable ? handleDragEnd : undefined}
|
||||
onDragOver={isDraggable ? (e) => handleDragOver(e, filterIndex) : undefined}
|
||||
onDragOver={
|
||||
isDraggable ? (e) => handleDragOver(e, filterIndex) : undefined
|
||||
}
|
||||
onDrop={isDraggable ? handleDrop : undefined}
|
||||
onDragLeave={isDraggable ? handleDragLeave : undefined} // Clear hover effect when leaving
|
||||
>
|
||||
|
|
@ -175,16 +210,17 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!isDraggable && showIndices &&
|
||||
<div className="w-4 flex-shrink-0" />}
|
||||
{!isDraggable && !showIndices &&
|
||||
<div className="w-4 flex-shrink-0" />}
|
||||
|
||||
{!isDraggable && showIndices && <div className="w-4 flex-shrink-0" />}
|
||||
{!isDraggable && !showIndices && (
|
||||
<div className="w-4 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<FilterItem
|
||||
filterIndex={showIndices ? filterIndex : undefined}
|
||||
filter={filterItem}
|
||||
onUpdate={(updatedFilter) => updateFilter(filterItem.id, updatedFilter)}
|
||||
onUpdate={(updatedFilter) =>
|
||||
updateFilter(filterItem.id, updatedFilter)
|
||||
}
|
||||
onRemoveFilter={() => removeFilter(filterItem.id)}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={cannotDelete}
|
||||
|
|
@ -192,10 +228,14 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
isConditional={isConditional}
|
||||
hideIndex={!showIndices}
|
||||
isDragging={draggedInd === filterIndex}
|
||||
onPropertyOrderChange={filterItem.isEvent ? (order: string) => {
|
||||
const newFilter = { ...filterItem, propertyOrder: order };
|
||||
updateFilter(filterItem.id, newFilter);
|
||||
} : undefined}
|
||||
onPropertyOrderChange={
|
||||
filterItem.isEvent
|
||||
? (order: string) => {
|
||||
const newFilter = { ...filterItem, propertyOrder: order };
|
||||
updateFilter(filterItem.id, newFilter);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
// Pass down if this is the first item for potential styling (e.g., no 'and'/'or' toggle)
|
||||
isFirst={filterIndex === 0}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,164 +1,244 @@
|
|||
import cn from 'classnames';
|
||||
import { ChevronRight, MousePointerClick, Search } from 'lucide-react';
|
||||
import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; // Assuming correct path
|
||||
import {
|
||||
ALargeSmall,
|
||||
ChevronRight,
|
||||
Hash,
|
||||
MousePointerClick,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { Input, Space, Typography } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
import { VList } from 'virtua';
|
||||
import { FilterType } from 'Types/filter/filterType';
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
export const getIconForFilter = (filter: Filter) => {
|
||||
return <MousePointerClick size={14} className="text-gray-400" />;
|
||||
type IconProps = { size: number; className?: string };
|
||||
type FilterIconMap = Record<FilterType, ComponentType<IconProps>>;
|
||||
|
||||
const iconProps: IconProps = { size: 14 };
|
||||
|
||||
const PropertyIconMap: FilterIconMap = {
|
||||
[FilterType.NUMBER]: Hash,
|
||||
[FilterType.INTEGER]: Hash,
|
||||
[FilterType.STRING]: ALargeSmall,
|
||||
};
|
||||
|
||||
const groupFiltersByCategory = (filters: Filter[]): Record<string, Filter[]> => {
|
||||
export const getIconForFilter = (filter: Filter): JSX.Element => {
|
||||
const Icon = filter.isEvent
|
||||
? MousePointerClick
|
||||
: PropertyIconMap[filter.dataType] || ALargeSmall;
|
||||
const className = filter.isEvent ? 'text-gray-400' : undefined;
|
||||
return <Icon {...iconProps} className={className} />;
|
||||
};
|
||||
|
||||
const groupFiltersByCategory = (
|
||||
filters: Filter[],
|
||||
): Record<string, Filter[]> => {
|
||||
if (!filters?.length) return {};
|
||||
return filters.reduce((acc, filter) => {
|
||||
const categoryKey = filter.category || 'Other';
|
||||
const category = categoryKey.charAt(0).toUpperCase() + categoryKey.slice(1);
|
||||
if (!acc[category]) acc[category] = [];
|
||||
acc[category].push(filter);
|
||||
return acc;
|
||||
}, {} as Record<string, Filter[]>);
|
||||
return filters.reduce(
|
||||
(acc, filter) => {
|
||||
const key = filter.category || 'Other';
|
||||
const cat = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
(acc[cat] ||= []).push(filter);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Filter[]>,
|
||||
);
|
||||
};
|
||||
|
||||
const getFilteredEntries = (query: string, groupedFilters: Record<string, Filter[]>) => {
|
||||
const trimmedQuery = query.trim().toLowerCase();
|
||||
if (!groupedFilters || Object.keys(groupedFilters).length === 0) {
|
||||
const getFilteredEntries = (
|
||||
query: string,
|
||||
grouped: Record<string, Filter[]>,
|
||||
) => {
|
||||
const trimmed = query.trim().toLowerCase();
|
||||
if (!Object.keys(grouped).length)
|
||||
return { matchingCategories: ['All'], matchingFilters: {} };
|
||||
}
|
||||
const allCategories = Object.keys(groupedFilters);
|
||||
if (!trimmedQuery) {
|
||||
return { matchingCategories: ['All', ...allCategories], matchingFilters: groupedFilters };
|
||||
}
|
||||
const matchingCategories = new Set<string>(['All']);
|
||||
const matchingFilters: Record<string, Filter[]> = {};
|
||||
Object.entries(groupedFilters).forEach(([categoryName, categoryFilters]) => {
|
||||
const categoryMatch = categoryName.toLowerCase().includes(trimmedQuery);
|
||||
let categoryHasMatchingFilters = false;
|
||||
const filteredItems = categoryFilters.filter(
|
||||
(filter: Filter) =>
|
||||
filter.displayName?.toLowerCase().includes(trimmedQuery) ||
|
||||
filter.name?.toLowerCase().includes(trimmedQuery)
|
||||
);
|
||||
if (filteredItems.length > 0) {
|
||||
matchingFilters[categoryName] = filteredItems;
|
||||
categoryHasMatchingFilters = true;
|
||||
}
|
||||
if (categoryMatch || categoryHasMatchingFilters) {
|
||||
matchingCategories.add(categoryName);
|
||||
if (categoryMatch && !categoryHasMatchingFilters) {
|
||||
matchingFilters[categoryName] = categoryFilters;
|
||||
}
|
||||
}
|
||||
});
|
||||
const sortedMatchingCategories = ['All', ...allCategories.filter(cat => matchingCategories.has(cat))];
|
||||
return { matchingCategories: sortedMatchingCategories, matchingFilters };
|
||||
};
|
||||
const categories = Object.keys(grouped);
|
||||
if (!trimmed)
|
||||
return {
|
||||
matchingCategories: ['All', ...categories],
|
||||
matchingFilters: grouped,
|
||||
};
|
||||
const matched = new Set<string>(['All']);
|
||||
const filtersMap: Record<string, Filter[]> = {};
|
||||
|
||||
categories.forEach((cat) => {
|
||||
const catMatch = cat.toLowerCase().includes(trimmed);
|
||||
const items = grouped[cat].filter(
|
||||
(f) =>
|
||||
f.displayName?.toLowerCase().includes(trimmed) ||
|
||||
f.name?.toLowerCase().includes(trimmed),
|
||||
);
|
||||
if (items.length) filtersMap[cat] = items;
|
||||
if (catMatch) filtersMap[cat] ||= grouped[cat];
|
||||
if (catMatch || items.length) matched.add(cat);
|
||||
});
|
||||
|
||||
return {
|
||||
matchingCategories: ['All', ...categories.filter((c) => matched.has(c))],
|
||||
matchingFilters: filtersMap,
|
||||
};
|
||||
};
|
||||
|
||||
const useDebounce = (value: any, delay = 300) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
const handler = setTimeout(() => setDebounced(value), delay);
|
||||
return () => clearTimeout(handler);
|
||||
}, [value, delay]);
|
||||
return debouncedValue;
|
||||
return debounced;
|
||||
};
|
||||
|
||||
// --- Sub-Components ---
|
||||
const FilterItem = React.memo(({ filter, onClick, showCategory }: {
|
||||
filter: Filter;
|
||||
onClick: (filter: Filter) => void;
|
||||
showCategory?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
className="flex items-center p-2 cursor-pointer gap-2 rounded-lg hover:bg-active-blue/10 text-sm"
|
||||
onClick={() => onClick(filter)}
|
||||
role="button" tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onClick(filter);
|
||||
}}
|
||||
>
|
||||
{showCategory && filter.category && (
|
||||
<div style={{ width: 110 }}
|
||||
className="text-neutral-500 flex items-center justify-between flex-shrink-0 mr-1 text-xs">
|
||||
<Typography.Text ellipsis={{ tooltip: true }}
|
||||
className="capitalize flex-1 text-gray-600">{filter.subCategory || filter.category}</Typography.Text>
|
||||
<ChevronRight size={14} className="ml-1 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
<Space className="flex-grow min-w-0 items-center">
|
||||
<span className="text-neutral-500/90 text-xs flex items-center">{getIconForFilter(filter)}</span>
|
||||
<Typography.Text ellipsis={{ tooltip: filter.displayName || filter.name }}
|
||||
className="flex-1">{filter.displayName || filter.name}</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
));
|
||||
const FilterItem = React.memo(
|
||||
({
|
||||
filter,
|
||||
onClick,
|
||||
showCategory,
|
||||
}: {
|
||||
filter: Filter;
|
||||
onClick: (f: Filter) => void;
|
||||
showCategory?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
className="flex items-center p-2 cursor-pointer gap-2 rounded-lg hover:bg-active-blue/10 text-sm"
|
||||
onClick={() => onClick(filter)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onClick(filter);
|
||||
}}
|
||||
>
|
||||
{showCategory && filter.category && (
|
||||
<div
|
||||
// style={{ width: 110 }}
|
||||
className="text-neutral-500 flex items-center justify-between flex-shrink-0 mr-1 text-xs"
|
||||
>
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: true }}
|
||||
className="capitalize flex-1 text-gray-600"
|
||||
>
|
||||
{filter.subCategory || filter.category}
|
||||
</Typography.Text>
|
||||
<ChevronRight size={14} className="ml-1 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
<Space className="flex-grow min-w-0 items-center">
|
||||
<span className="text-neutral-500/90 text-xs flex items-center">
|
||||
{getIconForFilter(filter)}
|
||||
</span>
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: filter.displayName || filter.name }}
|
||||
className="flex-1"
|
||||
>
|
||||
{filter.displayName || filter.name}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
const CategoryList = React.memo(({ categories, activeCategory, onSelect }: {
|
||||
categories: string[];
|
||||
activeCategory: string;
|
||||
onSelect: (category: string) => void;
|
||||
}) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
{categories.map((key) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => onSelect(key)}
|
||||
className={cn('rounded px-3 py-1.5 hover:bg-active-blue/10 capitalize cursor-pointer font-medium text-sm truncate', key === activeCategory && 'bg-active-blue/10 text-teal font-semibold')}
|
||||
title={key} role="button" tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onSelect(key);
|
||||
}}
|
||||
>{key}</div>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
const CategoryList = React.memo(
|
||||
({
|
||||
categories,
|
||||
activeCategory,
|
||||
onSelect,
|
||||
}: {
|
||||
categories: string[];
|
||||
activeCategory: string;
|
||||
onSelect: (c: string) => void;
|
||||
}) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
{categories.map((key) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => onSelect(key)}
|
||||
className={cn(
|
||||
'rounded px-3 py-1.5 hover:bg-active-blue/10 capitalize cursor-pointer font-medium text-sm truncate',
|
||||
key === activeCategory &&
|
||||
'bg-active-blue/10 text-teal font-semibold',
|
||||
)}
|
||||
title={key}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onSelect(key);
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
function FilterModal({ onFilterClick = () => null, filters = [] }: {
|
||||
onFilterClick: (filter: Filter) => void;
|
||||
function FilterModal({
|
||||
onFilterClick = () => null,
|
||||
filters = [],
|
||||
}: {
|
||||
onFilterClick: (f: Filter) => void;
|
||||
filters: Filter[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const debouncedQuery = useDebounce(searchQuery);
|
||||
const [category, setCategory] = useState('All');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const groupedFilters = useMemo(() => groupFiltersByCategory(filters), [filters]);
|
||||
const {
|
||||
matchingCategories,
|
||||
matchingFilters
|
||||
} = useMemo(() => getFilteredEntries(debouncedQuery, groupedFilters), [debouncedQuery, groupedFilters]);
|
||||
const groupedFilters = useMemo(
|
||||
() => groupFiltersByCategory(filters),
|
||||
[filters],
|
||||
);
|
||||
const { matchingCategories, matchingFilters } = useMemo(
|
||||
() => getFilteredEntries(debouncedQuery, groupedFilters),
|
||||
[debouncedQuery, groupedFilters],
|
||||
);
|
||||
const displayedFilters = useMemo(() => {
|
||||
if (category === 'All') {
|
||||
return matchingCategories.filter(cat => cat !== 'All').flatMap(cat => (matchingFilters[cat] || []).map(filter => ({
|
||||
...filter,
|
||||
category: cat
|
||||
})));
|
||||
return matchingCategories
|
||||
.filter((cat) => cat !== 'All')
|
||||
.flatMap((cat) =>
|
||||
(matchingFilters[cat] || []).map((filter) => ({
|
||||
...filter,
|
||||
category: cat,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return matchingFilters[category] || [];
|
||||
}, [category, matchingFilters, matchingCategories]);
|
||||
const isResultEmpty = useMemo(() => matchingCategories.length <= 1 && Object.keys(matchingFilters).length === 0, [matchingCategories, matchingFilters]);
|
||||
const isResultEmpty = useMemo(
|
||||
() =>
|
||||
matchingCategories.length <= 1 &&
|
||||
Object.keys(matchingFilters).length === 0,
|
||||
[matchingCategories, matchingFilters],
|
||||
);
|
||||
|
||||
const handleFilterClick = useCallback((filter: Filter) => {
|
||||
onFilterClick(filter);
|
||||
}, [onFilterClick]);
|
||||
const handleFilterClick = useCallback(
|
||||
(filter: Filter) => {
|
||||
onFilterClick(filter);
|
||||
},
|
||||
[onFilterClick],
|
||||
);
|
||||
const handleCategoryClick = useCallback((cat: string) => {
|
||||
setCategory(cat);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-[490px] max-h-[380px] grid grid-rows-[auto_1fr] overflow-hidden bg-white">
|
||||
|
||||
<div className="">
|
||||
<div>
|
||||
<Input
|
||||
ref={inputRef} placeholder={t('Search')} value={searchQuery}
|
||||
placeholder={t('Search')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
autoFocus allowClear
|
||||
autoFocus
|
||||
allowClear
|
||||
prefix={<Search size={16} className="text-gray-400 mr-1" />}
|
||||
className="mb-3 rounded-lg"
|
||||
/>
|
||||
|
|
@ -168,12 +248,15 @@ function FilterModal({ onFilterClick = () => null, filters = [] }: {
|
|||
{isResultEmpty ? (
|
||||
<div className="h-full flex items-center flex-col justify-center text-center">
|
||||
<AnimatedSVG name={ICONS.NO_SEARCH_RESULTS} size={30} />
|
||||
<div className="font-medium mt-4 text-neutral-600">{t('No results found')}</div>
|
||||
<Typography.Text type="secondary" className="text-sm">{t('Try different keywords')}</Typography.Text>
|
||||
<div className="font-medium mt-4 text-neutral-600">
|
||||
{t('No results found')}
|
||||
</div>
|
||||
<Typography.Text type="secondary" className="text-sm">
|
||||
{t('Try different keywords')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2 h-full">
|
||||
|
||||
<div className="w-36 flex-shrink-0 border-r border-gray-200 pr-2 h-full overflow-y-auto">
|
||||
<CategoryList
|
||||
categories={matchingCategories}
|
||||
|
|
@ -181,28 +264,18 @@ function FilterModal({ onFilterClick = () => null, filters = [] }: {
|
|||
onSelect={handleCategoryClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow min-w-0 h-full overflow-y-auto">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{displayedFilters.length > 0 ? (
|
||||
displayedFilters.map((filter: Filter) => (
|
||||
<FilterItem
|
||||
key={filter.id || filter.name}
|
||||
filter={filter}
|
||||
onClick={handleFilterClick}
|
||||
showCategory={true} // TODO: Show category based condition
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
category !== 'All' && (
|
||||
<div className="flex items-center justify-center h-full text-neutral-500 text-sm p-4 text-center">
|
||||
{t('No filters in category', { categoryName: category })}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-grow min-w-0 h-full">
|
||||
<VList style={{ height: 300 }}>
|
||||
{displayedFilters.map((filter) => (
|
||||
<FilterItem
|
||||
key={filter.id || filter.name}
|
||||
filter={filter}
|
||||
onClick={handleFilterClick}
|
||||
showCategory
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Popover, Spin } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
|
@ -14,67 +14,92 @@ interface FilterSelectionProps {
|
|||
loading?: boolean;
|
||||
}
|
||||
|
||||
const FilterSelection: React.FC<FilterSelectionProps> = observer(({
|
||||
filters,
|
||||
onFilterClick,
|
||||
children,
|
||||
disabled = false,
|
||||
isLive,
|
||||
loading = false // <-- Initialize loading prop
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const FilterSelection: React.FC<FilterSelectionProps> = observer(
|
||||
({
|
||||
filters,
|
||||
onFilterClick,
|
||||
children,
|
||||
disabled = false,
|
||||
isLive,
|
||||
loading = false, // <-- Initialize loading prop
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleFilterClick = useCallback((selectedFilter: Filter) => {
|
||||
if (loading) return;
|
||||
onFilterClick(selectedFilter);
|
||||
setOpen(false);
|
||||
}, [onFilterClick, loading]);
|
||||
const handleFilterClick = useCallback(
|
||||
(selectedFilter: Filter) => {
|
||||
if (loading) return;
|
||||
onFilterClick(selectedFilter);
|
||||
setOpen(false);
|
||||
},
|
||||
[onFilterClick, loading],
|
||||
);
|
||||
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
if (!disabled && !loading) {
|
||||
setOpen(newOpen);
|
||||
} else if (!newOpen) {
|
||||
setOpen(newOpen);
|
||||
}
|
||||
}, [disabled, loading]);
|
||||
const handleOpenChange = useCallback(
|
||||
(newOpen: boolean) => {
|
||||
if (!disabled && !loading) {
|
||||
setOpen(newOpen);
|
||||
} else if (!newOpen) {
|
||||
setOpen(newOpen);
|
||||
}
|
||||
},
|
||||
[disabled, loading],
|
||||
);
|
||||
|
||||
const content = (
|
||||
loading
|
||||
? <div className="p-4 flex justify-center items-center" style={{ minHeight: '100px', minWidth: '150px' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
: <FilterModal
|
||||
onFilterClick={handleFilterClick}
|
||||
filters={filters}
|
||||
/>
|
||||
);
|
||||
// const content = loading ? (
|
||||
// <div
|
||||
// className="p-4 flex justify-center items-center"
|
||||
// style={{ minHeight: '100px', minWidth: '150px' }}
|
||||
// >
|
||||
// <Spin />
|
||||
// </div>
|
||||
// ) : (
|
||||
// <FilterModal onFilterClick={handleFilterClick} filters={filters} />
|
||||
// );
|
||||
|
||||
const isDisabled = disabled || loading;
|
||||
const content = useMemo(
|
||||
() =>
|
||||
loading ? (
|
||||
<div
|
||||
className="p-4 flex justify-center items-center"
|
||||
style={{ minHeight: '100px', minWidth: '150px' }}
|
||||
>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<FilterModal onFilterClick={handleFilterClick} filters={filters} />
|
||||
),
|
||||
[loading, filters, handleFilterClick],
|
||||
);
|
||||
|
||||
const triggerElement = React.isValidElement(children)
|
||||
? React.cloneElement(children as React.ReactElement<any>, {
|
||||
disabled: isDisabled,
|
||||
className: cn(children.props.className, { 'opacity-70 cursor-not-allowed': loading }) // Example styling
|
||||
})
|
||||
: children;
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
return (
|
||||
// <div className={cn('relative flex-shrink-0')}>
|
||||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
placement="bottomLeft"
|
||||
// Consistent styling class name with your original
|
||||
overlayClassName="filter-selection-popover rounded-lg border border-gray-200 shadow-sm shadow-gray-200 overflow-hidden"
|
||||
destroyTooltipOnHide
|
||||
arrow={false}
|
||||
>
|
||||
{triggerElement}
|
||||
</Popover>
|
||||
// </div>
|
||||
);
|
||||
});
|
||||
const triggerElement = React.isValidElement(children)
|
||||
? React.cloneElement(children as React.ReactElement<any>, {
|
||||
disabled: isDisabled,
|
||||
className: cn(children.props.className, {
|
||||
'opacity-70 cursor-not-allowed': loading,
|
||||
}), // Example styling
|
||||
})
|
||||
: children;
|
||||
|
||||
return (
|
||||
// <div className={cn('relative flex-shrink-0')}>
|
||||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
placement="bottomLeft"
|
||||
// Consistent styling class name with your original
|
||||
overlayClassName="filter-selection-popover rounded-lg border border-gray-200 shadow-sm shadow-gray-200 overflow-hidden"
|
||||
destroyOnHidden={true}
|
||||
arrow={false}
|
||||
>
|
||||
{triggerElement}
|
||||
</Popover>
|
||||
// </div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default FilterSelection;
|
||||
|
|
|
|||
|
|
@ -55,27 +55,32 @@ function BaseDropDown(props: any) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
function FilterValue(props: Props) {
|
||||
const { filter, onUpdate, isConditional } = props; // Destructure props early
|
||||
const isAutoOpen = filter.autoOpen; // Assume parent now controls this correctly
|
||||
|
||||
const [durationValues, setDurationValues] = useState(() => ({
|
||||
minDuration: filter.value?.[0],
|
||||
maxDuration: filter.value?.length > 1 ? filter.value[1] : filter.value?.[0]
|
||||
maxDuration: filter.value?.length > 1 ? filter.value[1] : filter.value?.[0],
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (filter.type === FilterType.DURATION) {
|
||||
const incomingMin = filter.value?.[0];
|
||||
const incomingMax = filter.value?.length > 1 ? filter.value[1] : filter.value?.[0];
|
||||
if (durationValues.minDuration !== incomingMin || durationValues.maxDuration !== incomingMax) {
|
||||
setDurationValues({ minDuration: incomingMin, maxDuration: incomingMax });
|
||||
const incomingMax =
|
||||
filter.value?.length > 1 ? filter.value[1] : filter.value?.[0];
|
||||
if (
|
||||
durationValues.minDuration !== incomingMin ||
|
||||
durationValues.maxDuration !== incomingMax
|
||||
) {
|
||||
setDurationValues({
|
||||
minDuration: incomingMin,
|
||||
maxDuration: incomingMax,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [filter.value, filter.type]);
|
||||
|
||||
|
||||
const showCloseButton = filter.value.length > 1;
|
||||
const showOrButton = filter.value.length > 1;
|
||||
|
||||
|
|
@ -84,54 +89,88 @@ function FilterValue(props: Props) {
|
|||
onUpdate({ ...filter, value: newValue });
|
||||
}, [filter, onUpdate]);
|
||||
|
||||
const onApplyValues = useCallback((values: string[]) => {
|
||||
onUpdate({ ...filter, value: values });
|
||||
}, [filter, onUpdate]);
|
||||
const onApplyValues = useCallback(
|
||||
(values: string[]) => {
|
||||
onUpdate({ ...filter, value: values });
|
||||
},
|
||||
[filter, onUpdate],
|
||||
);
|
||||
|
||||
const onRemoveValue = useCallback((valueIndex: any) => {
|
||||
const newValue = filter.value.filter(
|
||||
(_: any, index: any) => index !== valueIndex
|
||||
);
|
||||
onUpdate({ ...filter, value: newValue });
|
||||
}, [filter, onUpdate]);
|
||||
const onRemoveValue = useCallback(
|
||||
(valueIndex: any) => {
|
||||
const newValue = filter.value.filter(
|
||||
(_: any, index: any) => index !== valueIndex,
|
||||
);
|
||||
onUpdate({ ...filter, value: newValue });
|
||||
},
|
||||
[filter, onUpdate],
|
||||
);
|
||||
|
||||
const stableOnChange = useCallback((e: any, item: any, valueIndex: any) => {
|
||||
const newValues = filter.value.map((val: any, _index: any) => {
|
||||
if (_index === valueIndex) {
|
||||
return item;
|
||||
}
|
||||
return val;
|
||||
});
|
||||
onUpdate({ ...filter, value: newValues });
|
||||
}, [filter, onUpdate]);
|
||||
const stableOnChange = useCallback(
|
||||
(e: any, item: any, valueIndex: any) => {
|
||||
const newValues = filter.value.map((val: any, _index: any) => {
|
||||
if (_index === valueIndex) {
|
||||
return item;
|
||||
}
|
||||
return val;
|
||||
});
|
||||
onUpdate({ ...filter, value: newValues });
|
||||
},
|
||||
[filter, onUpdate],
|
||||
);
|
||||
|
||||
const debounceOnSelect = useCallback(debounce(stableOnChange, 500), [stableOnChange]);
|
||||
const debounceOnSelect = useCallback(debounce(stableOnChange, 500), [
|
||||
stableOnChange,
|
||||
]);
|
||||
|
||||
const onDurationChange = useCallback((newValues: any) => {
|
||||
setDurationValues(current => ({ ...current, ...newValues }));
|
||||
setDurationValues((current) => ({ ...current, ...newValues }));
|
||||
}, []);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
if (filter.type === FilterType.DURATION) {
|
||||
const currentMinInProp = filter.value?.[0];
|
||||
const currentMaxInProp = filter.value?.length > 1 ? filter.value[1] : filter.value?.[0];
|
||||
const currentMaxInProp =
|
||||
filter.value?.length > 1 ? filter.value[1] : filter.value?.[0];
|
||||
|
||||
if (durationValues.minDuration !== currentMinInProp || durationValues.maxDuration !== currentMaxInProp) {
|
||||
if (
|
||||
durationValues.minDuration !== currentMinInProp ||
|
||||
durationValues.maxDuration !== currentMaxInProp
|
||||
) {
|
||||
onUpdate({
|
||||
...filter,
|
||||
value: [durationValues.minDuration, durationValues.maxDuration]
|
||||
value: [durationValues.minDuration, durationValues.maxDuration],
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [filter, onUpdate, filter.value, durationValues.minDuration, durationValues.maxDuration]); // Add durationValues dependency
|
||||
}, [
|
||||
filter,
|
||||
onUpdate,
|
||||
filter.value,
|
||||
durationValues.minDuration,
|
||||
durationValues.maxDuration,
|
||||
]); // Add durationValues dependency
|
||||
|
||||
const params = useMemo(() => {
|
||||
let baseParams: any = {
|
||||
type: filter.key,
|
||||
name: filter.name,
|
||||
isEvent: filter.isEvent,
|
||||
id: filter.id
|
||||
id: filter.id,
|
||||
// propertyName: filter.propertyName,
|
||||
// eventName: filter.name,
|
||||
};
|
||||
if (filter.isEvent) {
|
||||
baseParams.eventName = filter.name;
|
||||
}
|
||||
if (!filter.isEvent) {
|
||||
baseParams.propertyName = filter.name;
|
||||
|
||||
if (filter.eventName) {
|
||||
baseParams.eventName = filter.eventName;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.category === FilterCategory.METADATA) {
|
||||
baseParams = { type: FilterKey.METADATA, key: filter.key };
|
||||
}
|
||||
|
|
@ -143,7 +182,17 @@ function FilterValue(props: Props) {
|
|||
|
||||
const value = filter.value;
|
||||
|
||||
switch (filter.type) {
|
||||
switch (filter.dataType) {
|
||||
case FilterType.STRING:
|
||||
return (
|
||||
<ValueAutoComplete
|
||||
initialValues={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
onApplyValues={onApplyValues}
|
||||
params={params}
|
||||
commaQuery={true}
|
||||
/>
|
||||
);
|
||||
case FilterType.DOUBLE:
|
||||
return (
|
||||
<Input
|
||||
|
|
@ -170,7 +219,7 @@ function FilterValue(props: Props) {
|
|||
placeholder={filter.placeholder}
|
||||
options={[
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false }
|
||||
{ label: 'False', value: false },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
|
@ -191,6 +240,7 @@ function FilterValue(props: Props) {
|
|||
/>
|
||||
);
|
||||
case FilterType.NUMBER:
|
||||
case FilterType.INTEGER:
|
||||
return (
|
||||
<BaseFilterLocalAutoComplete
|
||||
value={value}
|
||||
|
|
@ -207,22 +257,16 @@ function FilterValue(props: Props) {
|
|||
isMultiple={false}
|
||||
/>
|
||||
);
|
||||
case FilterType.STRING:
|
||||
return <ValueAutoComplete
|
||||
initialValues={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
onApplyValues={onApplyValues}
|
||||
params={params}
|
||||
commaQuery={true}
|
||||
/>;
|
||||
case FilterType.DROPDOWN:
|
||||
return <BaseDropDown
|
||||
value={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
placeholder={filter.placeholder}
|
||||
options={filter.options}
|
||||
onApplyValues={onApplyValues}
|
||||
/>;
|
||||
return (
|
||||
<BaseDropDown
|
||||
value={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
placeholder={filter.placeholder}
|
||||
options={filter.options}
|
||||
onApplyValues={onApplyValues}
|
||||
/>
|
||||
);
|
||||
case FilterType.ISSUE:
|
||||
case FilterType.MULTIPLE_DROPDOWN:
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,9 +1,26 @@
|
|||
import React, { useState, useEffect, useCallback, useMemo, 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 { Button, Checkbox, Input, Tooltip, Popover, Spin, Typography, List, Divider, Space } from 'antd';
|
||||
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';
|
||||
|
|
@ -20,8 +37,8 @@ interface FilterParams {
|
|||
}
|
||||
|
||||
interface OptionType {
|
||||
value: string;
|
||||
label: string;
|
||||
value?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
@ -37,15 +54,15 @@ interface Props {
|
|||
|
||||
const ValueAutoComplete = observer(
|
||||
({
|
||||
initialValues,
|
||||
params,
|
||||
onApplyValues,
|
||||
placeholder = 'Select value(s)',
|
||||
mapValues,
|
||||
isAutoOpen = false,
|
||||
commaQuery = false,
|
||||
isDisabled = false
|
||||
}: Props) => {
|
||||
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);
|
||||
|
|
@ -62,22 +79,32 @@ const ValueAutoComplete = observer(
|
|||
return `${projectsStore.siteId}_${params.id}`;
|
||||
}, [projectsStore.siteId, params.id]);
|
||||
|
||||
const topValues: TopValue[] = filterKey ? filterStore.topValues[filterKey] || [] : [];
|
||||
const topValues: TopValue[] = filterKey
|
||||
? filterStore.topValues[filterKey] || []
|
||||
: [];
|
||||
|
||||
const mappedTopValues = useMemo(() => {
|
||||
return topValues.map((i) => ({ value: i.value, label: i.value }));
|
||||
const mappedTopValues: OptionType[] = useMemo(() => {
|
||||
return (
|
||||
topValues
|
||||
// .filter((i): i is { value: string } => typeof i.value === 'string')
|
||||
.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))
|
||||
filterStore
|
||||
.fetchTopValues({
|
||||
id: params.id,
|
||||
siteId: projectsStore.siteId + '',
|
||||
isEvent: params.isEvent || false,
|
||||
})
|
||||
.catch((error) => console.error('Failed to load top values', error))
|
||||
.finally(() => setLoadingTopValues(false));
|
||||
}
|
||||
}, [filterKey, params.id, projectsStore.siteId, filterStore]);
|
||||
|
|
@ -88,7 +115,6 @@ const ValueAutoComplete = observer(
|
|||
}
|
||||
}, [isAutoOpen, isDisabled]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingTopValues) return;
|
||||
if (showValueModal) {
|
||||
|
|
@ -99,39 +125,57 @@ const ValueAutoComplete = observer(
|
|||
}
|
||||
}, [showValueModal, loadingTopValues]);
|
||||
|
||||
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([]);
|
||||
const loadOptions = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const trimmedQuery = inputValue.trim();
|
||||
if (!trimmedQuery.length) {
|
||||
setOptions(mappedTopValues);
|
||||
setLoadingSearch(false);
|
||||
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]);
|
||||
setLoadingSearch(true);
|
||||
try {
|
||||
const searchType = params.name;
|
||||
if (!searchType) {
|
||||
console.warn('Search type (params.name) is missing.');
|
||||
setOptions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const autoCompleteParams: any = {
|
||||
q: trimmedQuery,
|
||||
};
|
||||
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [loadOptions]);
|
||||
if (params.propertyName) {
|
||||
autoCompleteParams.propertyName = params.propertyName;
|
||||
}
|
||||
|
||||
if (params.eventName) {
|
||||
autoCompleteParams.eventName = params.eventName;
|
||||
}
|
||||
|
||||
const data: { values: any[] }[] =
|
||||
await searchService.fetchAutoCompleteValues(autoCompleteParams);
|
||||
const _options =
|
||||
data.values?.map((i: any) => ({
|
||||
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);
|
||||
|
|
@ -147,7 +191,8 @@ const ValueAutoComplete = observer(
|
|||
}
|
||||
};
|
||||
|
||||
const isSelected = (item: OptionType) => selectedValues.includes(item.value);
|
||||
const isSelected = (item: OptionType) =>
|
||||
selectedValues.includes(item.value);
|
||||
|
||||
const applySelectedValues = () => {
|
||||
onApplyValues(selectedValues);
|
||||
|
|
@ -155,7 +200,12 @@ const ValueAutoComplete = observer(
|
|||
};
|
||||
|
||||
const applyQuery = () => {
|
||||
const vals = commaQuery ? query.split(',').map((i) => i.trim()).filter(Boolean) : [query.trim()].filter(Boolean);
|
||||
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);
|
||||
|
|
@ -187,13 +237,19 @@ const ValueAutoComplete = observer(
|
|||
return currentOptionsWithValue;
|
||||
}, [options, selectedValues]);
|
||||
|
||||
const queryBlocks = commaQuery ? query.split(',').map(s => s.trim()).filter(Boolean) : [query.trim()].filter(Boolean);
|
||||
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 ? ', ' : '');
|
||||
return queryBlocks
|
||||
.map((block) => `"${block}"`)
|
||||
.join(blocksAmount > 1 ? ', ' : '');
|
||||
}, [queryBlocks]);
|
||||
|
||||
|
||||
const onClearClick = (event: React.MouseEvent | React.KeyboardEvent) => {
|
||||
event.stopPropagation(); // Prevent popover toggle
|
||||
onApplyValues([]);
|
||||
|
|
@ -208,10 +264,7 @@ const ValueAutoComplete = observer(
|
|||
const isEmpty = initialValues[0] === '' || initialValues.length === 0;
|
||||
|
||||
const popoverContent = (
|
||||
<div
|
||||
style={{ width: 360 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ width: 360 }} onClick={(e) => e.stopPropagation()}>
|
||||
<Input.Search
|
||||
value={query}
|
||||
loading={loadingSearch}
|
||||
|
|
@ -226,18 +279,21 @@ const ValueAutoComplete = observer(
|
|||
size="small"
|
||||
locale={{
|
||||
emptyText: t(
|
||||
(loadingSearch || loadingTopValues) ? 'Loading...' :
|
||||
(query.length > 0 ? 'No results found' : 'No options available')
|
||||
)
|
||||
loadingSearch || loadingTopValues
|
||||
? 'Loading...'
|
||||
: query.length > 0
|
||||
? 'No results found'
|
||||
: 'No options available',
|
||||
),
|
||||
}}
|
||||
dataSource={sortedOptions}
|
||||
renderItem={item => (
|
||||
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) }
|
||||
{ 'bg-active-blue-faded': isSelected(item) },
|
||||
)}
|
||||
role="option"
|
||||
aria-selected={isSelected(item)}
|
||||
|
|
@ -245,8 +301,16 @@ const ValueAutoComplete = observer(
|
|||
>
|
||||
<Space>
|
||||
<Checkbox checked={isSelected(item)} readOnly tabIndex={-1} />
|
||||
<Text ellipsis={{ tooltip: { title: item.label, placement: 'topLeft', mouseEnterDelay: 0.5 } }}
|
||||
style={{ maxWidth: 'calc(360px - 80px)' }}>
|
||||
<Text
|
||||
ellipsis={{
|
||||
tooltip: {
|
||||
title: item.label,
|
||||
placement: 'topLeft',
|
||||
mouseEnterDelay: 0.5,
|
||||
},
|
||||
}}
|
||||
style={{ maxWidth: 'calc(360px - 80px)' }}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Space>
|
||||
|
|
@ -255,16 +319,23 @@ const ValueAutoComplete = observer(
|
|||
style={{
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
marginBottom: (query.trim().length > 0 && !loadingSearch) ? 8 : 0
|
||||
marginBottom: query.trim().length > 0 && !loadingSearch ? 8 : 0,
|
||||
}}
|
||||
/>
|
||||
{(query.trim().length > 0 && !loadingSearch && sortedOptions.length > 0) ? (
|
||||
{query.trim().length > 0 &&
|
||||
// sortedOptions.length > 0 &&
|
||||
!loadingSearch ? (
|
||||
<>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Button
|
||||
type="link"
|
||||
onClick={applyQuery}
|
||||
style={{ paddingLeft: 8, whiteSpace: 'normal', height: 'auto', lineHeight: 'inherit' }}
|
||||
style={{
|
||||
paddingLeft: 8,
|
||||
whiteSpace: 'normal',
|
||||
height: 'auto',
|
||||
lineHeight: 'inherit',
|
||||
}}
|
||||
>
|
||||
{t('Apply search')}: {queryStr}
|
||||
</Button>
|
||||
|
|
@ -301,7 +372,7 @@ const ValueAutoComplete = observer(
|
|||
onOpenChange={handleOpenChange}
|
||||
placement="bottomLeft"
|
||||
arrow={false}
|
||||
getPopupContainer={triggerNode => triggerNode || document.body}
|
||||
getPopupContainer={(triggerNode) => triggerNode || document.body}
|
||||
>
|
||||
<Button
|
||||
className="pr-8"
|
||||
|
|
@ -314,26 +385,40 @@ const ValueAutoComplete = observer(
|
|||
<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' }}>
|
||||
<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 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">
|
||||
|
|
@ -344,7 +429,10 @@ const ValueAutoComplete = observer(
|
|||
)}
|
||||
</>
|
||||
) : (
|
||||
<Text type={'secondary'} className={cn({ 'text-disabled': isDisabled })}>
|
||||
<Text
|
||||
type={'secondary'}
|
||||
className={cn({ 'text-disabled': isDisabled })}
|
||||
>
|
||||
{placeholder}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -368,17 +456,16 @@ const ValueAutoComplete = observer(
|
|||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
zIndex: 1,
|
||||
padding: '0 4px'
|
||||
padding: '0 4px',
|
||||
}}
|
||||
>
|
||||
<CloseCircleFilled />
|
||||
</span>
|
||||
)}
|
||||
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default ValueAutoComplete;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
import { filterService } from 'App/services';
|
||||
import { filterService, searchService } from 'App/services';
|
||||
import { Filter, COMMON_FILTERS } from './types/filterConstants';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { projectStore } from '@/mstore/index';
|
||||
|
|
@ -18,6 +18,14 @@ interface ProjectFilters {
|
|||
[projectId: string]: Filter[];
|
||||
}
|
||||
|
||||
interface TopValuesParams {
|
||||
id?: string;
|
||||
siteId?: string;
|
||||
source?: string;
|
||||
isAutoCapture?: boolean;
|
||||
isEvent?: boolean;
|
||||
}
|
||||
|
||||
export default class FilterStore {
|
||||
topValues: TopValues = {};
|
||||
filters: ProjectFilters = {};
|
||||
|
|
@ -33,6 +41,17 @@ export default class FilterStore {
|
|||
this.initCommonFilters();
|
||||
}
|
||||
|
||||
getEventOptions = (sietId: string) => {
|
||||
return this.getFilters(sietId)
|
||||
.filter((i: Filter) => i.isEvent)
|
||||
.map((i: Filter) => {
|
||||
return {
|
||||
label: i.displayName || i.name,
|
||||
value: i.name,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
setTopValues = (key: string, values: Record<string, any> | TopValue[]) => {
|
||||
const vals = Array.isArray(values) ? values : values.data;
|
||||
this.topValues[key] = vals?.filter(
|
||||
|
|
@ -44,19 +63,24 @@ export default class FilterStore {
|
|||
this.topValues = {};
|
||||
};
|
||||
|
||||
fetchTopValues = async (id: string, siteId: string, source?: string) => {
|
||||
const valKey = `${siteId}_${id}${source || ''}`;
|
||||
fetchTopValues = async (params: TopValuesParams) => {
|
||||
const valKey = `${params.siteId}_${params.id}${params.source || ''}`;
|
||||
|
||||
if (this.topValues[valKey] && this.topValues[valKey].length) {
|
||||
return Promise.resolve(this.topValues[valKey]);
|
||||
}
|
||||
const filter = this.filters[siteId]?.find((i) => i.id === id);
|
||||
const filter = this.filters[params.siteId + '']?.find(
|
||||
(i) => i.id === params.id,
|
||||
);
|
||||
if (!filter) {
|
||||
console.error('Filter not found in store:', id);
|
||||
console.error('Filter not found in store:', valKey);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return filterService
|
||||
.fetchTopValues(filter.name?.toLowerCase(), source)
|
||||
|
||||
return searchService
|
||||
.fetchTopValues({
|
||||
[params.isEvent ? 'eventName' : 'propertyName']: filter.name,
|
||||
})
|
||||
.then((response: []) => {
|
||||
this.setTopValues(valKey, response);
|
||||
});
|
||||
|
|
@ -84,7 +108,7 @@ export default class FilterStore {
|
|||
...filter,
|
||||
possibleTypes:
|
||||
filter.possibleTypes?.map((type) => type.toLowerCase()) || [],
|
||||
type: filter.possibleTypes?.[0].toLowerCase() || 'string',
|
||||
dataType: filter.dataType || 'string',
|
||||
category: category || 'custom',
|
||||
subCategory:
|
||||
category === 'events'
|
||||
|
|
@ -157,7 +181,6 @@ export default class FilterStore {
|
|||
|
||||
getEventFilters = async (eventName: string): Promise<Filter[]> => {
|
||||
const cacheKey = `${projectStore.activeSiteId}_${eventName}`;
|
||||
console.log('cacheKey store', cacheKey);
|
||||
if (this.filterCache[cacheKey]) {
|
||||
return this.filterCache[cacheKey];
|
||||
}
|
||||
|
|
@ -170,6 +193,7 @@ export default class FilterStore {
|
|||
this.pendingFetches[cacheKey] =
|
||||
this.fetchAndProcessPropertyFilters(eventName);
|
||||
const filters = await this.pendingFetches[cacheKey];
|
||||
console.log('filters', filters);
|
||||
|
||||
runInAction(() => {
|
||||
this.filterCache[cacheKey] = filters;
|
||||
|
|
@ -185,14 +209,19 @@ export default class FilterStore {
|
|||
|
||||
private fetchAndProcessPropertyFilters = async (
|
||||
eventName: string,
|
||||
isAutoCapture?: boolean,
|
||||
): Promise<Filter[]> => {
|
||||
const resp = await filterService.fetchProperties(eventName);
|
||||
const resp = await filterService.fetchProperties(eventName, isAutoCapture);
|
||||
const names = resp.data.map((i: any) => i['name']);
|
||||
|
||||
const activeSiteId = projectStore.activeSiteId + '';
|
||||
return (
|
||||
this.filters[activeSiteId]?.filter((i: any) => names.includes(i.name)) ||
|
||||
[]
|
||||
this.filters[activeSiteId]
|
||||
?.filter((i: any) => names.includes(i.name))
|
||||
.map((f: any) => ({
|
||||
...f,
|
||||
eventName,
|
||||
})) || []
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ type FilterData = Partial<FilterItem> & {
|
|||
operator?: string;
|
||||
sourceOperator?: string;
|
||||
source?: any;
|
||||
filters?: FilterData[]
|
||||
filters?: FilterData[];
|
||||
};
|
||||
|
||||
export const checkFilterValue = (value: unknown): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0 ? [''] : value.map(val => String(val ?? ''));
|
||||
return value.length === 0 ? [''] : value.map((val) => String(val ?? ''));
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
return [''];
|
||||
|
|
@ -74,7 +74,7 @@ export interface IFilterStore {
|
|||
value: unknown,
|
||||
operator?: string,
|
||||
sourceOperator?: string,
|
||||
source?: unknown
|
||||
source?: unknown,
|
||||
): void;
|
||||
}
|
||||
|
||||
|
|
@ -100,59 +100,69 @@ export default class FilterStore implements IFilterStore {
|
|||
constructor(
|
||||
initialFilters: FilterData[] = [],
|
||||
isConditional = false,
|
||||
isMobile = false
|
||||
isMobile = false,
|
||||
) {
|
||||
this.isConditional = isConditional;
|
||||
this.isMobile = isMobile;
|
||||
this.filters = initialFilters.map(
|
||||
(filterData) => this.createFilterItemFromData(filterData)
|
||||
this.filters = initialFilters.map((filterData) =>
|
||||
this.createFilterItemFromData(filterData),
|
||||
);
|
||||
|
||||
makeAutoObservable(this, {
|
||||
filters: observable.shallow,
|
||||
excludes: observable.shallow,
|
||||
eventsOrder: observable,
|
||||
startTimestamp: observable,
|
||||
endTimestamp: observable,
|
||||
name: observable,
|
||||
page: observable,
|
||||
limit: observable,
|
||||
autoOpen: observable,
|
||||
filterId: observable,
|
||||
eventsHeader: observable,
|
||||
merge: action,
|
||||
addFilter: action,
|
||||
replaceFilters: action,
|
||||
updateFilter: action,
|
||||
removeFilter: action,
|
||||
fromJson: action,
|
||||
fromData: action,
|
||||
addExcludeFilter: action,
|
||||
updateExcludeFilter: action,
|
||||
removeExcludeFilter: action,
|
||||
addFunnelDefaultFilters: action,
|
||||
addOrUpdateFilter: action,
|
||||
addFilterByKeyAndValue: action,
|
||||
isConditional: false,
|
||||
isMobile: false,
|
||||
eventsOrderSupport: false,
|
||||
ID_KEY: false
|
||||
}, { autoBind: true });
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
filters: observable.shallow,
|
||||
excludes: observable.shallow,
|
||||
eventsOrder: observable,
|
||||
startTimestamp: observable,
|
||||
endTimestamp: observable,
|
||||
name: observable,
|
||||
page: observable,
|
||||
limit: observable,
|
||||
autoOpen: observable,
|
||||
filterId: observable,
|
||||
eventsHeader: observable,
|
||||
merge: action,
|
||||
addFilter: action,
|
||||
replaceFilters: action,
|
||||
updateFilter: action,
|
||||
removeFilter: action,
|
||||
fromJson: action,
|
||||
fromData: action,
|
||||
addExcludeFilter: action,
|
||||
updateExcludeFilter: action,
|
||||
removeExcludeFilter: action,
|
||||
addFunnelDefaultFilters: action,
|
||||
addOrUpdateFilter: action,
|
||||
addFilterByKeyAndValue: action,
|
||||
isConditional: false,
|
||||
isMobile: false,
|
||||
eventsOrderSupport: false,
|
||||
ID_KEY: false,
|
||||
},
|
||||
{ autoBind: true },
|
||||
);
|
||||
}
|
||||
|
||||
merge(filterData: Partial<FilterStore>) {
|
||||
runInAction(() => {
|
||||
const validKeys = Object.keys(this).filter(key => typeof (this as any)[key] !== 'function' && key !== 'eventsOrderSupport' && key !== 'isConditional' && key !== 'isMobile');
|
||||
const validKeys = Object.keys(this).filter(
|
||||
(key) =>
|
||||
typeof (this as any)[key] !== 'function' &&
|
||||
key !== 'eventsOrderSupport' &&
|
||||
key !== 'isConditional' &&
|
||||
key !== 'isMobile',
|
||||
);
|
||||
for (const key in filterData) {
|
||||
if (validKeys.includes(key)) {
|
||||
(this as any)[key] = (filterData as any)[key];
|
||||
}
|
||||
}
|
||||
if (filterData.filters) {
|
||||
this.filters = filterData.filters.map(f => f);
|
||||
this.filters = filterData.filters.map((f) => f);
|
||||
}
|
||||
if (filterData.excludes) {
|
||||
this.excludes = filterData.excludes.map(f => f);
|
||||
this.excludes = filterData.excludes.map((f) => f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -165,10 +175,12 @@ export default class FilterStore implements IFilterStore {
|
|||
private createFilterItemFromData(filterData: FilterData): FilterItem {
|
||||
const dataWithValue = {
|
||||
...filterData,
|
||||
value: checkFilterValue(filterData.value)
|
||||
value: checkFilterValue(filterData.value),
|
||||
};
|
||||
if (Array.isArray(dataWithValue.filters)) {
|
||||
dataWithValue.filters = dataWithValue.filters.map(nestedFilter => this.createFilterItemFromData(nestedFilter));
|
||||
dataWithValue.filters = dataWithValue.filters.map((nestedFilter) =>
|
||||
this.createFilterItemFromData(nestedFilter),
|
||||
);
|
||||
}
|
||||
return new FilterItem(dataWithValue);
|
||||
}
|
||||
|
|
@ -179,7 +191,7 @@ export default class FilterStore implements IFilterStore {
|
|||
}
|
||||
|
||||
replaceFilters(newFilters: FilterItem[]) {
|
||||
this.filters = newFilters.map(f => f);
|
||||
this.filters = newFilters.map((f) => f);
|
||||
}
|
||||
|
||||
private updateFilterByIndex(index: number, filterData: FilterData) {
|
||||
|
|
@ -194,22 +206,26 @@ export default class FilterStore implements IFilterStore {
|
|||
}
|
||||
|
||||
updateFilter(filterId: string, filterData: FilterData) {
|
||||
const index = this.filters.findIndex(f => f.id === filterId);
|
||||
const index = this.filters.findIndex((f) => f.id === filterId);
|
||||
if (index > -1) {
|
||||
const updatedFilter = this.createFilterItemFromData(filterData);
|
||||
updatedFilter.id = filterId; // Ensure the ID remains the same
|
||||
this.filters[index] = updatedFilter;
|
||||
} else {
|
||||
console.warn(`FilterStore.updateFilter: Filter with id ${filterId} not found.`);
|
||||
console.warn(
|
||||
`FilterStore.updateFilter: Filter with id ${filterId} not found.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
removeFilter(filterId: string) {
|
||||
const index = this.filters.findIndex(f => f.id === filterId);
|
||||
const index = this.filters.findIndex((f) => f.id === filterId);
|
||||
if (index > -1) {
|
||||
this.filters.splice(index, 1);
|
||||
} else {
|
||||
console.warn(`FilterStore.removeFilter: Filter with id ${filterId} not found.`);
|
||||
console.warn(
|
||||
`FilterStore.removeFilter: Filter with id ${filterId} not found.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,20 +234,20 @@ export default class FilterStore implements IFilterStore {
|
|||
this.name = json.name ?? '';
|
||||
this.filters = Array.isArray(json.filters)
|
||||
? json.filters.map((filterJson: JsonData) =>
|
||||
new FilterItem().fromJson(filterJson)
|
||||
)
|
||||
new FilterItem().fromJson(filterJson),
|
||||
)
|
||||
: [];
|
||||
this.excludes = Array.isArray(json.excludes)
|
||||
? json.excludes.map((filterJson: JsonData) =>
|
||||
new FilterItem().fromJson(filterJson)
|
||||
)
|
||||
new FilterItem().fromJson(filterJson),
|
||||
)
|
||||
: [];
|
||||
this.eventsOrder = json.eventsOrder ?? 'then';
|
||||
this.startTimestamp = json.startTimestamp ?? 0;
|
||||
this.endTimestamp = json.endTimestamp ?? 0;
|
||||
this.page = json.page ?? 1;
|
||||
this.limit = json.limit ?? 10;
|
||||
this.autoOpen = json.autoOpen ?? false;
|
||||
// this.autoOpen = json.autoOpen ?? false;
|
||||
this.filterId = json.filterId ?? '';
|
||||
this.eventsHeader = json.eventsHeader ?? 'EVENTS';
|
||||
});
|
||||
|
|
@ -242,16 +258,16 @@ export default class FilterStore implements IFilterStore {
|
|||
runInAction(() => {
|
||||
this.name = data.name ?? '';
|
||||
this.filters = Array.isArray(data.filters)
|
||||
? data.filters.map((filterData: JsonData) =>
|
||||
this.createFilterItemFromData(filterData)
|
||||
// new FilterItem(undefined, this.isConditional, this.isMobile).fromData(filterData)
|
||||
)
|
||||
? data.filters.map(
|
||||
(filterData: JsonData) => this.createFilterItemFromData(filterData),
|
||||
// new FilterItem(undefined, this.isConditional, this.isMobile).fromData(filterData)
|
||||
)
|
||||
: [];
|
||||
this.excludes = Array.isArray(data.excludes)
|
||||
? data.excludes.map((filterData: JsonData) =>
|
||||
this.createFilterItemFromData(filterData)
|
||||
// new FilterItem(undefined, this.isConditional, this.isMobile).fromData(filterData)
|
||||
)
|
||||
? data.excludes.map(
|
||||
(filterData: JsonData) => this.createFilterItemFromData(filterData),
|
||||
// new FilterItem(undefined, this.isConditional, this.isMobile).fromData(filterData)
|
||||
)
|
||||
: [];
|
||||
this.eventsOrder = data.eventsOrder ?? 'then';
|
||||
this.startTimestamp = data.startTimestamp ?? 0;
|
||||
|
|
@ -271,14 +287,16 @@ export default class FilterStore implements IFilterStore {
|
|||
filters: this.filters.map((filterItem) => filterItem.toJson()),
|
||||
eventsOrder: this.eventsOrder,
|
||||
startTimestamp: this.startTimestamp,
|
||||
endTimestamp: this.endTimestamp
|
||||
endTimestamp: this.endTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
createFilterByKey(key: FilterKey | string): FilterItem {
|
||||
const sourceMap = this.isConditional ? conditionalFiltersMap : filtersMap;
|
||||
const filterTemplate = sourceMap[key as FilterKey];
|
||||
const newFilterData = filterTemplate ? { ...filterTemplate, value: [''] } : { key: key, value: [''] };
|
||||
const newFilterData = filterTemplate
|
||||
? { ...filterTemplate, value: [''] }
|
||||
: { key: key, value: [''] };
|
||||
return this.createFilterItemFromData(newFilterData); // Use helper
|
||||
}
|
||||
|
||||
|
|
@ -294,7 +312,7 @@ export default class FilterStore implements IFilterStore {
|
|||
endTimestamp: this.endTimestamp,
|
||||
eventsHeader: this.eventsHeader,
|
||||
page: this.page,
|
||||
limit: this.limit
|
||||
limit: this.limit,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -304,22 +322,26 @@ export default class FilterStore implements IFilterStore {
|
|||
}
|
||||
|
||||
updateExcludeFilter(filterId: string, filterData: FilterData) {
|
||||
const index = this.excludes.findIndex(f => f.id === filterId);
|
||||
const index = this.excludes.findIndex((f) => f.id === filterId);
|
||||
if (index > -1) {
|
||||
const updatedExclude = this.createFilterItemFromData(filterData);
|
||||
updatedExclude.id = filterId; // Ensure the ID remains the same
|
||||
this.excludes[index] = updatedExclude;
|
||||
} else {
|
||||
console.warn(`FilterStore.updateExcludeFilter: Exclude filter with id ${filterId} not found.`);
|
||||
console.warn(
|
||||
`FilterStore.updateExcludeFilter: Exclude filter with id ${filterId} not found.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
removeExcludeFilter(filterId: string) {
|
||||
const index = this.excludes.findIndex(f => f.id === filterId);
|
||||
const index = this.excludes.findIndex((f) => f.id === filterId);
|
||||
if (index > -1) {
|
||||
this.excludes.splice(index, 1);
|
||||
} else {
|
||||
console.warn(`FilterStore.removeExcludeFilter: Exclude filter with id ${filterId} not found.`);
|
||||
console.warn(
|
||||
`FilterStore.removeExcludeFilter: Exclude filter with id ${filterId} not found.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -331,10 +353,12 @@ export default class FilterStore implements IFilterStore {
|
|||
this.addFilter({
|
||||
...locationFilterData,
|
||||
value: [''],
|
||||
operator: 'isAny'
|
||||
operator: 'isAny',
|
||||
});
|
||||
} else {
|
||||
console.warn(`FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.LOCATION}`);
|
||||
console.warn(
|
||||
`FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.LOCATION}`,
|
||||
);
|
||||
}
|
||||
|
||||
const clickFilterData = filtersMap[FilterKey.CLICK];
|
||||
|
|
@ -342,10 +366,12 @@ export default class FilterStore implements IFilterStore {
|
|||
this.addFilter({
|
||||
...clickFilterData,
|
||||
value: [''],
|
||||
operator: 'onAny'
|
||||
operator: 'onAny',
|
||||
});
|
||||
} else {
|
||||
console.warn(`FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.CLICK}`);
|
||||
console.warn(
|
||||
`FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.CLICK}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -354,7 +380,7 @@ export default class FilterStore implements IFilterStore {
|
|||
const index = this.filters.findIndex((f) => f.key === filterData.key);
|
||||
const dataWithCheckedValue = {
|
||||
...filterData,
|
||||
value: checkFilterValue(filterData.value)
|
||||
value: checkFilterValue(filterData.value),
|
||||
};
|
||||
|
||||
if (index > -1) {
|
||||
|
|
@ -369,7 +395,7 @@ export default class FilterStore implements IFilterStore {
|
|||
value: unknown,
|
||||
operator?: string,
|
||||
sourceOperator?: string,
|
||||
source?: unknown
|
||||
source?: unknown,
|
||||
) {
|
||||
const sourceMap = this.isConditional ? conditionalFiltersMap : filtersMap;
|
||||
const defaultFilterData = sourceMap[key as FilterKey];
|
||||
|
|
@ -381,12 +407,20 @@ export default class FilterStore implements IFilterStore {
|
|||
value: checkFilterValue(value),
|
||||
operator: operator ?? defaultFilterData.operator,
|
||||
sourceOperator: sourceOperator ?? defaultFilterData.sourceOperator,
|
||||
source: source ?? defaultFilterData.source
|
||||
source: source ?? defaultFilterData.source,
|
||||
};
|
||||
this.addOrUpdateFilter(newFilterData);
|
||||
} else {
|
||||
console.warn(`FilterStore.addFilterByKeyAndValue: No default filter template found for key ${key}. Adding generic filter.`);
|
||||
this.addOrUpdateFilter({ key: key, value: checkFilterValue(value), operator, sourceOperator, source });
|
||||
console.warn(
|
||||
`FilterStore.addFilterByKeyAndValue: No default filter template found for key ${key}. Adding generic filter.`,
|
||||
);
|
||||
this.addOrUpdateFilter({
|
||||
key: key,
|
||||
value: checkFilterValue(value),
|
||||
operator,
|
||||
sourceOperator,
|
||||
source,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export interface Filter {
|
|||
displayName?: string;
|
||||
description?: string;
|
||||
possibleTypes?: string[];
|
||||
dataType?: string;
|
||||
autoCaptured?: boolean;
|
||||
metadataName?: string;
|
||||
category: string; // 'event' | 'filter' | 'action' | etc.
|
||||
|
|
@ -38,125 +39,217 @@ export interface Filter {
|
|||
export const OPERATORS = {
|
||||
string: [
|
||||
{ value: 'is', label: 'is', displayName: 'Is', description: 'Exact match' },
|
||||
{ value: 'isNot', label: 'is not', displayName: 'Is not', description: 'Not an exact match' },
|
||||
{ value: 'contains', label: 'contains', displayName: 'Contains', description: 'Contains the string' },
|
||||
{
|
||||
value: 'isNot',
|
||||
label: 'is not',
|
||||
displayName: 'Is not',
|
||||
description: 'Not an exact match',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'contains',
|
||||
displayName: 'Contains',
|
||||
description: 'Contains the string',
|
||||
},
|
||||
{
|
||||
value: 'doesNotContain',
|
||||
label: 'does not contain',
|
||||
displayName: 'Does not contain',
|
||||
description: 'Does not contain the string'
|
||||
description: 'Does not contain the string',
|
||||
},
|
||||
{
|
||||
value: 'startsWith',
|
||||
label: 'starts with',
|
||||
displayName: 'Starts with',
|
||||
description: 'Starts with the string',
|
||||
},
|
||||
{
|
||||
value: 'endsWith',
|
||||
label: 'ends with',
|
||||
displayName: 'Ends with',
|
||||
description: 'Ends with the string',
|
||||
},
|
||||
{
|
||||
value: 'isBlank',
|
||||
label: 'is blank',
|
||||
displayName: 'Is blank',
|
||||
description: 'Is empty or null',
|
||||
},
|
||||
{
|
||||
value: 'isNotBlank',
|
||||
label: 'is not blank',
|
||||
displayName: 'Is not blank',
|
||||
description: 'Is not empty or null',
|
||||
},
|
||||
{ value: 'startsWith', label: 'starts with', displayName: 'Starts with', description: 'Starts with the string' },
|
||||
{ value: 'endsWith', label: 'ends with', displayName: 'Ends with', description: 'Ends with the string' },
|
||||
{ value: 'isBlank', label: 'is blank', displayName: 'Is blank', description: 'Is empty or null' },
|
||||
{ value: 'isNotBlank', label: 'is not blank', displayName: 'Is not blank', description: 'Is not empty or null' }
|
||||
],
|
||||
|
||||
number: [
|
||||
{ value: 'equals', label: 'equals', displayName: 'Equals', description: 'Exactly equals the value' },
|
||||
{
|
||||
value: 'equals',
|
||||
label: 'equals',
|
||||
displayName: 'Equals',
|
||||
description: 'Exactly equals the value',
|
||||
},
|
||||
{
|
||||
value: 'doesNotEqual',
|
||||
label: 'does not equal', // Fixed: added space
|
||||
displayName: 'Does not equal',
|
||||
description: 'Does not equal the value'
|
||||
description: 'Does not equal the value',
|
||||
},
|
||||
{ value: 'greaterThan', label: 'greater than', displayName: 'Greater than', description: 'Greater than the value' },
|
||||
{
|
||||
value: 'lessThan', label: 'less than', // Fixed: added space and lowercased
|
||||
displayName: 'Less than', description: 'Less than the value'
|
||||
value: 'greaterThan',
|
||||
label: 'greater than',
|
||||
displayName: 'Greater than',
|
||||
description: 'Greater than the value',
|
||||
},
|
||||
{
|
||||
value: 'lessThan',
|
||||
label: 'less than', // Fixed: added space and lowercased
|
||||
displayName: 'Less than',
|
||||
description: 'Less than the value',
|
||||
},
|
||||
{
|
||||
value: 'greaterThanOrEquals',
|
||||
label: 'greater than or equals', // Fixed: added spaces and lowercased
|
||||
displayName: 'Greater than or equals',
|
||||
description: 'Greater than or equal to the value'
|
||||
description: 'Greater than or equal to the value',
|
||||
},
|
||||
{
|
||||
value: 'lessThanOrEquals',
|
||||
label: 'less than or equals', // Fixed: added spaces and lowercased
|
||||
displayName: 'Less than or equals',
|
||||
description: 'Less than or equal to the value'
|
||||
description: 'Less than or equal to the value',
|
||||
},
|
||||
{
|
||||
value: 'isBlank', label: 'is blank', // Fixed: added space and lowercased
|
||||
displayName: 'Is blank', description: 'Is empty or null'
|
||||
value: 'isBlank',
|
||||
label: 'is blank', // Fixed: added space and lowercased
|
||||
displayName: 'Is blank',
|
||||
description: 'Is empty or null',
|
||||
},
|
||||
{
|
||||
value: 'isNotBlank', label: 'is not blank', // Fixed: added spaces and lowercased
|
||||
displayName: 'Is not blank', description: 'Is not empty or null'
|
||||
}
|
||||
value: 'isNotBlank',
|
||||
label: 'is not blank', // Fixed: added spaces and lowercased
|
||||
displayName: 'Is not blank',
|
||||
description: 'Is not empty or null',
|
||||
},
|
||||
],
|
||||
|
||||
boolean: [
|
||||
{
|
||||
value: 'isTrue', label: 'is true', // Fixed: added space and lowercased
|
||||
displayName: 'Is true', description: 'Value is true'
|
||||
value: 'isTrue',
|
||||
label: 'is true', // Fixed: added space and lowercased
|
||||
displayName: 'Is true',
|
||||
description: 'Value is true',
|
||||
},
|
||||
{
|
||||
value: 'isFalse', label: 'is false', // Fixed: added space and lowercased
|
||||
displayName: 'Is false', description: 'Value is false'
|
||||
value: 'isFalse',
|
||||
label: 'is false', // Fixed: added space and lowercased
|
||||
displayName: 'Is false',
|
||||
description: 'Value is false',
|
||||
},
|
||||
{
|
||||
value: 'isBlank', label: 'is blank', // Fixed: added space and lowercased
|
||||
displayName: 'Is blank', description: 'Is null'
|
||||
value: 'isBlank',
|
||||
label: 'is blank', // Fixed: added space and lowercased
|
||||
displayName: 'Is blank',
|
||||
description: 'Is null',
|
||||
},
|
||||
{
|
||||
value: 'isNotBlank', label: 'is not blank', // Fixed: added spaces and lowercased
|
||||
displayName: 'Is not blank', description: 'Is not null'
|
||||
}
|
||||
value: 'isNotBlank',
|
||||
label: 'is not blank', // Fixed: added spaces and lowercased
|
||||
displayName: 'Is not blank',
|
||||
description: 'Is not null',
|
||||
},
|
||||
],
|
||||
|
||||
date: [
|
||||
{ value: 'on', label: 'on', displayName: 'On', description: 'On the exact date' },
|
||||
{
|
||||
value: 'notOn', label: 'not on', // Fixed: added space and lowercased
|
||||
displayName: 'Not on', description: 'Not on the exact date'
|
||||
},
|
||||
{ value: 'before', label: 'before', displayName: 'Before', description: 'Before the date' },
|
||||
{ value: 'after', label: 'after', displayName: 'After', description: 'After the date' },
|
||||
{
|
||||
value: 'onOrBefore', label: 'on or before', // Fixed: added spaces and lowercased
|
||||
displayName: 'On or before', description: 'On or before the date'
|
||||
value: 'on',
|
||||
label: 'on',
|
||||
displayName: 'On',
|
||||
description: 'On the exact date',
|
||||
},
|
||||
{
|
||||
value: 'onOrAfter', label: 'on or after', // Fixed: added spaces and lowercased
|
||||
displayName: 'On or after', description: 'On or after the date'
|
||||
value: 'notOn',
|
||||
label: 'not on', // Fixed: added space and lowercased
|
||||
displayName: 'Not on',
|
||||
description: 'Not on the exact date',
|
||||
},
|
||||
{
|
||||
value: 'isBlank', label: 'is blank', // Fixed: added space and lowercased
|
||||
displayName: 'Is blank', description: 'Is empty or null'
|
||||
value: 'before',
|
||||
label: 'before',
|
||||
displayName: 'Before',
|
||||
description: 'Before the date',
|
||||
},
|
||||
{
|
||||
value: 'isNotBlank', label: 'is not blank', // Fixed: added spaces and lowercased
|
||||
displayName: 'Is not blank', description: 'Is not empty or null'
|
||||
}
|
||||
value: 'after',
|
||||
label: 'after',
|
||||
displayName: 'After',
|
||||
description: 'After the date',
|
||||
},
|
||||
{
|
||||
value: 'onOrBefore',
|
||||
label: 'on or before', // Fixed: added spaces and lowercased
|
||||
displayName: 'On or before',
|
||||
description: 'On or before the date',
|
||||
},
|
||||
{
|
||||
value: 'onOrAfter',
|
||||
label: 'on or after', // Fixed: added spaces and lowercased
|
||||
displayName: 'On or after',
|
||||
description: 'On or after the date',
|
||||
},
|
||||
{
|
||||
value: 'isBlank',
|
||||
label: 'is blank', // Fixed: added space and lowercased
|
||||
displayName: 'Is blank',
|
||||
description: 'Is empty or null',
|
||||
},
|
||||
{
|
||||
value: 'isNotBlank',
|
||||
label: 'is not blank', // Fixed: added spaces and lowercased
|
||||
displayName: 'Is not blank',
|
||||
description: 'Is not empty or null',
|
||||
},
|
||||
],
|
||||
|
||||
array: [
|
||||
{ value: 'contains', label: 'contains', displayName: 'Contains', description: 'Array contains the value' },
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'contains',
|
||||
displayName: 'Contains',
|
||||
description: 'Array contains the value',
|
||||
},
|
||||
{
|
||||
value: 'doesNotContain',
|
||||
label: 'does not contain', // Fixed: added spaces and lowercased
|
||||
displayName: 'Does not contain',
|
||||
description: 'Array does not contain the value'
|
||||
description: 'Array does not contain the value',
|
||||
},
|
||||
{
|
||||
value: 'hasAny', label: 'has any', // Fixed: added space and lowercased
|
||||
displayName: 'Has any', description: 'Array has any of the values'
|
||||
value: 'hasAny',
|
||||
label: 'has any', // Fixed: added space and lowercased
|
||||
displayName: 'Has any',
|
||||
description: 'Array has any of the values',
|
||||
},
|
||||
{
|
||||
value: 'hasAll', label: 'has all', // Fixed: added space and lowercased
|
||||
displayName: 'Has all', description: 'Array has all of the values'
|
||||
value: 'hasAll',
|
||||
label: 'has all', // Fixed: added space and lowercased
|
||||
displayName: 'Has all',
|
||||
description: 'Array has all of the values',
|
||||
},
|
||||
{
|
||||
value: 'isEmpty', label: 'is empty', // Fixed: added space and lowercased
|
||||
displayName: 'Is empty', description: 'Array is empty'
|
||||
value: 'isEmpty',
|
||||
label: 'is empty', // Fixed: added space and lowercased
|
||||
displayName: 'Is empty',
|
||||
description: 'Array is empty',
|
||||
},
|
||||
{
|
||||
value: 'isNotEmpty', label: 'is not empty', // Fixed: added spaces and lowercased
|
||||
displayName: 'Is not empty', description: 'Array is not empty'
|
||||
}
|
||||
]
|
||||
value: 'isNotEmpty',
|
||||
label: 'is not empty', // Fixed: added spaces and lowercased
|
||||
displayName: 'Is not empty',
|
||||
description: 'Array is not empty',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const COMMON_FILTERS: Filter[] = [];
|
||||
|
|
|
|||
|
|
@ -23,13 +23,14 @@ export default class FilterItem {
|
|||
value?: string[];
|
||||
propertyOrder?: string;
|
||||
filters?: FilterItem[];
|
||||
autoOpen?: boolean;
|
||||
|
||||
constructor(data: any = {}) {
|
||||
makeAutoObservable(this);
|
||||
|
||||
if (Array.isArray(data.filters)) {
|
||||
data.filters = data.filters.map(
|
||||
(i: Record<string, any>) => new FilterItem(i)
|
||||
(i: Record<string, any>) => new FilterItem(i),
|
||||
);
|
||||
}
|
||||
data.operator = data.operator || 'is';
|
||||
|
|
@ -82,7 +83,7 @@ export default class FilterItem {
|
|||
source: this.name,
|
||||
filters: Array.isArray(this.filters)
|
||||
? this.filters.map((i) => i.toJson())
|
||||
: []
|
||||
: [],
|
||||
};
|
||||
|
||||
const isMetadata = this.category === FilterCategory.METADATA;
|
||||
|
|
|
|||
|
|
@ -395,12 +395,12 @@ export default class Widget {
|
|||
} else if (this.metricType === FUNNEL) {
|
||||
_data.funnel = new Funnel().fromJSON(data);
|
||||
} else if (this.metricType === TABLE) {
|
||||
const count = data[0]['count'];
|
||||
const vals = data[0]['values'].map((s: any) =>
|
||||
const count = data['count'];
|
||||
const vals = data['values'].map((s: any) =>
|
||||
new SessionsByRow().fromJson(s, count, this.metricOf),
|
||||
);
|
||||
_data['values'] = vals
|
||||
_data['total'] = data[0]['total'];
|
||||
_data['values'] = vals;
|
||||
_data['total'] = data['total'];
|
||||
} else {
|
||||
if (data.hasOwnProperty('chart')) {
|
||||
_data['value'] = data.value;
|
||||
|
|
|
|||
|
|
@ -24,8 +24,12 @@ export default class FilterService {
|
|||
return await response.json();
|
||||
};
|
||||
|
||||
fetchProperties = async (name: string) => {
|
||||
let path = `/pa/PROJECT_ID/properties/search?event_name=${name}`;
|
||||
fetchProperties = async (
|
||||
eventName: string,
|
||||
isAutoCapture: boolean = false,
|
||||
) => {
|
||||
// en = eventName, ac = isAutoCapture
|
||||
let path = `/pa/PROJECT_ID/properties/search?en=${eventName}&ac=${isAutoCapture}`;
|
||||
const response = await this.client.get(path);
|
||||
return await response.json();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,8 +46,20 @@ export default class SearchService extends BaseService {
|
|||
return j.data;
|
||||
}
|
||||
|
||||
async fetchTopValues(params: {}): Promise<any> {
|
||||
const r = await this.client.get(
|
||||
'/pa/PROJECT_ID/properties/autocomplete',
|
||||
params,
|
||||
);
|
||||
const j = await r.json();
|
||||
return j.data;
|
||||
}
|
||||
|
||||
async fetchAutoCompleteValues(params: {}): Promise<any> {
|
||||
const r = await this.client.get('/PROJECT_ID/events/search', params);
|
||||
const r = await this.client.get(
|
||||
'/pa/PROJECT_ID/properties/autocomplete',
|
||||
params,
|
||||
);
|
||||
const j = await r.json();
|
||||
return j.data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,9 +210,9 @@ export enum FilterKey {
|
|||
MISSING_RESOURCE = 'missingResource',
|
||||
SLOW_SESSION = 'slowSession',
|
||||
CLICK_RAGE = 'clickRage',
|
||||
CLICK = 'click',
|
||||
INPUT = 'input',
|
||||
LOCATION = 'location',
|
||||
CLICK = 'CLICK',
|
||||
INPUT = 'INPUT',
|
||||
LOCATION = 'LOCATION',
|
||||
VIEW = 'view',
|
||||
CONSOLE = 'console',
|
||||
METADATA = 'metadata',
|
||||
|
|
@ -279,11 +279,7 @@ export enum FilterKey {
|
|||
AVG_REQUEST_LOADT_IME = 'avgRequestLoadTime',
|
||||
AVG_RESPONSE_TIME = 'avgResponseTime',
|
||||
AVG_SESSION_DURATION = 'avgSessionDuration',
|
||||
AVG_TILL_FIRST_BYTE = 'avgTillFirstByte',
|
||||
AVG_TIME_TO_INTERACTIVE = 'avgTimeToInteractive',
|
||||
AVG_TIME_TO_RENDER = 'avgTimeToRender',
|
||||
AVG_USED_JS_HEAP_SIZE = 'avgUsedJsHeapSize',
|
||||
AVG_VISITED_PAGES = 'avgVisitedPages',
|
||||
|
||||
COUNT_REQUESTS = 'countRequests',
|
||||
COUNT_SESSIONS = 'countSessions',
|
||||
|
||||
|
|
@ -305,10 +301,6 @@ export enum FilterKey {
|
|||
PAGES_RESPONSE_TIME = 'pagesResponseTime',
|
||||
PAGES_RESPONSE_TIME_DISTRIBUTION = 'pagesResponseTimeDistribution',
|
||||
SESSIONS_PER_BROWSER = 'sessionsPerBrowser',
|
||||
SLOWEST_DOMAINS = 'slowestDomains',
|
||||
SPEED_LOCATION = 'speedLocation',
|
||||
TIME_TO_RENDER = 'timeToRender',
|
||||
IMPACTED_SESSIONS_BY_SLOW_PAGES = 'impactedSessionsBySlowPages',
|
||||
|
||||
CLICKMAP_URL = 'clickMapUrl',
|
||||
FEATURE_FLAG = 'featureFlag',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue