@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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/api/client.test.ts +76 -76
  3. package/src/api/client.ts +16 -16
  4. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +24 -24
  5. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +39 -39
  6. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +1 -1
  7. package/src/components/FilterRuleEditor/RuleRow.tsx +8 -8
  8. package/src/components/FunnelCard/FunnelCard.stories.tsx +118 -118
  9. package/src/components/FunnelCard/FunnelCard.test.ts +48 -48
  10. package/src/components/FunnelCard/FunnelCard.test.tsx +40 -40
  11. package/src/components/FunnelCard/FunnelCard.tsx +3 -3
  12. package/src/components/FunnelPreview/FunnelPreview.test.tsx +63 -63
  13. package/src/components/FunnelPreview/FunnelPreview.tsx +11 -11
  14. package/src/components/FunnelPreview/StageBreakdown.tsx +5 -5
  15. package/src/components/FunnelPreview/example.tsx +43 -43
  16. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +70 -70
  17. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +63 -63
  18. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +12 -12
  19. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +5 -5
  20. package/src/components/FunnelRunHistory/RunFilters.tsx +8 -8
  21. package/src/components/FunnelRunHistory/RunRow.tsx +7 -7
  22. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +9 -9
  23. package/src/components/FunnelRunHistory/types.ts +6 -6
  24. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +41 -41
  25. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +6 -6
  26. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +5 -5
  27. package/src/components/FunnelStageBuilder/StageActions.test.tsx +7 -7
  28. package/src/components/FunnelStageBuilder/StageActions.tsx +4 -4
  29. package/src/components/FunnelStageBuilder/StageCard.tsx +3 -3
  30. package/src/components/FunnelStageBuilder/StageForm.tsx +14 -14
  31. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +42 -42
  32. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +31 -31
  33. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +4 -4
  34. package/src/components/FunnelVisualFlow/StageNode.tsx +8 -8
  35. package/src/components/FunnelVisualFlow/example.tsx +7 -7
  36. package/src/core/engine.test.ts +271 -271
  37. package/src/core/engine.ts +60 -60
  38. package/src/core/evaluator.example.ts +31 -31
  39. package/src/core/evaluator.test.ts +92 -92
  40. package/src/core/evaluator.ts +23 -23
  41. package/src/core/field-resolver.example.ts +1 -1
  42. package/src/core/field-resolver.test.ts +4 -4
  43. package/src/store/create-funnel-store.test.ts +32 -32
  44. package/src/store/create-funnel-store.ts +3 -3
  45. package/src/store/types.ts +2 -2
  46. package/src/types/index.ts +64 -64
@@ -26,13 +26,13 @@ import { applyOperator } from './operators';
26
26
  * const investor = { name: 'John', firm: { stage: 'Series A' } };
27
27
  *
28
28
  * evaluateRule(investor, {
29
- * field_path: 'firm.stage',
29
+ * fieldPath: 'firm.stage',
30
30
  * operator: 'eq',
31
31
  * value: 'Series A'
32
32
  * }); // true
33
33
  *
34
34
  * evaluateRule(investor, {
35
- * field_path: 'firm.stage',
35
+ * fieldPath: 'firm.stage',
36
36
  * operator: 'eq',
37
37
  * value: 'Seed',
38
38
  * negate: true
@@ -41,7 +41,7 @@ import { applyOperator } from './operators';
41
41
  */
42
42
  export function evaluateRule<T>(entity: T, rule: FilterRule): boolean {
43
43
  // Resolve field value from entity
44
- const actualValue = resolveField(entity, rule.field_path);
44
+ const actualValue = resolveField(entity, rule.fieldPath);
45
45
 
46
46
  // Apply operator
47
47
  const result = applyOperator(rule.operator, actualValue, rule.value);
@@ -60,15 +60,15 @@ export function evaluateRule<T>(entity: T, rule: FilterRule): boolean {
60
60
  * @example
61
61
  * ```typescript
62
62
  * const result = evaluateRuleWithResult(investor, {
63
- * field_path: 'firm.stage',
63
+ * fieldPath: 'firm.stage',
64
64
  * operator: 'eq',
65
65
  * value: 'Series A'
66
66
  * });
67
67
  * // {
68
- * // field_path: 'firm.stage',
68
+ * // fieldPath: 'firm.stage',
69
69
  * // operator: 'eq',
70
70
  * // value: 'Series A',
71
- * // actual_value: 'Series A',
71
+ * // actualValue: 'Series A',
72
72
  * // matched: true
73
73
  * // }
74
74
  * ```
@@ -79,7 +79,7 @@ export function evaluateRuleWithResult<T>(
79
79
  ): RuleResult {
80
80
  try {
81
81
  // Resolve field value
82
- const actualValue = resolveField(entity, rule.field_path);
82
+ const actualValue = resolveField(entity, rule.fieldPath);
83
83
 
84
84
  // Apply operator
85
85
  const operatorResult = applyOperator(rule.operator, actualValue, rule.value);
@@ -88,19 +88,19 @@ export function evaluateRuleWithResult<T>(
88
88
  const matched = rule.negate ? !operatorResult : operatorResult;
89
89
 
90
90
  return {
91
- field_path: rule.field_path,
91
+ fieldPath: rule.fieldPath,
92
92
  operator: rule.operator,
93
93
  value: rule.value,
94
- actual_value: actualValue,
94
+ actualValue: actualValue,
95
95
  matched,
96
96
  };
97
97
  } catch (error) {
98
98
  // Handle evaluation errors gracefully
99
99
  return {
100
- field_path: rule.field_path,
100
+ fieldPath: rule.fieldPath,
101
101
  operator: rule.operator,
102
102
  value: rule.value,
103
- actual_value: undefined,
103
+ actualValue: undefined,
104
104
  matched: false,
105
105
  error: error instanceof Error ? error.message : String(error),
106
106
  };
@@ -119,8 +119,8 @@ export function evaluateRuleWithResult<T>(
119
119
  * const investor = { name: 'John', firm: { stage: 'Series A', aum: 100000000 } };
120
120
  *
121
121
  * evaluateRulesAND(investor, [
122
- * { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
123
- * { field_path: 'firm.aum', operator: 'gte', value: 50000000 }
122
+ * { fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
123
+ * { fieldPath: 'firm.aum', operator: 'gte', value: 50000000 }
124
124
  * ]); // true (both rules match)
125
125
  * ```
126
126
  */
@@ -142,8 +142,8 @@ export function evaluateRulesAND<T>(entity: T, rules: FilterRule[]): boolean {
142
142
  * const investor = { name: 'John', firm: { stage: 'Seed' } };
143
143
  *
144
144
  * evaluateRulesOR(investor, [
145
- * { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
146
- * { field_path: 'firm.stage', operator: 'eq', value: 'Seed' }
145
+ * { fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
146
+ * { fieldPath: 'firm.stage', operator: 'eq', value: 'Seed' }
147
147
  * ]); // true (second rule matches)
148
148
  * ```
149
149
  */
@@ -188,15 +188,15 @@ export function evaluateRules<T>(
188
188
  * @example
189
189
  * ```typescript
190
190
  * const result = evaluateRulesWithResults(investor, [
191
- * { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
192
- * { field_path: 'firm.aum', operator: 'gte', value: 50000000 }
191
+ * { fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
192
+ * { fieldPath: 'firm.aum', operator: 'gte', value: 50000000 }
193
193
  * ], 'AND');
194
194
  * // {
195
195
  * // matched: true,
196
196
  * // logic: 'AND',
197
- * // rule_results: [
198
- * // { field_path: 'firm.stage', operator: 'eq', value: 'Series A', actual_value: 'Series A', matched: true },
199
- * // { field_path: 'firm.aum', operator: 'gte', value: 50000000, actual_value: 100000000, matched: true }
197
+ * // ruleResults: [
198
+ * // { fieldPath: 'firm.stage', operator: 'eq', value: 'Series A', actualValue: 'Series A', matched: true },
199
+ * // { fieldPath: 'firm.aum', operator: 'gte', value: 50000000, actualValue: 100000000, matched: true }
200
200
  * // ]
201
201
  * // }
202
202
  * ```
@@ -208,7 +208,7 @@ export function evaluateRulesWithResults<T>(
208
208
  ): {
209
209
  matched: boolean;
210
210
  logic: 'AND' | 'OR';
211
- rule_results: RuleResult[];
211
+ ruleResults: RuleResult[];
212
212
  } {
213
213
  const ruleResults = rules.map(rule => evaluateRuleWithResult(entity, rule));
214
214
 
@@ -220,7 +220,7 @@ export function evaluateRulesWithResults<T>(
220
220
  return {
221
221
  matched,
222
222
  logic,
223
- rule_results: ruleResults,
223
+ ruleResults: ruleResults,
224
224
  };
225
225
  }
226
226
 
@@ -241,7 +241,7 @@ export function evaluateRulesWithResults<T>(
241
241
  * ];
242
242
  *
243
243
  * filterEntities(investors, [
244
- * { field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] }
244
+ * { fieldPath: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] }
245
245
  * ]);
246
246
  * // [
247
247
  * // { name: 'Jane', firm: { stage: 'Series A' } },
@@ -27,7 +27,7 @@ const investor = {
27
27
  response_rate: 0.85,
28
28
  deal_count: 12,
29
29
  },
30
- created_at: new Date('2024-01-01'),
30
+ createdAt: new Date('2024-01-01'),
31
31
  };
32
32
 
33
33
  // Resolve simple fields
@@ -31,8 +31,8 @@ describe('resolveField', () => {
31
31
 
32
32
  it('resolves top-level date field', () => {
33
33
  const date = new Date('2024-01-01');
34
- const entity = { created_at: date };
35
- expect(resolveField(entity, 'created_at')).toBe(date);
34
+ const entity = { createdAt: date };
35
+ expect(resolveField(entity, 'createdAt')).toBe(date);
36
36
  });
37
37
 
38
38
  it('resolves top-level null field', () => {
@@ -228,7 +228,7 @@ describe('resolveField', () => {
228
228
  response_rate: 0.85,
229
229
  deal_count: 12,
230
230
  },
231
- created_at: new Date('2024-01-01'),
231
+ createdAt: new Date('2024-01-01'),
232
232
  };
233
233
 
234
234
  expect(resolveField(investor, 'name')).toBe('John Doe');
@@ -237,7 +237,7 @@ describe('resolveField', () => {
237
237
  expect(resolveField(investor, 'tags[0]')).toBe('active');
238
238
  expect(resolveField(investor, 'profile.linkedin_url')).toBe('https://linkedin.com/in/johndoe');
239
239
  expect(resolveField(investor, 'metrics.response_rate')).toBe(0.85);
240
- expect(resolveField(investor, 'created_at')).toBeInstanceOf(Date);
240
+ expect(resolveField(investor, 'createdAt')).toBeInstanceOf(Date);
241
241
  });
242
242
 
243
243
  it('resolves recipe entity fields', () => {
@@ -54,53 +54,53 @@ const mockFunnel: Funnel = {
54
54
  name: 'Test Funnel',
55
55
  description: 'Test description',
56
56
  status: 'active',
57
- input_type: 'contacts',
57
+ inputType: 'contacts',
58
58
  stages: [
59
59
  {
60
60
  id: 'stage-1',
61
61
  order: 0,
62
62
  name: 'Stage 1',
63
- filter_logic: 'AND',
63
+ filterLogic: 'AND',
64
64
  rules: [],
65
- match_action: 'continue',
66
- no_match_action: 'exclude',
65
+ matchAction: 'continue',
66
+ noMatchAction: 'exclude',
67
67
  },
68
68
  {
69
69
  id: 'stage-2',
70
70
  order: 1,
71
71
  name: 'Stage 2',
72
- filter_logic: 'OR',
72
+ filterLogic: 'OR',
73
73
  rules: [],
74
- match_action: 'output',
75
- no_match_action: 'exclude',
74
+ matchAction: 'output',
75
+ noMatchAction: 'exclude',
76
76
  },
77
77
  ],
78
- created_at: new Date().toISOString(),
79
- updated_at: new Date().toISOString(),
78
+ createdAt: new Date().toISOString(),
79
+ updatedAt: new Date().toISOString(),
80
80
  };
81
81
 
82
82
  const mockFunnel2: Funnel = {
83
83
  id: 'funnel-2',
84
84
  name: 'Test Funnel 2',
85
85
  status: 'draft',
86
- input_type: 'organizations',
86
+ inputType: 'organizations',
87
87
  stages: [],
88
- created_at: new Date().toISOString(),
89
- updated_at: new Date().toISOString(),
88
+ createdAt: new Date().toISOString(),
89
+ updatedAt: new Date().toISOString(),
90
90
  };
91
91
 
92
92
  const mockRun: FunnelRun = {
93
93
  id: 'run-1',
94
- funnel_id: 'funnel-1',
94
+ funnelId: 'funnel-1',
95
95
  status: 'completed',
96
- trigger_type: 'manual',
97
- started_at: new Date().toISOString(),
98
- completed_at: new Date().toISOString(),
99
- total_input: 100,
100
- total_matched: 75,
101
- total_excluded: 25,
102
- total_tagged: 50,
103
- stage_stats: {},
96
+ triggerType: 'manual',
97
+ startedAt: new Date().toISOString(),
98
+ completedAt: new Date().toISOString(),
99
+ totalInput: 100,
100
+ totalMatched: 75,
101
+ totalExcluded: 25,
102
+ totalTagged: 50,
103
+ stageStats: {},
104
104
  };
105
105
 
106
106
  // ============================================================================
@@ -175,12 +175,12 @@ describe('createFunnelStore', () => {
175
175
  vi.mocked(apiClient.listFunnels).mockResolvedValue(mockResponse);
176
176
 
177
177
  const { loadFunnels } = useFunnelStore.getState();
178
- await loadFunnels({ status: 'active', page: 2, page_size: 10 });
178
+ await loadFunnels({ status: 'active', page: 2, pageSize: 10 });
179
179
 
180
180
  expect(apiClient.listFunnels).toHaveBeenCalledWith({
181
181
  status: 'active',
182
182
  page: 2,
183
- page_size: 10,
183
+ pageSize: 10,
184
184
  });
185
185
 
186
186
  const state = useFunnelStore.getState();
@@ -249,7 +249,7 @@ describe('createFunnelStore', () => {
249
249
  const input: CreateFunnelInput = {
250
250
  name: 'Test Funnel',
251
251
  status: 'active',
252
- input_type: 'contacts',
252
+ inputType: 'contacts',
253
253
  stages: [],
254
254
  };
255
255
 
@@ -268,7 +268,7 @@ describe('createFunnelStore', () => {
268
268
  const input: CreateFunnelInput = {
269
269
  name: '',
270
270
  status: 'active',
271
- input_type: 'contacts',
271
+ inputType: 'contacts',
272
272
  stages: [],
273
273
  };
274
274
 
@@ -505,10 +505,10 @@ describe('createFunnelStore', () => {
505
505
  id: 'stage-3',
506
506
  order: 2,
507
507
  name: 'Stage 3',
508
- filter_logic: 'AND',
508
+ filterLogic: 'AND',
509
509
  rules: [],
510
- match_action: 'continue',
511
- no_match_action: 'exclude',
510
+ matchAction: 'continue',
511
+ noMatchAction: 'exclude',
512
512
  };
513
513
 
514
514
  vi.mocked(apiClient.createStage).mockResolvedValue(newStage);
@@ -517,10 +517,10 @@ describe('createFunnelStore', () => {
517
517
  await createStage('funnel-1', {
518
518
  order: 2,
519
519
  name: 'Stage 3',
520
- filter_logic: 'AND',
520
+ filterLogic: 'AND',
521
521
  rules: [],
522
- match_action: 'continue',
523
- no_match_action: 'exclude',
522
+ matchAction: 'continue',
523
+ noMatchAction: 'exclude',
524
524
  });
525
525
 
526
526
  const state = useFunnelStore.getState();
@@ -574,7 +574,7 @@ describe('createFunnelStore', () => {
574
574
  vi.mocked(apiClient.runFunnel).mockResolvedValue(mockRun);
575
575
 
576
576
  const { runFunnel } = useFunnelStore.getState();
577
- const result = await runFunnel('funnel-1', { trigger_type: 'manual' });
577
+ const result = await runFunnel('funnel-1', { triggerType: 'manual' });
578
578
 
579
579
  expect(result).toEqual(mockRun);
580
580
  expect(useFunnelStore.getState().runs).toHaveLength(1);
@@ -74,7 +74,7 @@ export function createFunnelStore<TEntity = any>(
74
74
  next: response.next,
75
75
  previous: response.previous,
76
76
  currentPage: filters?.page || 1,
77
- pageSize: filters?.page_size || 20,
77
+ pageSize: filters?.pageSize || 20,
78
78
  },
79
79
  isLoading: false,
80
80
  });
@@ -204,12 +204,12 @@ export function createFunnelStore<TEntity = any>(
204
204
  name: `${funnel.name} (Copy)`,
205
205
  description: funnel.description,
206
206
  status: 'draft', // Always create as draft
207
- input_type: funnel.input_type,
207
+ inputType: funnel.inputType,
208
208
  stages: funnel.stages.map((stage, index) => ({
209
209
  ...stage,
210
210
  order: index, // Preserve order
211
211
  })),
212
- completion_tags: funnel.completion_tags,
212
+ completionTags: funnel.completionTags,
213
213
  metadata: funnel.metadata,
214
214
  };
215
215
 
@@ -190,7 +190,7 @@ export interface FunnelStore<TEntity = any> {
190
190
  runFunnel: (
191
191
  id: string,
192
192
  options?: {
193
- trigger_type?: 'manual' | 'scheduled' | 'webhook' | 'api';
193
+ triggerType?: 'manual' | 'scheduled' | 'webhook' | 'api';
194
194
  metadata?: Record<string, any>;
195
195
  }
196
196
  ) => Promise<FunnelRun>;
@@ -206,7 +206,7 @@ export interface FunnelStore<TEntity = any> {
206
206
  filters?: {
207
207
  status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
208
208
  page?: number;
209
- page_size?: number;
209
+ pageSize?: number;
210
210
  }
211
211
  ) => Promise<void>;
212
212