feat(ui): dynamic fitlers - apply filters to cards
This commit is contained in:
parent
6a100561bf
commit
9ee8cbd24a
19 changed files with 1007 additions and 756 deletions
|
|
@ -1,11 +1,16 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { EventsList, FilterList } from 'Shared/Filters/FilterList';
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button, Space } from 'antd';
|
||||
import { Button, Divider, Space } from 'antd';
|
||||
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
|
||||
import ExcludeFilters from './ExcludeFilters';
|
||||
import SeriesName from './SeriesName';
|
||||
import FilterListHeader from 'Shared/Filters/FilterList/FilterListHeader';
|
||||
import FilterSelection from 'Shared/Filters/FilterSelection';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
import { Plus } from '.store/lucide-react-virtual-9282d60eb0/package';
|
||||
import UnifiedFilterList from 'Shared/Filters/FilterList/UnifiedFilterList';
|
||||
import { useStore } from '@/mstore';
|
||||
|
||||
const FilterCountLabels = observer(
|
||||
(props: { filters: any; toggleExpand: any }) => {
|
||||
|
|
@ -38,7 +43,7 @@ const FilterCountLabels = observer(
|
|||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const FilterSeriesHeader = observer(
|
||||
|
|
@ -62,8 +67,8 @@ const FilterSeriesHeader = observer(
|
|||
'px-4 ps-2 h-12 flex items-center relative bg-white border-gray-lighter border-t border-l border-r rounded-t-xl',
|
||||
{
|
||||
hidden: props.hidden,
|
||||
'rounded-b-xl': !props.expanded,
|
||||
},
|
||||
'rounded-b-xl': !props.expanded
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Space className="mr-auto" size={30}>
|
||||
|
|
@ -106,7 +111,7 @@ const FilterSeriesHeader = observer(
|
|||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface Props {
|
||||
|
|
@ -130,7 +135,8 @@ interface Props {
|
|||
|
||||
function FilterSeries(props: Props) {
|
||||
const {
|
||||
observeChanges = () => {},
|
||||
observeChanges = () => {
|
||||
},
|
||||
canDelete,
|
||||
hideHeader = false,
|
||||
emptyMessage = 'Add an event or filter step to define the series.',
|
||||
|
|
@ -142,12 +148,17 @@ function FilterSeries(props: Props) {
|
|||
removeEvents,
|
||||
collapseState,
|
||||
onToggleCollapse,
|
||||
excludeCategory,
|
||||
excludeCategory
|
||||
} = props;
|
||||
const { filterStore } = useStore();
|
||||
const expanded = isHeatmap || !collapseState;
|
||||
const setExpanded = onToggleCollapse;
|
||||
const { series, seriesIndex } = props;
|
||||
|
||||
const allFilterOptions: Filter[] = filterStore.getCurrentProjectFilters();
|
||||
const eventOptions: Filter[] = allFilterOptions.filter((i) => i.isEvent);
|
||||
const propertyOptions: Filter[] = allFilterOptions.filter((i) => !i.isEvent);
|
||||
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
series.filter.updateFilter(filterIndex, filter);
|
||||
observeChanges();
|
||||
|
|
@ -174,6 +185,8 @@ function FilterSeries(props: Props) {
|
|||
observeChanges();
|
||||
};
|
||||
|
||||
console.log('series.filter.filters', series.filter.filters);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canExclude && <ExcludeFilters filter={series.filter} />}
|
||||
|
|
@ -216,33 +229,79 @@ function FilterSeries(props: Props) {
|
|||
{expanded ? (
|
||||
<>
|
||||
{removeEvents ? null : (
|
||||
<EventsList
|
||||
filter={series.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
supportsEmpty={supportsEmpty}
|
||||
onFilterMove={onFilterMove}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
onAddFilter={onAddFilter}
|
||||
mergeUp={!hideHeader}
|
||||
mergeDown
|
||||
cannotAdd={isHeatmap}
|
||||
excludeCategory={excludeCategory}
|
||||
/>
|
||||
<div className="bg-white rounded-b-xl border p-4">
|
||||
<FilterListHeader
|
||||
title={'Events'}
|
||||
showEventsOrder={series.filter.filters.filter((f: any) => f.isEvent).length > 0}
|
||||
orderProps={{
|
||||
eventsOrder: series.filter.eventsOrder,
|
||||
eventsOrderSupport: ['then', 'and', 'or']
|
||||
}}
|
||||
onChangeOrder={onChangeEventsOrder}
|
||||
filterSelection={
|
||||
<FilterSelection
|
||||
filters={eventOptions}
|
||||
onFilterClick={(newFilter: Filter) => {
|
||||
onAddFilter(newFilter);
|
||||
}}
|
||||
>
|
||||
<Button type="default" size="small">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} strokeWidth={1} />
|
||||
<span>Add</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
}
|
||||
/>
|
||||
|
||||
<UnifiedFilterList
|
||||
title="Events"
|
||||
filters={series.filter.filters.filter((f: any) => f.isEvent)}
|
||||
isDraggable={true}
|
||||
showIndices={true}
|
||||
className="mt-2"
|
||||
handleRemove={onRemoveFilter}
|
||||
handleUpdate={onUpdateFilter}
|
||||
handleAdd={onAddFilter}
|
||||
handleMove={onFilterMove}
|
||||
/>
|
||||
|
||||
<Divider className="my-2" />
|
||||
|
||||
<FilterListHeader
|
||||
title={'Filters'}
|
||||
showEventsOrder={series.filter.filters.map((f: any) => !f.isEvent).length > 0}
|
||||
filterSelection={
|
||||
<FilterSelection
|
||||
filters={propertyOptions}
|
||||
onFilterClick={(newFilter: Filter) => {
|
||||
onAddFilter(newFilter);
|
||||
}}
|
||||
>
|
||||
<Button type="default" size="small">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} strokeWidth={1} />
|
||||
<span>Add</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
}
|
||||
/>
|
||||
|
||||
<UnifiedFilterList
|
||||
title="Events"
|
||||
filters={series.filter.filters.filter((f: any) => !f.isEvent)}
|
||||
isDraggable={false}
|
||||
showIndices={false}
|
||||
className="mt-2"
|
||||
handleRemove={onRemoveFilter}
|
||||
handleUpdate={onUpdateFilter}
|
||||
handleAdd={onAddFilter}
|
||||
handleMove={onFilterMove}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FilterList
|
||||
filter={series.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
supportsEmpty={supportsEmpty}
|
||||
onFilterMove={onFilterMove}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
onAddFilter={onAddFilter}
|
||||
mergeUp={!removeEvents}
|
||||
excludeCategory={excludeCategory}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ interface Props {
|
|||
isSubItem?: boolean;
|
||||
subFilterIndex?: number;
|
||||
propertyOrder?: string;
|
||||
onToggleOperator?: (newOp: string) => void;
|
||||
onPropertyOrderChange?: (newOp: string) => void;
|
||||
parentEventFilterOptions?: Filter[];
|
||||
isDragging?: boolean;
|
||||
isFirst?: boolean;
|
||||
|
|
@ -47,7 +47,7 @@ function FilterItem(props: Props) {
|
|||
isSubItem = false,
|
||||
subFilterIndex = 0, // Default to 0
|
||||
propertyOrder,
|
||||
onToggleOperator,
|
||||
onPropertyOrderChange,
|
||||
parentEventFilterOptions,
|
||||
isDragging,
|
||||
isFirst = false // Default to false
|
||||
|
|
@ -70,27 +70,59 @@ function FilterItem(props: Props) {
|
|||
const operatorOptions = getOperatorsByType(filter.type);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true; // Mounted flag
|
||||
|
||||
async function loadFilters() {
|
||||
if (!isSubItem && filter.isEvent && filter.name) {
|
||||
const shouldFetch = !isSubItem && filter.isEvent && filter.name;
|
||||
const fetchName = filter.name; // Capture value at effect start
|
||||
|
||||
if (shouldFetch) {
|
||||
try {
|
||||
setEventFiltersLoading(true);
|
||||
const options = await filterStore.getEventFilters(filter.name);
|
||||
setEventFilterOptions(options);
|
||||
// Only set loading if not already loading for this specific fetch
|
||||
if (isMounted) setEventFiltersLoading(true);
|
||||
|
||||
const options = await filterStore.getEventFilters(fetchName);
|
||||
|
||||
// Check mount status AND if the relevant dependencies are still the same
|
||||
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.
|
||||
setEventFilterOptions(options);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load event filters:', error);
|
||||
setEventFilterOptions([]);
|
||||
if (isMounted && filter.name === fetchName && !isSubItem && filter.isEvent) {
|
||||
setEventFilterOptions([]);
|
||||
}
|
||||
} finally {
|
||||
setEventFiltersLoading(false);
|
||||
if (isMounted && filter.name === fetchName && !isSubItem && filter.isEvent) {
|
||||
setEventFiltersLoading(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (eventFilterOptions.length > 0) {
|
||||
setEventFilterOptions([]);
|
||||
// Reset state only if necessary and component is mounted
|
||||
if (isMounted) {
|
||||
// Avoid calling setState if already in the desired state
|
||||
if (eventFilterOptions.length > 0) {
|
||||
setEventFilterOptions([]);
|
||||
}
|
||||
// Might need to check loading state too if it could be stuck true
|
||||
if (eventFiltersLoading) {
|
||||
setEventFiltersLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadFilters();
|
||||
}, [filter.name, filter.isEvent, isSubItem, filterStore]);
|
||||
|
||||
return () => {
|
||||
isMounted = false; // Cleanup on unmount
|
||||
};
|
||||
// Dependencies should be the minimal primitive values or stable references
|
||||
// that determine *if* and *what* to fetch.
|
||||
}, [filter.name, filter.isEvent, isSubItem, filterStore]); //
|
||||
|
||||
const canShowValues = useMemo(
|
||||
() =>
|
||||
|
|
@ -169,7 +201,7 @@ function FilterItem(props: Props) {
|
|||
const newSubFilter = {
|
||||
...selectedFilter,
|
||||
value: selectedFilter.value || [''],
|
||||
operator: selectedFilter.operator || getOperatorsByType(selectedFilter.type)[0]?.value // Default operator
|
||||
operator: selectedFilter.operator || 'is'
|
||||
};
|
||||
onUpdate({
|
||||
...filter,
|
||||
|
|
@ -193,7 +225,6 @@ 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 */}
|
||||
|
|
@ -203,20 +234,20 @@ function FilterItem(props: Props) {
|
|||
|
||||
{isSubItem && (
|
||||
<div
|
||||
className="flex-shrink-0 w-14 text-right text-sm text-neutral-500/90 pr-2">
|
||||
className="flex-shrink-0 w-14 text-right text-neutral-500/90 pr-2">
|
||||
{subFilterIndex === 0 && (
|
||||
<Typography.Text className="text-inherit">
|
||||
where
|
||||
</Typography.Text>
|
||||
)}
|
||||
{subFilterIndex !== 0 && propertyOrder && onToggleOperator && (
|
||||
{subFilterIndex !== 0 && propertyOrder && onPropertyOrderChange && (
|
||||
<Typography.Text
|
||||
className={cn(
|
||||
'text-inherit',
|
||||
!readonly && 'cursor-pointer hover:text-main transition-colors'
|
||||
)}
|
||||
onClick={() =>
|
||||
!readonly && onToggleOperator(propertyOrder === 'and' ? 'or' : 'and')
|
||||
!readonly && onPropertyOrderChange(propertyOrder === 'and' ? 'or' : 'and')
|
||||
}
|
||||
>
|
||||
{propertyOrder}
|
||||
|
|
@ -227,7 +258,7 @@ function FilterItem(props: Props) {
|
|||
|
||||
{/* Main content area */}
|
||||
<div
|
||||
className="flex flex-grow flex-wrap gap-x-1 items-center">
|
||||
className="flex flex-grow flex-wrap gap-x-2 items-center">
|
||||
<FilterSelection
|
||||
filters={filterSelections}
|
||||
onFilterClick={replaceFilter}
|
||||
|
|
@ -254,7 +285,7 @@ function FilterItem(props: Props) {
|
|||
|
||||
{/* Category/SubCategory */}
|
||||
{hasCategory && (
|
||||
<span className="text-neutral-500/90 capitalize text-sm truncate">
|
||||
<span className="text-neutral-500/90 capitalize truncate">
|
||||
{categoryPart}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -263,7 +294,7 @@ function FilterItem(props: Props) {
|
|||
<span className="text-neutral-400">•</span>
|
||||
)}
|
||||
|
||||
<span className="text-sm text-black truncate">
|
||||
<span className="text-black truncate">
|
||||
{hasName ? namePart : (hasCategory ? '' : defaultText)} {/* Show name or placeholder */}
|
||||
</span>
|
||||
</Space>
|
||||
|
|
@ -301,7 +332,7 @@ function FilterItem(props: Props) {
|
|||
{canShowValues &&
|
||||
(readonly ? (
|
||||
<div
|
||||
className="rounded bg-gray-lightest text-gray-dark px-2 py-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis border border-gray-light max-w-xs"
|
||||
className="rounded bg-gray-lightest text-gray-dark px-2 py-1 whitespace-nowrap overflow-hidden text-ellipsis border border-gray-light max-w-xs"
|
||||
title={filter.value.join(', ')}
|
||||
>
|
||||
{filter.value
|
||||
|
|
@ -359,20 +390,20 @@ function FilterItem(props: Props) {
|
|||
{filteredSubFilters.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'relative w-full mt-3 mb-2 flex flex-col gap-2' // Relative parent for border
|
||||
'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 // Dynamic margin based on parent index visibility
|
||||
subFilterMarginLeftClass
|
||||
)} style={{ height: 'calc(100% - 4px)' }} />
|
||||
|
||||
{filteredSubFilters.map((subFilter: any, index: number) => (
|
||||
<div
|
||||
key={`subfilter-wrapper-${filter.id || filterIndex}-${subFilter.key || index}`}
|
||||
className={cn('relative', subFilterPaddingLeftClass)} // Apply padding to the wrapper, keep relative
|
||||
className={cn('relative', subFilterPaddingLeftClass)}
|
||||
>
|
||||
<FilterItem
|
||||
filter={subFilter}
|
||||
|
|
@ -382,14 +413,14 @@ function FilterItem(props: Props) {
|
|||
saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={disableDelete}
|
||||
readonly={readonly}
|
||||
hideIndex={true} // Sub-items always hide index
|
||||
hideIndex={true}
|
||||
hideDelete={hideDelete}
|
||||
isConditional={isConditional}
|
||||
isSubItem={true}
|
||||
propertyOrder={filter.propertyOrder || 'and'} // Sub-items use parent's propertyOrder
|
||||
onToggleOperator={onToggleOperator} // Pass down the parent's toggle function
|
||||
parentEventFilterOptions={isSubItem ? parentEventFilterOptions : eventFilterOptions} // Pass options down
|
||||
isFirst={index === 0} // Mark the first sub-filter
|
||||
propertyOrder={filter.propertyOrder || 'and'}
|
||||
onPropertyOrderChange={onPropertyOrderChange}
|
||||
parentEventFilterOptions={isSubItem ? parentEventFilterOptions : eventFilterOptions}
|
||||
isFirst={index === 0}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -252,7 +252,10 @@ export const EventsList = observer((props: Props) => {
|
|||
|
||||
<div className="ml-auto">
|
||||
{!hideEventsOrder && (
|
||||
<EventsOrder filter={filter} onChange={props.onChangeEventsOrder} />
|
||||
<EventsOrder orderProps={{
|
||||
eventsOrder: filter.eventsOrder,
|
||||
eventsOrderSupport: filter.eventsOrderSupport
|
||||
}} onChange={props.onChangeEventsOrder} />
|
||||
)}
|
||||
{actions &&
|
||||
actions.map((action, index) => <div key={index}>{action}</div>)}
|
||||
|
|
|
|||
|
|
@ -63,9 +63,6 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
|
||||
const calculateNewPosition = useCallback(
|
||||
(hoverIndex: number, hoverPosition: string) => {
|
||||
// Calculate the target *visual* position
|
||||
// If hovering top half, target index is hoverIndex.
|
||||
// If bottom half, target index is hoverIndex + 1.
|
||||
return hoverPosition === 'bottom' ? hoverIndex + 1 : hoverIndex;
|
||||
},
|
||||
[]
|
||||
|
|
@ -124,13 +121,10 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
|
||||
let newPosition = calculateNewPosition(hoverIndex, hoverPosition);
|
||||
|
||||
// Important: Adjust newPosition if dragging downwards past the original position
|
||||
// because the removal shifts subsequent indices up.
|
||||
if (dragInd < newPosition) {
|
||||
newPosition--;
|
||||
}
|
||||
|
||||
// Only call move if the position actually changed
|
||||
if (dragInd !== newPosition && !(dragInd === hoverIndex && hoverPosition === 'top') && !(dragInd === hoverIndex - 1 && hoverPosition === 'bottom')) {
|
||||
handleMove(dragInd, newPosition);
|
||||
}
|
||||
|
|
@ -148,21 +142,18 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback(() => {
|
||||
// Only clear if leaving the specific item, not just moving within it
|
||||
setHoveredItem({ i: null, position: null });
|
||||
}, []);
|
||||
|
||||
return filters.length ? (
|
||||
<div className={cn('flex flex-col gap-2', className)} style={style}>
|
||||
<div className={cn('flex flex-col', className)} style={style}>
|
||||
{filters.map((filterItem: any, filterIndex: number) => (
|
||||
<div
|
||||
key={`filter-${filterItem.key || filterIndex}`}
|
||||
className={cn('flex gap-2 items-start hover:bg-active-blue/5 -mx-5 px-5 pe-3 transition-colors duration-100 relative', { // Lighter hover, keep relative
|
||||
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,
|
||||
// Add top/bottom borders based on hover state for visual feedback
|
||||
'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',
|
||||
// Add negative margin to compensate for border height only when border is visible
|
||||
'-mt-0.5': hoveredItem.i === filterIndex && hoveredItem.position === 'top',
|
||||
'-mb-0.5': hoveredItem.i === filterIndex && hoveredItem.position === 'bottom'
|
||||
})}
|
||||
|
|
@ -177,7 +168,6 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
{isDraggable && filters.length > 1 && (
|
||||
<div
|
||||
className="cursor-grab text-neutral-500 hover:text-neutral-700 pt-[4px] flex-shrink-0" // Align handle visually
|
||||
// Draggable is set on parent div
|
||||
style={{ cursor: draggedInd !== null ? 'grabbing' : 'grab' }}
|
||||
title="Drag to reorder"
|
||||
>
|
||||
|
|
@ -186,22 +176,26 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
|||
)}
|
||||
|
||||
{!isDraggable && showIndices &&
|
||||
<div className="w-4 flex-shrink-0" />} {/* Placeholder for alignment if not draggable but indices shown */}
|
||||
<div className="w-4 flex-shrink-0" />}
|
||||
{!isDraggable && !showIndices &&
|
||||
<div className="w-4 flex-shrink-0" />} {/* Placeholder for alignment if not draggable and no indices */}
|
||||
<div className="w-4 flex-shrink-0" />}
|
||||
|
||||
|
||||
<FilterItem
|
||||
filterIndex={showIndices ? filterIndex : undefined}
|
||||
filter={filterItem}
|
||||
onUpdate={(updatedFilter) => updateFilter(filterItem.key, updatedFilter)}
|
||||
onRemoveFilter={() => removeFilter(filterItem.key)}
|
||||
onUpdate={(updatedFilter) => updateFilter(filterItem.id, updatedFilter)}
|
||||
onRemoveFilter={() => removeFilter(filterItem.id)}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={cannotDelete}
|
||||
readonly={readonly}
|
||||
isConditional={isConditional}
|
||||
hideIndex={!showIndices}
|
||||
isDragging={draggedInd === filterIndex}
|
||||
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,28 +1,26 @@
|
|||
import React from 'react';
|
||||
import { Dropdown, Menu, Button } from 'antd'; // Import Dropdown, Menu, Button
|
||||
import { DownOutlined } from '@ant-design/icons'; // Optional: Icon for the button
|
||||
import { Dropdown, Menu, Button, Typography } from 'antd';
|
||||
|
||||
interface OptionType {
|
||||
label: React.ReactNode; // Label can be text or other React elements
|
||||
value: string | number; // Value is typically string or number
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
options: OptionType[];
|
||||
value?: string | number; // Should match the type of OptionType.value
|
||||
value?: string | number;
|
||||
onChange: (
|
||||
event: unknown, // Keep original signature for compatibility upstream
|
||||
event: unknown,
|
||||
payload: { name: string; value: string | number | undefined }
|
||||
) => void;
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
allowClear?: boolean; // Prop name from original component
|
||||
popupClassName?: string; // Use this for the dropdown overlay class
|
||||
allowClear?: boolean;
|
||||
popupClassName?: string;
|
||||
}
|
||||
|
||||
// Define a special key for the clear action
|
||||
const CLEAR_VALUE_KEY = '__antd_clear_value__';
|
||||
|
||||
function FilterOperator(props: Props) {
|
||||
|
|
@ -33,37 +31,30 @@ function FilterOperator(props: Props) {
|
|||
onChange,
|
||||
isDisabled = false,
|
||||
className = '',
|
||||
placeholder = 'Select', // Default placeholder
|
||||
placeholder = 'select', // Default placeholder
|
||||
allowClear = false, // Default from original component
|
||||
popupClassName = 'shadow-lg border border-gray-200 rounded-md w-fit' // Default popup class
|
||||
} = props;
|
||||
|
||||
// Find the label of the currently selected option
|
||||
const selectedOption = options.find(option => option.value === value);
|
||||
const displayLabel = selectedOption ? selectedOption.label : placeholder;
|
||||
const displayLabel = selectedOption ? selectedOption.label :
|
||||
<Typography.Text className="text-neutral-600">{placeholder}</Typography.Text>;
|
||||
|
||||
// Handler for menu item clicks
|
||||
const handleMenuClick = (e: { key: string }) => {
|
||||
let selectedValue: string | number | undefined;
|
||||
|
||||
if (e.key === CLEAR_VALUE_KEY) {
|
||||
// Handle the clear action
|
||||
selectedValue = undefined;
|
||||
} else {
|
||||
// Find the option corresponding to the key (which we set as the value)
|
||||
// Antd Menu keys are strings, so convert value to string for comparison/lookup if needed
|
||||
const clickedOption = options.find(option => String(option.value) === e.key);
|
||||
selectedValue = clickedOption?.value;
|
||||
}
|
||||
|
||||
// Call the original onChange prop with the expected structure
|
||||
onChange(null, { name: name, value: selectedValue });
|
||||
};
|
||||
|
||||
// Construct the menu items
|
||||
const menu = (
|
||||
<Menu onClick={handleMenuClick} selectedKeys={value !== undefined ? [String(value)] : []}>
|
||||
{/* Add Clear Option if allowClear is true and a value is selected */}
|
||||
{allowClear && value !== undefined && (
|
||||
<>
|
||||
<Menu.Item key={CLEAR_VALUE_KEY} danger>
|
||||
|
|
@ -85,12 +76,11 @@ function FilterOperator(props: Props) {
|
|||
<>
|
||||
<Dropdown
|
||||
overlay={menu}
|
||||
trigger={['click']} // Open dropdown on click
|
||||
trigger={['click']}
|
||||
disabled={isDisabled}
|
||||
overlayClassName={popupClassName} // Apply the custom class to the overlay
|
||||
overlayClassName={popupClassName}
|
||||
>
|
||||
{/* The Button acts as the trigger */}
|
||||
<Button type="default" size="small" disabled={isDisabled} className="w-fit text-sm">
|
||||
<Button type="default" size="small" disabled={isDisabled} className="w-fit">
|
||||
{displayLabel}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
// Import Spin and potentially classnames
|
||||
import { Popover, Spin } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
|
@ -11,8 +10,8 @@ interface FilterSelectionProps {
|
|||
onFilterClick: (filter: Filter) => void;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
isLive?: boolean; // This prop seems unused, consider removing if not needed downstream
|
||||
loading?: boolean; // <-- Add loading prop
|
||||
isLive?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const FilterSelection: React.FC<FilterSelectionProps> = observer(({
|
||||
|
|
@ -26,43 +25,32 @@ const FilterSelection: React.FC<FilterSelectionProps> = observer(({
|
|||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleFilterClick = useCallback((selectedFilter: Filter) => {
|
||||
// Don't do anything if loading - though modal shouldn't be clickable then anyway
|
||||
if (loading) return;
|
||||
onFilterClick(selectedFilter);
|
||||
setOpen(false);
|
||||
}, [onFilterClick, loading]);
|
||||
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
// Prevent opening if disabled or loading
|
||||
if (!disabled && !loading) {
|
||||
setOpen(newOpen);
|
||||
} else if (!newOpen) {
|
||||
// Allow closing even if disabled/loading (e.g., clicking outside)
|
||||
setOpen(newOpen);
|
||||
}
|
||||
}, [disabled, loading]);
|
||||
|
||||
// Determine the content for the Popover
|
||||
const content = (
|
||||
loading
|
||||
// Show a spinner centered in the popover content area while loading
|
||||
? <div className="p-4 flex justify-center items-center" style={{ minHeight: '100px', minWidth: '150px' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
// Otherwise, show the filter modal
|
||||
: <FilterModal
|
||||
onFilterClick={handleFilterClick}
|
||||
filters={filters}
|
||||
// If FilterModal needs to know about the live status, pass it down:
|
||||
// isLive={isLive}
|
||||
/>
|
||||
);
|
||||
|
||||
// Combine disabled and loading states for the trigger element
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
// Clone the trigger element (children) and pass the effective disabled state
|
||||
// Also add loading class for potential styling (e.g., opacity)
|
||||
const triggerElement = React.isValidElement(children)
|
||||
? React.cloneElement(children as React.ReactElement<any>, {
|
||||
disabled: isDisabled,
|
||||
|
|
@ -71,21 +59,20 @@ const FilterSelection: React.FC<FilterSelectionProps> = observer(({
|
|||
: children;
|
||||
|
||||
return (
|
||||
// Add a class to the wrapper if needed, e.g., for opacity when loading
|
||||
// <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>
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { FilterKey, FilterCategory, FilterType } from 'Types/filter/filterType';
|
||||
import { debounce } from 'App/utils';
|
||||
import { assist as assistRoute, isRoute } from 'App/routes';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import FilterDuration from '../FilterDuration';
|
||||
import FilterValueDropdown from '../FilterValueDropdown';
|
||||
import FilterAutoCompleteLocal from '../FilterAutoCompleteLocal';
|
||||
import FilterAutoComplete from '../FilterAutoComplete';
|
||||
import ValueAutoComplete from 'Shared/Filters/FilterValue/ValueAutoComplete';
|
||||
import { Input, Select } from 'antd';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
|
|
@ -18,233 +18,260 @@ interface Props {
|
|||
isConditional?: boolean;
|
||||
}
|
||||
|
||||
function BaseFilterLocalAutoComplete(props: any) {
|
||||
return (
|
||||
<FilterAutoCompleteLocal
|
||||
value={props.value}
|
||||
showCloseButton={props.showCloseButton}
|
||||
onApplyValues={props.onApplyValues}
|
||||
onRemoveValue={props.onRemoveValue}
|
||||
onSelect={props.onSelect}
|
||||
icon={props.icon}
|
||||
placeholder={props.placeholder}
|
||||
isAutoOpen={props.isAutoOpen}
|
||||
modalProps={props.modalProps}
|
||||
type={props.type}
|
||||
allowDecimals={props.allowDecimals}
|
||||
isMultiple={props.isMultiple}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BaseDropDown(props: any) {
|
||||
return (
|
||||
<FilterValueDropdown
|
||||
value={props.value}
|
||||
isAutoOpen={props.isAutoOpen}
|
||||
placeholder={props.placeholder}
|
||||
options={props.options}
|
||||
onApplyValues={props.onApplyValues}
|
||||
search={props.search}
|
||||
onAddValue={props.onAddValue}
|
||||
onRemoveValue={props.onRemoveValue}
|
||||
showCloseButton={props.showCloseButton}
|
||||
showOrButton={props.showOrButton}
|
||||
isMultiple={props.isMultiple}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function FilterValue(props: Props) {
|
||||
const { filter } = props;
|
||||
const isAutoOpen = filter.autoOpen;
|
||||
const { filter, onUpdate, isConditional } = props; // Destructure props early
|
||||
const isAutoOpen = filter.autoOpen; // Assume parent now controls this correctly
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isAutoOpen) {
|
||||
setTimeout(() => {
|
||||
filter.autoOpen = false;
|
||||
}, 250);
|
||||
}
|
||||
}, [isAutoOpen]);
|
||||
const [durationValues, setDurationValues] = useState({
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}, [filter.value, filter.type]);
|
||||
|
||||
|
||||
const showCloseButton = filter.value.length > 1;
|
||||
const showOrButton = filter.value.length > 1;
|
||||
|
||||
const onAddValue = () => {
|
||||
const onAddValue = useCallback(() => {
|
||||
const newValue = filter.value.concat('');
|
||||
props.onUpdate({ ...filter, value: newValue });
|
||||
};
|
||||
onUpdate({ ...filter, value: newValue });
|
||||
}, [filter, onUpdate]);
|
||||
|
||||
const onApplyValues = (values: string[]) => {
|
||||
props.onUpdate({ ...filter, value: values });
|
||||
};
|
||||
const onApplyValues = useCallback((values: string[]) => {
|
||||
onUpdate({ ...filter, value: values });
|
||||
}, [filter, onUpdate]);
|
||||
|
||||
const onRemoveValue = (valueIndex: any) => {
|
||||
const onRemoveValue = useCallback((valueIndex: any) => {
|
||||
const newValue = filter.value.filter(
|
||||
(_: any, index: any) => index !== valueIndex
|
||||
);
|
||||
props.onUpdate({ ...filter, value: newValue });
|
||||
};
|
||||
onUpdate({ ...filter, value: newValue });
|
||||
}, [filter, onUpdate]);
|
||||
|
||||
const onChange = (e: any, item: any, valueIndex: any) => {
|
||||
const newValues = filter.value.map((_: any, _index: any) => {
|
||||
const stableOnChange = useCallback((e: any, item: any, valueIndex: any) => {
|
||||
const newValues = filter.value.map((val: any, _index: any) => {
|
||||
if (_index === valueIndex) {
|
||||
return item;
|
||||
}
|
||||
return _;
|
||||
return val;
|
||||
});
|
||||
props.onUpdate({ ...filter, value: newValues });
|
||||
};
|
||||
onUpdate({ ...filter, value: newValues });
|
||||
}, [filter, onUpdate]);
|
||||
|
||||
const debounceOnSelect = React.useCallback(debounce(onChange, 500), [
|
||||
onChange
|
||||
]);
|
||||
const debounceOnSelect = useCallback(debounce(stableOnChange, 500), [stableOnChange]);
|
||||
|
||||
const onDurationChange = (newValues: any) => {
|
||||
setDurationValues({ ...durationValues, ...newValues });
|
||||
};
|
||||
const onDurationChange = useCallback((newValues: any) => {
|
||||
setDurationValues(current => ({ ...current, ...newValues }));
|
||||
}, []);
|
||||
|
||||
const handleBlur = () => {
|
||||
const handleBlur = useCallback(() => {
|
||||
if (filter.type === FilterType.DURATION) {
|
||||
const { maxDuration, minDuration } = filter;
|
||||
if (maxDuration || minDuration) return;
|
||||
if (
|
||||
maxDuration !== durationValues.maxDuration ||
|
||||
minDuration !== durationValues.minDuration
|
||||
) {
|
||||
props.onUpdate({
|
||||
const currentMinInProp = filter.value?.[0];
|
||||
const currentMaxInProp = filter.value?.length > 1 ? filter.value[1] : filter.value?.[0];
|
||||
|
||||
if (durationValues.minDuration !== currentMinInProp || durationValues.maxDuration !== currentMaxInProp) {
|
||||
onUpdate({
|
||||
...filter,
|
||||
value: [durationValues.minDuration, durationValues.maxDuration]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [filter, onUpdate, filter.value, durationValues.minDuration, durationValues.maxDuration]); // Add durationValues dependency
|
||||
|
||||
const getParams = (key: any) => {
|
||||
let params: any = {
|
||||
const params = useMemo(() => {
|
||||
let baseParams: any = {
|
||||
type: filter.key,
|
||||
name: filter.name,
|
||||
isEvent: filter.isEvent,
|
||||
id: filter.id
|
||||
};
|
||||
switch (filter.category) {
|
||||
case FilterCategory.METADATA:
|
||||
params = { type: FilterKey.METADATA, key };
|
||||
if (filter.category === FilterCategory.METADATA) {
|
||||
baseParams = { type: FilterKey.METADATA, key: filter.key };
|
||||
}
|
||||
|
||||
if (isRoute(ASSIST_ROUTE, window.location.pathname)) {
|
||||
params = { ...params, live: true };
|
||||
baseParams = { ...baseParams, live: true };
|
||||
}
|
||||
return baseParams;
|
||||
}, [filter.key, filter.name, filter.isEvent, filter.id, filter.category]);
|
||||
|
||||
return params;
|
||||
};
|
||||
const value = filter.value;
|
||||
|
||||
const renderValueFiled = (value: any[]) => {
|
||||
const showOrButton = filter.value.length > 1;
|
||||
|
||||
function BaseFilterLocalAutoComplete(props) {
|
||||
switch (filter.type) {
|
||||
case FilterType.DOUBLE:
|
||||
return (
|
||||
<FilterAutoCompleteLocal
|
||||
<Input
|
||||
type="number"
|
||||
value={value}
|
||||
size="small"
|
||||
className="rounded-lg"
|
||||
style={{ width: '80px' }}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
onUpdate({ ...filter, value: newValue });
|
||||
}}
|
||||
placeholder={filter.placeholder}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
);
|
||||
case FilterType.BOOLEAN:
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
size="small"
|
||||
style={{ width: '80px' }}
|
||||
onChange={(value: any) => onUpdate({ ...filter, value })}
|
||||
placeholder={filter.placeholder}
|
||||
options={[
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case FilterType.NUMBER_MULTIPLE:
|
||||
return (
|
||||
<BaseFilterLocalAutoComplete
|
||||
value={value}
|
||||
showCloseButton={showCloseButton}
|
||||
onApplyValues={onApplyValues}
|
||||
onRemoveValue={(index) => onRemoveValue(index)}
|
||||
onSelect={(e, item, index) => debounceOnSelect(e, item, index)}
|
||||
onRemoveValue={onRemoveValue}
|
||||
onSelect={debounceOnSelect}
|
||||
icon={filter.icon}
|
||||
placeholder={filter.placeholder}
|
||||
isAutoOpen={isAutoOpen}
|
||||
modalProps={{ placeholder: '' }}
|
||||
{...props}
|
||||
type="number"
|
||||
isMultiple={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BaseDropDown(props) {
|
||||
case FilterType.NUMBER:
|
||||
return (
|
||||
<FilterValueDropdown
|
||||
<BaseFilterLocalAutoComplete
|
||||
value={value}
|
||||
showCloseButton={showCloseButton}
|
||||
onApplyValues={onApplyValues}
|
||||
onRemoveValue={onRemoveValue}
|
||||
onSelect={debounceOnSelect}
|
||||
icon={filter.icon}
|
||||
placeholder={filter.placeholder}
|
||||
isAutoOpen={isAutoOpen}
|
||||
modalProps={{ placeholder: '' }}
|
||||
type="number"
|
||||
allowDecimals={false}
|
||||
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}
|
||||
/>;
|
||||
case FilterType.ISSUE:
|
||||
case FilterType.MULTIPLE_DROPDOWN:
|
||||
return (
|
||||
<BaseDropDown
|
||||
value={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
placeholder={filter.placeholder}
|
||||
options={filter.options}
|
||||
onApplyValues={onApplyValues}
|
||||
{...props}
|
||||
search
|
||||
onAddValue={onAddValue}
|
||||
onRemoveValue={onRemoveValue}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (filter.type) {
|
||||
case FilterType.NUMBER_MULTIPLE:
|
||||
return (
|
||||
<BaseFilterLocalAutoComplete
|
||||
type="number"
|
||||
placeholder={filter.placeholder}
|
||||
/>
|
||||
);
|
||||
case FilterType.NUMBER:
|
||||
return (
|
||||
<BaseFilterLocalAutoComplete
|
||||
type="number"
|
||||
allowDecimals={false}
|
||||
isMultiple={false}
|
||||
placeholder={filter.placeholder}
|
||||
/>
|
||||
);
|
||||
case FilterType.STRING:
|
||||
return <ValueAutoComplete
|
||||
initialValues={value}
|
||||
case FilterType.DURATION:
|
||||
return (
|
||||
<FilterDuration
|
||||
onChange={onDurationChange}
|
||||
onBlur={handleBlur}
|
||||
minDuration={durationValues.minDuration}
|
||||
maxDuration={durationValues.maxDuration}
|
||||
isConditional={isConditional}
|
||||
/>
|
||||
);
|
||||
case FilterType.MULTIPLE:
|
||||
return (
|
||||
<FilterAutoComplete
|
||||
value={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
// showCloseButton={showCloseButton}
|
||||
// showOrButton={showOrButton}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
onApplyValues={onApplyValues}
|
||||
// onRemoveValue={(index) => onRemoveValue(index)}
|
||||
// method="GET"
|
||||
// endpoint="/PROJECT_ID/events/search"
|
||||
params={getParams(filter.key)}
|
||||
// headerText=""
|
||||
// placeholder={filter.placeholder}
|
||||
commaQuery={true}
|
||||
// onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
// icon={filter.icon}
|
||||
// modalProps={{ placeholder: 'Search' }}
|
||||
/>;
|
||||
// return <BaseFilterLocalAutoComplete placeholder={filter.placeholder} />;
|
||||
// return <FilterAutoComplete
|
||||
// value={value}
|
||||
// isAutoOpen={isAutoOpen}
|
||||
// showCloseButton={showCloseButton}
|
||||
// showOrButton={showOrButton}
|
||||
// onApplyValues={onApplyValues}
|
||||
// onRemoveValue={(index) => onRemoveValue(index)}
|
||||
// method="GET"
|
||||
// endpoint="/PROJECT_ID/events/search"
|
||||
// params={getParams(filter.key)}
|
||||
// headerText=""
|
||||
// placeholder={filter.placeholder}
|
||||
// onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
// icon={filter.icon}
|
||||
// modalProps={{ placeholder: 'Search' }}
|
||||
// />;
|
||||
case FilterType.DROPDOWN:
|
||||
return <BaseDropDown />;
|
||||
case FilterType.ISSUE:
|
||||
case FilterType.MULTIPLE_DROPDOWN:
|
||||
return (
|
||||
<BaseDropDown
|
||||
search
|
||||
onAddValue={onAddValue}
|
||||
onRemoveValue={(ind) => onRemoveValue(ind)}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
placeholder={filter.placeholder}
|
||||
/>
|
||||
);
|
||||
case FilterType.DURATION:
|
||||
return (
|
||||
<FilterDuration
|
||||
onChange={onDurationChange}
|
||||
onBlur={handleBlur}
|
||||
minDuration={durationValues.minDuration}
|
||||
maxDuration={durationValues.maxDuration}
|
||||
isConditional={props.isConditional}
|
||||
/>
|
||||
);
|
||||
case FilterType.MULTIPLE:
|
||||
return (
|
||||
<FilterAutoComplete
|
||||
value={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
onApplyValues={onApplyValues}
|
||||
onRemoveValue={(index) => onRemoveValue(index)}
|
||||
method="GET"
|
||||
endpoint="/PROJECT_ID/events/search"
|
||||
params={getParams(filter.key)}
|
||||
headerText=""
|
||||
placeholder={filter.placeholder}
|
||||
onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
icon={filter.icon}
|
||||
modalProps={{ placeholder: 'Search' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<
|
||||
// id="ignore-outside"
|
||||
// className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
|
||||
// 'grid-cols-2': filter.hasSource
|
||||
// })}
|
||||
>
|
||||
{renderValueFiled(filter.value)}
|
||||
</>
|
||||
);
|
||||
onRemoveValue={onRemoveValue}
|
||||
method="GET"
|
||||
endpoint="/PROJECT_ID/events/search" // TODO: Replace PROJECT_ID dynamically if needed
|
||||
params={params}
|
||||
headerText=""
|
||||
placeholder={filter.placeholder}
|
||||
onSelect={stableOnChange}
|
||||
icon={filter.icon}
|
||||
modalProps={{ placeholder: 'Search' }}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
console.warn('Unsupported filter type in FilterValue:', filter.type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default observer(FilterValue);
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ interface OptionType {
|
|||
label: string;
|
||||
}
|
||||
|
||||
// Removed custom TruncatedText, will use Typography.Text ellipsis
|
||||
|
||||
interface Props {
|
||||
initialValues: string[];
|
||||
params: FilterParams;
|
||||
|
|
@ -57,7 +55,7 @@ const ValueAutoComplete = observer(
|
|||
const [loadingSearch, setLoadingSearch] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null); // Ref for the main trigger button
|
||||
|
||||
const filterKey = useMemo(() => {
|
||||
if (!projectsStore.siteId || !params.id) return null;
|
||||
|
|
@ -92,13 +90,14 @@ const ValueAutoComplete = observer(
|
|||
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingTopValues) return;
|
||||
if (showValueModal) {
|
||||
setSelectedValues(initialValues.filter((i) => i && i.length > 0));
|
||||
setQuery('');
|
||||
setOptions(mappedTopValues.length > 0 ? mappedTopValues : []);
|
||||
setLoadingSearch(false);
|
||||
}
|
||||
}, [showValueModal, initialValues, mappedTopValues]);
|
||||
}, [showValueModal, loadingTopValues]);
|
||||
|
||||
const loadOptions = useCallback(async (inputValue: string) => {
|
||||
const trimmedQuery = inputValue.trim();
|
||||
|
|
@ -195,8 +194,8 @@ const ValueAutoComplete = observer(
|
|||
}, [queryBlocks]);
|
||||
|
||||
|
||||
const onClearClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
const onClearClick = (event: React.MouseEvent | React.KeyboardEvent) => {
|
||||
event.stopPropagation(); // Prevent popover toggle
|
||||
onApplyValues([]);
|
||||
setShowValueModal(false);
|
||||
};
|
||||
|
|
@ -206,7 +205,7 @@ const ValueAutoComplete = observer(
|
|||
setShowValueModal(visible);
|
||||
};
|
||||
|
||||
const isEmpty = initialValues.length === 0;
|
||||
const isEmpty = initialValues[0] === '' || initialValues.length === 0;
|
||||
|
||||
const popoverContent = (
|
||||
<div
|
||||
|
|
@ -302,37 +301,15 @@ const ValueAutoComplete = observer(
|
|||
onOpenChange={handleOpenChange}
|
||||
placement="bottomLeft"
|
||||
arrow={false}
|
||||
getPopupContainer={triggerNode => triggerNode || document.body} // Ensure it attaches correctly
|
||||
getPopupContainer={triggerNode => triggerNode || document.body}
|
||||
>
|
||||
{/* className={cn(*/}
|
||||
{/* 'rounded-lg px-2 cursor-pointer bg-white border border-gray-light text-ellipsis hover:border-main',*/}
|
||||
{/* 'transition-colors duration-100 flex-shrink-0 max-w-xs h-[26px] items-center gap-1', // Fixed height, ensure items-center*/}
|
||||
{/* { 'opacity-70 pointer-events-none': disableDelete || readonly }*/}
|
||||
{/*)}*/}
|
||||
<Button // Main trigger container using Ant Design classes
|
||||
// className={cn(
|
||||
// // 'ant-input', // Mimic Ant Input appearance
|
||||
// 'relative rounded-xl px-2 cursor-pointer bg-white border border-gray-light text-ellipsis hover:border-main',
|
||||
// 'transition-colors duration-100 flex-shrink-0 max-w-xs h-[24px] items-center gap-1 pr-8',
|
||||
// {
|
||||
// // 'cursor-pointer hover:border-primary': !isDisabled, // Use theme primary color for hover
|
||||
// 'cursor-not-allowed bg-disabled border-disabled': isDisabled, // Ant disabled styles
|
||||
// // 'border-primary shadow-outline-primary': showValueModal && !isDisabled, // Ant focus styles
|
||||
// // 'border': true, // Base border
|
||||
// // 'rounded': true // Base rounded corners
|
||||
// }
|
||||
// )}
|
||||
<Button
|
||||
className="pr-8"
|
||||
size="small"
|
||||
// style={{ height: 26, lineHeight: 'normal' }} // Adjust styling
|
||||
ref={triggerRef}
|
||||
disabled={isDisabled}
|
||||
onMouseEnter={() => !isDisabled && setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
role={isDisabled ? undefined : 'button'}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={showValueModal}
|
||||
aria-disabled={isDisabled}
|
||||
>
|
||||
<Space size={4} wrap className="w-full overflow-hidden">
|
||||
{!isEmpty ? (
|
||||
|
|
@ -367,25 +344,37 @@ const ValueAutoComplete = observer(
|
|||
)}
|
||||
</>
|
||||
) : (
|
||||
<Text type={isDisabled ? 'secondary' : undefined} className={cn({ 'text-disabled': isDisabled })}>
|
||||
<Text type={'secondary'} className={cn({ 'text-disabled': isDisabled })}>
|
||||
{placeholder}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{!isEmpty && hovered && !isDisabled && (
|
||||
// Using Button for clear for better accessibility/styling consistency
|
||||
<Button
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2"
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CloseCircleFilled className="text-neutral-400 hover:text-neutral-600" />}
|
||||
onClick={onClearClick}
|
||||
<span
|
||||
role="button"
|
||||
aria-label={t('Clear selection')}
|
||||
onClick={onClearClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onClearClick(e);
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onTouchStart={(e) => e.stopPropagation()}
|
||||
style={{ height: '100%', border: 'none', background: 'transparent', zIndex: 1 }} // Ensure clickable area
|
||||
/>
|
||||
tabIndex={0} // Make it focusable if needed
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center justify-center text-neutral-400 hover:text-neutral-600"
|
||||
style={{
|
||||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
zIndex: 1,
|
||||
padding: '0 4px'
|
||||
}}
|
||||
>
|
||||
<CloseCircleFilled />
|
||||
</span>
|
||||
)}
|
||||
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,131 +1,107 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import UnifiedFilterList from 'Shared/Filters/FilterList/UnifiedFilterList';
|
||||
import FilterSelection from 'Shared/Filters/FilterSelection';
|
||||
import { Button, Divider } from 'antd';
|
||||
import { Button, Divider, Card } from 'antd';
|
||||
import { Plus } from 'lucide-react';
|
||||
import cn from 'classnames';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
import FilterListHeader from 'Shared/Filters/FilterList/FilterListHeader';
|
||||
|
||||
let debounceFetch: any = () => {
|
||||
};
|
||||
|
||||
function SessionFilters() {
|
||||
const { searchStore, projectsStore, filterStore } =
|
||||
useStore();
|
||||
|
||||
const { searchStore, filterStore } = useStore();
|
||||
const searchInstance = searchStore.instance;
|
||||
const saveRequestPayloads =
|
||||
projectsStore.instance?.saveRequestPayloads ?? false;
|
||||
|
||||
const allFilterOptions: Filter[] = filterStore.getCurrentProjectFilters();
|
||||
const eventOptions = allFilterOptions.filter(i => i.isEvent);
|
||||
const propertyOptions = allFilterOptions.filter(i => !i.isEvent);
|
||||
const eventOptions = allFilterOptions.filter((i) => i.isEvent);
|
||||
const propertyOptions = allFilterOptions.filter((i) => !i.isEvent);
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
filter.autoOpen = true;
|
||||
searchStore.addFilter(filter);
|
||||
const onAddFilter = (filter: Filter) => {
|
||||
searchStore.addFilter({ ...filter, autoOpen: true });
|
||||
};
|
||||
|
||||
const onChangeEventsOrder = (e: any, { value }: any) => {
|
||||
const onChangeEventsOrder = (
|
||||
_e: React.MouseEvent<HTMLButtonElement>,
|
||||
{ value }: { value: string }
|
||||
) => {
|
||||
searchStore.edit({
|
||||
eventsOrder: value
|
||||
});
|
||||
};
|
||||
|
||||
const eventFilters = searchInstance.filters.filter((i) => i.isEvent);
|
||||
const attributeFilters = searchInstance.filters.filter((i) => !i.isEvent);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'bg-white',
|
||||
'py-2 px-4 rounded-xl border border-gray-lighter pt-4'
|
||||
)}
|
||||
>
|
||||
<FilterListHeader
|
||||
title={'Events'}
|
||||
showEventsOrder={searchInstance.filters.filter(i => i.isEvent).length > 0}
|
||||
orderProps={searchInstance}
|
||||
onChangeOrder={onChangeEventsOrder}
|
||||
filterSelection={
|
||||
<FilterSelection
|
||||
filters={eventOptions}
|
||||
onFilterClick={(newFilter) => {
|
||||
console.log('newFilter', newFilter);
|
||||
onAddFilter(newFilter);
|
||||
}}>
|
||||
<Button type="default" size="small">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} strokeWidth={1} />
|
||||
<span>Add Event</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
}
|
||||
/>
|
||||
<Card className="rounded-lg" classNames={{ body: '!p-4' }}>
|
||||
<FilterListHeader
|
||||
title={'Events'}
|
||||
showEventsOrder={eventFilters.length > 0}
|
||||
orderProps={searchInstance}
|
||||
onChangeOrder={onChangeEventsOrder}
|
||||
filterSelection={
|
||||
<FilterSelection
|
||||
filters={eventOptions}
|
||||
onFilterClick={(newFilter: Filter) => {
|
||||
onAddFilter(newFilter);
|
||||
}}
|
||||
>
|
||||
<Button type="default" size="small">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} strokeWidth={1} />
|
||||
<span>Add</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
}
|
||||
/>
|
||||
|
||||
<UnifiedFilterList
|
||||
title="Events"
|
||||
filters={searchInstance.filters.filter(i => i.isEvent)}
|
||||
isDraggable={true}
|
||||
showIndices={true}
|
||||
className="mt-2"
|
||||
handleRemove={function(key: string): void {
|
||||
searchStore.removeFilter(key);
|
||||
}}
|
||||
handleUpdate={function(key: string, updatedFilter: any): void {
|
||||
searchStore.updateFilter(key, updatedFilter);
|
||||
}}
|
||||
handleAdd={function(newFilter: Filter): void {
|
||||
searchStore.addFilter(newFilter);
|
||||
}}
|
||||
handleMove={function(draggedIndex: number, newPosition: number): void {
|
||||
searchStore.moveFilter(draggedIndex, newPosition);
|
||||
}}
|
||||
/>
|
||||
<UnifiedFilterList
|
||||
title="Events"
|
||||
filters={eventFilters}
|
||||
isDraggable={true}
|
||||
showIndices={true}
|
||||
className="mt-2"
|
||||
handleRemove={searchStore.removeFilter}
|
||||
handleUpdate={searchStore.updateFilter}
|
||||
handleAdd={searchStore.addFilter}
|
||||
handleMove={searchStore.moveFilter}
|
||||
/>
|
||||
|
||||
<Divider className="my-3" />
|
||||
<Divider className="my-3" />
|
||||
|
||||
<FilterListHeader
|
||||
title={'Filters'}
|
||||
filterSelection={
|
||||
<FilterSelection
|
||||
filters={propertyOptions}
|
||||
onFilterClick={(newFilter) => {
|
||||
onAddFilter(newFilter);
|
||||
}}
|
||||
>
|
||||
<Button type="default" size="small">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} strokeWidth={1} />
|
||||
<span>Filter</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
} />
|
||||
<FilterListHeader
|
||||
title={'Filters'}
|
||||
filterSelection={
|
||||
<FilterSelection
|
||||
filters={propertyOptions}
|
||||
onFilterClick={(newFilter: Filter) => {
|
||||
onAddFilter(newFilter);
|
||||
}}
|
||||
>
|
||||
<Button type="default" size="small">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} strokeWidth={1} />
|
||||
<span>Add</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
}
|
||||
/>
|
||||
|
||||
<UnifiedFilterList
|
||||
title="Filters"
|
||||
filters={searchInstance.filters.filter(i => !i.isEvent)}
|
||||
className="mt-2"
|
||||
isDraggable={false}
|
||||
showIndices={false}
|
||||
handleRemove={function(key: string): void {
|
||||
searchStore.removeFilter(key);
|
||||
}}
|
||||
handleUpdate={function(key: string, updatedFilter: any): void {
|
||||
searchStore.updateFilter(key, updatedFilter);
|
||||
}}
|
||||
handleAdd={function(newFilter: Filter): void {
|
||||
searchStore.addFilter(newFilter);
|
||||
}}
|
||||
handleMove={function(draggedIndex: number, newPosition: number): void {
|
||||
searchStore.moveFilter(draggedIndex, newPosition);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<UnifiedFilterList
|
||||
title="Filters"
|
||||
filters={attributeFilters}
|
||||
className="mt-2"
|
||||
isDraggable={false}
|
||||
showIndices={false}
|
||||
handleRemove={searchStore.removeFilter}
|
||||
handleUpdate={searchStore.updateFilter}
|
||||
handleAdd={searchStore.addFilter}
|
||||
handleMove={searchStore.moveFilter}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import SessionHeader from './components/SessionHeader';
|
|||
import LatestSessionsMessage from './components/LatestSessionsMessage';
|
||||
|
||||
function SessionsTabOverview() {
|
||||
usePageTitle('Sessions - OpenReplay');
|
||||
// usePageTitle('Sessions - OpenReplay');
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
import { filterService } from 'App/services';
|
||||
import { Filter, Operator, COMMON_FILTERS, getOperatorsByType } from './types/filterConstants';
|
||||
import { Filter, COMMON_FILTERS } from './types/filterConstants';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { projectStore } from '@/mstore/index';
|
||||
|
||||
|
|
@ -88,14 +88,14 @@ export default class FilterStore {
|
|||
icon: FilterKey.LOCATION, // TODO - use actual icons
|
||||
isEvent: category === 'events',
|
||||
value: filter.value || [],
|
||||
propertyOrder: 'and'
|
||||
propertyOrder: 'and',
|
||||
operator: filter.operator || 'is'
|
||||
}));
|
||||
};
|
||||
|
||||
addOperatorsToFilters = (filters: Filter[]): Filter[] => {
|
||||
return filters.map(filter => ({
|
||||
...filter
|
||||
// operators: filter.operators?.length ? filter.operators : getOperatorsByType(filter.possibleTypes || [])
|
||||
...filter,
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { sessionStore, settingsStore } from 'App/mstore';
|
|||
import SavedSearch, { ISavedSearch } from 'App/mstore/types/savedSearch';
|
||||
import { iTag } from '@/services/NotesService';
|
||||
import { issues_types } from 'Types/session/issue';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
|
|
@ -63,7 +64,58 @@ class SearchStore {
|
|||
latestRequestTime: number | null = null;
|
||||
latestList = List();
|
||||
alertMetricId: number | null = null;
|
||||
instance = new Search();
|
||||
instance = new Search({
|
||||
// rangeValue: LAST_24_HOURS,
|
||||
startDate: Date.now() - 24 * 60 * 60 * 1000,
|
||||
endDate: Date.now(),
|
||||
filters: [
|
||||
{
|
||||
id: Math.random().toString(36).substring(7),
|
||||
name: 'CLICK',
|
||||
category: 'events',
|
||||
type: 'string',
|
||||
value: ['/client/account'],
|
||||
operator: 'is',
|
||||
isEvent: true,
|
||||
filters: [
|
||||
{
|
||||
id: Math.random().toString(36).substring(7),
|
||||
name: 'select',
|
||||
category: 'filters',
|
||||
type: 'bool',
|
||||
value: ['true'],
|
||||
operator: 'is'
|
||||
},
|
||||
{
|
||||
id: Math.random().toString(36).substring(7),
|
||||
name: 'label',
|
||||
category: 'filters',
|
||||
type: 'double',
|
||||
value: [1],
|
||||
operator: 'is'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: Math.random().toString(36).substring(7),
|
||||
name: 'Browser',
|
||||
category: 'filters',
|
||||
type: 'string',
|
||||
value: ['/client/account'],
|
||||
operator: 'is',
|
||||
isEvent: false
|
||||
}
|
||||
],
|
||||
groupByUser: false,
|
||||
sort: 'start',
|
||||
order: 'desc',
|
||||
viewed: false,
|
||||
eventsCount: 0,
|
||||
suspicious: false,
|
||||
consoleLevel: '',
|
||||
strict: true,
|
||||
eventsOrder: 'start'
|
||||
});
|
||||
savedSearch: ISavedSearch = new SavedSearch();
|
||||
filterSearchList: any = {};
|
||||
currentPage = 1;
|
||||
|
|
@ -284,11 +336,11 @@ class SearchStore {
|
|||
const index = filter.isEvent
|
||||
? -1
|
||||
: this.instance.filters.findIndex(
|
||||
(i: FilterItem) => i.key === filter.key
|
||||
(i: Filter) => i.id === filter.id
|
||||
);
|
||||
|
||||
// new random key
|
||||
filter.value = checkFilterValue(filter.value);
|
||||
filter.operator = 'is';
|
||||
filter.filters = filter.filters
|
||||
? filter.filters.map((subFilter: any) => ({
|
||||
...subFilter,
|
||||
|
|
@ -305,7 +357,7 @@ class SearchStore {
|
|||
oldFilter.merge(updatedFilter);
|
||||
this.updateFilter(index, updatedFilter);
|
||||
} else {
|
||||
filter.key = Math.random().toString(36).substring(7);
|
||||
// filter.key = Math.random().toString(36).substring(7);
|
||||
this.instance.filters.push(filter);
|
||||
this.instance = new Search({
|
||||
...this.instance.toData()
|
||||
|
|
@ -355,9 +407,9 @@ class SearchStore {
|
|||
this.instance = Object.assign(this.instance, search);
|
||||
};
|
||||
|
||||
updateFilter = (key: string, search: Partial<FilterItem>) => {
|
||||
updateFilter = (id: string, search: Partial<FilterItem>) => {
|
||||
const newFilters = this.instance.filters.map((f: any) => {
|
||||
if (f.key === key) {
|
||||
if (f.id === id) {
|
||||
return {
|
||||
...f,
|
||||
...search
|
||||
|
|
@ -372,9 +424,9 @@ class SearchStore {
|
|||
});
|
||||
};
|
||||
|
||||
removeFilter = (key: string) => {
|
||||
removeFilter = (id: string) => {
|
||||
const newFilters = this.instance.filters.filter(
|
||||
(f: any) => f.key !== key
|
||||
(f: any) => f.id !== id
|
||||
);
|
||||
|
||||
this.instance = new Search({
|
||||
|
|
|
|||
|
|
@ -1,12 +1,29 @@
|
|||
import { makeAutoObservable, runInAction, observable, action } from 'mobx';
|
||||
import { filtersMap, conditionalFiltersMap } from 'Types/filter/newFilter';
|
||||
import { action, makeAutoObservable, observable, runInAction } from 'mobx';
|
||||
import { conditionalFiltersMap, filtersMap } from 'Types/filter/newFilter';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import FilterItem from './filterItem';
|
||||
import { JsonData } from '@/mstore/types/filterConstants';
|
||||
|
||||
export const checkFilterValue = (value: any) =>
|
||||
Array.isArray(value) ? (value.length === 0 ? [''] : value) : [value];
|
||||
type FilterData = Partial<FilterItem> & {
|
||||
key?: FilterKey | string;
|
||||
value?: any;
|
||||
operator?: string;
|
||||
sourceOperator?: string;
|
||||
source?: any;
|
||||
filters?: FilterData[]
|
||||
};
|
||||
|
||||
export interface IFilter {
|
||||
export const checkFilterValue = (value: unknown): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0 ? [''] : value.map(val => String(val ?? ''));
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
return [''];
|
||||
}
|
||||
return [String(value)];
|
||||
};
|
||||
|
||||
export interface IFilterStore {
|
||||
filterId: string;
|
||||
name: string;
|
||||
filters: FilterItem[];
|
||||
|
|
@ -20,227 +37,356 @@ export interface IFilter {
|
|||
limit: number;
|
||||
autoOpen: boolean;
|
||||
|
||||
merge(filter: any): void;
|
||||
|
||||
addFilter(filter: any): void;
|
||||
|
||||
replaceFilters(filters: any): void;
|
||||
|
||||
updateFilter(index: number, filter: any): void;
|
||||
merge(filterData: Partial<FilterStore>): void;
|
||||
|
||||
updateKey(key: string, value: any): void;
|
||||
|
||||
removeFilter(index: number): void;
|
||||
addFilter(filterData: FilterData): void;
|
||||
|
||||
fromJson(json: any, isHeatmap?: boolean): IFilter;
|
||||
replaceFilters(newFilters: FilterItem[]): void;
|
||||
|
||||
fromData(data: any): IFilter;
|
||||
updateFilter(filterId: string, filterData: FilterData): void;
|
||||
|
||||
toJsonDrilldown(): any;
|
||||
removeFilter(filterId: string): void;
|
||||
|
||||
createFilterBykey(key: string): FilterItem;
|
||||
fromJson(json: JsonData, isHeatmap?: boolean): this;
|
||||
|
||||
toJson(): any;
|
||||
fromData(data: JsonData): this;
|
||||
|
||||
addExcludeFilter(filter: FilterItem): void;
|
||||
toJsonDrilldown(): JsonData;
|
||||
|
||||
updateExcludeFilter(index: number, filter: FilterItem): void;
|
||||
createFilterByKey(key: FilterKey | string): FilterItem;
|
||||
|
||||
removeExcludeFilter(index: number): void;
|
||||
toJson(): JsonData;
|
||||
|
||||
addExcludeFilter(filterData: FilterData): void;
|
||||
|
||||
updateExcludeFilter(filterId: string, filterData: FilterData): void;
|
||||
|
||||
removeExcludeFilter(filterId: string): void;
|
||||
|
||||
addFunnelDefaultFilters(): void;
|
||||
|
||||
toData(): any;
|
||||
addOrUpdateFilter(filterData: FilterData): void;
|
||||
|
||||
addOrUpdateFilter(filter: any): void;
|
||||
addFilterByKeyAndValue(
|
||||
key: FilterKey | string,
|
||||
value: unknown,
|
||||
operator?: string,
|
||||
sourceOperator?: string,
|
||||
source?: unknown
|
||||
): void;
|
||||
}
|
||||
|
||||
export default class Filter implements IFilter {
|
||||
public static get ID_KEY(): string {
|
||||
return 'filterId';
|
||||
}
|
||||
export default class FilterStore implements IFilterStore {
|
||||
public static readonly ID_KEY: string = 'filterId';
|
||||
|
||||
filterId: string = '';
|
||||
name: string = '';
|
||||
autoOpen = false;
|
||||
autoOpen: boolean = false;
|
||||
filters: FilterItem[] = [];
|
||||
excludes: FilterItem[] = [];
|
||||
eventsOrder: string = 'then';
|
||||
eventsOrderSupport: string[] = ['then', 'or', 'and'];
|
||||
readonly eventsOrderSupport: string[] = ['then', 'or', 'and'];
|
||||
startTimestamp: number = 0;
|
||||
endTimestamp: number = 0;
|
||||
eventsHeader: string = 'EVENTS';
|
||||
page: number = 1;
|
||||
limit: number = 10;
|
||||
|
||||
private readonly isConditional: boolean;
|
||||
private readonly isMobile: boolean;
|
||||
|
||||
constructor(
|
||||
filters: any[] = [],
|
||||
private readonly isConditional = false,
|
||||
private readonly isMobile = false
|
||||
initialFilters: FilterData[] = [],
|
||||
isConditional = false,
|
||||
isMobile = false
|
||||
) {
|
||||
this.isConditional = isConditional;
|
||||
this.isMobile = isMobile;
|
||||
this.filters = initialFilters.map(
|
||||
(filterData) => this.createFilterItemFromData(filterData)
|
||||
);
|
||||
|
||||
makeAutoObservable(this, {
|
||||
filters: observable,
|
||||
filters: observable.shallow,
|
||||
excludes: observable.shallow,
|
||||
eventsOrder: observable,
|
||||
startTimestamp: observable,
|
||||
endTimestamp: observable,
|
||||
|
||||
addFilter: action,
|
||||
removeFilter: action,
|
||||
updateKey: action,
|
||||
name: observable,
|
||||
page: observable,
|
||||
limit: observable,
|
||||
autoOpen: observable,
|
||||
filterId: observable,
|
||||
eventsHeader: observable,
|
||||
merge: action,
|
||||
addExcludeFilter: action,
|
||||
addFilter: action,
|
||||
replaceFilters: action,
|
||||
updateFilter: action,
|
||||
replaceFilters: action
|
||||
});
|
||||
this.filters = filters.map((i) => new FilterItem(i));
|
||||
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(filter: any) {
|
||||
merge(filterData: Partial<FilterStore>) {
|
||||
runInAction(() => {
|
||||
Object.assign(this, filter);
|
||||
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);
|
||||
}
|
||||
if (filterData.excludes) {
|
||||
this.excludes = filterData.excludes.map(f => f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addFilter(filter: any) {
|
||||
filter.value = [''];
|
||||
if (Array.isArray(filter.filters)) {
|
||||
filter.filters = filter.filters.map((i: Record<string, any>) => {
|
||||
i.value = [''];
|
||||
return new FilterItem(i);
|
||||
});
|
||||
}
|
||||
this.filters.push(new FilterItem(filter));
|
||||
}
|
||||
|
||||
replaceFilters(filters: any) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
updateFilter(index: number, filter: any) {
|
||||
this.filters[index] = new FilterItem(filter);
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
// @ts-ignore fix later
|
||||
// @ts-ignore
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
removeFilter(index: number) {
|
||||
this.filters.splice(index, 1);
|
||||
private createFilterItemFromData(filterData: FilterData): FilterItem {
|
||||
const dataWithValue = {
|
||||
...filterData,
|
||||
value: checkFilterValue(filterData.value)
|
||||
};
|
||||
if (Array.isArray(dataWithValue.filters)) {
|
||||
dataWithValue.filters = dataWithValue.filters.map(nestedFilter => this.createFilterItemFromData(nestedFilter));
|
||||
}
|
||||
return new FilterItem(dataWithValue);
|
||||
}
|
||||
|
||||
fromJson(json: any, isHeatmap?: boolean) {
|
||||
this.name = json.name;
|
||||
this.filters = json.filters.map((i: Record<string, any>) =>
|
||||
new FilterItem(undefined, this.isConditional, this.isMobile).fromJson(
|
||||
i,
|
||||
undefined,
|
||||
isHeatmap
|
||||
)
|
||||
);
|
||||
this.eventsOrder = json.eventsOrder;
|
||||
addFilter(filterData: FilterData) {
|
||||
const newFilter = this.createFilterItemFromData(filterData);
|
||||
this.filters.push(newFilter);
|
||||
}
|
||||
|
||||
replaceFilters(newFilters: FilterItem[]) {
|
||||
this.filters = newFilters.map(f => f);
|
||||
}
|
||||
|
||||
private updateFilterByIndex(index: number, filterData: FilterData) {
|
||||
if (index >= 0 && index < this.filters.length) {
|
||||
const originalId = this.filters[index].id;
|
||||
const updatedFilter = this.createFilterItemFromData(filterData);
|
||||
updatedFilter.id = originalId; // Ensure ID is not lost
|
||||
this.filters[index] = updatedFilter;
|
||||
} else {
|
||||
console.warn(`FilterStore.updateFilterByIndex: Invalid index ${index}`);
|
||||
}
|
||||
}
|
||||
|
||||
updateFilter(filterId: string, filterData: FilterData) {
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
|
||||
removeFilter(filterId: string) {
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
|
||||
fromJson(json: JsonData, isHeatmap?: boolean): this {
|
||||
runInAction(() => {
|
||||
this.name = json.name ?? '';
|
||||
this.filters = Array.isArray(json.filters)
|
||||
? json.filters.map((filterJson: JsonData) =>
|
||||
new FilterItem().fromJson(filterJson)
|
||||
)
|
||||
: [];
|
||||
this.excludes = Array.isArray(json.excludes)
|
||||
? json.excludes.map((filterJson: JsonData) =>
|
||||
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.filterId = json.filterId ?? '';
|
||||
this.eventsHeader = json.eventsHeader ?? 'EVENTS';
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
fromData(data: any) {
|
||||
this.name = data.name;
|
||||
this.filters = data.filters.map((i: Record<string, any>) =>
|
||||
new FilterItem(undefined, this.isConditional, this.isMobile).fromData(i)
|
||||
);
|
||||
this.eventsOrder = data.eventsOrder;
|
||||
fromData(data: JsonData): this {
|
||||
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)
|
||||
)
|
||||
: [];
|
||||
this.excludes = Array.isArray(data.excludes)
|
||||
? 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;
|
||||
this.endTimestamp = data.endTimestamp ?? 0;
|
||||
this.page = data.page ?? 1;
|
||||
this.limit = data.limit ?? 10;
|
||||
this.autoOpen = data.autoOpen ?? false;
|
||||
this.filterId = data.filterId ?? '';
|
||||
this.eventsHeader = data.eventsHeader ?? 'EVENTS';
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
toJsonDrilldown() {
|
||||
const json = {
|
||||
toJsonDrilldown(): JsonData {
|
||||
return {
|
||||
name: this.name,
|
||||
filters: this.filters.map((i) => i.toJson()),
|
||||
filters: this.filters.map((filterItem) => filterItem.toJson()),
|
||||
eventsOrder: this.eventsOrder,
|
||||
startTimestamp: this.startTimestamp,
|
||||
endTimestamp: this.endTimestamp
|
||||
};
|
||||
return json;
|
||||
}
|
||||
|
||||
createFilterBykey(key: string) {
|
||||
const usedMap = this.isConditional ? conditionalFiltersMap : filtersMap;
|
||||
return usedMap[key] ? new FilterItem(usedMap[key]) : new FilterItem();
|
||||
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: [''] };
|
||||
return this.createFilterItemFromData(newFilterData); // Use helper
|
||||
}
|
||||
|
||||
toJson() {
|
||||
const json = {
|
||||
toJson(): JsonData {
|
||||
return {
|
||||
name: this.name,
|
||||
filters: this.filters.map((i) => i.toJson()),
|
||||
eventsOrder: this.eventsOrder
|
||||
filterId: this.filterId,
|
||||
autoOpen: this.autoOpen,
|
||||
filters: this.filters.map((filterItem) => filterItem?.toJson()),
|
||||
excludes: this.excludes.map((filterItem) => filterItem?.toJson()),
|
||||
eventsOrder: this.eventsOrder,
|
||||
startTimestamp: this.startTimestamp,
|
||||
endTimestamp: this.endTimestamp,
|
||||
eventsHeader: this.eventsHeader,
|
||||
page: this.page,
|
||||
limit: this.limit
|
||||
};
|
||||
return json;
|
||||
}
|
||||
|
||||
addExcludeFilter(filter: FilterItem) {
|
||||
this.excludes.push(filter);
|
||||
addExcludeFilter(filterData: FilterData) {
|
||||
const newExclude = this.createFilterItemFromData(filterData);
|
||||
this.excludes.push(newExclude);
|
||||
}
|
||||
|
||||
updateExcludeFilter(index: number, filter: FilterItem) {
|
||||
this.excludes[index] = new FilterItem(filter);
|
||||
updateExcludeFilter(filterId: string, filterData: FilterData) {
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
|
||||
removeExcludeFilter(index: number) {
|
||||
this.excludes.splice(index, 1);
|
||||
removeExcludeFilter(filterId: string) {
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
|
||||
addFunnelDefaultFilters() {
|
||||
this.filters = [];
|
||||
this.addFilter({
|
||||
...filtersMap[FilterKey.LOCATION],
|
||||
value: [''],
|
||||
operator: 'isAny'
|
||||
});
|
||||
this.addFilter({
|
||||
...filtersMap[FilterKey.CLICK],
|
||||
value: [''],
|
||||
operator: 'onAny'
|
||||
runInAction(() => {
|
||||
this.filters = []; // Clear existing filters
|
||||
const locationFilterData = filtersMap[FilterKey.LOCATION];
|
||||
if (locationFilterData) {
|
||||
this.addFilter({
|
||||
...locationFilterData,
|
||||
value: [''],
|
||||
operator: 'isAny'
|
||||
});
|
||||
} else {
|
||||
console.warn(`FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.LOCATION}`);
|
||||
}
|
||||
|
||||
const clickFilterData = filtersMap[FilterKey.CLICK];
|
||||
if (clickFilterData) {
|
||||
this.addFilter({
|
||||
...clickFilterData,
|
||||
value: [''],
|
||||
operator: 'onAny'
|
||||
});
|
||||
} else {
|
||||
console.warn(`FilterStore.addFunnelDefaultFilters: Default filter not found for key ${FilterKey.CLICK}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toData() {
|
||||
return {
|
||||
name: this.name,
|
||||
filters: this.filters.map((i) => i.toJson()),
|
||||
eventsOrder: this.eventsOrder
|
||||
addOrUpdateFilter(filterData: FilterData) {
|
||||
const index = this.filters.findIndex((f) => f.key === filterData.key);
|
||||
const dataWithCheckedValue = {
|
||||
...filterData,
|
||||
value: checkFilterValue(filterData.value)
|
||||
};
|
||||
}
|
||||
|
||||
addOrUpdateFilter(filter: any) {
|
||||
const index = this.filters.findIndex((i) => i.key === filter.key);
|
||||
filter.value = checkFilterValue;
|
||||
|
||||
if (index > -1) {
|
||||
this.updateFilter(index, filter);
|
||||
this.updateFilterByIndex(index, dataWithCheckedValue);
|
||||
} else {
|
||||
this.addFilter(filter);
|
||||
this.addFilter(dataWithCheckedValue);
|
||||
}
|
||||
}
|
||||
|
||||
addFilterByKeyAndValue(
|
||||
key: any,
|
||||
value: any,
|
||||
operator: undefined,
|
||||
sourceOperator: undefined,
|
||||
source: undefined
|
||||
key: FilterKey | string,
|
||||
value: unknown,
|
||||
operator?: string,
|
||||
sourceOperator?: string,
|
||||
source?: unknown
|
||||
) {
|
||||
let defaultFilter = { ...filtersMap[key] };
|
||||
if (defaultFilter) {
|
||||
defaultFilter = { ...defaultFilter, value: checkFilterValue(value) };
|
||||
if (operator) {
|
||||
defaultFilter.operator = operator;
|
||||
}
|
||||
if (sourceOperator) {
|
||||
defaultFilter.sourceOperator = sourceOperator;
|
||||
}
|
||||
if (source) {
|
||||
defaultFilter.source = source;
|
||||
}
|
||||
this.addOrUpdateFilter(defaultFilter);
|
||||
const sourceMap = this.isConditional ? conditionalFiltersMap : filtersMap;
|
||||
const defaultFilterData = sourceMap[key as FilterKey];
|
||||
|
||||
if (defaultFilterData) {
|
||||
const newFilterData: FilterData = {
|
||||
...defaultFilterData,
|
||||
key: key,
|
||||
value: checkFilterValue(value),
|
||||
operator: operator ?? defaultFilterData.operator,
|
||||
sourceOperator: sourceOperator ?? defaultFilterData.sourceOperator,
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export type JsonData = Record<string, any>;
|
||||
|
||||
export interface Operator {
|
||||
value: string;
|
||||
label: string;
|
||||
|
|
@ -13,13 +15,13 @@ export interface FilterProperty {
|
|||
}
|
||||
|
||||
export interface Filter {
|
||||
id?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
possibleTypes?: string[];
|
||||
autoCaptured: boolean;
|
||||
metadataName: string;
|
||||
autoCaptured?: boolean;
|
||||
metadataName?: string;
|
||||
category: string; // 'event' | 'filter' | 'action' | etc.
|
||||
subCategory?: string;
|
||||
type?: string; // 'number' | 'string' | 'boolean' | etc.
|
||||
|
|
@ -30,81 +32,130 @@ export interface Filter {
|
|||
isEvent?: boolean;
|
||||
value?: string[];
|
||||
propertyOrder?: string;
|
||||
filters?: Filter[];
|
||||
}
|
||||
|
||||
export const OPERATORS = {
|
||||
string: [
|
||||
{ value: 'is', label: 'is', displayName: 'Is', description: 'Exact match' },
|
||||
{ value: 'isNot', label: 'isNot', displayName: 'Is not', description: 'Not an 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: 'doesNotContain',
|
||||
label: 'doesNotContain',
|
||||
label: 'does not contain',
|
||||
displayName: 'Does not contain',
|
||||
description: 'Does not contain the string'
|
||||
},
|
||||
{ value: 'startsWith', label: 'startsWith', displayName: 'Starts with', description: 'Starts with the string' },
|
||||
{ value: 'endsWith', label: 'endsWith', displayName: 'Ends with', description: 'Ends with the string' },
|
||||
{ value: 'isBlank', label: 'isBlank', displayName: 'Is blank', description: 'Is empty or null' },
|
||||
{ value: 'isNotBlank', label: 'isNotBlank', 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: 'doesNotEqual',
|
||||
label: 'doesNotEqual',
|
||||
label: 'does not equal', // Fixed: added space
|
||||
displayName: 'Does not equal',
|
||||
description: 'Does not equal the value'
|
||||
},
|
||||
{ value: 'greaterThan', label: 'greaterThan', displayName: 'Greater than', description: 'Greater than the value' },
|
||||
{ value: 'lessThan', label: 'lessThan', 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: 'greaterThanOrEquals',
|
||||
label: 'greater than or equals', // Fixed: added spaces and lowercased
|
||||
displayName: 'Greater than or equals',
|
||||
description: 'Greater than or equal to the value'
|
||||
},
|
||||
{
|
||||
value: 'lessThanOrEquals',
|
||||
label: 'lessThanOrEquals',
|
||||
label: 'less than or equals', // Fixed: added spaces and lowercased
|
||||
displayName: 'Less than or equals',
|
||||
description: 'Less than or equal to the value'
|
||||
},
|
||||
{ value: 'isBlank', label: 'isBlank', displayName: 'Is blank', description: 'Is empty or null' },
|
||||
{ value: 'isNotBlank', label: 'isNotBlank', displayName: 'Is not blank', description: 'Is not 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'
|
||||
}
|
||||
],
|
||||
|
||||
boolean: [
|
||||
{ value: 'isTrue', label: 'isTrue', displayName: 'Is true', description: 'Value is true' },
|
||||
{ value: 'isFalse', label: 'isFalse', displayName: 'Is false', description: 'Value is false' },
|
||||
{ value: 'isBlank', label: 'isBlank', displayName: 'Is blank', description: 'Is null' },
|
||||
{ value: 'isNotBlank', label: 'isNotBlank', displayName: 'Is not blank', description: 'Is not null' }
|
||||
{
|
||||
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: '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'
|
||||
}
|
||||
],
|
||||
|
||||
date: [
|
||||
{ value: 'on', label: 'on', displayName: 'On', description: 'On the exact date' },
|
||||
{ value: 'notOn', label: 'notOn', displayName: 'Not on', description: 'Not 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: 'onOrBefore', displayName: 'On or before', description: 'On or before the date' },
|
||||
{ value: 'onOrAfter', label: 'onOrAfter', displayName: 'On or after', description: 'On or after the date' },
|
||||
{ value: 'isBlank', label: 'isBlank', displayName: 'Is blank', description: 'Is empty or null' },
|
||||
{ value: 'isNotBlank', label: 'isNotBlank', displayName: 'Is not blank', description: 'Is not empty or null' }
|
||||
{
|
||||
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: 'doesNotContain',
|
||||
label: 'doesNotContain',
|
||||
label: 'does not contain', // Fixed: added spaces and lowercased
|
||||
displayName: 'Does not contain',
|
||||
description: 'Array does not contain the value'
|
||||
},
|
||||
{ value: 'hasAny', label: 'hasAny', displayName: 'Has any', description: 'Array has any of the values' },
|
||||
{ value: 'hasAll', label: 'hasAll', displayName: 'Has all', description: 'Array has all of the values' },
|
||||
{ value: 'isEmpty', label: 'isEmpty', displayName: 'Is empty', description: 'Array is empty' },
|
||||
{ value: 'isNotEmpty', label: 'isNotEmpty', displayName: 'Is not empty', description: 'Array is not empty' }
|
||||
{
|
||||
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: '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'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
|
@ -113,7 +164,7 @@ export const COMMON_FILTERS: Filter[] = [];
|
|||
export const getOperatorsByType = (type: string): Operator[] => {
|
||||
let operators: Operator[] = [];
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
switch (type?.toLowerCase()) {
|
||||
case 'string':
|
||||
operators = OPERATORS.string;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,38 +1,30 @@
|
|||
import { FilterCategory, FilterKey, FilterType } from 'Types/filter/filterType';
|
||||
import {
|
||||
conditionalFiltersMap,
|
||||
filtersMap,
|
||||
mobileConditionalFiltersMap
|
||||
} from 'Types/filter/newFilter';
|
||||
import { FilterCategory, FilterKey } from 'Types/filter/filterType';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import { FilterProperty, Operator } from '@/mstore/types/filterConstants';
|
||||
|
||||
import { pageUrlOperators } from '@/constants/filterOptions';
|
||||
type JsonData = Record<string, any>;
|
||||
|
||||
export default class FilterItem {
|
||||
type: string = '';
|
||||
category: FilterCategory = FilterCategory.METADATA;
|
||||
subCategory: string = '';
|
||||
key: string = '';
|
||||
label: string = '';
|
||||
value: any = [''];
|
||||
isEvent: boolean = false;
|
||||
operator: string = '';
|
||||
hasSource: boolean = false;
|
||||
source: string = '';
|
||||
sourceOperator: string = '';
|
||||
sourceOperatorOptions: any = [];
|
||||
filters: FilterItem[] = [];
|
||||
operatorOptions: any[] = [];
|
||||
options: any[] = [];
|
||||
isActive: boolean = true;
|
||||
completed: number = 0;
|
||||
dropped: number = 0;
|
||||
id: string = '';
|
||||
name: string = '';
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
possibleTypes?: string[];
|
||||
autoCaptured?: boolean;
|
||||
metadataName?: string;
|
||||
category: string; // 'event' | 'filter' | 'action' | etc.
|
||||
subCategory?: string;
|
||||
type?: string; // 'number' | 'string' | 'boolean' | etc.
|
||||
icon?: string;
|
||||
properties?: FilterProperty[];
|
||||
operator?: string;
|
||||
operators?: Operator[];
|
||||
isEvent?: boolean;
|
||||
value?: string[];
|
||||
propertyOrder?: string;
|
||||
filters?: FilterItem[];
|
||||
|
||||
constructor(
|
||||
data: any = {},
|
||||
private readonly isConditional?: boolean,
|
||||
private readonly isMobile?: boolean
|
||||
) {
|
||||
constructor(data: any = {}) {
|
||||
makeAutoObservable(this);
|
||||
|
||||
if (Array.isArray(data.filters)) {
|
||||
|
|
@ -40,6 +32,7 @@ export default class FilterItem {
|
|||
(i: Record<string, any>) => new FilterItem(i)
|
||||
);
|
||||
}
|
||||
data.operator = data.operator || 'is';
|
||||
|
||||
this.merge(data);
|
||||
}
|
||||
|
|
@ -58,99 +51,51 @@ export default class FilterItem {
|
|||
|
||||
fromData(data: any) {
|
||||
Object.assign(this, data);
|
||||
this.type = data.type;
|
||||
this.key = data.key;
|
||||
this.label = data.label;
|
||||
this.operatorOptions = data.operatorOptions;
|
||||
this.hasSource = data.hasSource;
|
||||
this.type = 'string';
|
||||
this.name = data.type;
|
||||
this.category = data.category;
|
||||
this.subCategory = data.subCategory;
|
||||
this.sourceOperatorOptions = data.sourceOperatorOptions;
|
||||
this.value = data.value;
|
||||
this.isEvent = Boolean(data.isEvent);
|
||||
this.operator = data.operator;
|
||||
this.source = data.source;
|
||||
this.sourceOperator = data.sourceOperator;
|
||||
this.filters = data.filters;
|
||||
this.isActive = Boolean(data.isActive);
|
||||
this.completed = data.completed;
|
||||
this.dropped = data.dropped;
|
||||
this.options = data.options;
|
||||
this.filters = data.filters.map((i: JsonData) => new FilterItem(i));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fromJson(json: any, mainFilterKey = '', isHeatmap?: boolean) {
|
||||
const isMetadata = json.type === FilterKey.METADATA;
|
||||
let _filter: any =
|
||||
(isMetadata ? filtersMap[`_${json.source}`] : filtersMap[json.type]) ||
|
||||
{};
|
||||
if (this.isConditional) {
|
||||
if (this.isMobile) {
|
||||
_filter =
|
||||
mobileConditionalFiltersMap[_filter.key] ||
|
||||
mobileConditionalFiltersMap[_filter.source];
|
||||
} else {
|
||||
_filter =
|
||||
conditionalFiltersMap[_filter.key] ||
|
||||
conditionalFiltersMap[_filter.source];
|
||||
}
|
||||
}
|
||||
if (mainFilterKey) {
|
||||
const mainFilter = filtersMap[mainFilterKey];
|
||||
const subFilterMap = {};
|
||||
mainFilter.filters.forEach((option: any) => {
|
||||
// @ts-ignore
|
||||
subFilterMap[option.key] = option;
|
||||
});
|
||||
// @ts-ignore
|
||||
_filter = subFilterMap[json.type];
|
||||
}
|
||||
this.type = _filter.type;
|
||||
this.key = _filter.key;
|
||||
this.label = _filter.label;
|
||||
this.operatorOptions = _filter.operatorOptions;
|
||||
this.hasSource = _filter.hasSource;
|
||||
this.category = _filter.category;
|
||||
this.subCategory = _filter.subCategory;
|
||||
this.sourceOperatorOptions = _filter.sourceOperatorOptions;
|
||||
if (isHeatmap && this.key === FilterKey.LOCATION) {
|
||||
this.operatorOptions = pageUrlOperators;
|
||||
}
|
||||
this.options = _filter.options;
|
||||
this.isEvent = Boolean(_filter.isEvent);
|
||||
|
||||
this.value = !json.value || json.value.length === 0 ? [''] : json.value;
|
||||
this.operator = json.operator;
|
||||
this.source = isMetadata ? `_${json.source}` : json.source;
|
||||
this.sourceOperator = json.sourceOperator;
|
||||
|
||||
this.filters =
|
||||
_filter.type === FilterType.SUB_FILTERS && json.filters
|
||||
? json.filters.map((i: any) => new FilterItem().fromJson(i, json.type))
|
||||
: [];
|
||||
|
||||
this.completed = json.completed;
|
||||
this.dropped = json.dropped;
|
||||
fromJson(data: JsonData) {
|
||||
this.type = 'string';
|
||||
this.name = data.type;
|
||||
this.category = data.category;
|
||||
this.subCategory = data.subCategory;
|
||||
this.operator = data.operator;
|
||||
this.value = data.value || [''];
|
||||
this.filters = data.filters.map((i: JsonData) => new FilterItem(i));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toJson(): any {
|
||||
const isMetadata = this.category === FilterCategory.METADATA;
|
||||
const json = {
|
||||
type: isMetadata ? FilterKey.METADATA : this.key,
|
||||
const json: any = {
|
||||
type: this.name,
|
||||
isEvent: Boolean(this.isEvent),
|
||||
value: this.value?.map((i: any) => (i ? i.toString() : '')) || [],
|
||||
operator: this.operator,
|
||||
source: isMetadata ? this.key.replace(/^_/, '') : this.source,
|
||||
sourceOperator: this.sourceOperator,
|
||||
source: this.name,
|
||||
filters: Array.isArray(this.filters)
|
||||
? this.filters.map((i) => i.toJson())
|
||||
: []
|
||||
};
|
||||
if (this.type === FilterKey.DURATION) {
|
||||
json.value = this.value.map((i: any) => (!i ? 0 : i));
|
||||
|
||||
const isMetadata = this.category === FilterCategory.METADATA;
|
||||
if (isMetadata) {
|
||||
json.type = FilterKey.METADATA;
|
||||
json.source = this.name;
|
||||
json.sourceOperator = this.operator;
|
||||
}
|
||||
|
||||
if (this.type === FilterKey.DURATION) {
|
||||
json.value = this.value?.map((i: any) => (!i ? 0 : i));
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// import Filter from 'Types/filter';
|
||||
import { makeAutoObservable, observable, action } from 'mobx';
|
||||
import Filter from './filter';
|
||||
import FilterStore from './filter';
|
||||
import { JsonData } from '@/mstore/types/filterConstants';
|
||||
|
||||
export default class FilterSeries {
|
||||
public static get ID_KEY(): string {
|
||||
|
|
@ -8,38 +8,37 @@ export default class FilterSeries {
|
|||
}
|
||||
|
||||
seriesId?: any = undefined;
|
||||
|
||||
name: string = 'Series 1';
|
||||
|
||||
filter: Filter = new Filter();
|
||||
filter: FilterStore = new FilterStore();
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
name: observable,
|
||||
filter: observable,
|
||||
filter: observable.shallow,
|
||||
|
||||
update: action,
|
||||
update: action
|
||||
});
|
||||
}
|
||||
|
||||
update(key, value) {
|
||||
update(key: any, value: any) {
|
||||
// @ts-ignore
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
fromJson(json, isHeatmap = false) {
|
||||
fromJson(json: JsonData, isHeatmap = false) {
|
||||
this.seriesId = json.seriesId;
|
||||
this.name = json.name;
|
||||
this.filter = new Filter().fromJson(
|
||||
this.filter = new FilterStore().fromJson(
|
||||
json.filter || { filters: [] },
|
||||
isHeatmap,
|
||||
isHeatmap
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
fromData(data) {
|
||||
fromData(data: any) {
|
||||
this.seriesId = data.seriesId;
|
||||
this.name = data.name;
|
||||
this.filter = new Filter().fromData(data.filter);
|
||||
this.filter = new FilterStore().fromData(data.filter);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +46,7 @@ export default class FilterSeries {
|
|||
return {
|
||||
seriesId: this.seriesId,
|
||||
name: this.name,
|
||||
filter: this.filter.toJson(),
|
||||
filter: this.filter.toJson()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import {
|
|||
DATE_RANGE_VALUES,
|
||||
getDateRangeFromValue
|
||||
} from 'App/dateRange';
|
||||
import Filter, { IFilter } from 'App/mstore/types/filter';
|
||||
import FilterItem from 'App/mstore/types/filterItem';
|
||||
import { makeAutoObservable, observable } from 'mobx';
|
||||
import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period';
|
||||
import { roundToNextMinutes } from '@/utils';
|
||||
// import { Filter } from '@/mstore/types/filterConstants';
|
||||
|
||||
// @ts-ignore
|
||||
const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS;
|
||||
|
|
@ -25,7 +25,7 @@ interface ISearch {
|
|||
userDevice?: string;
|
||||
fid0?: string;
|
||||
events: Event[];
|
||||
filters: FilterItem[];
|
||||
filters: Filter[];
|
||||
minDuration?: number;
|
||||
maxDuration?: number;
|
||||
custom: Record<string, any>;
|
||||
|
|
@ -54,7 +54,7 @@ export default class Search {
|
|||
userDevice?: string;
|
||||
fid0?: string;
|
||||
events: Event[];
|
||||
filters: FilterItem[];
|
||||
filters: Filter[];
|
||||
minDuration?: number;
|
||||
maxDuration?: number;
|
||||
custom: Record<string, any>;
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.img-crcle {
|
||||
.img-circle {
|
||||
border-radius: 50%;
|
||||
box-shadow: 1px 1px 1px 1px rgba(0, 0, 0 0.3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,11 +182,13 @@ export enum IssueCategory {
|
|||
|
||||
export enum FilterType {
|
||||
STRING = 'string',
|
||||
NUMBER = 'number',
|
||||
DOUBLE = 'double',
|
||||
INTEGER = 'int',
|
||||
ISSUE = 'ISSUE',
|
||||
BOOLEAN = 'BOOLEAN',
|
||||
NUMBER = 'NUMBER',
|
||||
BOOLEAN = 'bool',
|
||||
NUMBER_MULTIPLE = 'NUMBER_MULTIPLE',
|
||||
DURATION = 'DURATION',
|
||||
DURATION = 'duration',
|
||||
MULTIPLE = 'MULTIPLE',
|
||||
SUB_FILTERS = 'SUB_FILTERS',
|
||||
COUNTRY = 'COUNTRY',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue