|
|
|
@@ -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"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|