feat: add 'notTriggeredWithin' operator to segment filters for enhanced event tracking
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemWithDescription,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@plunk/ui';
|
||||
@@ -16,26 +17,27 @@ import {Check, ChevronsUpDown, GripVertical, Plus, Search, Trash2} from 'lucide-
|
||||
import {memo, useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {network} from '../lib/network';
|
||||
|
||||
const STANDARD_OPERATORS: {value: SegmentFilterOperator; label: string}[] = [
|
||||
{value: 'equals', label: 'Equals'},
|
||||
{value: 'notEquals', label: 'Not equals'},
|
||||
{value: 'contains', label: 'Contains'},
|
||||
{value: 'notContains', label: 'Does not contain'},
|
||||
{value: 'greaterThan', label: 'Greater than'},
|
||||
{value: 'lessThan', label: 'Less than'},
|
||||
{value: 'greaterThanOrEqual', label: 'Greater than or equal to'},
|
||||
{value: 'lessThanOrEqual', label: 'Less than or equal to'},
|
||||
{value: 'exists', label: 'Exists'},
|
||||
{value: 'notExists', label: 'Does not exist'},
|
||||
{value: 'within', label: 'Younger than (time)'},
|
||||
{value: 'olderThan', label: 'Older than (time)'},
|
||||
const STANDARD_OPERATORS: {value: SegmentFilterOperator; label: string; description: string}[] = [
|
||||
{value: 'equals', label: 'Equals', description: 'Exact match'},
|
||||
{value: 'notEquals', label: 'Not equals', description: 'Anything other than this value'},
|
||||
{value: 'contains', label: 'Contains', description: 'Value includes this text'},
|
||||
{value: 'notContains', label: 'Does not contain', description: 'Value does not include this text'},
|
||||
{value: 'greaterThan', label: 'Greater than', description: 'Value is higher than'},
|
||||
{value: 'lessThan', label: 'Less than', description: 'Value is lower than'},
|
||||
{value: 'greaterThanOrEqual', label: 'Greater than or equal', description: 'Value is at least'},
|
||||
{value: 'lessThanOrEqual', label: 'Less than or equal', description: 'Value is at most'},
|
||||
{value: 'exists', label: 'Has a value', description: 'Field is set to anything'},
|
||||
{value: 'notExists', label: 'Has no value', description: 'Field is empty or unset'},
|
||||
{value: 'within', label: 'Less than X ago', description: 'Date is within the last X days/hours'},
|
||||
{value: 'olderThan', label: 'More than X ago', description: 'Date is older than X days/hours'},
|
||||
];
|
||||
|
||||
const EVENT_OPERATORS: {value: SegmentFilterOperator; label: string}[] = [
|
||||
{value: 'triggered', label: 'Ever occurred'},
|
||||
{value: 'triggeredWithin', label: 'Occurred within'},
|
||||
{value: 'triggeredOlderThan', label: 'Occurred over (time) ago'},
|
||||
{value: 'notTriggered', label: 'Never occurred'},
|
||||
const EVENT_OPERATORS: {value: SegmentFilterOperator; label: string; description: string}[] = [
|
||||
{value: 'triggered', label: 'Ever occurred', description: 'This event has happened at least once'},
|
||||
{value: 'triggeredWithin', label: 'Occurred within', description: 'Happened at least once in the last X days/hours'},
|
||||
{value: 'triggeredOlderThan', label: 'Occurred, but not recently', description: 'Has happened before, but not in the last X days/hours'},
|
||||
{value: 'notTriggered', label: 'Never occurred', description: 'This event has never happened'},
|
||||
{value: 'notTriggeredWithin', label: 'Not occurred within', description: 'Has not happened in the last X days/hours — includes contacts who never triggered this'},
|
||||
];
|
||||
|
||||
const TIME_UNITS = [
|
||||
@@ -186,7 +188,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
}, []);
|
||||
|
||||
const needsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(filter.operator);
|
||||
const needsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(filter.operator);
|
||||
const needsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan', 'notTriggeredWithin'].includes(filter.operator);
|
||||
|
||||
// Get field type from available fields
|
||||
const fieldOption = useMemo(
|
||||
@@ -236,7 +238,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
const selectedField = availableFields.find(f => f.value === value);
|
||||
const newFieldType = selectedField?.type || 'string';
|
||||
const isEvent = newFieldType === 'event' || newFieldType === 'email';
|
||||
const currentOperatorIsEvent = ['triggered', 'triggeredWithin', 'triggeredOlderThan', 'notTriggered'].includes(
|
||||
const currentOperatorIsEvent = ['triggered', 'triggeredWithin', 'triggeredOlderThan', 'notTriggered', 'notTriggeredWithin'].includes(
|
||||
filter.operator,
|
||||
);
|
||||
|
||||
@@ -273,7 +275,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
newUnit = undefined;
|
||||
|
||||
// If the new operator doesn't support units but we had them, ensure value is appropriate
|
||||
const newOperatorNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(
|
||||
const newOperatorNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan', 'notTriggeredWithin'].includes(
|
||||
newOperator,
|
||||
);
|
||||
if (!newOperatorNeedsUnit) {
|
||||
@@ -407,10 +409,10 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
// Check if we're switching between operators that need different value types
|
||||
const oldNeedsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(oldOperator);
|
||||
const newNeedsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(newOperator);
|
||||
const oldNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(
|
||||
const oldNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan', 'notTriggeredWithin'].includes(
|
||||
oldOperator,
|
||||
);
|
||||
const newNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(
|
||||
const newNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan', 'notTriggeredWithin'].includes(
|
||||
newOperator,
|
||||
);
|
||||
|
||||
@@ -426,28 +428,29 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
updatedFilter.unit = undefined;
|
||||
} else if (newNeedsUnit && !oldNeedsUnit) {
|
||||
updatedFilter.unit = 'days';
|
||||
// Set default numeric value if needed
|
||||
if (typeof updatedFilter.value !== 'number') {
|
||||
updatedFilter.value = 7;
|
||||
}
|
||||
updatedFilter.value = typeof updatedFilter.value === 'number' && updatedFilter.value > 0 ? updatedFilter.value : 7;
|
||||
}
|
||||
|
||||
// If switching to an operator that needs a value but we don't have one, set default
|
||||
if (newNeedsValue && !oldNeedsValue) {
|
||||
// Skip when entering a unit-based operator — it already set the value above
|
||||
if (newNeedsValue && !oldNeedsValue && !newNeedsUnit) {
|
||||
updatedFilter.value = getDefaultValueForType(fieldType);
|
||||
}
|
||||
|
||||
onChange(updatedFilter);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectTrigger className="text-sm h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{operators.map(op => (
|
||||
<SelectItem key={op.value} value={op.value}>
|
||||
{op.label}
|
||||
</SelectItem>
|
||||
<SelectItemWithDescription
|
||||
key={op.value}
|
||||
value={op.value}
|
||||
title={op.label}
|
||||
description={op.description}
|
||||
/>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -466,14 +469,14 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
type="number"
|
||||
value={filter.value as number}
|
||||
onChange={e => onChange({...filter, value: parseInt(e.target.value) || 0})}
|
||||
className="text-sm flex-1"
|
||||
className="text-sm flex-1 bg-white"
|
||||
min="1"
|
||||
/>
|
||||
<Select
|
||||
value={filter.unit || 'days'}
|
||||
onValueChange={(v: 'days' | 'hours' | 'minutes') => onChange({...filter, unit: v})}
|
||||
>
|
||||
<SelectTrigger className="text-sm w-[110px]">
|
||||
<SelectTrigger className="text-sm h-9 w-[110px] bg-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -490,7 +493,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
value={String(filter.value ?? 'true')}
|
||||
onValueChange={v => onChange({...filter, value: v === 'true'})}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectTrigger className="text-sm h-9 bg-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -506,7 +509,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
const val = e.target.value;
|
||||
onChange({...filter, value: val === '' ? 0 : parseFloat(val) || 0});
|
||||
}}
|
||||
className="text-sm"
|
||||
className="text-sm bg-white"
|
||||
placeholder="Enter number"
|
||||
/>
|
||||
) : fieldType === 'date' ? (
|
||||
@@ -514,14 +517,14 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
type="date"
|
||||
value={filter.value ? String(filter.value).split('T')[0] : ''}
|
||||
onChange={e => onChange({...filter, value: e.target.value})}
|
||||
className="text-sm"
|
||||
className="text-sm bg-white"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
type="text"
|
||||
value={String(filter.value ?? '')}
|
||||
onChange={e => onChange({...filter, value: e.target.value})}
|
||||
className="text-sm"
|
||||
className="text-sm bg-white"
|
||||
placeholder="Enter value"
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user