@startsimpli/funnels 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/client.test.ts +76 -76
- package/src/api/client.ts +16 -16
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +24 -24
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +39 -39
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +1 -1
- package/src/components/FilterRuleEditor/RuleRow.tsx +8 -8
- package/src/components/FunnelCard/FunnelCard.stories.tsx +118 -118
- package/src/components/FunnelCard/FunnelCard.test.ts +48 -48
- package/src/components/FunnelCard/FunnelCard.test.tsx +40 -40
- package/src/components/FunnelCard/FunnelCard.tsx +3 -3
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +63 -63
- package/src/components/FunnelPreview/FunnelPreview.tsx +11 -11
- package/src/components/FunnelPreview/StageBreakdown.tsx +5 -5
- package/src/components/FunnelPreview/example.tsx +43 -43
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +70 -70
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +63 -63
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +12 -12
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +5 -5
- package/src/components/FunnelRunHistory/RunFilters.tsx +8 -8
- package/src/components/FunnelRunHistory/RunRow.tsx +7 -7
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +9 -9
- package/src/components/FunnelRunHistory/types.ts +6 -6
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +41 -41
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +6 -6
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +5 -5
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +7 -7
- package/src/components/FunnelStageBuilder/StageActions.tsx +4 -4
- package/src/components/FunnelStageBuilder/StageCard.tsx +3 -3
- package/src/components/FunnelStageBuilder/StageForm.tsx +14 -14
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +42 -42
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +31 -31
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +4 -4
- package/src/components/FunnelVisualFlow/StageNode.tsx +8 -8
- package/src/components/FunnelVisualFlow/example.tsx +7 -7
- package/src/core/engine.test.ts +271 -271
- package/src/core/engine.ts +60 -60
- package/src/core/evaluator.example.ts +31 -31
- package/src/core/evaluator.test.ts +92 -92
- package/src/core/evaluator.ts +23 -23
- package/src/core/field-resolver.example.ts +1 -1
- package/src/core/field-resolver.test.ts +4 -4
- package/src/store/create-funnel-store.test.ts +32 -32
- package/src/store/create-funnel-store.ts +3 -3
- package/src/store/types.ts +2 -2
- package/src/types/index.ts +64 -64
|
@@ -43,7 +43,7 @@ export function RunRow({
|
|
|
43
43
|
}: RunRowProps) {
|
|
44
44
|
const matchRate =
|
|
45
45
|
run.status === 'completed'
|
|
46
|
-
? calculateMatchRate(run.
|
|
46
|
+
? calculateMatchRate(run.totalMatched, run.totalInput)
|
|
47
47
|
: null;
|
|
48
48
|
|
|
49
49
|
return (
|
|
@@ -61,9 +61,9 @@ export function RunRow({
|
|
|
61
61
|
<td className="px-4 py-3">
|
|
62
62
|
<span
|
|
63
63
|
className="text-sm text-gray-900"
|
|
64
|
-
title={formatFullTimestamp(run.
|
|
64
|
+
title={formatFullTimestamp(run.startedAt)}
|
|
65
65
|
>
|
|
66
|
-
{formatRelativeTime(run.
|
|
66
|
+
{formatRelativeTime(run.startedAt)}
|
|
67
67
|
</span>
|
|
68
68
|
</td>
|
|
69
69
|
|
|
@@ -75,28 +75,28 @@ export function RunRow({
|
|
|
75
75
|
{/* Trigger */}
|
|
76
76
|
<td className="px-4 py-3">
|
|
77
77
|
<span className="text-sm text-gray-700 capitalize">
|
|
78
|
-
{run.
|
|
78
|
+
{run.triggerType}
|
|
79
79
|
</span>
|
|
80
80
|
</td>
|
|
81
81
|
|
|
82
82
|
{/* Duration */}
|
|
83
83
|
<td className="px-4 py-3">
|
|
84
84
|
<span className="text-sm text-gray-900">
|
|
85
|
-
{formatDuration(run.
|
|
85
|
+
{formatDuration(run.durationMs)}
|
|
86
86
|
</span>
|
|
87
87
|
</td>
|
|
88
88
|
|
|
89
89
|
{/* Input */}
|
|
90
90
|
<td className="px-4 py-3 text-right">
|
|
91
91
|
<span className="text-sm font-medium text-gray-900">
|
|
92
|
-
{formatNumber(run.
|
|
92
|
+
{formatNumber(run.totalInput)}
|
|
93
93
|
</span>
|
|
94
94
|
</td>
|
|
95
95
|
|
|
96
96
|
{/* Matched */}
|
|
97
97
|
<td className="px-4 py-3 text-right">
|
|
98
98
|
<span className="text-sm font-medium text-green-600">
|
|
99
|
-
{run.status === 'completed' ? formatNumber(run.
|
|
99
|
+
{run.status === 'completed' ? formatNumber(run.totalMatched) : '-'}
|
|
100
100
|
</span>
|
|
101
101
|
</td>
|
|
102
102
|
|
|
@@ -30,15 +30,15 @@ export function StageBreakdownList({
|
|
|
30
30
|
return (
|
|
31
31
|
<div className={`space-y-3 ${className}`}>
|
|
32
32
|
{stages.map((stage, index) => {
|
|
33
|
-
const delta = stage.
|
|
33
|
+
const delta = stage.matchedCount - stage.inputCount;
|
|
34
34
|
const matchRate =
|
|
35
|
-
stage.
|
|
36
|
-
? Math.round((stage.
|
|
35
|
+
stage.inputCount > 0
|
|
36
|
+
? Math.round((stage.matchedCount / stage.inputCount) * 100)
|
|
37
37
|
: 0;
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
<div
|
|
41
|
-
key={stage.
|
|
41
|
+
key={stage.stageId}
|
|
42
42
|
className="p-3 bg-gray-50 rounded-lg border border-gray-200"
|
|
43
43
|
>
|
|
44
44
|
{/* Stage Header */}
|
|
@@ -47,7 +47,7 @@ export function StageBreakdownList({
|
|
|
47
47
|
{index + 1}
|
|
48
48
|
</span>
|
|
49
49
|
<h4 className="text-sm font-semibold text-gray-900">
|
|
50
|
-
{stage.
|
|
50
|
+
{stage.stageName}
|
|
51
51
|
</h4>
|
|
52
52
|
</div>
|
|
53
53
|
|
|
@@ -57,7 +57,7 @@ export function StageBreakdownList({
|
|
|
57
57
|
<div>
|
|
58
58
|
<div className="text-xs text-gray-600 mb-1">Input</div>
|
|
59
59
|
<div className="text-lg font-bold text-blue-600">
|
|
60
|
-
{formatNumber(stage.
|
|
60
|
+
{formatNumber(stage.inputCount)}
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
63
63
|
|
|
@@ -65,7 +65,7 @@ export function StageBreakdownList({
|
|
|
65
65
|
<div>
|
|
66
66
|
<div className="text-xs text-gray-600 mb-1">Matched</div>
|
|
67
67
|
<div className="text-lg font-bold text-green-600">
|
|
68
|
-
{formatNumber(stage.
|
|
68
|
+
{formatNumber(stage.matchedCount)}
|
|
69
69
|
</div>
|
|
70
70
|
</div>
|
|
71
71
|
|
|
@@ -95,10 +95,10 @@ export function StageBreakdownList({
|
|
|
95
95
|
)}
|
|
96
96
|
|
|
97
97
|
{/* Error Count */}
|
|
98
|
-
{stage.
|
|
98
|
+
{stage.errorCount && stage.errorCount > 0 && (
|
|
99
99
|
<div className="mt-2 pt-2 border-t border-gray-200">
|
|
100
100
|
<div className="text-sm text-red-600 text-center">
|
|
101
|
-
⚠ {formatNumber(stage.
|
|
101
|
+
⚠ {formatNumber(stage.errorCount)} errors
|
|
102
102
|
</div>
|
|
103
103
|
</div>
|
|
104
104
|
)}
|
|
@@ -11,17 +11,17 @@ import { FunnelRun, FunnelRunStatus, TriggerType } from '../../types';
|
|
|
11
11
|
*/
|
|
12
12
|
export interface RunFilters {
|
|
13
13
|
status?: FunnelRunStatus | 'all';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
triggerType?: TriggerType | 'all';
|
|
15
|
+
dateRange?: 'all' | 'today' | 'week' | 'month' | 'custom';
|
|
16
|
+
startDate?: Date;
|
|
17
|
+
endDate?: Date;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Sort configuration for run history
|
|
22
22
|
*/
|
|
23
23
|
export interface RunSort {
|
|
24
|
-
field: '
|
|
24
|
+
field: 'startedAt' | 'status' | 'matchRate' | 'durationMs';
|
|
25
25
|
direction: 'asc' | 'desc';
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -30,7 +30,7 @@ export interface RunSort {
|
|
|
30
30
|
*/
|
|
31
31
|
export interface Pagination {
|
|
32
32
|
page: number;
|
|
33
|
-
|
|
33
|
+
pageSize: number;
|
|
34
34
|
total: number;
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -95,10 +95,10 @@ function createEmptyFunnel(): Funnel {
|
|
|
95
95
|
name: 'VC Outreach Funnel',
|
|
96
96
|
description: 'Filter and prioritize venture capital firms for outreach',
|
|
97
97
|
status: 'draft',
|
|
98
|
-
|
|
98
|
+
inputType: 'both',
|
|
99
99
|
stages: [],
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
createdAt: new Date().toISOString(),
|
|
101
|
+
updatedAt: new Date().toISOString(),
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -109,55 +109,55 @@ function createSampleFunnel(): Funnel {
|
|
|
109
109
|
name: 'VC Outreach Funnel',
|
|
110
110
|
description: 'Filter and prioritize venture capital firms for outreach',
|
|
111
111
|
status: 'draft',
|
|
112
|
-
|
|
112
|
+
inputType: 'both',
|
|
113
113
|
stages: [
|
|
114
114
|
{
|
|
115
115
|
id: 'stage-1',
|
|
116
116
|
order: 0,
|
|
117
117
|
name: 'High ICP Score',
|
|
118
118
|
description: 'Firms with strong ideal customer profile match',
|
|
119
|
-
|
|
119
|
+
filterLogic: 'AND',
|
|
120
120
|
rules: [
|
|
121
121
|
{
|
|
122
|
-
|
|
122
|
+
fieldPath: 'firm.stage',
|
|
123
123
|
operator: 'in',
|
|
124
124
|
value: ['Seed', 'Series A'],
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
|
-
|
|
127
|
+
fieldPath: 'firm.check_size_min',
|
|
128
128
|
operator: 'lte',
|
|
129
129
|
value: 500000,
|
|
130
130
|
},
|
|
131
131
|
],
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
matchAction: 'tag_continue',
|
|
133
|
+
noMatchAction: 'continue',
|
|
134
|
+
matchTags: ['high-priority', 'icp-match'],
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
id: 'stage-2',
|
|
138
138
|
order: 1,
|
|
139
139
|
name: 'Has Contact Info',
|
|
140
140
|
description: 'Ensure we have investor email or LinkedIn',
|
|
141
|
-
|
|
141
|
+
filterLogic: 'OR',
|
|
142
142
|
rules: [
|
|
143
143
|
{
|
|
144
|
-
|
|
144
|
+
fieldPath: 'investor.email',
|
|
145
145
|
operator: 'isnotnull',
|
|
146
146
|
value: null,
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
|
-
|
|
149
|
+
fieldPath: 'investor.linkedin_url',
|
|
150
150
|
operator: 'isnotnull',
|
|
151
151
|
value: null,
|
|
152
152
|
},
|
|
153
153
|
],
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
matchAction: 'output',
|
|
155
|
+
noMatchAction: 'exclude',
|
|
156
|
+
noMatchTags: ['no-contact-info'],
|
|
157
157
|
},
|
|
158
158
|
],
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
updatedAt: new Date().toISOString(),
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -216,10 +216,10 @@ export const SingleStage: Story = {
|
|
|
216
216
|
order: 0,
|
|
217
217
|
name: 'Stage 1',
|
|
218
218
|
description: '',
|
|
219
|
-
|
|
219
|
+
filterLogic: 'AND',
|
|
220
220
|
rules: [],
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
matchAction: 'continue',
|
|
222
|
+
noMatchAction: 'continue',
|
|
223
223
|
},
|
|
224
224
|
];
|
|
225
225
|
return <InteractiveFunnelStageBuilder initialFunnel={funnel} />;
|
|
@@ -235,10 +235,10 @@ export const MultipleStages: Story = {
|
|
|
235
235
|
order: i,
|
|
236
236
|
name: `Stage ${i + 1}`,
|
|
237
237
|
description: `Description for stage ${i + 1}`,
|
|
238
|
-
|
|
238
|
+
filterLogic: 'AND' as const,
|
|
239
239
|
rules: [],
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
matchAction: 'continue' as const,
|
|
241
|
+
noMatchAction: 'continue' as const,
|
|
242
242
|
}));
|
|
243
243
|
return <InteractiveFunnelStageBuilder initialFunnel={funnel} />;
|
|
244
244
|
},
|
|
@@ -253,19 +253,19 @@ export const WithValidationErrors: Story = {
|
|
|
253
253
|
id: 'stage-1',
|
|
254
254
|
order: 0,
|
|
255
255
|
name: 'Duplicate Name',
|
|
256
|
-
|
|
256
|
+
filterLogic: 'AND',
|
|
257
257
|
rules: [],
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
matchAction: 'continue',
|
|
259
|
+
noMatchAction: 'continue',
|
|
260
260
|
},
|
|
261
261
|
{
|
|
262
262
|
id: 'stage-2',
|
|
263
263
|
order: 1,
|
|
264
264
|
name: 'Duplicate Name', // Duplicate!
|
|
265
|
-
|
|
265
|
+
filterLogic: 'AND',
|
|
266
266
|
rules: [],
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
matchAction: 'continue',
|
|
268
|
+
noMatchAction: 'continue',
|
|
269
269
|
},
|
|
270
270
|
];
|
|
271
271
|
return <InteractiveFunnelStageBuilder initialFunnel={funnel} />;
|
|
@@ -281,30 +281,30 @@ export const WithDifferentActions: Story = {
|
|
|
281
281
|
id: 'stage-1',
|
|
282
282
|
order: 0,
|
|
283
283
|
name: 'Tag & Continue',
|
|
284
|
-
|
|
284
|
+
filterLogic: 'AND',
|
|
285
285
|
rules: [],
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
matchAction: 'tag_continue',
|
|
287
|
+
noMatchAction: 'continue',
|
|
288
|
+
matchTags: ['qualified', 'high-priority'],
|
|
289
289
|
},
|
|
290
290
|
{
|
|
291
291
|
id: 'stage-2',
|
|
292
292
|
order: 1,
|
|
293
293
|
name: 'Output',
|
|
294
|
-
|
|
294
|
+
filterLogic: 'OR',
|
|
295
295
|
rules: [],
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
matchAction: 'output',
|
|
297
|
+
noMatchAction: 'exclude',
|
|
298
298
|
},
|
|
299
299
|
{
|
|
300
300
|
id: 'stage-3',
|
|
301
301
|
order: 2,
|
|
302
302
|
name: 'Tag & Exclude',
|
|
303
|
-
|
|
303
|
+
filterLogic: 'AND',
|
|
304
304
|
rules: [],
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
matchAction: 'continue',
|
|
306
|
+
noMatchAction: 'tag_exclude',
|
|
307
|
+
noMatchTags: ['rejected', 'low-priority'],
|
|
308
308
|
},
|
|
309
309
|
];
|
|
310
310
|
return <InteractiveFunnelStageBuilder initialFunnel={funnel} />;
|
|
@@ -32,19 +32,19 @@ function createTestFunnel(stageCount: number = 2): Funnel {
|
|
|
32
32
|
name: 'Test Funnel',
|
|
33
33
|
description: 'Test funnel',
|
|
34
34
|
status: 'draft',
|
|
35
|
-
|
|
35
|
+
inputType: 'contacts',
|
|
36
36
|
stages: Array.from({ length: stageCount }, (_, i) => ({
|
|
37
37
|
id: `stage-${i}`,
|
|
38
38
|
order: i,
|
|
39
39
|
name: `Stage ${i + 1}`,
|
|
40
40
|
description: '',
|
|
41
|
-
|
|
41
|
+
filterLogic: 'AND' as const,
|
|
42
42
|
rules: [],
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
matchAction: 'continue' as const,
|
|
44
|
+
noMatchAction: 'continue' as const,
|
|
45
45
|
})),
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
createdAt: new Date().toISOString(),
|
|
47
|
+
updatedAt: new Date().toISOString(),
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -59,12 +59,12 @@ function createEmptyStage(order: number): FunnelStage {
|
|
|
59
59
|
order,
|
|
60
60
|
name: `Stage ${order + 1}`,
|
|
61
61
|
description: '',
|
|
62
|
-
|
|
62
|
+
filterLogic: 'AND',
|
|
63
63
|
rules: [],
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
matchAction: 'continue',
|
|
65
|
+
noMatchAction: 'continue',
|
|
66
|
+
matchTags: [],
|
|
67
|
+
noMatchTags: [],
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -13,10 +13,10 @@ function createTestStage(): FunnelStage {
|
|
|
13
13
|
id: 'stage-1',
|
|
14
14
|
order: 0,
|
|
15
15
|
name: 'Test Stage',
|
|
16
|
-
|
|
16
|
+
filterLogic: 'AND',
|
|
17
17
|
rules: [],
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
matchAction: 'continue',
|
|
19
|
+
noMatchAction: 'continue',
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -131,7 +131,7 @@ describe('StageActions', () => {
|
|
|
131
131
|
|
|
132
132
|
it('displays current match action', () => {
|
|
133
133
|
const stage = createTestStage();
|
|
134
|
-
stage.
|
|
134
|
+
stage.matchAction = 'tag_continue';
|
|
135
135
|
|
|
136
136
|
const onMatchActionChange = vi.fn();
|
|
137
137
|
const onNoMatchActionChange = vi.fn();
|
|
@@ -150,7 +150,7 @@ describe('StageActions', () => {
|
|
|
150
150
|
|
|
151
151
|
it('displays current no match action', () => {
|
|
152
152
|
const stage = createTestStage();
|
|
153
|
-
stage.
|
|
153
|
+
stage.noMatchAction = 'tag_exclude';
|
|
154
154
|
|
|
155
155
|
const onMatchActionChange = vi.fn();
|
|
156
156
|
const onNoMatchActionChange = vi.fn();
|
|
@@ -169,7 +169,7 @@ describe('StageActions', () => {
|
|
|
169
169
|
|
|
170
170
|
it('shows description for match action', () => {
|
|
171
171
|
const stage = createTestStage();
|
|
172
|
-
stage.
|
|
172
|
+
stage.matchAction = 'output';
|
|
173
173
|
|
|
174
174
|
const onMatchActionChange = vi.fn();
|
|
175
175
|
const onNoMatchActionChange = vi.fn();
|
|
@@ -187,7 +187,7 @@ describe('StageActions', () => {
|
|
|
187
187
|
|
|
188
188
|
it('shows description for no match action', () => {
|
|
189
189
|
const stage = createTestStage();
|
|
190
|
-
stage.
|
|
190
|
+
stage.noMatchAction = 'exclude';
|
|
191
191
|
|
|
192
192
|
const onMatchActionChange = vi.fn();
|
|
193
193
|
const onNoMatchActionChange = vi.fn();
|
|
@@ -85,7 +85,7 @@ export function StageActions({
|
|
|
85
85
|
</label>
|
|
86
86
|
<select
|
|
87
87
|
id={`match-action-${stage.id}`}
|
|
88
|
-
value={stage.
|
|
88
|
+
value={stage.matchAction}
|
|
89
89
|
onChange={(e) => onMatchActionChange(e.target.value as MatchAction)}
|
|
90
90
|
className="form-select"
|
|
91
91
|
>
|
|
@@ -96,7 +96,7 @@ export function StageActions({
|
|
|
96
96
|
))}
|
|
97
97
|
</select>
|
|
98
98
|
<p className="form-hint">
|
|
99
|
-
{MATCH_ACTIONS.find((a) => a.value === stage.
|
|
99
|
+
{MATCH_ACTIONS.find((a) => a.value === stage.matchAction)?.description}
|
|
100
100
|
</p>
|
|
101
101
|
</div>
|
|
102
102
|
|
|
@@ -107,7 +107,7 @@ export function StageActions({
|
|
|
107
107
|
</label>
|
|
108
108
|
<select
|
|
109
109
|
id={`no-match-action-${stage.id}`}
|
|
110
|
-
value={stage.
|
|
110
|
+
value={stage.noMatchAction}
|
|
111
111
|
onChange={(e) => onNoMatchActionChange(e.target.value as NoMatchAction)}
|
|
112
112
|
className="form-select"
|
|
113
113
|
>
|
|
@@ -118,7 +118,7 @@ export function StageActions({
|
|
|
118
118
|
))}
|
|
119
119
|
</select>
|
|
120
120
|
<p className="form-hint">
|
|
121
|
-
{NO_MATCH_ACTIONS.find((a) => a.value === stage.
|
|
121
|
+
{NO_MATCH_ACTIONS.find((a) => a.value === stage.noMatchAction)?.description}
|
|
122
122
|
</p>
|
|
123
123
|
</div>
|
|
124
124
|
</div>
|
|
@@ -174,15 +174,15 @@ export function StageCard({
|
|
|
174
174
|
</div>
|
|
175
175
|
<div className="summary-item">
|
|
176
176
|
<span className="summary-label">Logic:</span>
|
|
177
|
-
<span className="summary-value">{stage.
|
|
177
|
+
<span className="summary-value">{stage.filterLogic}</span>
|
|
178
178
|
</div>
|
|
179
179
|
<div className="summary-item">
|
|
180
180
|
<span className="summary-label">On Match:</span>
|
|
181
|
-
<span className="summary-value">{stage.
|
|
181
|
+
<span className="summary-value">{stage.matchAction}</span>
|
|
182
182
|
</div>
|
|
183
183
|
<div className="summary-item">
|
|
184
184
|
<span className="summary-label">On No Match:</span>
|
|
185
|
-
<span className="summary-value">{stage.
|
|
185
|
+
<span className="summary-value">{stage.noMatchAction}</span>
|
|
186
186
|
</div>
|
|
187
187
|
</div>
|
|
188
188
|
)}
|
|
@@ -95,35 +95,35 @@ export function StageForm({
|
|
|
95
95
|
* Handle filter logic change
|
|
96
96
|
*/
|
|
97
97
|
const handleFilterLogicChange = useCallback((logic: FilterLogic) => {
|
|
98
|
-
onUpdate({ ...stage,
|
|
98
|
+
onUpdate({ ...stage, filterLogic: logic });
|
|
99
99
|
}, [stage, onUpdate]);
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* Handle match action change
|
|
103
103
|
*/
|
|
104
104
|
const handleMatchActionChange = useCallback((action: MatchAction) => {
|
|
105
|
-
onUpdate({ ...stage,
|
|
105
|
+
onUpdate({ ...stage, matchAction: action });
|
|
106
106
|
}, [stage, onUpdate]);
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Handle no match action change
|
|
110
110
|
*/
|
|
111
111
|
const handleNoMatchActionChange = useCallback((action: NoMatchAction) => {
|
|
112
|
-
onUpdate({ ...stage,
|
|
112
|
+
onUpdate({ ...stage, noMatchAction: action });
|
|
113
113
|
}, [stage, onUpdate]);
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* Handle match tags change
|
|
117
117
|
*/
|
|
118
118
|
const handleMatchTagsChange = useCallback((tags: string[]) => {
|
|
119
|
-
onUpdate({ ...stage,
|
|
119
|
+
onUpdate({ ...stage, matchTags: tags });
|
|
120
120
|
}, [stage, onUpdate]);
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Handle no match tags change
|
|
124
124
|
*/
|
|
125
125
|
const handleNoMatchTagsChange = useCallback((tags: string[]) => {
|
|
126
|
-
onUpdate({ ...stage,
|
|
126
|
+
onUpdate({ ...stage, noMatchTags: tags });
|
|
127
127
|
}, [stage, onUpdate]);
|
|
128
128
|
|
|
129
129
|
/**
|
|
@@ -173,13 +173,13 @@ export function StageForm({
|
|
|
173
173
|
<button
|
|
174
174
|
type="button"
|
|
175
175
|
onClick={() => handleFilterLogicChange('AND')}
|
|
176
|
-
className={`toggle-button ${stage.
|
|
176
|
+
className={`toggle-button ${stage.filterLogic === 'AND' ? 'active' : ''}`}
|
|
177
177
|
>
|
|
178
178
|
<input
|
|
179
179
|
type="radio"
|
|
180
180
|
name={`filter-logic-${stage.id}`}
|
|
181
181
|
value="AND"
|
|
182
|
-
checked={stage.
|
|
182
|
+
checked={stage.filterLogic === 'AND'}
|
|
183
183
|
onChange={() => handleFilterLogicChange('AND')}
|
|
184
184
|
className="sr-only"
|
|
185
185
|
/>
|
|
@@ -188,13 +188,13 @@ export function StageForm({
|
|
|
188
188
|
<button
|
|
189
189
|
type="button"
|
|
190
190
|
onClick={() => handleFilterLogicChange('OR')}
|
|
191
|
-
className={`toggle-button ${stage.
|
|
191
|
+
className={`toggle-button ${stage.filterLogic === 'OR' ? 'active' : ''}`}
|
|
192
192
|
>
|
|
193
193
|
<input
|
|
194
194
|
type="radio"
|
|
195
195
|
name={`filter-logic-${stage.id}`}
|
|
196
196
|
value="OR"
|
|
197
|
-
checked={stage.
|
|
197
|
+
checked={stage.filterLogic === 'OR'}
|
|
198
198
|
onChange={() => handleFilterLogicChange('OR')}
|
|
199
199
|
className="sr-only"
|
|
200
200
|
/>
|
|
@@ -202,7 +202,7 @@ export function StageForm({
|
|
|
202
202
|
</button>
|
|
203
203
|
</div>
|
|
204
204
|
<p className="form-hint">
|
|
205
|
-
{stage.
|
|
205
|
+
{stage.filterLogic === 'AND'
|
|
206
206
|
? 'All rules must match for this stage to pass'
|
|
207
207
|
: 'At least one rule must match for this stage to pass'}
|
|
208
208
|
</p>
|
|
@@ -216,11 +216,11 @@ export function StageForm({
|
|
|
216
216
|
/>
|
|
217
217
|
|
|
218
218
|
{/* Match tags */}
|
|
219
|
-
{(stage.
|
|
219
|
+
{(stage.matchAction === 'tag' || stage.matchAction === 'tag_continue') && (
|
|
220
220
|
<div className="form-group">
|
|
221
221
|
<label className="form-label">Tags on Match</label>
|
|
222
222
|
<TagInput
|
|
223
|
-
tags={stage.
|
|
223
|
+
tags={stage.matchTags || []}
|
|
224
224
|
onChange={handleMatchTagsChange}
|
|
225
225
|
placeholder="Add tag..."
|
|
226
226
|
/>
|
|
@@ -231,11 +231,11 @@ export function StageForm({
|
|
|
231
231
|
)}
|
|
232
232
|
|
|
233
233
|
{/* No match tags */}
|
|
234
|
-
{stage.
|
|
234
|
+
{stage.noMatchAction === 'tag_exclude' && (
|
|
235
235
|
<div className="form-group">
|
|
236
236
|
<label className="form-label">Tags on No Match</label>
|
|
237
237
|
<TagInput
|
|
238
|
-
tags={stage.
|
|
238
|
+
tags={stage.noMatchTags || []}
|
|
239
239
|
onChange={handleNoMatchTagsChange}
|
|
240
240
|
placeholder="Add tag..."
|
|
241
241
|
/>
|