@startsimpli/funnels 0.1.4 → 0.1.5

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 (151) hide show
  1. package/package.json +9 -31
  2. package/src/api/README.md +507 -0
  3. package/src/api/adapter.ts +106 -0
  4. package/src/api/client.test.ts +640 -0
  5. package/src/api/client.ts +385 -0
  6. package/src/api/default-adapter.ts +243 -0
  7. package/src/api/index.ts +24 -0
  8. package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
  9. package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
  10. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
  11. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
  12. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
  13. package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
  14. package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
  15. package/src/components/FilterRuleEditor/README.md +291 -0
  16. package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
  17. package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
  18. package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
  19. package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
  20. package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
  21. package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
  22. package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
  23. package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
  24. package/src/components/FilterRuleEditor/constants.ts +64 -0
  25. package/src/components/FilterRuleEditor/index.ts +14 -0
  26. package/src/components/FunnelCard/DESIGN.md +447 -0
  27. package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
  28. package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
  29. package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
  30. package/src/components/FunnelCard/FunnelCard.tsx +204 -0
  31. package/src/components/FunnelCard/FunnelStats.tsx +68 -0
  32. package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
  33. package/src/components/FunnelCard/INSTALLATION.md +304 -0
  34. package/src/components/FunnelCard/MatchBar.tsx +49 -0
  35. package/src/components/FunnelCard/README.md +294 -0
  36. package/src/components/FunnelCard/StageIndicator.tsx +62 -0
  37. package/src/components/FunnelCard/StatusBadge.tsx +52 -0
  38. package/src/components/FunnelCard/index.ts +14 -0
  39. package/src/components/FunnelPreview/EntityCard.tsx +72 -0
  40. package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
  41. package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
  42. package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
  43. package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
  44. package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
  45. package/src/components/FunnelPreview/README.md +337 -0
  46. package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
  47. package/src/components/FunnelPreview/example.tsx +286 -0
  48. package/src/components/FunnelPreview/index.ts +14 -0
  49. package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
  50. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
  51. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
  52. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
  53. package/src/components/FunnelRunHistory/README.md +325 -0
  54. package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
  55. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
  56. package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
  57. package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
  58. package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
  59. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
  60. package/src/components/FunnelRunHistory/index.ts +51 -0
  61. package/src/components/FunnelRunHistory/types.ts +40 -0
  62. package/src/components/FunnelRunHistory/utils.test.ts +126 -0
  63. package/src/components/FunnelRunHistory/utils.ts +100 -0
  64. package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
  65. package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
  66. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
  67. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
  68. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
  69. package/src/components/FunnelStageBuilder/README.md +341 -0
  70. package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
  71. package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
  72. package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
  73. package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
  74. package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
  75. package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
  76. package/src/components/FunnelStageBuilder/index.ts +21 -0
  77. package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
  78. package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
  79. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
  80. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
  81. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
  82. package/src/components/FunnelVisualFlow/README.md +323 -0
  83. package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
  84. package/src/components/FunnelVisualFlow/example.tsx +227 -0
  85. package/src/components/FunnelVisualFlow/index.ts +10 -0
  86. package/src/components/index.ts +102 -0
  87. package/src/core/README.md +307 -0
  88. package/src/core/engine.test.ts +1087 -0
  89. package/src/core/engine.ts +329 -0
  90. package/src/core/evaluator.example.ts +353 -0
  91. package/src/core/evaluator.test.ts +639 -0
  92. package/src/core/evaluator.ts +261 -0
  93. package/src/core/field-resolver.example.ts +175 -0
  94. package/src/core/field-resolver.test.ts +541 -0
  95. package/src/core/field-resolver.ts +247 -0
  96. package/src/core/index.ts +34 -0
  97. package/src/core/operators.test.ts +539 -0
  98. package/src/core/operators.ts +241 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/useDebouncedValue.ts +28 -0
  101. package/src/index.ts +155 -0
  102. package/src/store/README.md +342 -0
  103. package/src/store/create-funnel-store.test.ts +686 -0
  104. package/src/store/create-funnel-store.ts +538 -0
  105. package/src/store/index.ts +9 -0
  106. package/src/store/types.ts +294 -0
  107. package/src/stories/CrossDomain.stories.tsx +149 -0
  108. package/src/stories/Welcome.stories.tsx +81 -0
  109. package/src/stories/demo-data/index.ts +3 -0
  110. package/src/stories/demo-data/investors.ts +216 -0
  111. package/src/stories/demo-data/leads.ts +223 -0
  112. package/src/stories/demo-data/recipes.ts +217 -0
  113. package/src/test/setup.ts +5 -0
  114. package/src/types/index.ts +843 -0
  115. package/dist/client-3ESO2NHy.d.ts +0 -310
  116. package/dist/client-CZu03ACp.d.cts +0 -310
  117. package/dist/components/index.cjs +0 -3241
  118. package/dist/components/index.cjs.map +0 -1
  119. package/dist/components/index.css.map +0 -1
  120. package/dist/components/index.d.cts +0 -726
  121. package/dist/components/index.d.ts +0 -726
  122. package/dist/components/index.js +0 -3194
  123. package/dist/components/index.js.map +0 -1
  124. package/dist/core/index.cjs +0 -500
  125. package/dist/core/index.cjs.map +0 -1
  126. package/dist/core/index.d.cts +0 -359
  127. package/dist/core/index.d.ts +0 -359
  128. package/dist/core/index.js +0 -486
  129. package/dist/core/index.js.map +0 -1
  130. package/dist/hooks/index.cjs +0 -20
  131. package/dist/hooks/index.cjs.map +0 -1
  132. package/dist/hooks/index.d.cts +0 -11
  133. package/dist/hooks/index.d.ts +0 -11
  134. package/dist/hooks/index.js +0 -18
  135. package/dist/hooks/index.js.map +0 -1
  136. package/dist/index-BGDEXbuz.d.cts +0 -434
  137. package/dist/index-BGDEXbuz.d.ts +0 -434
  138. package/dist/index.cjs +0 -4499
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.css +0 -198
  141. package/dist/index.css.map +0 -1
  142. package/dist/index.d.cts +0 -99
  143. package/dist/index.d.ts +0 -99
  144. package/dist/index.js +0 -4421
  145. package/dist/index.js.map +0 -1
  146. package/dist/store/index.cjs +0 -389
  147. package/dist/store/index.cjs.map +0 -1
  148. package/dist/store/index.d.cts +0 -225
  149. package/dist/store/index.d.ts +0 -225
  150. package/dist/store/index.js +0 -386
  151. package/dist/store/index.js.map +0 -1
@@ -0,0 +1,538 @@
1
+ /**
2
+ * Funnel Store Factory
3
+ *
4
+ * Creates isolated Zustand store instances for funnel management.
5
+ * Factory pattern allows multiple editors to coexist without state conflicts.
6
+ *
7
+ * Features:
8
+ * - Optimistic updates with automatic rollback on error
9
+ * - Dirty tracking for unsaved changes
10
+ * - Loading states for better UX
11
+ * - Error handling with automatic retry
12
+ * - Selection state management
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+
17
+ import { create, StoreApi, UseBoundStore } from 'zustand';
18
+ import type { FunnelApiClient, FunnelListFilters } from '../api/client';
19
+ import type { FunnelStore } from './types';
20
+ import { createInitialState } from './types';
21
+ import type {
22
+ Funnel,
23
+ FunnelStage,
24
+ FunnelRun,
25
+ CreateFunnelInput,
26
+ UpdateFunnelInput,
27
+ CreateStageInput,
28
+ UpdateStageInput,
29
+ } from '../types';
30
+
31
+ /**
32
+ * Create funnel store with API client
33
+ *
34
+ * Factory pattern - each call creates a new isolated store instance.
35
+ * Perfect for multi-editor scenarios or testing.
36
+ *
37
+ * @param apiClient - API client for server operations
38
+ * @returns Zustand store hook
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * const adapter = new FetchAdapter({ headers: { 'Authorization': 'Bearer token' } });
43
+ * const apiClient = new FunnelApiClient(adapter, 'https://api.example.com');
44
+ * const useFunnelStore = createFunnelStore(apiClient);
45
+ *
46
+ * // In React component
47
+ * function FunnelEditor() {
48
+ * const { funnels, loadFunnels, createFunnel } = useFunnelStore();
49
+ * // ...
50
+ * }
51
+ * ```
52
+ */
53
+ export function createFunnelStore<TEntity = any>(
54
+ apiClient: FunnelApiClient
55
+ ): UseBoundStore<StoreApi<FunnelStore<TEntity>>> {
56
+ return create<FunnelStore<TEntity>>((set, get) => ({
57
+ // Initialize state
58
+ ...createInitialState<TEntity>(),
59
+
60
+ // =========================================================================
61
+ // Funnel Actions
62
+ // =========================================================================
63
+
64
+ loadFunnels: async (filters?: FunnelListFilters) => {
65
+ set({ isLoading: true, error: null });
66
+
67
+ try {
68
+ const response = await apiClient.listFunnels<TEntity>(filters);
69
+
70
+ set({
71
+ funnels: response.results,
72
+ pagination: {
73
+ count: response.count,
74
+ next: response.next,
75
+ previous: response.previous,
76
+ currentPage: filters?.page || 1,
77
+ pageSize: filters?.page_size || 20,
78
+ },
79
+ isLoading: false,
80
+ });
81
+ } catch (error) {
82
+ set({
83
+ error: error as Error,
84
+ isLoading: false,
85
+ });
86
+ throw error;
87
+ }
88
+ },
89
+
90
+ selectFunnel: (id: string | null) => {
91
+ const { funnels } = get();
92
+
93
+ if (id === null) {
94
+ set({ selectedFunnel: null, selectedStage: null });
95
+ return;
96
+ }
97
+
98
+ const funnel = funnels.find((f) => f.id === id);
99
+ set({
100
+ selectedFunnel: funnel || null,
101
+ selectedStage: null, // Clear stage selection when changing funnels
102
+ });
103
+ },
104
+
105
+ createFunnel: async (data: CreateFunnelInput<TEntity>) => {
106
+ set({ isLoading: true, error: null });
107
+
108
+ try {
109
+ const funnel = await apiClient.createFunnel<TEntity>(data);
110
+
111
+ set((state) => ({
112
+ funnels: [...state.funnels, funnel],
113
+ pagination: {
114
+ ...state.pagination,
115
+ count: state.pagination.count + 1,
116
+ },
117
+ isLoading: false,
118
+ }));
119
+
120
+ return funnel;
121
+ } catch (error) {
122
+ set({
123
+ error: error as Error,
124
+ isLoading: false,
125
+ });
126
+ throw error;
127
+ }
128
+ },
129
+
130
+ updateFunnel: async (
131
+ id: string,
132
+ data: Partial<UpdateFunnelInput<TEntity>>
133
+ ) => {
134
+ // Save state for rollback
135
+ get()._saveRollbackState();
136
+
137
+ // Optimistic update
138
+ set((state) => ({
139
+ funnels: state.funnels.map((f) =>
140
+ f.id === id ? ({ ...f, ...data } as Funnel<TEntity>) : f
141
+ ),
142
+ selectedFunnel:
143
+ state.selectedFunnel?.id === id
144
+ ? ({ ...state.selectedFunnel, ...data } as Funnel<TEntity>)
145
+ : state.selectedFunnel,
146
+ isDirty: false, // Clear dirty flag on save
147
+ }));
148
+
149
+ try {
150
+ const updated = await apiClient.updateFunnel<TEntity>(id, data);
151
+
152
+ // Update with server response
153
+ set((state) => ({
154
+ funnels: state.funnels.map((f) => (f.id === id ? updated : f)),
155
+ selectedFunnel:
156
+ state.selectedFunnel?.id === id ? updated : state.selectedFunnel,
157
+ }));
158
+
159
+ get()._clearRollback();
160
+ return updated;
161
+ } catch (error) {
162
+ // Rollback optimistic update
163
+ get()._rollback();
164
+ set({ error: error as Error });
165
+ throw error;
166
+ }
167
+ },
168
+
169
+ deleteFunnel: async (id: string) => {
170
+ // Save state for rollback
171
+ get()._saveRollbackState();
172
+
173
+ // Optimistic update
174
+ set((state) => ({
175
+ funnels: state.funnels.filter((f) => f.id !== id),
176
+ selectedFunnel: state.selectedFunnel?.id === id ? null : state.selectedFunnel,
177
+ pagination: {
178
+ ...state.pagination,
179
+ count: state.pagination.count - 1,
180
+ },
181
+ }));
182
+
183
+ try {
184
+ await apiClient.deleteFunnel(id);
185
+ get()._clearRollback();
186
+ } catch (error) {
187
+ // Rollback optimistic update
188
+ get()._rollback();
189
+ set({ error: error as Error });
190
+ throw error;
191
+ }
192
+ },
193
+
194
+ duplicateFunnel: async (id: string) => {
195
+ const { funnels } = get();
196
+ const funnel = funnels.find((f) => f.id === id);
197
+
198
+ if (!funnel) {
199
+ throw new Error(`Funnel ${id} not found`);
200
+ }
201
+
202
+ // Create copy with new name
203
+ const copy: CreateFunnelInput<TEntity> = {
204
+ name: `${funnel.name} (Copy)`,
205
+ description: funnel.description,
206
+ status: 'draft', // Always create as draft
207
+ input_type: funnel.input_type,
208
+ stages: funnel.stages.map((stage, index) => ({
209
+ ...stage,
210
+ order: index, // Preserve order
211
+ })),
212
+ completion_tags: funnel.completion_tags,
213
+ metadata: funnel.metadata,
214
+ };
215
+
216
+ return get().createFunnel(copy);
217
+ },
218
+
219
+ // =========================================================================
220
+ // Stage Actions
221
+ // =========================================================================
222
+
223
+ selectStage: (stageId: string | null) => {
224
+ const { selectedFunnel } = get();
225
+
226
+ if (!selectedFunnel) {
227
+ set({ selectedStage: null });
228
+ return;
229
+ }
230
+
231
+ if (stageId === null) {
232
+ set({ selectedStage: null });
233
+ return;
234
+ }
235
+
236
+ const stage = selectedFunnel.stages.find((s) => s.id === stageId);
237
+ set({ selectedStage: stage || null });
238
+ },
239
+
240
+ createStage: async (funnelId: string, data: CreateStageInput<TEntity>) => {
241
+ set({ isLoading: true, error: null });
242
+
243
+ try {
244
+ const stage = await apiClient.createStage<TEntity>(funnelId, data);
245
+
246
+ // Update funnel with new stage
247
+ set((state) => ({
248
+ funnels: state.funnels.map((f) =>
249
+ f.id === funnelId
250
+ ? { ...f, stages: [...f.stages, stage] }
251
+ : f
252
+ ),
253
+ selectedFunnel:
254
+ state.selectedFunnel?.id === funnelId
255
+ ? { ...state.selectedFunnel, stages: [...state.selectedFunnel.stages, stage] }
256
+ : state.selectedFunnel,
257
+ isLoading: false,
258
+ }));
259
+
260
+ return stage;
261
+ } catch (error) {
262
+ set({
263
+ error: error as Error,
264
+ isLoading: false,
265
+ });
266
+ throw error;
267
+ }
268
+ },
269
+
270
+ updateStage: async (
271
+ funnelId: string,
272
+ stageId: string,
273
+ data: Partial<UpdateStageInput<TEntity>>
274
+ ) => {
275
+ // Save state for rollback
276
+ get()._saveRollbackState();
277
+
278
+ // Optimistic update
279
+ set((state) => ({
280
+ funnels: state.funnels.map((f) =>
281
+ f.id === funnelId
282
+ ? {
283
+ ...f,
284
+ stages: f.stages.map((s) =>
285
+ s.id === stageId ? ({ ...s, ...data } as FunnelStage<TEntity>) : s
286
+ ),
287
+ }
288
+ : f
289
+ ),
290
+ selectedFunnel:
291
+ state.selectedFunnel?.id === funnelId
292
+ ? {
293
+ ...state.selectedFunnel,
294
+ stages: state.selectedFunnel.stages.map((s) =>
295
+ s.id === stageId ? ({ ...s, ...data } as FunnelStage<TEntity>) : s
296
+ ),
297
+ }
298
+ : state.selectedFunnel,
299
+ selectedStage:
300
+ state.selectedStage?.id === stageId
301
+ ? ({ ...state.selectedStage, ...data } as FunnelStage<TEntity>)
302
+ : state.selectedStage,
303
+ isDirty: false,
304
+ }));
305
+
306
+ try {
307
+ const updated = await apiClient.updateStage<TEntity>(
308
+ funnelId,
309
+ stageId,
310
+ data
311
+ );
312
+
313
+ // Update with server response
314
+ set((state) => ({
315
+ funnels: state.funnels.map((f) =>
316
+ f.id === funnelId
317
+ ? {
318
+ ...f,
319
+ stages: f.stages.map((s) => (s.id === stageId ? updated : s)),
320
+ }
321
+ : f
322
+ ),
323
+ selectedFunnel:
324
+ state.selectedFunnel?.id === funnelId
325
+ ? {
326
+ ...state.selectedFunnel,
327
+ stages: state.selectedFunnel.stages.map((s) =>
328
+ s.id === stageId ? updated : s
329
+ ),
330
+ }
331
+ : state.selectedFunnel,
332
+ selectedStage:
333
+ state.selectedStage?.id === stageId ? updated : state.selectedStage,
334
+ }));
335
+
336
+ get()._clearRollback();
337
+ return updated;
338
+ } catch (error) {
339
+ // Rollback optimistic update
340
+ get()._rollback();
341
+ set({ error: error as Error });
342
+ throw error;
343
+ }
344
+ },
345
+
346
+ deleteStage: async (funnelId: string, stageId: string) => {
347
+ // Save state for rollback
348
+ get()._saveRollbackState();
349
+
350
+ // Optimistic update
351
+ set((state) => ({
352
+ funnels: state.funnels.map((f) =>
353
+ f.id === funnelId
354
+ ? {
355
+ ...f,
356
+ stages: f.stages.filter((s) => s.id !== stageId),
357
+ }
358
+ : f
359
+ ),
360
+ selectedFunnel:
361
+ state.selectedFunnel?.id === funnelId
362
+ ? {
363
+ ...state.selectedFunnel,
364
+ stages: state.selectedFunnel.stages.filter((s) => s.id !== stageId),
365
+ }
366
+ : state.selectedFunnel,
367
+ selectedStage: state.selectedStage?.id === stageId ? null : state.selectedStage,
368
+ }));
369
+
370
+ try {
371
+ await apiClient.deleteStage(funnelId, stageId);
372
+ get()._clearRollback();
373
+ } catch (error) {
374
+ // Rollback optimistic update
375
+ get()._rollback();
376
+ set({ error: error as Error });
377
+ throw error;
378
+ }
379
+ },
380
+
381
+ reorderStages: async (funnelId: string, stageIds: string[]) => {
382
+ const { selectedFunnel } = get();
383
+
384
+ if (!selectedFunnel || selectedFunnel.id !== funnelId) {
385
+ throw new Error('Funnel must be selected to reorder stages');
386
+ }
387
+
388
+ // Save state for rollback
389
+ get()._saveRollbackState();
390
+
391
+ // Optimistic update - reorder stages
392
+ const reorderedStages = stageIds
393
+ .map((id, index) => {
394
+ const stage = selectedFunnel.stages.find((s) => s.id === id);
395
+ return stage ? { ...stage, order: index } : null;
396
+ })
397
+ .filter((s): s is FunnelStage<TEntity> => s !== null);
398
+
399
+ set((state) => ({
400
+ funnels: state.funnels.map((f) =>
401
+ f.id === funnelId ? { ...f, stages: reorderedStages } : f
402
+ ),
403
+ selectedFunnel: { ...selectedFunnel, stages: reorderedStages },
404
+ isDirty: false,
405
+ }));
406
+
407
+ try {
408
+ // Update each stage with new order
409
+ await Promise.all(
410
+ reorderedStages.map((stage) =>
411
+ apiClient.updateStage(funnelId, stage.id, { order: stage.order })
412
+ )
413
+ );
414
+
415
+ get()._clearRollback();
416
+ } catch (error) {
417
+ // Rollback optimistic update
418
+ get()._rollback();
419
+ set({ error: error as Error });
420
+ throw error;
421
+ }
422
+ },
423
+
424
+ // =========================================================================
425
+ // Run Actions
426
+ // =========================================================================
427
+
428
+ runFunnel: async (id: string, options?) => {
429
+ set({ isLoading: true, error: null });
430
+
431
+ try {
432
+ const run = await apiClient.runFunnel(id, options);
433
+
434
+ // Add to runs list
435
+ set((state) => ({
436
+ runs: [run, ...state.runs],
437
+ isLoading: false,
438
+ }));
439
+
440
+ return run;
441
+ } catch (error) {
442
+ set({
443
+ error: error as Error,
444
+ isLoading: false,
445
+ });
446
+ throw error;
447
+ }
448
+ },
449
+
450
+ loadRuns: async (funnelId: string, filters?) => {
451
+ set({ isLoading: true, error: null });
452
+
453
+ try {
454
+ const response = await apiClient.getFunnelRuns(funnelId, filters);
455
+
456
+ set({
457
+ runs: response.results,
458
+ isLoading: false,
459
+ });
460
+ } catch (error) {
461
+ set({
462
+ error: error as Error,
463
+ isLoading: false,
464
+ });
465
+ throw error;
466
+ }
467
+ },
468
+
469
+ cancelRun: async (runId: string) => {
470
+ set({ isLoading: true, error: null });
471
+
472
+ try {
473
+ const run = await apiClient.cancelFunnelRun(runId);
474
+
475
+ // Update run in list
476
+ set((state) => ({
477
+ runs: state.runs.map((r) => (r.id === runId ? run : r)),
478
+ isLoading: false,
479
+ }));
480
+
481
+ return run;
482
+ } catch (error) {
483
+ set({
484
+ error: error as Error,
485
+ isLoading: false,
486
+ });
487
+ throw error;
488
+ }
489
+ },
490
+
491
+ // =========================================================================
492
+ // UI State Actions
493
+ // =========================================================================
494
+
495
+ setDirty: (dirty: boolean) => {
496
+ set({ isDirty: dirty });
497
+ },
498
+
499
+ clearError: () => {
500
+ set({ error: null });
501
+ },
502
+
503
+ reset: () => {
504
+ set(createInitialState<TEntity>());
505
+ },
506
+
507
+ // =========================================================================
508
+ // Internal Actions (Optimistic Updates)
509
+ // =========================================================================
510
+
511
+ _saveRollbackState: () => {
512
+ const { funnels, selectedFunnel } = get();
513
+ set({
514
+ rollbackState: {
515
+ funnels: JSON.parse(JSON.stringify(funnels)) as Funnel<TEntity>[],
516
+ selectedFunnel: selectedFunnel
517
+ ? (JSON.parse(JSON.stringify(selectedFunnel)) as Funnel<TEntity>)
518
+ : null,
519
+ },
520
+ });
521
+ },
522
+
523
+ _rollback: () => {
524
+ const { rollbackState } = get();
525
+ if (rollbackState) {
526
+ set({
527
+ funnels: rollbackState.funnels,
528
+ selectedFunnel: rollbackState.selectedFunnel,
529
+ rollbackState: null,
530
+ });
531
+ }
532
+ },
533
+
534
+ _clearRollback: () => {
535
+ set({ rollbackState: null });
536
+ },
537
+ }));
538
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Store exports for @startsimpli/funnels
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export { createFunnelStore } from './create-funnel-store';
8
+ export type { FunnelStore } from './types';
9
+ export { createInitialState } from './types';