@startsimpli/funnels 0.1.4 → 0.1.6
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 +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3241
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3194
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -20
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -389
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -386
- package/dist/store/index.js.map +0 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelVisualFlow Example Usage
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to integrate FunnelVisualFlow
|
|
5
|
+
* into a real application with state management, API calls, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useEffect } from 'react';
|
|
9
|
+
import { FunnelVisualFlow } from './FunnelVisualFlow';
|
|
10
|
+
import { Funnel, FunnelStage, FunnelRun } from '../../types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Example: Funnel Details Page
|
|
14
|
+
*
|
|
15
|
+
* Shows a funnel's visual flow with latest run data.
|
|
16
|
+
* Clicking a stage opens a details modal.
|
|
17
|
+
* Clicking an edge shows excluded entities.
|
|
18
|
+
*/
|
|
19
|
+
export function FunnelDetailsPage({ funnelId }: { funnelId: string }) {
|
|
20
|
+
const [funnel, setFunnel] = useState<Funnel | null>(null);
|
|
21
|
+
const [latestRun, setLatestRun] = useState<FunnelRun | null>(null);
|
|
22
|
+
const [selectedStage, setSelectedStage] = useState<FunnelStage | null>(null);
|
|
23
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
24
|
+
|
|
25
|
+
// Load funnel and latest run
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
async function loadData() {
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
try {
|
|
30
|
+
// Fetch funnel
|
|
31
|
+
const funnelRes = await fetch(`/api/funnels/${funnelId}`);
|
|
32
|
+
const funnelData = await funnelRes.json();
|
|
33
|
+
setFunnel(funnelData);
|
|
34
|
+
|
|
35
|
+
// Fetch latest run
|
|
36
|
+
const runsRes = await fetch(`/api/funnels/${funnelId}/runs?limit=1`);
|
|
37
|
+
const runsData = await runsRes.json();
|
|
38
|
+
if (runsData.results.length > 0) {
|
|
39
|
+
setLatestRun(runsData.results[0]);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Failed to load funnel:', error);
|
|
43
|
+
} finally {
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
loadData();
|
|
49
|
+
}, [funnelId]);
|
|
50
|
+
|
|
51
|
+
const handleStageClick = (stage: FunnelStage) => {
|
|
52
|
+
setSelectedStage(stage);
|
|
53
|
+
// Could open a modal, navigate to stage details, etc.
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleEdgeClick = (fromStageId: string, toStageId: string) => {
|
|
57
|
+
// Show excluded entities between these stages
|
|
58
|
+
console.log('Show excluded entities:', fromStageId, '->', toStageId);
|
|
59
|
+
// Could fetch excluded entities and show in a modal
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (isLoading) {
|
|
63
|
+
return <div>Loading funnel...</div>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!funnel) {
|
|
67
|
+
return <div>Funnel not found</div>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="funnel-details-page">
|
|
72
|
+
<header className="page-header">
|
|
73
|
+
<h1>{funnel.name}</h1>
|
|
74
|
+
{funnel.description && <p>{funnel.description}</p>}
|
|
75
|
+
</header>
|
|
76
|
+
|
|
77
|
+
<div className="funnel-flow-container">
|
|
78
|
+
<FunnelVisualFlow
|
|
79
|
+
funnel={funnel}
|
|
80
|
+
runData={latestRun || undefined}
|
|
81
|
+
onStageClick={handleStageClick}
|
|
82
|
+
onEdgeClick={handleEdgeClick}
|
|
83
|
+
height={800}
|
|
84
|
+
className="shadow-lg"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Stage Details Modal */}
|
|
89
|
+
{selectedStage && (
|
|
90
|
+
<StageDetailsModal
|
|
91
|
+
stage={selectedStage}
|
|
92
|
+
onClose={() => setSelectedStage(null)}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{/* Latest Run Info */}
|
|
97
|
+
{latestRun && (
|
|
98
|
+
<div className="run-info">
|
|
99
|
+
<h2>Latest Run</h2>
|
|
100
|
+
<p>Status: {latestRun.status}</p>
|
|
101
|
+
<p>Started: {new Date(latestRun.started_at).toLocaleString()}</p>
|
|
102
|
+
<p>
|
|
103
|
+
Results: {latestRun.total_matched} / {latestRun.total_input} matched
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Example: Stage Details Modal
|
|
113
|
+
*/
|
|
114
|
+
function StageDetailsModal({
|
|
115
|
+
stage,
|
|
116
|
+
onClose,
|
|
117
|
+
}: {
|
|
118
|
+
stage: FunnelStage;
|
|
119
|
+
onClose: () => void;
|
|
120
|
+
}) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="modal-overlay" onClick={onClose}>
|
|
123
|
+
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
|
124
|
+
<header className="modal-header">
|
|
125
|
+
<h2>{stage.name}</h2>
|
|
126
|
+
<button onClick={onClose}>×</button>
|
|
127
|
+
</header>
|
|
128
|
+
|
|
129
|
+
<div className="modal-body">
|
|
130
|
+
{stage.description && <p>{stage.description}</p>}
|
|
131
|
+
|
|
132
|
+
<h3>Rules ({stage.rules.length})</h3>
|
|
133
|
+
<ul>
|
|
134
|
+
{stage.rules.map((rule, index) => (
|
|
135
|
+
<li key={index}>
|
|
136
|
+
{rule.field_path} {rule.operator} {JSON.stringify(rule.value)}
|
|
137
|
+
</li>
|
|
138
|
+
))}
|
|
139
|
+
</ul>
|
|
140
|
+
|
|
141
|
+
<h3>Actions</h3>
|
|
142
|
+
<p>Match: {stage.match_action}</p>
|
|
143
|
+
<p>No Match: {stage.no_match_action}</p>
|
|
144
|
+
|
|
145
|
+
{stage.match_tags && stage.match_tags.length > 0 && (
|
|
146
|
+
<>
|
|
147
|
+
<h3>Match Tags</h3>
|
|
148
|
+
<ul>
|
|
149
|
+
{stage.match_tags.map((tag) => (
|
|
150
|
+
<li key={tag}>{tag}</li>
|
|
151
|
+
))}
|
|
152
|
+
</ul>
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Example: Embedded in Dashboard
|
|
163
|
+
*
|
|
164
|
+
* Shows multiple funnels with their visual flows in a grid.
|
|
165
|
+
*/
|
|
166
|
+
export function FunnelsDashboard() {
|
|
167
|
+
const [funnels, setFunnels] = useState<Funnel[]>([]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
async function loadFunnels() {
|
|
171
|
+
const res = await fetch('/api/funnels');
|
|
172
|
+
const data = await res.json();
|
|
173
|
+
setFunnels(data.results);
|
|
174
|
+
}
|
|
175
|
+
loadFunnels();
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div className="funnels-dashboard">
|
|
180
|
+
<h1>Active Funnels</h1>
|
|
181
|
+
|
|
182
|
+
<div className="funnels-grid">
|
|
183
|
+
{funnels.map((funnel) => (
|
|
184
|
+
<div key={funnel.id} className="funnel-card">
|
|
185
|
+
<h2>{funnel.name}</h2>
|
|
186
|
+
<FunnelVisualFlow
|
|
187
|
+
funnel={funnel}
|
|
188
|
+
height={400}
|
|
189
|
+
onStageClick={(stage) => {
|
|
190
|
+
console.log('Stage clicked:', stage.name);
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Example: Side-by-Side Comparison
|
|
202
|
+
*
|
|
203
|
+
* Compare two funnels visually.
|
|
204
|
+
*/
|
|
205
|
+
export function FunnelComparison({
|
|
206
|
+
funnelA,
|
|
207
|
+
funnelB,
|
|
208
|
+
}: {
|
|
209
|
+
funnelA: Funnel;
|
|
210
|
+
funnelB: Funnel;
|
|
211
|
+
}) {
|
|
212
|
+
return (
|
|
213
|
+
<div className="funnel-comparison">
|
|
214
|
+
<div className="comparison-side">
|
|
215
|
+
<h2>{funnelA.name}</h2>
|
|
216
|
+
<FunnelVisualFlow funnel={funnelA} height={600} />
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div className="comparison-divider" />
|
|
220
|
+
|
|
221
|
+
<div className="comparison-side">
|
|
222
|
+
<h2>{funnelB.name}</h2>
|
|
223
|
+
<FunnelVisualFlow funnel={funnelB} height={600} />
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelVisualFlow Component Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { FunnelVisualFlow } from './FunnelVisualFlow';
|
|
6
|
+
export type { FunnelVisualFlowProps } from './FunnelVisualFlow';
|
|
7
|
+
export { StageNode } from './StageNode';
|
|
8
|
+
export type { StageNodeData } from './StageNode';
|
|
9
|
+
export { FlowLegend } from './FlowLegend';
|
|
10
|
+
export { getCircledNumber } from './FunnelVisualFlow';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/funnels/components
|
|
3
|
+
*
|
|
4
|
+
* React components for building funnel UIs
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Export FunnelPreview
|
|
10
|
+
export {
|
|
11
|
+
FunnelPreview,
|
|
12
|
+
PreviewStats,
|
|
13
|
+
StageBreakdown,
|
|
14
|
+
EntityCard,
|
|
15
|
+
LoadingPreview,
|
|
16
|
+
} from './FunnelPreview';
|
|
17
|
+
export type {
|
|
18
|
+
FunnelPreviewProps,
|
|
19
|
+
PreviewResult,
|
|
20
|
+
StagePreviewStats,
|
|
21
|
+
} from './FunnelPreview';
|
|
22
|
+
|
|
23
|
+
// Export FunnelCard
|
|
24
|
+
export {
|
|
25
|
+
FunnelCard,
|
|
26
|
+
StatusBadge,
|
|
27
|
+
StageIndicator,
|
|
28
|
+
MatchBar,
|
|
29
|
+
FunnelStats,
|
|
30
|
+
} from './FunnelCard';
|
|
31
|
+
export type { FunnelCardProps } from './FunnelCard';
|
|
32
|
+
|
|
33
|
+
// Export FunnelVisualFlow
|
|
34
|
+
export {
|
|
35
|
+
FunnelVisualFlow,
|
|
36
|
+
StageNode,
|
|
37
|
+
FlowLegend,
|
|
38
|
+
getCircledNumber,
|
|
39
|
+
} from './FunnelVisualFlow';
|
|
40
|
+
export type {
|
|
41
|
+
FunnelVisualFlowProps,
|
|
42
|
+
StageNodeData,
|
|
43
|
+
} from './FunnelVisualFlow';
|
|
44
|
+
|
|
45
|
+
// Export FilterRuleEditor
|
|
46
|
+
export {
|
|
47
|
+
FilterRuleEditor,
|
|
48
|
+
LogicToggle,
|
|
49
|
+
FieldSelector,
|
|
50
|
+
OperatorSelector,
|
|
51
|
+
RuleRow,
|
|
52
|
+
TextValueInput,
|
|
53
|
+
NumberValueInput,
|
|
54
|
+
DateValueInput,
|
|
55
|
+
BooleanValueInput,
|
|
56
|
+
ChoiceValueInput,
|
|
57
|
+
MultiChoiceValueInput,
|
|
58
|
+
OPERATOR_LABELS,
|
|
59
|
+
NULL_VALUE_OPERATORS,
|
|
60
|
+
MULTI_VALUE_OPERATORS,
|
|
61
|
+
} from './FilterRuleEditor';
|
|
62
|
+
export type { FilterRuleEditorProps } from './FilterRuleEditor';
|
|
63
|
+
|
|
64
|
+
// Export FunnelStageBuilder
|
|
65
|
+
export {
|
|
66
|
+
FunnelStageBuilder,
|
|
67
|
+
StageCard,
|
|
68
|
+
StageForm,
|
|
69
|
+
StageActions,
|
|
70
|
+
TagInput,
|
|
71
|
+
AddStageButton,
|
|
72
|
+
} from './FunnelStageBuilder';
|
|
73
|
+
export type {
|
|
74
|
+
FunnelStageBuilderProps,
|
|
75
|
+
StageCardProps,
|
|
76
|
+
StageFormProps,
|
|
77
|
+
StageActionsProps,
|
|
78
|
+
TagInputProps,
|
|
79
|
+
AddStageButtonProps,
|
|
80
|
+
} from './FunnelStageBuilder';
|
|
81
|
+
|
|
82
|
+
// Export FunnelRunHistory
|
|
83
|
+
export {
|
|
84
|
+
FunnelRunHistory,
|
|
85
|
+
RunStatusBadge,
|
|
86
|
+
RunFilters,
|
|
87
|
+
RunRow,
|
|
88
|
+
RunActions,
|
|
89
|
+
RunDetailsModal,
|
|
90
|
+
StageBreakdownList,
|
|
91
|
+
formatDuration,
|
|
92
|
+
formatRelativeTime,
|
|
93
|
+
calculateMatchRate,
|
|
94
|
+
formatNumber,
|
|
95
|
+
formatFullTimestamp,
|
|
96
|
+
} from './FunnelRunHistory';
|
|
97
|
+
export type {
|
|
98
|
+
RunFiltersType,
|
|
99
|
+
RunSort,
|
|
100
|
+
Pagination,
|
|
101
|
+
RunAction,
|
|
102
|
+
} from './FunnelRunHistory';
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# @simpli/funnels - Core Evaluation Engine
|
|
2
|
+
|
|
3
|
+
BRUTALLY GENERIC rule evaluation engine for filtering ANY entity type.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The core evaluation engine consists of three main components:
|
|
8
|
+
|
|
9
|
+
1. **Field Resolver** (`field-resolver.ts`) - Resolves field values from entities using dot-notation paths
|
|
10
|
+
2. **Operators** (`operators.ts`) - Implements all comparison operators (eq, gt, contains, in, etc.)
|
|
11
|
+
3. **Evaluator** (`evaluator.ts`) - Combines field resolution + operators to evaluate filter rules
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { evaluateRule, filterEntities } from '@simpli/funnels';
|
|
17
|
+
import type { FilterRule } from '@simpli/funnels';
|
|
18
|
+
|
|
19
|
+
// Define your entity type (any shape!)
|
|
20
|
+
interface Investor {
|
|
21
|
+
name: string;
|
|
22
|
+
firm: {
|
|
23
|
+
stage: string;
|
|
24
|
+
aum: number;
|
|
25
|
+
};
|
|
26
|
+
tags: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const investors: Investor[] = [
|
|
30
|
+
{ name: 'John', firm: { stage: 'Series A', aum: 100000000 }, tags: ['qualified'] },
|
|
31
|
+
{ name: 'Jane', firm: { stage: 'Seed', aum: 50000000 }, tags: ['active'] },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Single rule evaluation
|
|
35
|
+
const rule: FilterRule = {
|
|
36
|
+
field_path: 'firm.aum',
|
|
37
|
+
operator: 'gte',
|
|
38
|
+
value: 100000000
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
console.log(evaluateRule(investors[0], rule)); // true
|
|
42
|
+
console.log(evaluateRule(investors[1], rule)); // false
|
|
43
|
+
|
|
44
|
+
// Filter array of entities
|
|
45
|
+
const rules: FilterRule[] = [
|
|
46
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
47
|
+
{ field_path: 'tags', operator: 'has_tag', value: 'qualified' }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const qualified = filterEntities(investors, rules, 'AND');
|
|
51
|
+
// Returns: [{ name: 'John', ... }]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Supported Operators
|
|
55
|
+
|
|
56
|
+
### Equality
|
|
57
|
+
- `eq` - Equal to
|
|
58
|
+
- `ne` - Not equal to
|
|
59
|
+
|
|
60
|
+
### Comparison (numbers, dates, strings)
|
|
61
|
+
- `gt` - Greater than
|
|
62
|
+
- `lt` - Less than
|
|
63
|
+
- `gte` - Greater than or equal
|
|
64
|
+
- `lte` - Less than or equal
|
|
65
|
+
|
|
66
|
+
### String Operations (case-insensitive)
|
|
67
|
+
- `contains` - String contains substring
|
|
68
|
+
- `not_contains` - String does not contain substring
|
|
69
|
+
- `startswith` - String starts with
|
|
70
|
+
- `endswith` - String ends with
|
|
71
|
+
- `matches` - Regex match
|
|
72
|
+
|
|
73
|
+
### Array Operations
|
|
74
|
+
- `in` - Value is in array
|
|
75
|
+
- `not_in` - Value is not in array
|
|
76
|
+
- `has_any` - Array has any of these values
|
|
77
|
+
- `has_all` - Array has all of these values
|
|
78
|
+
|
|
79
|
+
### Null Checks
|
|
80
|
+
- `isnull` - Field is null/undefined
|
|
81
|
+
- `isnotnull` - Field is not null/undefined
|
|
82
|
+
|
|
83
|
+
### Tag Operations
|
|
84
|
+
- `has_tag` - Entity has tag (case-insensitive)
|
|
85
|
+
- `not_has_tag` - Entity does not have tag
|
|
86
|
+
|
|
87
|
+
### Boolean
|
|
88
|
+
- `is_true` - Boolean is true
|
|
89
|
+
- `is_false` - Boolean is false
|
|
90
|
+
|
|
91
|
+
## Field Resolution
|
|
92
|
+
|
|
93
|
+
The evaluator uses dot-notation paths to access nested fields:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const entity = {
|
|
97
|
+
name: 'John',
|
|
98
|
+
firm: {
|
|
99
|
+
stage: 'Series A',
|
|
100
|
+
aum: 100000000
|
|
101
|
+
},
|
|
102
|
+
tags: ['qualified', 'active'],
|
|
103
|
+
metrics: {
|
|
104
|
+
arr_usd: 5000000
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Simple field
|
|
109
|
+
{ field_path: 'name', operator: 'eq', value: 'John' }
|
|
110
|
+
|
|
111
|
+
// Nested field
|
|
112
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series A' }
|
|
113
|
+
|
|
114
|
+
// Deep nesting
|
|
115
|
+
{ field_path: 'metrics.arr_usd', operator: 'gte', value: 1000000 }
|
|
116
|
+
|
|
117
|
+
// Array field
|
|
118
|
+
{ field_path: 'tags', operator: 'has_tag', value: 'qualified' }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Type Handling
|
|
122
|
+
|
|
123
|
+
The evaluator automatically handles type conversions:
|
|
124
|
+
|
|
125
|
+
### Numbers
|
|
126
|
+
```typescript
|
|
127
|
+
// Direct comparison
|
|
128
|
+
{ field_path: 'age', operator: 'gt', value: 30 }
|
|
129
|
+
|
|
130
|
+
// Numeric string comparison
|
|
131
|
+
{ field_path: 'age', operator: 'gt', value: '30' } // Coerced to number
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Dates
|
|
135
|
+
```typescript
|
|
136
|
+
// Date object
|
|
137
|
+
{ field_path: 'created_at', operator: 'gte', value: new Date('2024-01-01') }
|
|
138
|
+
|
|
139
|
+
// Date string
|
|
140
|
+
{ field_path: 'created_at', operator: 'gte', value: '2024-01-01' }
|
|
141
|
+
|
|
142
|
+
// Mixed (Date vs string)
|
|
143
|
+
{ field_path: 'created_at', operator: 'eq', value: '2024-01-15' } // Works!
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Strings
|
|
147
|
+
```typescript
|
|
148
|
+
// Case-insensitive operations
|
|
149
|
+
{ field_path: 'name', operator: 'contains', value: 'john' } // Matches 'John Doe'
|
|
150
|
+
{ field_path: 'email', operator: 'endswith', value: '@ACME.COM' } // Matches '@acme.com'
|
|
151
|
+
|
|
152
|
+
// Regex
|
|
153
|
+
{ field_path: 'email', operator: 'matches', value: '^[a-z]+@example\\.com$' }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Arrays
|
|
157
|
+
```typescript
|
|
158
|
+
// Check if value is in array
|
|
159
|
+
{ field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] }
|
|
160
|
+
|
|
161
|
+
// Check if array has value
|
|
162
|
+
{ field_path: 'tags', operator: 'has_tag', value: 'qualified' }
|
|
163
|
+
|
|
164
|
+
// Check if array has all values
|
|
165
|
+
{ field_path: 'tags', operator: 'has_all', value: ['active', 'qualified'] }
|
|
166
|
+
|
|
167
|
+
// Check if array has any value
|
|
168
|
+
{ field_path: 'tags', operator: 'has_any', value: ['premium', 'enterprise'] }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Null/Undefined
|
|
172
|
+
```typescript
|
|
173
|
+
// Check if field is null
|
|
174
|
+
{ field_path: 'email', operator: 'isnull', value: null }
|
|
175
|
+
|
|
176
|
+
// Check if field exists
|
|
177
|
+
{ field_path: 'profile.linkedin_url', operator: 'isnotnull', value: null }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Multiple Rules
|
|
181
|
+
|
|
182
|
+
Combine multiple rules with AND/OR logic:
|
|
183
|
+
|
|
184
|
+
### AND Logic (all rules must match)
|
|
185
|
+
```typescript
|
|
186
|
+
const rules: FilterRule[] = [
|
|
187
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
188
|
+
{ field_path: 'firm.aum', operator: 'gte', value: 100000000 },
|
|
189
|
+
{ field_path: 'tags', operator: 'has_tag', value: 'qualified' }
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
// All rules must pass
|
|
193
|
+
const qualified = filterEntities(investors, rules, 'AND');
|
|
194
|
+
|
|
195
|
+
// Or using helper
|
|
196
|
+
const match = evaluateRulesAND(investor, rules);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### OR Logic (at least one rule must match)
|
|
200
|
+
```typescript
|
|
201
|
+
const rules: FilterRule[] = [
|
|
202
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
203
|
+
{ field_path: 'firm.aum', operator: 'gte', value: 500000000 } // OR very large
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
// Any rule can pass
|
|
207
|
+
const flexible = filterEntities(investors, rules, 'OR');
|
|
208
|
+
|
|
209
|
+
// Or using helper
|
|
210
|
+
const match = evaluateRulesOR(investor, rules);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Negation
|
|
214
|
+
|
|
215
|
+
Use the `negate` flag to invert a rule's result:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// Match investors NOT in Series A
|
|
219
|
+
const rule: FilterRule = {
|
|
220
|
+
field_path: 'firm.stage',
|
|
221
|
+
operator: 'eq',
|
|
222
|
+
value: 'Series A',
|
|
223
|
+
negate: true // Invert the result
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Match investors WITHOUT 'archived' tag
|
|
227
|
+
const notArchived: FilterRule = {
|
|
228
|
+
field_path: 'tags',
|
|
229
|
+
operator: 'has_tag',
|
|
230
|
+
value: 'archived',
|
|
231
|
+
negate: true
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Detailed Results
|
|
236
|
+
|
|
237
|
+
Get diagnostic information about rule evaluation:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { evaluateRuleWithResult, evaluateRulesWithResults } from '@simpli/funnels';
|
|
241
|
+
|
|
242
|
+
// Single rule with details
|
|
243
|
+
const result = evaluateRuleWithResult(investor, {
|
|
244
|
+
field_path: 'firm.aum',
|
|
245
|
+
operator: 'gte',
|
|
246
|
+
value: 100000000
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
console.log(result);
|
|
250
|
+
// {
|
|
251
|
+
// field_path: 'firm.aum',
|
|
252
|
+
// operator: 'gte',
|
|
253
|
+
// value: 100000000,
|
|
254
|
+
// actual_value: 150000000,
|
|
255
|
+
// matched: true
|
|
256
|
+
// }
|
|
257
|
+
|
|
258
|
+
// Multiple rules with details
|
|
259
|
+
const results = evaluateRulesWithResults(investor, rules, 'AND');
|
|
260
|
+
|
|
261
|
+
console.log(results);
|
|
262
|
+
// {
|
|
263
|
+
// matched: true,
|
|
264
|
+
// logic: 'AND',
|
|
265
|
+
// rule_results: [
|
|
266
|
+
// { field_path: 'firm.stage', operator: 'eq', value: 'Series A', actual_value: 'Series A', matched: true },
|
|
267
|
+
// { field_path: 'firm.aum', operator: 'gte', value: 100000000, actual_value: 150000000, matched: true }
|
|
268
|
+
// ]
|
|
269
|
+
// }
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Testing
|
|
273
|
+
|
|
274
|
+
The evaluation engine has comprehensive test coverage:
|
|
275
|
+
|
|
276
|
+
- `operators.test.ts` - 105 tests covering all operators with various data types
|
|
277
|
+
- `evaluator.test.ts` - 76 tests covering rule evaluation, multiple rules, filtering
|
|
278
|
+
- `field-resolver.test.ts` - 57 tests covering field resolution logic
|
|
279
|
+
|
|
280
|
+
Total: **286 tests** ensuring robust operation.
|
|
281
|
+
|
|
282
|
+
Run tests:
|
|
283
|
+
```bash
|
|
284
|
+
npm test
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Examples
|
|
288
|
+
|
|
289
|
+
See `evaluator.example.ts` for comprehensive examples including:
|
|
290
|
+
- Investor qualification funnels
|
|
291
|
+
- Recipe filtering with dietary restrictions
|
|
292
|
+
- GitHub repository filtering
|
|
293
|
+
- Multi-stage funnel simulation
|
|
294
|
+
- Advanced patterns (negation, regex, OR logic)
|
|
295
|
+
|
|
296
|
+
## Philosophy
|
|
297
|
+
|
|
298
|
+
This evaluator follows the **BRUTALLY GENERIC** principle:
|
|
299
|
+
|
|
300
|
+
- Works with ANY entity type (no domain-specific code)
|
|
301
|
+
- Field paths use simple dot-notation (no magic)
|
|
302
|
+
- Operators are pure functions (no side effects)
|
|
303
|
+
- Type coercion is automatic and sensible
|
|
304
|
+
- Null/undefined handling is consistent
|
|
305
|
+
- Errors are graceful (no crashes)
|
|
306
|
+
|
|
307
|
+
**The goal: Write rules once, filter anything.**
|