diff --git a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx
index b80a456cb..a59072905 100644
--- a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx
+++ b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx
@@ -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(
);
- },
+ }
);
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
+ }
)}
>
@@ -106,7 +111,7 @@ const FilterSeriesHeader = observer(
);
- },
+ }
);
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 (
{canExclude &&
}
@@ -216,33 +229,79 @@ function FilterSeries(props: Props) {
{expanded ? (
<>
{removeEvents ? null : (
-
+
+
f.isEvent).length > 0}
+ orderProps={{
+ eventsOrder: series.filter.eventsOrder,
+ eventsOrderSupport: ['then', 'and', 'or']
+ }}
+ onChangeOrder={onChangeEventsOrder}
+ filterSelection={
+ {
+ onAddFilter(newFilter);
+ }}
+ >
+
+
+ }
+ />
+
+ f.isEvent)}
+ isDraggable={true}
+ showIndices={true}
+ className="mt-2"
+ handleRemove={onRemoveFilter}
+ handleUpdate={onUpdateFilter}
+ handleAdd={onAddFilter}
+ handleMove={onFilterMove}
+ />
+
+
+
+ !f.isEvent).length > 0}
+ filterSelection={
+ {
+ onAddFilter(newFilter);
+ }}
+ >
+
+
+ }
+ />
+
+ !f.isEvent)}
+ isDraggable={false}
+ showIndices={false}
+ className="mt-2"
+ handleRemove={onRemoveFilter}
+ handleUpdate={onUpdateFilter}
+ handleAdd={onAddFilter}
+ handleMove={onFilterMove}
+ />
+
)}
-
>
) : null}
diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
index 4181a6198..68b9723fc 100644
--- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
+++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
@@ -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 (
{/* Use items-start */}
-
{!isSubItem && !hideIndex && filterIndex !== undefined && filterIndex >= 0 && (
{/* Align index top */}
@@ -203,20 +234,20 @@ function FilterItem(props: Props) {
{isSubItem && (
+ className="flex-shrink-0 w-14 text-right text-neutral-500/90 pr-2">
{subFilterIndex === 0 && (
where
)}
- {subFilterIndex !== 0 && propertyOrder && onToggleOperator && (
+ {subFilterIndex !== 0 && propertyOrder && onPropertyOrderChange && (
- !readonly && onToggleOperator(propertyOrder === 'and' ? 'or' : 'and')
+ !readonly && onPropertyOrderChange(propertyOrder === 'and' ? 'or' : 'and')
}
>
{propertyOrder}
@@ -227,7 +258,7 @@ function FilterItem(props: Props) {
{/* Main content area */}
+ className="flex flex-grow flex-wrap gap-x-2 items-center">
+
{categoryPart}
)}
@@ -263,7 +294,7 @@ function FilterItem(props: Props) {
•
)}
-
+
{hasName ? namePart : (hasCategory ? '' : defaultText)} {/* Show name or placeholder */}
@@ -301,7 +332,7 @@ function FilterItem(props: Props) {
{canShowValues &&
(readonly ? (
{filter.value
@@ -359,20 +390,20 @@ function FilterItem(props: Props) {
{filteredSubFilters.length > 0 && (
{/* Dashed line */}
{filteredSubFilters.map((subFilter: any, index: number) => (
))}
diff --git a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx
index e3213a74f..6d8269955 100644
--- a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx
+++ b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx
@@ -252,7 +252,10 @@ export const EventsList = observer((props: Props) => {
{!hideEventsOrder && (
-
+
)}
{actions &&
actions.map((action, index) =>
{action}
)}
diff --git a/frontend/app/components/shared/Filters/FilterList/UnifiedFilterList.tsx b/frontend/app/components/shared/Filters/FilterList/UnifiedFilterList.tsx
index e8722438e..6c7f61b31 100644
--- a/frontend/app/components/shared/Filters/FilterList/UnifiedFilterList.tsx
+++ b/frontend/app/components/shared/Filters/FilterList/UnifiedFilterList.tsx
@@ -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 ? (
-
+
{filters.map((filterItem: any, filterIndex: number) => (
{
{isDraggable && filters.length > 1 && (
@@ -186,22 +176,26 @@ const UnifiedFilterList = (props: UnifiedFilterListProps) => {
)}
{!isDraggable && showIndices &&
-
} {/* Placeholder for alignment if not draggable but indices shown */}
+
}
{!isDraggable && !showIndices &&
-
} {/* Placeholder for alignment if not draggable and no indices */}
+
}
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}
/>
diff --git a/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx
index dc066f183..113e6e8ea 100644
--- a/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx
+++ b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx
@@ -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 :
+ {placeholder};
- // Handler for menu item clicks
const handleMenuClick = (e: { key: string }) => {
let selectedValue: string | number | undefined;
if (e.key === CLEAR_VALUE_KEY) {
- // Handle the clear action
selectedValue = undefined;
} else {
- // Find the option corresponding to the key (which we set as the value)
- // Antd Menu keys are strings, so convert value to string for comparison/lookup if needed
const clickedOption = options.find(option => String(option.value) === e.key);
selectedValue = clickedOption?.value;
}
- // Call the original onChange prop with the expected structure
onChange(null, { name: name, value: selectedValue });
};
- // Construct the menu items
const menu = (