@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.
- 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
package/dist/index.cjs
DELETED
|
@@ -1,4499 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var zustand = require('zustand');
|
|
4
|
-
var react = require('react');
|
|
5
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
-
var react$1 = require('@xyflow/react');
|
|
7
|
-
require('@xyflow/react/dist/style.css');
|
|
8
|
-
var core = require('@dnd-kit/core');
|
|
9
|
-
var sortable = require('@dnd-kit/sortable');
|
|
10
|
-
var utilities = require('@dnd-kit/utilities');
|
|
11
|
-
|
|
12
|
-
// src/types/index.ts
|
|
13
|
-
function isFunnel(value) {
|
|
14
|
-
const f = value;
|
|
15
|
-
return typeof f === "object" && f !== null && typeof f.id === "string" && typeof f.name === "string" && ["draft", "active", "paused", "archived"].includes(f.status) && Array.isArray(f.stages);
|
|
16
|
-
}
|
|
17
|
-
function isStage(value) {
|
|
18
|
-
const s = value;
|
|
19
|
-
return typeof s === "object" && s !== null && typeof s.id === "string" && typeof s.name === "string" && typeof s.order === "number" && ["AND", "OR"].includes(s.filter_logic) && Array.isArray(s.rules);
|
|
20
|
-
}
|
|
21
|
-
function isFilterRule(value) {
|
|
22
|
-
const r = value;
|
|
23
|
-
return typeof r === "object" && r !== null && typeof r.field_path === "string" && typeof r.operator === "string" && r.value !== void 0;
|
|
24
|
-
}
|
|
25
|
-
function isFunnelRun(value) {
|
|
26
|
-
const r = value;
|
|
27
|
-
return typeof r === "object" && r !== null && typeof r.id === "string" && typeof r.funnel_id === "string" && ["pending", "running", "completed", "failed", "cancelled"].includes(r.status);
|
|
28
|
-
}
|
|
29
|
-
function isFunnelResult(value) {
|
|
30
|
-
const r = value;
|
|
31
|
-
return typeof r === "object" && r !== null && r.entity !== void 0 && typeof r.matched === "boolean" && Array.isArray(r.accumulated_tags);
|
|
32
|
-
}
|
|
33
|
-
function isFieldDefinition(value) {
|
|
34
|
-
const f = value;
|
|
35
|
-
return typeof f === "object" && f !== null && typeof f.name === "string" && typeof f.label === "string" && typeof f.type === "string" && Array.isArray(f.operators);
|
|
36
|
-
}
|
|
37
|
-
function getValidOperators(fieldType) {
|
|
38
|
-
switch (fieldType) {
|
|
39
|
-
case "string":
|
|
40
|
-
return [
|
|
41
|
-
"eq",
|
|
42
|
-
"ne",
|
|
43
|
-
"contains",
|
|
44
|
-
"not_contains",
|
|
45
|
-
"startswith",
|
|
46
|
-
"endswith",
|
|
47
|
-
"matches",
|
|
48
|
-
"in",
|
|
49
|
-
"not_in",
|
|
50
|
-
"isnull",
|
|
51
|
-
"isnotnull"
|
|
52
|
-
];
|
|
53
|
-
case "number":
|
|
54
|
-
return [
|
|
55
|
-
"eq",
|
|
56
|
-
"ne",
|
|
57
|
-
"gt",
|
|
58
|
-
"lt",
|
|
59
|
-
"gte",
|
|
60
|
-
"lte",
|
|
61
|
-
"in",
|
|
62
|
-
"not_in",
|
|
63
|
-
"isnull",
|
|
64
|
-
"isnotnull"
|
|
65
|
-
];
|
|
66
|
-
case "boolean":
|
|
67
|
-
return ["eq", "ne", "is_true", "is_false", "isnull", "isnotnull"];
|
|
68
|
-
case "date":
|
|
69
|
-
return [
|
|
70
|
-
"eq",
|
|
71
|
-
"ne",
|
|
72
|
-
"gt",
|
|
73
|
-
"lt",
|
|
74
|
-
"gte",
|
|
75
|
-
"lte",
|
|
76
|
-
"isnull",
|
|
77
|
-
"isnotnull"
|
|
78
|
-
];
|
|
79
|
-
case "array":
|
|
80
|
-
return [
|
|
81
|
-
"in",
|
|
82
|
-
"not_in",
|
|
83
|
-
"has_any",
|
|
84
|
-
"has_all",
|
|
85
|
-
"isnull",
|
|
86
|
-
"isnotnull"
|
|
87
|
-
];
|
|
88
|
-
case "tag":
|
|
89
|
-
return ["has_tag", "not_has_tag"];
|
|
90
|
-
case "object":
|
|
91
|
-
return ["isnull", "isnotnull"];
|
|
92
|
-
case "any":
|
|
93
|
-
default:
|
|
94
|
-
return [
|
|
95
|
-
"eq",
|
|
96
|
-
"ne",
|
|
97
|
-
"gt",
|
|
98
|
-
"lt",
|
|
99
|
-
"gte",
|
|
100
|
-
"lte",
|
|
101
|
-
"contains",
|
|
102
|
-
"not_contains",
|
|
103
|
-
"startswith",
|
|
104
|
-
"endswith",
|
|
105
|
-
"in",
|
|
106
|
-
"not_in",
|
|
107
|
-
"isnull",
|
|
108
|
-
"isnotnull"
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function isValidOperator(operator, fieldType) {
|
|
113
|
-
const validOps = getValidOperators(fieldType);
|
|
114
|
-
return validOps.includes(operator);
|
|
115
|
-
}
|
|
116
|
-
function validateFilterRule(rule) {
|
|
117
|
-
const errors = [];
|
|
118
|
-
if (!rule.field_path) {
|
|
119
|
-
errors.push("field_path is required");
|
|
120
|
-
}
|
|
121
|
-
if (!rule.operator) {
|
|
122
|
-
errors.push("operator is required");
|
|
123
|
-
}
|
|
124
|
-
const nullOps = ["isnull", "isnotnull", "is_true", "is_false"];
|
|
125
|
-
if (!nullOps.includes(rule.operator) && rule.value === void 0) {
|
|
126
|
-
errors.push(`value is required for operator '${rule.operator}'`);
|
|
127
|
-
}
|
|
128
|
-
return errors;
|
|
129
|
-
}
|
|
130
|
-
function validateStage(stage) {
|
|
131
|
-
const errors = [];
|
|
132
|
-
if (!stage.name) {
|
|
133
|
-
errors.push("name is required");
|
|
134
|
-
}
|
|
135
|
-
if (typeof stage.order !== "number") {
|
|
136
|
-
errors.push("order must be a number");
|
|
137
|
-
}
|
|
138
|
-
if (!["AND", "OR"].includes(stage.filter_logic)) {
|
|
139
|
-
errors.push("filter_logic must be AND or OR");
|
|
140
|
-
}
|
|
141
|
-
if (!Array.isArray(stage.rules)) {
|
|
142
|
-
errors.push("rules must be an array");
|
|
143
|
-
} else {
|
|
144
|
-
stage.rules.forEach((rule, i) => {
|
|
145
|
-
const ruleErrors = validateFilterRule(rule);
|
|
146
|
-
ruleErrors.forEach((err) => errors.push(`rules[${i}]: ${err}`));
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
return errors;
|
|
150
|
-
}
|
|
151
|
-
function validateFunnel(funnel) {
|
|
152
|
-
const errors = [];
|
|
153
|
-
if (!funnel.name) {
|
|
154
|
-
errors.push("name is required");
|
|
155
|
-
}
|
|
156
|
-
if (!["draft", "active", "paused", "archived"].includes(funnel.status)) {
|
|
157
|
-
errors.push("status must be draft, active, paused, or archived");
|
|
158
|
-
}
|
|
159
|
-
if (!Array.isArray(funnel.stages)) {
|
|
160
|
-
errors.push("stages must be an array");
|
|
161
|
-
} else {
|
|
162
|
-
funnel.stages.forEach((stage, i) => {
|
|
163
|
-
const stageErrors = validateStage(stage);
|
|
164
|
-
stageErrors.forEach((err) => errors.push(`stages[${i}]: ${err}`));
|
|
165
|
-
});
|
|
166
|
-
const orders = funnel.stages.map((s) => s.order).sort((a, b) => a - b);
|
|
167
|
-
const expectedOrders = Array.from({ length: orders.length }, (_, i) => i);
|
|
168
|
-
if (JSON.stringify(orders) !== JSON.stringify(expectedOrders)) {
|
|
169
|
-
errors.push("stage orders must be sequential starting from 0");
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return errors;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// src/core/field-resolver.ts
|
|
176
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
177
|
-
function parseFieldPath(fieldPath) {
|
|
178
|
-
const segments = [];
|
|
179
|
-
let current = "";
|
|
180
|
-
let inBracket = false;
|
|
181
|
-
for (let i = 0; i < fieldPath.length; i++) {
|
|
182
|
-
const char = fieldPath[i];
|
|
183
|
-
if (char === "[") {
|
|
184
|
-
if (current) {
|
|
185
|
-
segments.push(current);
|
|
186
|
-
current = "";
|
|
187
|
-
}
|
|
188
|
-
inBracket = true;
|
|
189
|
-
} else if (char === "]") {
|
|
190
|
-
if (current) {
|
|
191
|
-
segments.push(current);
|
|
192
|
-
current = "";
|
|
193
|
-
}
|
|
194
|
-
inBracket = false;
|
|
195
|
-
} else if (char === "." && !inBracket) {
|
|
196
|
-
if (current) {
|
|
197
|
-
segments.push(current);
|
|
198
|
-
current = "";
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
current += char;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (current) {
|
|
205
|
-
segments.push(current);
|
|
206
|
-
}
|
|
207
|
-
return segments;
|
|
208
|
-
}
|
|
209
|
-
function resolveField(entity, fieldPath) {
|
|
210
|
-
if (entity == null) {
|
|
211
|
-
return void 0;
|
|
212
|
-
}
|
|
213
|
-
if (!fieldPath || fieldPath.trim() === "") {
|
|
214
|
-
return void 0;
|
|
215
|
-
}
|
|
216
|
-
const segments = parseFieldPath(fieldPath);
|
|
217
|
-
let current = entity;
|
|
218
|
-
for (const segment of segments) {
|
|
219
|
-
if (current == null) {
|
|
220
|
-
return void 0;
|
|
221
|
-
}
|
|
222
|
-
if (DANGEROUS_KEYS.has(segment)) {
|
|
223
|
-
return void 0;
|
|
224
|
-
}
|
|
225
|
-
if (typeof current === "object" && segment in current) {
|
|
226
|
-
current = current[segment];
|
|
227
|
-
} else {
|
|
228
|
-
return void 0;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return current;
|
|
232
|
-
}
|
|
233
|
-
function setField(entity, fieldPath, value) {
|
|
234
|
-
if (entity == null) {
|
|
235
|
-
throw new Error("Cannot set field on null or undefined entity");
|
|
236
|
-
}
|
|
237
|
-
if (!fieldPath || fieldPath.trim() === "") {
|
|
238
|
-
throw new Error("Field path cannot be empty");
|
|
239
|
-
}
|
|
240
|
-
const segments = parseFieldPath(fieldPath);
|
|
241
|
-
let current = entity;
|
|
242
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
243
|
-
const segment = segments[i];
|
|
244
|
-
const nextSegment = segments[i + 1];
|
|
245
|
-
if (DANGEROUS_KEYS.has(segment)) {
|
|
246
|
-
throw new Error(`Dangerous field path segment: "${segment}"`);
|
|
247
|
-
}
|
|
248
|
-
if (!(segment in current)) {
|
|
249
|
-
const isNextArray = /^\d+$/.test(nextSegment);
|
|
250
|
-
current[segment] = isNextArray ? [] : {};
|
|
251
|
-
}
|
|
252
|
-
current = current[segment];
|
|
253
|
-
}
|
|
254
|
-
const lastSegment = segments[segments.length - 1];
|
|
255
|
-
if (DANGEROUS_KEYS.has(lastSegment)) {
|
|
256
|
-
throw new Error(`Dangerous field path segment: "${lastSegment}"`);
|
|
257
|
-
}
|
|
258
|
-
current[lastSegment] = value;
|
|
259
|
-
}
|
|
260
|
-
function hasField(entity, fieldPath) {
|
|
261
|
-
const value = resolveField(entity, fieldPath);
|
|
262
|
-
return value !== void 0;
|
|
263
|
-
}
|
|
264
|
-
function getFields(entity, fieldPaths) {
|
|
265
|
-
const result = {};
|
|
266
|
-
for (const path of fieldPaths) {
|
|
267
|
-
result[path] = resolveField(entity, path);
|
|
268
|
-
}
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// src/core/operators.ts
|
|
273
|
-
function applyOperator(operator, actual, expected) {
|
|
274
|
-
switch (operator) {
|
|
275
|
-
// ========================================================================
|
|
276
|
-
// Equality
|
|
277
|
-
// ========================================================================
|
|
278
|
-
case "eq":
|
|
279
|
-
return compareValues(actual, expected, (a, e) => a === e);
|
|
280
|
-
case "ne":
|
|
281
|
-
return compareValues(actual, expected, (a, e) => a !== e);
|
|
282
|
-
// ========================================================================
|
|
283
|
-
// Comparison (numbers, dates, strings)
|
|
284
|
-
// ========================================================================
|
|
285
|
-
case "gt":
|
|
286
|
-
return compareValues(actual, expected, (a, e) => a > e);
|
|
287
|
-
case "lt":
|
|
288
|
-
return compareValues(actual, expected, (a, e) => a < e);
|
|
289
|
-
case "gte":
|
|
290
|
-
return compareValues(actual, expected, (a, e) => a >= e);
|
|
291
|
-
case "lte":
|
|
292
|
-
return compareValues(actual, expected, (a, e) => a <= e);
|
|
293
|
-
// ========================================================================
|
|
294
|
-
// String operations (case-insensitive)
|
|
295
|
-
// ========================================================================
|
|
296
|
-
case "contains":
|
|
297
|
-
if (actual == null) return false;
|
|
298
|
-
if (expected == null) return false;
|
|
299
|
-
return String(actual).toLowerCase().includes(String(expected).toLowerCase());
|
|
300
|
-
case "not_contains":
|
|
301
|
-
if (actual == null) return true;
|
|
302
|
-
if (expected == null) return true;
|
|
303
|
-
return !String(actual).toLowerCase().includes(String(expected).toLowerCase());
|
|
304
|
-
case "startswith":
|
|
305
|
-
if (actual == null) return false;
|
|
306
|
-
if (expected == null) return false;
|
|
307
|
-
return String(actual).toLowerCase().startsWith(String(expected).toLowerCase());
|
|
308
|
-
case "endswith":
|
|
309
|
-
if (actual == null) return false;
|
|
310
|
-
if (expected == null) return false;
|
|
311
|
-
return String(actual).toLowerCase().endsWith(String(expected).toLowerCase());
|
|
312
|
-
case "matches":
|
|
313
|
-
if (actual == null) return false;
|
|
314
|
-
if (expected == null) return false;
|
|
315
|
-
try {
|
|
316
|
-
const regex = new RegExp(String(expected));
|
|
317
|
-
return regex.test(String(actual));
|
|
318
|
-
} catch {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
// ========================================================================
|
|
322
|
-
// Array/Set operations
|
|
323
|
-
// ========================================================================
|
|
324
|
-
case "in":
|
|
325
|
-
if (!Array.isArray(expected)) return false;
|
|
326
|
-
return expected.some((item) => compareValues(actual, item, (a, e) => a === e));
|
|
327
|
-
case "not_in":
|
|
328
|
-
if (!Array.isArray(expected)) return true;
|
|
329
|
-
return !expected.some((item) => compareValues(actual, item, (a, e) => a === e));
|
|
330
|
-
case "has_any":
|
|
331
|
-
if (!Array.isArray(actual)) return false;
|
|
332
|
-
if (!Array.isArray(expected)) return false;
|
|
333
|
-
return expected.some(
|
|
334
|
-
(expectedItem) => actual.some((actualItem) => compareValues(actualItem, expectedItem, (a, e) => a === e))
|
|
335
|
-
);
|
|
336
|
-
case "has_all":
|
|
337
|
-
if (!Array.isArray(actual)) return false;
|
|
338
|
-
if (!Array.isArray(expected)) return false;
|
|
339
|
-
return expected.every(
|
|
340
|
-
(expectedItem) => actual.some((actualItem) => compareValues(actualItem, expectedItem, (a, e) => a === e))
|
|
341
|
-
);
|
|
342
|
-
// ========================================================================
|
|
343
|
-
// Null checks
|
|
344
|
-
// ========================================================================
|
|
345
|
-
case "isnull":
|
|
346
|
-
return actual === null || actual === void 0;
|
|
347
|
-
case "isnotnull":
|
|
348
|
-
return actual !== null && actual !== void 0;
|
|
349
|
-
// ========================================================================
|
|
350
|
-
// Tag operations (tags are arrays of strings)
|
|
351
|
-
// ========================================================================
|
|
352
|
-
case "has_tag":
|
|
353
|
-
if (!Array.isArray(actual)) return false;
|
|
354
|
-
if (expected == null) return false;
|
|
355
|
-
const expectedTag = String(expected).toLowerCase();
|
|
356
|
-
return actual.some((tag) => String(tag).toLowerCase() === expectedTag);
|
|
357
|
-
case "not_has_tag":
|
|
358
|
-
if (!Array.isArray(actual)) return true;
|
|
359
|
-
if (expected == null) return true;
|
|
360
|
-
const expectedTagNot = String(expected).toLowerCase();
|
|
361
|
-
return !actual.some((tag) => String(tag).toLowerCase() === expectedTagNot);
|
|
362
|
-
// ========================================================================
|
|
363
|
-
// Boolean
|
|
364
|
-
// ========================================================================
|
|
365
|
-
case "is_true":
|
|
366
|
-
return actual === true;
|
|
367
|
-
case "is_false":
|
|
368
|
-
return actual === false;
|
|
369
|
-
default:
|
|
370
|
-
throw new Error(`Unknown operator: ${operator}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
function compareValues(actual, expected, compare) {
|
|
374
|
-
if (actual === null || actual === void 0) {
|
|
375
|
-
return expected === null || expected === void 0;
|
|
376
|
-
}
|
|
377
|
-
if (expected === null || expected === void 0) {
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
if (isDate(actual) && isDate(expected)) {
|
|
381
|
-
const actualTs = toTimestamp(actual);
|
|
382
|
-
const expectedTs = toTimestamp(expected);
|
|
383
|
-
if (actualTs === null || expectedTs === null) return false;
|
|
384
|
-
return compare(actualTs, expectedTs);
|
|
385
|
-
}
|
|
386
|
-
if (isDate(actual) || isDate(expected)) {
|
|
387
|
-
const actualTs = toTimestamp(actual);
|
|
388
|
-
const expectedTs = toTimestamp(expected);
|
|
389
|
-
if (actualTs === null || expectedTs === null) return false;
|
|
390
|
-
return compare(actualTs, expectedTs);
|
|
391
|
-
}
|
|
392
|
-
if (typeof actual === "number" && typeof expected === "number") {
|
|
393
|
-
return compare(actual, expected);
|
|
394
|
-
}
|
|
395
|
-
if (isNumeric(actual) && isNumeric(expected)) {
|
|
396
|
-
return compare(Number(actual), Number(expected));
|
|
397
|
-
}
|
|
398
|
-
return compare(actual, expected);
|
|
399
|
-
}
|
|
400
|
-
function isDate(value) {
|
|
401
|
-
if (value instanceof Date) return true;
|
|
402
|
-
if (typeof value !== "string") return false;
|
|
403
|
-
const datePattern = /^\d{4}-\d{2}-\d{2}|^\d{1,2}\/\d{1,2}\/\d{4}/;
|
|
404
|
-
return datePattern.test(value);
|
|
405
|
-
}
|
|
406
|
-
function toTimestamp(value) {
|
|
407
|
-
if (value instanceof Date) {
|
|
408
|
-
const ts = value.getTime();
|
|
409
|
-
return isNaN(ts) ? null : ts;
|
|
410
|
-
}
|
|
411
|
-
if (typeof value === "string" || typeof value === "number") {
|
|
412
|
-
const date = new Date(value);
|
|
413
|
-
const ts = date.getTime();
|
|
414
|
-
return isNaN(ts) ? null : ts;
|
|
415
|
-
}
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
function isNumeric(value) {
|
|
419
|
-
if (typeof value === "number") return !isNaN(value);
|
|
420
|
-
if (typeof value !== "string") return false;
|
|
421
|
-
return !isNaN(Number(value)) && value.trim() !== "";
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// src/core/evaluator.ts
|
|
425
|
-
function evaluateRule(entity, rule) {
|
|
426
|
-
const actualValue = resolveField(entity, rule.field_path);
|
|
427
|
-
const result = applyOperator(rule.operator, actualValue, rule.value);
|
|
428
|
-
return rule.negate ? !result : result;
|
|
429
|
-
}
|
|
430
|
-
function evaluateRuleWithResult(entity, rule) {
|
|
431
|
-
try {
|
|
432
|
-
const actualValue = resolveField(entity, rule.field_path);
|
|
433
|
-
const operatorResult = applyOperator(rule.operator, actualValue, rule.value);
|
|
434
|
-
const matched = rule.negate ? !operatorResult : operatorResult;
|
|
435
|
-
return {
|
|
436
|
-
field_path: rule.field_path,
|
|
437
|
-
operator: rule.operator,
|
|
438
|
-
value: rule.value,
|
|
439
|
-
actual_value: actualValue,
|
|
440
|
-
matched
|
|
441
|
-
};
|
|
442
|
-
} catch (error) {
|
|
443
|
-
return {
|
|
444
|
-
field_path: rule.field_path,
|
|
445
|
-
operator: rule.operator,
|
|
446
|
-
value: rule.value,
|
|
447
|
-
actual_value: void 0,
|
|
448
|
-
matched: false,
|
|
449
|
-
error: error instanceof Error ? error.message : String(error)
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
function evaluateRulesAND(entity, rules) {
|
|
454
|
-
if (!rules || rules.length === 0) return true;
|
|
455
|
-
return rules.every((rule) => evaluateRule(entity, rule));
|
|
456
|
-
}
|
|
457
|
-
function evaluateRulesOR(entity, rules) {
|
|
458
|
-
if (!rules || rules.length === 0) return true;
|
|
459
|
-
return rules.some((rule) => evaluateRule(entity, rule));
|
|
460
|
-
}
|
|
461
|
-
function evaluateRules(entity, rules, logic = "AND") {
|
|
462
|
-
return logic === "AND" ? evaluateRulesAND(entity, rules) : evaluateRulesOR(entity, rules);
|
|
463
|
-
}
|
|
464
|
-
function evaluateRulesWithResults(entity, rules, logic = "AND") {
|
|
465
|
-
const ruleResults = rules.map((rule) => evaluateRuleWithResult(entity, rule));
|
|
466
|
-
const matched = logic === "AND" ? ruleResults.every((r) => r.matched) : ruleResults.some((r) => r.matched);
|
|
467
|
-
return {
|
|
468
|
-
matched,
|
|
469
|
-
logic,
|
|
470
|
-
rule_results: ruleResults
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
function filterEntities(entities, rules, logic = "AND") {
|
|
474
|
-
if (!entities || entities.length === 0) return [];
|
|
475
|
-
if (!rules || rules.length === 0) return entities;
|
|
476
|
-
return entities.filter((entity) => evaluateRules(entity, rules, logic));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// src/core/engine.ts
|
|
480
|
-
function evaluateRule2(_entity, _rule) {
|
|
481
|
-
throw new Error("Not implemented - BEAD: fund-your-startup-a0b8. evaluateRule must import from rule evaluator.");
|
|
482
|
-
}
|
|
483
|
-
var FunnelEngine = class {
|
|
484
|
-
/**
|
|
485
|
-
* Execute a funnel on a set of entities
|
|
486
|
-
*
|
|
487
|
-
* @param funnel - The funnel definition to execute
|
|
488
|
-
* @param entities - Input entities to process
|
|
489
|
-
* @returns ExecutionResult with matched/excluded entities and stats
|
|
490
|
-
*/
|
|
491
|
-
execute(funnel, entities) {
|
|
492
|
-
const startTime = Date.now();
|
|
493
|
-
const results = entities.map((entity) => ({
|
|
494
|
-
entity,
|
|
495
|
-
matched: true,
|
|
496
|
-
// Start as matched, exclude as needed
|
|
497
|
-
accumulated_tags: [],
|
|
498
|
-
context: {},
|
|
499
|
-
stage_results: []
|
|
500
|
-
}));
|
|
501
|
-
const stageStats = {};
|
|
502
|
-
const errors = [];
|
|
503
|
-
const sortedStages = [...funnel.stages].sort((a, b) => a.order - b.order);
|
|
504
|
-
for (const stage of sortedStages) {
|
|
505
|
-
const stageStartTime = Date.now();
|
|
506
|
-
const inputEntities = results.filter((r) => r.matched && !r.excluded_at_stage);
|
|
507
|
-
const stats = {
|
|
508
|
-
stage_id: stage.id,
|
|
509
|
-
stage_name: stage.name,
|
|
510
|
-
input_count: inputEntities.length,
|
|
511
|
-
matched_count: 0,
|
|
512
|
-
not_matched_count: 0,
|
|
513
|
-
excluded_count: 0,
|
|
514
|
-
tagged_count: 0,
|
|
515
|
-
continued_count: 0,
|
|
516
|
-
error_count: 0
|
|
517
|
-
};
|
|
518
|
-
for (const result of inputEntities) {
|
|
519
|
-
try {
|
|
520
|
-
const stageResult = this.processStage(stage, result.entity);
|
|
521
|
-
result.stage_results.push(stageResult);
|
|
522
|
-
if (stageResult.matched) {
|
|
523
|
-
stats.matched_count++;
|
|
524
|
-
} else {
|
|
525
|
-
stats.not_matched_count++;
|
|
526
|
-
}
|
|
527
|
-
if (stageResult.tags_added && stageResult.tags_added.length > 0) {
|
|
528
|
-
result.accumulated_tags.push(...stageResult.tags_added);
|
|
529
|
-
stats.tagged_count++;
|
|
530
|
-
}
|
|
531
|
-
if (stageResult.context_added) {
|
|
532
|
-
result.context = { ...result.context, ...stageResult.context_added };
|
|
533
|
-
}
|
|
534
|
-
if (stageResult.excluded) {
|
|
535
|
-
result.matched = false;
|
|
536
|
-
result.excluded_at_stage = stage.id;
|
|
537
|
-
stats.excluded_count++;
|
|
538
|
-
} else if (stageResult.continued) {
|
|
539
|
-
stats.continued_count++;
|
|
540
|
-
}
|
|
541
|
-
} catch (error) {
|
|
542
|
-
stats.error_count++;
|
|
543
|
-
errors.push(`Stage ${stage.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
stats.duration_ms = Date.now() - stageStartTime;
|
|
547
|
-
stageStats[stage.id] = stats;
|
|
548
|
-
}
|
|
549
|
-
if (funnel.completion_tags && funnel.completion_tags.length > 0) {
|
|
550
|
-
for (const result of results) {
|
|
551
|
-
if (result.matched) {
|
|
552
|
-
result.accumulated_tags.push(...funnel.completion_tags);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
const matched = results.filter((r) => r.matched);
|
|
557
|
-
const excluded = results.filter((r) => !r.matched);
|
|
558
|
-
const totalTagged = results.filter((r) => r.accumulated_tags.length > 0).length;
|
|
559
|
-
return {
|
|
560
|
-
matched,
|
|
561
|
-
excluded,
|
|
562
|
-
total_input: entities.length,
|
|
563
|
-
total_matched: matched.length,
|
|
564
|
-
total_excluded: excluded.length,
|
|
565
|
-
total_tagged: totalTagged,
|
|
566
|
-
stage_stats: stageStats,
|
|
567
|
-
duration_ms: Date.now() - startTime,
|
|
568
|
-
errors: errors.length > 0 ? errors : void 0
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Process a single entity through a stage
|
|
573
|
-
*
|
|
574
|
-
* @param stage - The stage to process
|
|
575
|
-
* @param entity - The entity to evaluate
|
|
576
|
-
* @returns StageResult with match status and actions taken
|
|
577
|
-
*/
|
|
578
|
-
processStage(stage, entity) {
|
|
579
|
-
const ruleResults = [];
|
|
580
|
-
let matched = false;
|
|
581
|
-
if (stage.custom_evaluator) {
|
|
582
|
-
try {
|
|
583
|
-
matched = stage.custom_evaluator(entity);
|
|
584
|
-
} catch (error) {
|
|
585
|
-
matched = false;
|
|
586
|
-
}
|
|
587
|
-
} else if (stage.rules.length === 0) {
|
|
588
|
-
matched = true;
|
|
589
|
-
} else {
|
|
590
|
-
for (const rule of stage.rules) {
|
|
591
|
-
const ruleResult = evaluateRule2();
|
|
592
|
-
ruleResults.push(ruleResult);
|
|
593
|
-
}
|
|
594
|
-
if (stage.filter_logic === "AND") {
|
|
595
|
-
matched = ruleResults.every((r) => r.matched);
|
|
596
|
-
} else {
|
|
597
|
-
matched = ruleResults.some((r) => r.matched);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
let action;
|
|
601
|
-
let tagsAdded = [];
|
|
602
|
-
let contextAdded;
|
|
603
|
-
let excluded = false;
|
|
604
|
-
let continued = false;
|
|
605
|
-
if (matched) {
|
|
606
|
-
action = stage.match_action;
|
|
607
|
-
if (stage.match_tags && stage.match_tags.length > 0) {
|
|
608
|
-
tagsAdded = [...stage.match_tags];
|
|
609
|
-
}
|
|
610
|
-
if (stage.match_context) {
|
|
611
|
-
contextAdded = stage.match_context;
|
|
612
|
-
}
|
|
613
|
-
switch (stage.match_action) {
|
|
614
|
-
case "continue":
|
|
615
|
-
continued = true;
|
|
616
|
-
break;
|
|
617
|
-
case "tag":
|
|
618
|
-
excluded = true;
|
|
619
|
-
break;
|
|
620
|
-
case "tag_continue":
|
|
621
|
-
continued = true;
|
|
622
|
-
break;
|
|
623
|
-
case "output":
|
|
624
|
-
continued = false;
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
} else {
|
|
628
|
-
action = stage.no_match_action;
|
|
629
|
-
if (stage.no_match_tags && stage.no_match_tags.length > 0) {
|
|
630
|
-
tagsAdded = [...stage.no_match_tags];
|
|
631
|
-
}
|
|
632
|
-
switch (stage.no_match_action) {
|
|
633
|
-
case "continue":
|
|
634
|
-
continued = true;
|
|
635
|
-
break;
|
|
636
|
-
case "exclude":
|
|
637
|
-
excluded = true;
|
|
638
|
-
break;
|
|
639
|
-
case "tag_exclude":
|
|
640
|
-
excluded = true;
|
|
641
|
-
break;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
return {
|
|
645
|
-
stage_id: stage.id,
|
|
646
|
-
stage_name: stage.name,
|
|
647
|
-
matched,
|
|
648
|
-
rule_results: ruleResults.length > 0 ? ruleResults : void 0,
|
|
649
|
-
action,
|
|
650
|
-
tags_added: tagsAdded.length > 0 ? tagsAdded : void 0,
|
|
651
|
-
context_added: contextAdded,
|
|
652
|
-
excluded,
|
|
653
|
-
continued
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
|
|
658
|
-
// src/api/adapter.ts
|
|
659
|
-
function createApiError(message, status, response, cause) {
|
|
660
|
-
const error = new Error(message);
|
|
661
|
-
error.name = "ApiError";
|
|
662
|
-
error.status = status;
|
|
663
|
-
error.response = response;
|
|
664
|
-
error.cause = cause;
|
|
665
|
-
if (response?.code) {
|
|
666
|
-
error.code = response.code;
|
|
667
|
-
}
|
|
668
|
-
return error;
|
|
669
|
-
}
|
|
670
|
-
function isApiError(error) {
|
|
671
|
-
return error instanceof Error && error.name === "ApiError";
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// src/api/default-adapter.ts
|
|
675
|
-
var FetchAdapter = class {
|
|
676
|
-
constructor(config = {}) {
|
|
677
|
-
this.config = {
|
|
678
|
-
headers: config.headers || {},
|
|
679
|
-
timeout: config.timeout || 3e4,
|
|
680
|
-
parseResponse: config.parseResponse || ((res) => res.json()),
|
|
681
|
-
onError: config.onError || (() => {
|
|
682
|
-
})
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
/**
|
|
686
|
-
* Build fetch options
|
|
687
|
-
*/
|
|
688
|
-
buildOptions(method, data, params) {
|
|
689
|
-
const options = {
|
|
690
|
-
method,
|
|
691
|
-
headers: {
|
|
692
|
-
"Content-Type": "application/json",
|
|
693
|
-
...this.config.headers
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
if (data !== void 0) {
|
|
697
|
-
options.body = JSON.stringify(data);
|
|
698
|
-
}
|
|
699
|
-
return options;
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Build URL with query params
|
|
703
|
-
*/
|
|
704
|
-
buildUrl(url, params) {
|
|
705
|
-
if (!params || Object.keys(params).length === 0) {
|
|
706
|
-
return url;
|
|
707
|
-
}
|
|
708
|
-
const searchParams = new URLSearchParams();
|
|
709
|
-
Object.entries(params).forEach(([key, value]) => {
|
|
710
|
-
if (value !== void 0 && value !== null) {
|
|
711
|
-
if (Array.isArray(value)) {
|
|
712
|
-
value.forEach((v) => searchParams.append(key, String(v)));
|
|
713
|
-
} else {
|
|
714
|
-
searchParams.append(key, String(value));
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
});
|
|
718
|
-
const queryString = searchParams.toString();
|
|
719
|
-
return queryString ? `${url}?${queryString}` : url;
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Fetch with timeout
|
|
723
|
-
*/
|
|
724
|
-
async fetchWithTimeout(url, options) {
|
|
725
|
-
const controller = new AbortController();
|
|
726
|
-
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
727
|
-
try {
|
|
728
|
-
const response = await fetch(url, {
|
|
729
|
-
...options,
|
|
730
|
-
signal: controller.signal
|
|
731
|
-
});
|
|
732
|
-
clearTimeout(timeoutId);
|
|
733
|
-
return response;
|
|
734
|
-
} catch (error) {
|
|
735
|
-
clearTimeout(timeoutId);
|
|
736
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
737
|
-
throw createApiError(
|
|
738
|
-
`Request timeout after ${this.config.timeout}ms`,
|
|
739
|
-
void 0,
|
|
740
|
-
void 0,
|
|
741
|
-
error
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
throw error;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Handle response
|
|
749
|
-
*/
|
|
750
|
-
async handleResponse(response) {
|
|
751
|
-
if (response.ok) {
|
|
752
|
-
if (response.status === 204) {
|
|
753
|
-
return void 0;
|
|
754
|
-
}
|
|
755
|
-
try {
|
|
756
|
-
return await this.config.parseResponse(response);
|
|
757
|
-
} catch (error2) {
|
|
758
|
-
throw createApiError(
|
|
759
|
-
"Failed to parse response",
|
|
760
|
-
response.status,
|
|
761
|
-
void 0,
|
|
762
|
-
error2
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
let errorBody;
|
|
767
|
-
try {
|
|
768
|
-
errorBody = await response.json();
|
|
769
|
-
} catch {
|
|
770
|
-
errorBody = { detail: response.statusText };
|
|
771
|
-
}
|
|
772
|
-
const error = createApiError(
|
|
773
|
-
errorBody.detail || errorBody.message || `HTTP ${response.status}`,
|
|
774
|
-
response.status,
|
|
775
|
-
errorBody
|
|
776
|
-
);
|
|
777
|
-
this.config.onError(error);
|
|
778
|
-
throw error;
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Execute request
|
|
782
|
-
*/
|
|
783
|
-
async request(method, url, data, params) {
|
|
784
|
-
const fullUrl = this.buildUrl(url, params);
|
|
785
|
-
const options = this.buildOptions(method, data, params);
|
|
786
|
-
try {
|
|
787
|
-
const response = await this.fetchWithTimeout(fullUrl, options);
|
|
788
|
-
return await this.handleResponse(response);
|
|
789
|
-
} catch (error) {
|
|
790
|
-
if (error.name === "ApiError") {
|
|
791
|
-
throw error;
|
|
792
|
-
}
|
|
793
|
-
const apiError = createApiError(
|
|
794
|
-
"Network request failed",
|
|
795
|
-
void 0,
|
|
796
|
-
void 0,
|
|
797
|
-
error
|
|
798
|
-
);
|
|
799
|
-
this.config.onError(apiError);
|
|
800
|
-
throw apiError;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* HTTP GET
|
|
805
|
-
*/
|
|
806
|
-
async get(url, params) {
|
|
807
|
-
return this.request("GET", url, void 0, params);
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* HTTP POST
|
|
811
|
-
*/
|
|
812
|
-
async post(url, data) {
|
|
813
|
-
return this.request("POST", url, data);
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* HTTP PATCH
|
|
817
|
-
*/
|
|
818
|
-
async patch(url, data) {
|
|
819
|
-
return this.request("PATCH", url, data);
|
|
820
|
-
}
|
|
821
|
-
/**
|
|
822
|
-
* HTTP DELETE
|
|
823
|
-
*/
|
|
824
|
-
async delete(url) {
|
|
825
|
-
return this.request("DELETE", url);
|
|
826
|
-
}
|
|
827
|
-
};
|
|
828
|
-
|
|
829
|
-
// src/api/client.ts
|
|
830
|
-
var FunnelApiClient = class {
|
|
831
|
-
constructor(adapter, baseUrl) {
|
|
832
|
-
this.adapter = adapter;
|
|
833
|
-
this.baseUrl = baseUrl;
|
|
834
|
-
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* Build full URL for endpoint
|
|
838
|
-
*/
|
|
839
|
-
url(path) {
|
|
840
|
-
return `${this.baseUrl}${path}`;
|
|
841
|
-
}
|
|
842
|
-
// ============================================================================
|
|
843
|
-
// Funnel CRUD
|
|
844
|
-
// ============================================================================
|
|
845
|
-
/**
|
|
846
|
-
* List funnels with optional filters
|
|
847
|
-
*
|
|
848
|
-
* @param filters - Optional filters (status, owner, pagination, etc)
|
|
849
|
-
* @returns Paginated list of funnels
|
|
850
|
-
*/
|
|
851
|
-
async listFunnels(filters) {
|
|
852
|
-
return this.adapter.get(
|
|
853
|
-
this.url("/api/v1/funnels/"),
|
|
854
|
-
filters
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Get single funnel by ID
|
|
859
|
-
*
|
|
860
|
-
* @param id - Funnel ID
|
|
861
|
-
* @returns Funnel detail
|
|
862
|
-
* @throws ApiError with status 404 if not found
|
|
863
|
-
*/
|
|
864
|
-
async getFunnel(id) {
|
|
865
|
-
return this.adapter.get(this.url(`/api/v1/funnels/${id}/`));
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Create new funnel
|
|
869
|
-
*
|
|
870
|
-
* @param data - Funnel creation data
|
|
871
|
-
* @returns Created funnel
|
|
872
|
-
* @throws ApiError with status 400 if validation fails
|
|
873
|
-
*/
|
|
874
|
-
async createFunnel(data) {
|
|
875
|
-
return this.adapter.post(this.url("/api/v1/funnels/"), data);
|
|
876
|
-
}
|
|
877
|
-
/**
|
|
878
|
-
* Update existing funnel
|
|
879
|
-
*
|
|
880
|
-
* @param id - Funnel ID
|
|
881
|
-
* @param data - Funnel update data
|
|
882
|
-
* @returns Updated funnel
|
|
883
|
-
* @throws ApiError with status 404 if not found, 400 if validation fails
|
|
884
|
-
*/
|
|
885
|
-
async updateFunnel(id, data) {
|
|
886
|
-
return this.adapter.patch(
|
|
887
|
-
this.url(`/api/v1/funnels/${id}/`),
|
|
888
|
-
data
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Delete funnel
|
|
893
|
-
*
|
|
894
|
-
* @param id - Funnel ID
|
|
895
|
-
* @throws ApiError with status 404 if not found
|
|
896
|
-
*/
|
|
897
|
-
async deleteFunnel(id) {
|
|
898
|
-
return this.adapter.delete(this.url(`/api/v1/funnels/${id}/`));
|
|
899
|
-
}
|
|
900
|
-
// ============================================================================
|
|
901
|
-
// Stage CRUD
|
|
902
|
-
// ============================================================================
|
|
903
|
-
/**
|
|
904
|
-
* Create stage in funnel
|
|
905
|
-
*
|
|
906
|
-
* @param funnelId - Funnel ID
|
|
907
|
-
* @param data - Stage creation data
|
|
908
|
-
* @returns Created stage
|
|
909
|
-
* @throws ApiError with status 404 if funnel not found, 400 if validation fails
|
|
910
|
-
*/
|
|
911
|
-
async createStage(funnelId, data) {
|
|
912
|
-
return this.adapter.post(
|
|
913
|
-
this.url(`/api/v1/funnels/${funnelId}/stages/`),
|
|
914
|
-
data
|
|
915
|
-
);
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* Update stage
|
|
919
|
-
*
|
|
920
|
-
* @param funnelId - Funnel ID
|
|
921
|
-
* @param stageId - Stage ID
|
|
922
|
-
* @param data - Stage update data
|
|
923
|
-
* @returns Updated stage
|
|
924
|
-
* @throws ApiError with status 404 if not found, 400 if validation fails
|
|
925
|
-
*/
|
|
926
|
-
async updateStage(funnelId, stageId, data) {
|
|
927
|
-
return this.adapter.patch(
|
|
928
|
-
this.url(`/api/v1/funnels/${funnelId}/stages/${stageId}/`),
|
|
929
|
-
data
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Delete stage
|
|
934
|
-
*
|
|
935
|
-
* @param funnelId - Funnel ID
|
|
936
|
-
* @param stageId - Stage ID
|
|
937
|
-
* @throws ApiError with status 404 if not found
|
|
938
|
-
*/
|
|
939
|
-
async deleteStage(funnelId, stageId) {
|
|
940
|
-
return this.adapter.delete(
|
|
941
|
-
this.url(`/api/v1/funnels/${funnelId}/stages/${stageId}/`)
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
// ============================================================================
|
|
945
|
-
// Run Operations
|
|
946
|
-
// ============================================================================
|
|
947
|
-
/**
|
|
948
|
-
* Trigger funnel run
|
|
949
|
-
*
|
|
950
|
-
* @param funnelId - Funnel ID
|
|
951
|
-
* @param options - Optional run configuration (trigger_type, metadata, etc)
|
|
952
|
-
* @returns Created funnel run (status: pending or running)
|
|
953
|
-
* @throws ApiError with status 404 if funnel not found, 400 if validation fails
|
|
954
|
-
*/
|
|
955
|
-
async runFunnel(funnelId, options) {
|
|
956
|
-
return this.adapter.post(
|
|
957
|
-
this.url(`/api/v1/funnels/${funnelId}/run/`),
|
|
958
|
-
options || {}
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Get funnel run history
|
|
963
|
-
*
|
|
964
|
-
* @param funnelId - Funnel ID
|
|
965
|
-
* @param filters - Optional filters (status, pagination, etc)
|
|
966
|
-
* @returns List of funnel runs
|
|
967
|
-
* @throws ApiError with status 404 if funnel not found
|
|
968
|
-
*/
|
|
969
|
-
async getFunnelRuns(funnelId, filters) {
|
|
970
|
-
return this.adapter.get(
|
|
971
|
-
this.url(`/api/v1/funnels/${funnelId}/runs/`),
|
|
972
|
-
filters
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* Get single run detail
|
|
977
|
-
*
|
|
978
|
-
* @param runId - Run ID
|
|
979
|
-
* @returns Funnel run detail
|
|
980
|
-
* @throws ApiError with status 404 if not found
|
|
981
|
-
*/
|
|
982
|
-
async getFunnelRun(runId) {
|
|
983
|
-
return this.adapter.get(this.url(`/api/v1/funnel-runs/${runId}/`));
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Get run results (entities that were processed)
|
|
987
|
-
*
|
|
988
|
-
* @param runId - Run ID
|
|
989
|
-
* @param filters - Optional filters (matched, pagination, etc)
|
|
990
|
-
* @returns Paginated list of results
|
|
991
|
-
* @throws ApiError with status 404 if run not found
|
|
992
|
-
*/
|
|
993
|
-
async getFunnelResults(runId, filters) {
|
|
994
|
-
return this.adapter.get(
|
|
995
|
-
this.url(`/api/v1/funnel-runs/${runId}/results/`),
|
|
996
|
-
filters
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Cancel running funnel
|
|
1001
|
-
*
|
|
1002
|
-
* @param runId - Run ID
|
|
1003
|
-
* @returns Updated run with status 'cancelled'
|
|
1004
|
-
* @throws ApiError with status 404 if not found, 400 if already completed
|
|
1005
|
-
*/
|
|
1006
|
-
async cancelFunnelRun(runId) {
|
|
1007
|
-
return this.adapter.post(
|
|
1008
|
-
this.url(`/api/v1/funnel-runs/${runId}/cancel/`),
|
|
1009
|
-
{}
|
|
1010
|
-
);
|
|
1011
|
-
}
|
|
1012
|
-
// ============================================================================
|
|
1013
|
-
// Client-Side Preview (Local Evaluation)
|
|
1014
|
-
// ============================================================================
|
|
1015
|
-
/**
|
|
1016
|
-
* Preview funnel with sample entities (client-side evaluation)
|
|
1017
|
-
*
|
|
1018
|
-
* Useful for testing funnel logic before running on full dataset.
|
|
1019
|
-
* Does NOT hit the server - evaluates locally.
|
|
1020
|
-
*
|
|
1021
|
-
* Note: This requires the evaluation engine to be available client-side.
|
|
1022
|
-
* If not available, this will throw an error.
|
|
1023
|
-
*
|
|
1024
|
-
* @param funnel - Funnel definition
|
|
1025
|
-
* @param sampleEntities - Sample entities to test
|
|
1026
|
-
* @returns Preview results showing which entities would match/exclude
|
|
1027
|
-
*/
|
|
1028
|
-
async previewFunnel(funnel, sampleEntities) {
|
|
1029
|
-
throw new Error(
|
|
1030
|
-
"Client-side preview requires evaluation engine. Use server-side preview endpoint instead: POST /api/v1/funnels/{id}/preview/"
|
|
1031
|
-
);
|
|
1032
|
-
}
|
|
1033
|
-
/**
|
|
1034
|
-
* Server-side preview (recommended)
|
|
1035
|
-
*
|
|
1036
|
-
* Send sample entities to server for evaluation.
|
|
1037
|
-
* Useful for testing funnel logic before running on full dataset.
|
|
1038
|
-
*
|
|
1039
|
-
* @param funnelId - Funnel ID
|
|
1040
|
-
* @param sampleEntities - Sample entities to test
|
|
1041
|
-
* @returns Preview results
|
|
1042
|
-
* @throws ApiError with status 404 if funnel not found
|
|
1043
|
-
*/
|
|
1044
|
-
async previewFunnelServer(funnelId, sampleEntities) {
|
|
1045
|
-
return this.adapter.post(
|
|
1046
|
-
this.url(`/api/v1/funnels/${funnelId}/preview/`),
|
|
1047
|
-
{ entities: sampleEntities }
|
|
1048
|
-
);
|
|
1049
|
-
}
|
|
1050
|
-
};
|
|
1051
|
-
|
|
1052
|
-
// src/store/types.ts
|
|
1053
|
-
var createInitialState = () => ({
|
|
1054
|
-
funnels: [],
|
|
1055
|
-
selectedFunnel: null,
|
|
1056
|
-
selectedStage: null,
|
|
1057
|
-
runs: [],
|
|
1058
|
-
pagination: {
|
|
1059
|
-
count: 0,
|
|
1060
|
-
next: null,
|
|
1061
|
-
previous: null,
|
|
1062
|
-
currentPage: 1,
|
|
1063
|
-
pageSize: 20
|
|
1064
|
-
},
|
|
1065
|
-
isLoading: false,
|
|
1066
|
-
error: null,
|
|
1067
|
-
isDirty: false,
|
|
1068
|
-
rollbackState: null
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
// src/store/create-funnel-store.ts
|
|
1072
|
-
function createFunnelStore(apiClient) {
|
|
1073
|
-
return zustand.create((set, get) => ({
|
|
1074
|
-
// Initialize state
|
|
1075
|
-
...createInitialState(),
|
|
1076
|
-
// =========================================================================
|
|
1077
|
-
// Funnel Actions
|
|
1078
|
-
// =========================================================================
|
|
1079
|
-
loadFunnels: async (filters) => {
|
|
1080
|
-
set({ isLoading: true, error: null });
|
|
1081
|
-
try {
|
|
1082
|
-
const response = await apiClient.listFunnels(filters);
|
|
1083
|
-
set({
|
|
1084
|
-
funnels: response.results,
|
|
1085
|
-
pagination: {
|
|
1086
|
-
count: response.count,
|
|
1087
|
-
next: response.next,
|
|
1088
|
-
previous: response.previous,
|
|
1089
|
-
currentPage: filters?.page || 1,
|
|
1090
|
-
pageSize: filters?.page_size || 20
|
|
1091
|
-
},
|
|
1092
|
-
isLoading: false
|
|
1093
|
-
});
|
|
1094
|
-
} catch (error) {
|
|
1095
|
-
set({
|
|
1096
|
-
error,
|
|
1097
|
-
isLoading: false
|
|
1098
|
-
});
|
|
1099
|
-
throw error;
|
|
1100
|
-
}
|
|
1101
|
-
},
|
|
1102
|
-
selectFunnel: (id) => {
|
|
1103
|
-
const { funnels } = get();
|
|
1104
|
-
if (id === null) {
|
|
1105
|
-
set({ selectedFunnel: null, selectedStage: null });
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
const funnel = funnels.find((f) => f.id === id);
|
|
1109
|
-
set({
|
|
1110
|
-
selectedFunnel: funnel || null,
|
|
1111
|
-
selectedStage: null
|
|
1112
|
-
// Clear stage selection when changing funnels
|
|
1113
|
-
});
|
|
1114
|
-
},
|
|
1115
|
-
createFunnel: async (data) => {
|
|
1116
|
-
set({ isLoading: true, error: null });
|
|
1117
|
-
try {
|
|
1118
|
-
const funnel = await apiClient.createFunnel(data);
|
|
1119
|
-
set((state) => ({
|
|
1120
|
-
funnels: [...state.funnels, funnel],
|
|
1121
|
-
pagination: {
|
|
1122
|
-
...state.pagination,
|
|
1123
|
-
count: state.pagination.count + 1
|
|
1124
|
-
},
|
|
1125
|
-
isLoading: false
|
|
1126
|
-
}));
|
|
1127
|
-
return funnel;
|
|
1128
|
-
} catch (error) {
|
|
1129
|
-
set({
|
|
1130
|
-
error,
|
|
1131
|
-
isLoading: false
|
|
1132
|
-
});
|
|
1133
|
-
throw error;
|
|
1134
|
-
}
|
|
1135
|
-
},
|
|
1136
|
-
updateFunnel: async (id, data) => {
|
|
1137
|
-
get()._saveRollbackState();
|
|
1138
|
-
set((state) => ({
|
|
1139
|
-
funnels: state.funnels.map(
|
|
1140
|
-
(f) => f.id === id ? { ...f, ...data } : f
|
|
1141
|
-
),
|
|
1142
|
-
selectedFunnel: state.selectedFunnel?.id === id ? { ...state.selectedFunnel, ...data } : state.selectedFunnel,
|
|
1143
|
-
isDirty: false
|
|
1144
|
-
// Clear dirty flag on save
|
|
1145
|
-
}));
|
|
1146
|
-
try {
|
|
1147
|
-
const updated = await apiClient.updateFunnel(id, data);
|
|
1148
|
-
set((state) => ({
|
|
1149
|
-
funnels: state.funnels.map((f) => f.id === id ? updated : f),
|
|
1150
|
-
selectedFunnel: state.selectedFunnel?.id === id ? updated : state.selectedFunnel
|
|
1151
|
-
}));
|
|
1152
|
-
get()._clearRollback();
|
|
1153
|
-
return updated;
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
get()._rollback();
|
|
1156
|
-
set({ error });
|
|
1157
|
-
throw error;
|
|
1158
|
-
}
|
|
1159
|
-
},
|
|
1160
|
-
deleteFunnel: async (id) => {
|
|
1161
|
-
get()._saveRollbackState();
|
|
1162
|
-
set((state) => ({
|
|
1163
|
-
funnels: state.funnels.filter((f) => f.id !== id),
|
|
1164
|
-
selectedFunnel: state.selectedFunnel?.id === id ? null : state.selectedFunnel,
|
|
1165
|
-
pagination: {
|
|
1166
|
-
...state.pagination,
|
|
1167
|
-
count: state.pagination.count - 1
|
|
1168
|
-
}
|
|
1169
|
-
}));
|
|
1170
|
-
try {
|
|
1171
|
-
await apiClient.deleteFunnel(id);
|
|
1172
|
-
get()._clearRollback();
|
|
1173
|
-
} catch (error) {
|
|
1174
|
-
get()._rollback();
|
|
1175
|
-
set({ error });
|
|
1176
|
-
throw error;
|
|
1177
|
-
}
|
|
1178
|
-
},
|
|
1179
|
-
duplicateFunnel: async (id) => {
|
|
1180
|
-
const { funnels } = get();
|
|
1181
|
-
const funnel = funnels.find((f) => f.id === id);
|
|
1182
|
-
if (!funnel) {
|
|
1183
|
-
throw new Error(`Funnel ${id} not found`);
|
|
1184
|
-
}
|
|
1185
|
-
const copy = {
|
|
1186
|
-
name: `${funnel.name} (Copy)`,
|
|
1187
|
-
description: funnel.description,
|
|
1188
|
-
status: "draft",
|
|
1189
|
-
// Always create as draft
|
|
1190
|
-
input_type: funnel.input_type,
|
|
1191
|
-
stages: funnel.stages.map((stage, index) => ({
|
|
1192
|
-
...stage,
|
|
1193
|
-
order: index
|
|
1194
|
-
// Preserve order
|
|
1195
|
-
})),
|
|
1196
|
-
completion_tags: funnel.completion_tags,
|
|
1197
|
-
metadata: funnel.metadata
|
|
1198
|
-
};
|
|
1199
|
-
return get().createFunnel(copy);
|
|
1200
|
-
},
|
|
1201
|
-
// =========================================================================
|
|
1202
|
-
// Stage Actions
|
|
1203
|
-
// =========================================================================
|
|
1204
|
-
selectStage: (stageId) => {
|
|
1205
|
-
const { selectedFunnel } = get();
|
|
1206
|
-
if (!selectedFunnel) {
|
|
1207
|
-
set({ selectedStage: null });
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
if (stageId === null) {
|
|
1211
|
-
set({ selectedStage: null });
|
|
1212
|
-
return;
|
|
1213
|
-
}
|
|
1214
|
-
const stage = selectedFunnel.stages.find((s) => s.id === stageId);
|
|
1215
|
-
set({ selectedStage: stage || null });
|
|
1216
|
-
},
|
|
1217
|
-
createStage: async (funnelId, data) => {
|
|
1218
|
-
set({ isLoading: true, error: null });
|
|
1219
|
-
try {
|
|
1220
|
-
const stage = await apiClient.createStage(funnelId, data);
|
|
1221
|
-
set((state) => ({
|
|
1222
|
-
funnels: state.funnels.map(
|
|
1223
|
-
(f) => f.id === funnelId ? { ...f, stages: [...f.stages, stage] } : f
|
|
1224
|
-
),
|
|
1225
|
-
selectedFunnel: state.selectedFunnel?.id === funnelId ? { ...state.selectedFunnel, stages: [...state.selectedFunnel.stages, stage] } : state.selectedFunnel,
|
|
1226
|
-
isLoading: false
|
|
1227
|
-
}));
|
|
1228
|
-
return stage;
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
set({
|
|
1231
|
-
error,
|
|
1232
|
-
isLoading: false
|
|
1233
|
-
});
|
|
1234
|
-
throw error;
|
|
1235
|
-
}
|
|
1236
|
-
},
|
|
1237
|
-
updateStage: async (funnelId, stageId, data) => {
|
|
1238
|
-
get()._saveRollbackState();
|
|
1239
|
-
set((state) => ({
|
|
1240
|
-
funnels: state.funnels.map(
|
|
1241
|
-
(f) => f.id === funnelId ? {
|
|
1242
|
-
...f,
|
|
1243
|
-
stages: f.stages.map(
|
|
1244
|
-
(s) => s.id === stageId ? { ...s, ...data } : s
|
|
1245
|
-
)
|
|
1246
|
-
} : f
|
|
1247
|
-
),
|
|
1248
|
-
selectedFunnel: state.selectedFunnel?.id === funnelId ? {
|
|
1249
|
-
...state.selectedFunnel,
|
|
1250
|
-
stages: state.selectedFunnel.stages.map(
|
|
1251
|
-
(s) => s.id === stageId ? { ...s, ...data } : s
|
|
1252
|
-
)
|
|
1253
|
-
} : state.selectedFunnel,
|
|
1254
|
-
selectedStage: state.selectedStage?.id === stageId ? { ...state.selectedStage, ...data } : state.selectedStage,
|
|
1255
|
-
isDirty: false
|
|
1256
|
-
}));
|
|
1257
|
-
try {
|
|
1258
|
-
const updated = await apiClient.updateStage(
|
|
1259
|
-
funnelId,
|
|
1260
|
-
stageId,
|
|
1261
|
-
data
|
|
1262
|
-
);
|
|
1263
|
-
set((state) => ({
|
|
1264
|
-
funnels: state.funnels.map(
|
|
1265
|
-
(f) => f.id === funnelId ? {
|
|
1266
|
-
...f,
|
|
1267
|
-
stages: f.stages.map((s) => s.id === stageId ? updated : s)
|
|
1268
|
-
} : f
|
|
1269
|
-
),
|
|
1270
|
-
selectedFunnel: state.selectedFunnel?.id === funnelId ? {
|
|
1271
|
-
...state.selectedFunnel,
|
|
1272
|
-
stages: state.selectedFunnel.stages.map(
|
|
1273
|
-
(s) => s.id === stageId ? updated : s
|
|
1274
|
-
)
|
|
1275
|
-
} : state.selectedFunnel,
|
|
1276
|
-
selectedStage: state.selectedStage?.id === stageId ? updated : state.selectedStage
|
|
1277
|
-
}));
|
|
1278
|
-
get()._clearRollback();
|
|
1279
|
-
return updated;
|
|
1280
|
-
} catch (error) {
|
|
1281
|
-
get()._rollback();
|
|
1282
|
-
set({ error });
|
|
1283
|
-
throw error;
|
|
1284
|
-
}
|
|
1285
|
-
},
|
|
1286
|
-
deleteStage: async (funnelId, stageId) => {
|
|
1287
|
-
get()._saveRollbackState();
|
|
1288
|
-
set((state) => ({
|
|
1289
|
-
funnels: state.funnels.map(
|
|
1290
|
-
(f) => f.id === funnelId ? {
|
|
1291
|
-
...f,
|
|
1292
|
-
stages: f.stages.filter((s) => s.id !== stageId)
|
|
1293
|
-
} : f
|
|
1294
|
-
),
|
|
1295
|
-
selectedFunnel: state.selectedFunnel?.id === funnelId ? {
|
|
1296
|
-
...state.selectedFunnel,
|
|
1297
|
-
stages: state.selectedFunnel.stages.filter((s) => s.id !== stageId)
|
|
1298
|
-
} : state.selectedFunnel,
|
|
1299
|
-
selectedStage: state.selectedStage?.id === stageId ? null : state.selectedStage
|
|
1300
|
-
}));
|
|
1301
|
-
try {
|
|
1302
|
-
await apiClient.deleteStage(funnelId, stageId);
|
|
1303
|
-
get()._clearRollback();
|
|
1304
|
-
} catch (error) {
|
|
1305
|
-
get()._rollback();
|
|
1306
|
-
set({ error });
|
|
1307
|
-
throw error;
|
|
1308
|
-
}
|
|
1309
|
-
},
|
|
1310
|
-
reorderStages: async (funnelId, stageIds) => {
|
|
1311
|
-
const { selectedFunnel } = get();
|
|
1312
|
-
if (!selectedFunnel || selectedFunnel.id !== funnelId) {
|
|
1313
|
-
throw new Error("Funnel must be selected to reorder stages");
|
|
1314
|
-
}
|
|
1315
|
-
get()._saveRollbackState();
|
|
1316
|
-
const reorderedStages = stageIds.map((id, index) => {
|
|
1317
|
-
const stage = selectedFunnel.stages.find((s) => s.id === id);
|
|
1318
|
-
return stage ? { ...stage, order: index } : null;
|
|
1319
|
-
}).filter((s) => s !== null);
|
|
1320
|
-
set((state) => ({
|
|
1321
|
-
funnels: state.funnels.map(
|
|
1322
|
-
(f) => f.id === funnelId ? { ...f, stages: reorderedStages } : f
|
|
1323
|
-
),
|
|
1324
|
-
selectedFunnel: { ...selectedFunnel, stages: reorderedStages },
|
|
1325
|
-
isDirty: false
|
|
1326
|
-
}));
|
|
1327
|
-
try {
|
|
1328
|
-
await Promise.all(
|
|
1329
|
-
reorderedStages.map(
|
|
1330
|
-
(stage) => apiClient.updateStage(funnelId, stage.id, { order: stage.order })
|
|
1331
|
-
)
|
|
1332
|
-
);
|
|
1333
|
-
get()._clearRollback();
|
|
1334
|
-
} catch (error) {
|
|
1335
|
-
get()._rollback();
|
|
1336
|
-
set({ error });
|
|
1337
|
-
throw error;
|
|
1338
|
-
}
|
|
1339
|
-
},
|
|
1340
|
-
// =========================================================================
|
|
1341
|
-
// Run Actions
|
|
1342
|
-
// =========================================================================
|
|
1343
|
-
runFunnel: async (id, options) => {
|
|
1344
|
-
set({ isLoading: true, error: null });
|
|
1345
|
-
try {
|
|
1346
|
-
const run = await apiClient.runFunnel(id, options);
|
|
1347
|
-
set((state) => ({
|
|
1348
|
-
runs: [run, ...state.runs],
|
|
1349
|
-
isLoading: false
|
|
1350
|
-
}));
|
|
1351
|
-
return run;
|
|
1352
|
-
} catch (error) {
|
|
1353
|
-
set({
|
|
1354
|
-
error,
|
|
1355
|
-
isLoading: false
|
|
1356
|
-
});
|
|
1357
|
-
throw error;
|
|
1358
|
-
}
|
|
1359
|
-
},
|
|
1360
|
-
loadRuns: async (funnelId, filters) => {
|
|
1361
|
-
set({ isLoading: true, error: null });
|
|
1362
|
-
try {
|
|
1363
|
-
const response = await apiClient.getFunnelRuns(funnelId, filters);
|
|
1364
|
-
set({
|
|
1365
|
-
runs: response.results,
|
|
1366
|
-
isLoading: false
|
|
1367
|
-
});
|
|
1368
|
-
} catch (error) {
|
|
1369
|
-
set({
|
|
1370
|
-
error,
|
|
1371
|
-
isLoading: false
|
|
1372
|
-
});
|
|
1373
|
-
throw error;
|
|
1374
|
-
}
|
|
1375
|
-
},
|
|
1376
|
-
cancelRun: async (runId) => {
|
|
1377
|
-
set({ isLoading: true, error: null });
|
|
1378
|
-
try {
|
|
1379
|
-
const run = await apiClient.cancelFunnelRun(runId);
|
|
1380
|
-
set((state) => ({
|
|
1381
|
-
runs: state.runs.map((r) => r.id === runId ? run : r),
|
|
1382
|
-
isLoading: false
|
|
1383
|
-
}));
|
|
1384
|
-
return run;
|
|
1385
|
-
} catch (error) {
|
|
1386
|
-
set({
|
|
1387
|
-
error,
|
|
1388
|
-
isLoading: false
|
|
1389
|
-
});
|
|
1390
|
-
throw error;
|
|
1391
|
-
}
|
|
1392
|
-
},
|
|
1393
|
-
// =========================================================================
|
|
1394
|
-
// UI State Actions
|
|
1395
|
-
// =========================================================================
|
|
1396
|
-
setDirty: (dirty) => {
|
|
1397
|
-
set({ isDirty: dirty });
|
|
1398
|
-
},
|
|
1399
|
-
clearError: () => {
|
|
1400
|
-
set({ error: null });
|
|
1401
|
-
},
|
|
1402
|
-
reset: () => {
|
|
1403
|
-
set(createInitialState());
|
|
1404
|
-
},
|
|
1405
|
-
// =========================================================================
|
|
1406
|
-
// Internal Actions (Optimistic Updates)
|
|
1407
|
-
// =========================================================================
|
|
1408
|
-
_saveRollbackState: () => {
|
|
1409
|
-
const { funnels, selectedFunnel } = get();
|
|
1410
|
-
set({
|
|
1411
|
-
rollbackState: {
|
|
1412
|
-
funnels: JSON.parse(JSON.stringify(funnels)),
|
|
1413
|
-
selectedFunnel: selectedFunnel ? JSON.parse(JSON.stringify(selectedFunnel)) : null
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
|
-
},
|
|
1417
|
-
_rollback: () => {
|
|
1418
|
-
const { rollbackState } = get();
|
|
1419
|
-
if (rollbackState) {
|
|
1420
|
-
set({
|
|
1421
|
-
funnels: rollbackState.funnels,
|
|
1422
|
-
selectedFunnel: rollbackState.selectedFunnel,
|
|
1423
|
-
rollbackState: null
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
},
|
|
1427
|
-
_clearRollback: () => {
|
|
1428
|
-
set({ rollbackState: null });
|
|
1429
|
-
}
|
|
1430
|
-
}));
|
|
1431
|
-
}
|
|
1432
|
-
function useDebouncedValue(value, delay = 300) {
|
|
1433
|
-
const [debouncedValue, setDebouncedValue] = react.useState(value);
|
|
1434
|
-
react.useEffect(() => {
|
|
1435
|
-
const handler = setTimeout(() => {
|
|
1436
|
-
setDebouncedValue(value);
|
|
1437
|
-
}, delay);
|
|
1438
|
-
return () => {
|
|
1439
|
-
clearTimeout(handler);
|
|
1440
|
-
};
|
|
1441
|
-
}, [value, delay]);
|
|
1442
|
-
return debouncedValue;
|
|
1443
|
-
}
|
|
1444
|
-
function PreviewStats({
|
|
1445
|
-
totalMatched,
|
|
1446
|
-
totalExcluded,
|
|
1447
|
-
matchPercentage,
|
|
1448
|
-
className = ""
|
|
1449
|
-
}) {
|
|
1450
|
-
const total = totalMatched + totalExcluded;
|
|
1451
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-2 ${className}`, children: [
|
|
1452
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative h-8 bg-gray-200 rounded-lg overflow-hidden", children: [
|
|
1453
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1454
|
-
"div",
|
|
1455
|
-
{
|
|
1456
|
-
className: "absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300 flex items-center justify-center",
|
|
1457
|
-
style: { width: `${matchPercentage}%` },
|
|
1458
|
-
role: "progressbar",
|
|
1459
|
-
"aria-valuenow": matchPercentage,
|
|
1460
|
-
"aria-valuemin": 0,
|
|
1461
|
-
"aria-valuemax": 100,
|
|
1462
|
-
"aria-label": `${totalMatched} of ${total} matched (${matchPercentage}%)`,
|
|
1463
|
-
children: matchPercentage > 15 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold text-white", children: [
|
|
1464
|
-
totalMatched.toLocaleString(),
|
|
1465
|
-
"/",
|
|
1466
|
-
total.toLocaleString(),
|
|
1467
|
-
" (",
|
|
1468
|
-
matchPercentage,
|
|
1469
|
-
"%)"
|
|
1470
|
-
] })
|
|
1471
|
-
}
|
|
1472
|
-
),
|
|
1473
|
-
matchPercentage <= 15 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold text-gray-600", children: [
|
|
1474
|
-
totalMatched.toLocaleString(),
|
|
1475
|
-
"/",
|
|
1476
|
-
total.toLocaleString(),
|
|
1477
|
-
" (",
|
|
1478
|
-
matchPercentage,
|
|
1479
|
-
"%)"
|
|
1480
|
-
] }) })
|
|
1481
|
-
] }),
|
|
1482
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-sm", children: [
|
|
1483
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1484
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm bg-green-500" }),
|
|
1485
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-gray-700", children: [
|
|
1486
|
-
totalMatched.toLocaleString(),
|
|
1487
|
-
" Matched"
|
|
1488
|
-
] })
|
|
1489
|
-
] }),
|
|
1490
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1491
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm bg-gray-300" }),
|
|
1492
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-gray-700", children: [
|
|
1493
|
-
totalExcluded.toLocaleString(),
|
|
1494
|
-
" Excluded"
|
|
1495
|
-
] })
|
|
1496
|
-
] })
|
|
1497
|
-
] })
|
|
1498
|
-
] });
|
|
1499
|
-
}
|
|
1500
|
-
function StageBreakdown({
|
|
1501
|
-
stageStats,
|
|
1502
|
-
stages,
|
|
1503
|
-
className = ""
|
|
1504
|
-
}) {
|
|
1505
|
-
const sortedStages = [...stages].sort((a, b) => a.order - b.order);
|
|
1506
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, children: [
|
|
1507
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: "Stage Breakdown" }),
|
|
1508
|
-
/* @__PURE__ */ jsxRuntime.jsx("ol", { className: "space-y-2", children: sortedStages.map((stage, index) => {
|
|
1509
|
-
const stats = stageStats[stage.id];
|
|
1510
|
-
if (!stats) return null;
|
|
1511
|
-
const isLast = index === sortedStages.length - 1;
|
|
1512
|
-
const excludedCount = stats.excluded_count;
|
|
1513
|
-
const remainingCount = stats.remaining_count;
|
|
1514
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1515
|
-
"li",
|
|
1516
|
-
{
|
|
1517
|
-
className: "flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg",
|
|
1518
|
-
children: [
|
|
1519
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [
|
|
1520
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-700 rounded-full text-xs font-bold", children: index + 1 }),
|
|
1521
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900 truncate", children: stage.name })
|
|
1522
|
-
] }),
|
|
1523
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-sm", children: [
|
|
1524
|
-
excludedCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-red-600 font-medium", children: [
|
|
1525
|
-
"-",
|
|
1526
|
-
excludedCount.toLocaleString()
|
|
1527
|
-
] }),
|
|
1528
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1529
|
-
"span",
|
|
1530
|
-
{
|
|
1531
|
-
className: `font-semibold ${isLast ? "text-green-600" : "text-gray-700"}`,
|
|
1532
|
-
children: [
|
|
1533
|
-
remainingCount.toLocaleString(),
|
|
1534
|
-
" ",
|
|
1535
|
-
isLast ? "final" : "left"
|
|
1536
|
-
]
|
|
1537
|
-
}
|
|
1538
|
-
)
|
|
1539
|
-
] })
|
|
1540
|
-
]
|
|
1541
|
-
},
|
|
1542
|
-
stage.id
|
|
1543
|
-
);
|
|
1544
|
-
}) })
|
|
1545
|
-
] });
|
|
1546
|
-
}
|
|
1547
|
-
function defaultEntityRenderer(entity) {
|
|
1548
|
-
if (entity.name) {
|
|
1549
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1550
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-gray-900", children: entity.name }),
|
|
1551
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-600 mt-1", children: Object.keys(entity).filter((key) => key !== "name").slice(0, 3).map((key) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-2", children: [
|
|
1552
|
-
key,
|
|
1553
|
-
": ",
|
|
1554
|
-
String(entity[key]).slice(0, 20)
|
|
1555
|
-
] }, key)) })
|
|
1556
|
-
] });
|
|
1557
|
-
}
|
|
1558
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-700 font-mono", children: /* @__PURE__ */ jsxRuntime.jsxs("pre", { className: "whitespace-pre-wrap break-all", children: [
|
|
1559
|
-
JSON.stringify(entity, null, 2).slice(0, 150),
|
|
1560
|
-
JSON.stringify(entity, null, 2).length > 150 ? "..." : ""
|
|
1561
|
-
] }) });
|
|
1562
|
-
}
|
|
1563
|
-
function EntityCard({
|
|
1564
|
-
entity,
|
|
1565
|
-
renderEntity = defaultEntityRenderer,
|
|
1566
|
-
className = ""
|
|
1567
|
-
}) {
|
|
1568
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1569
|
-
"article",
|
|
1570
|
-
{
|
|
1571
|
-
className: `p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:border-gray-300 transition-colors ${className}`,
|
|
1572
|
-
children: renderEntity(entity)
|
|
1573
|
-
}
|
|
1574
|
-
);
|
|
1575
|
-
}
|
|
1576
|
-
function LoadingPreview() {
|
|
1577
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-pulse", role: "status", "aria-live": "polite", children: [
|
|
1578
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Loading preview..." }),
|
|
1579
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 mb-6", children: [
|
|
1580
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 bg-gray-200 rounded-lg" }),
|
|
1581
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
|
|
1582
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-32 bg-gray-200 rounded" }),
|
|
1583
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-32 bg-gray-200 rounded" })
|
|
1584
|
-
] })
|
|
1585
|
-
] }),
|
|
1586
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6", children: [
|
|
1587
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-40 bg-gray-200 rounded mb-3" }),
|
|
1588
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1589
|
-
"div",
|
|
1590
|
-
{
|
|
1591
|
-
className: "h-12 bg-gray-100 rounded-lg flex items-center px-3 gap-3",
|
|
1592
|
-
children: [
|
|
1593
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 bg-gray-200 rounded-full" }),
|
|
1594
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-gray-200 rounded flex-1" }),
|
|
1595
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-16 bg-gray-200 rounded" })
|
|
1596
|
-
]
|
|
1597
|
-
},
|
|
1598
|
-
i
|
|
1599
|
-
)) })
|
|
1600
|
-
] }),
|
|
1601
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
1602
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-48 bg-gray-200 rounded mb-3" }),
|
|
1603
|
-
[1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-20 bg-gray-100 border border-gray-200 rounded-lg p-3", children: [
|
|
1604
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-gray-200 rounded w-3/4 mb-2" }),
|
|
1605
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 bg-gray-200 rounded w-1/2" })
|
|
1606
|
-
] }, i))
|
|
1607
|
-
] })
|
|
1608
|
-
] });
|
|
1609
|
-
}
|
|
1610
|
-
function convertToPreviewResult(execResult, maxEntities = 10) {
|
|
1611
|
-
const { matched, total_matched, total_excluded, stage_stats } = execResult;
|
|
1612
|
-
const total = total_matched + total_excluded;
|
|
1613
|
-
const matchPercentage = total > 0 ? Math.round(total_matched / total * 100) : 0;
|
|
1614
|
-
const previewEntities = matched.slice(0, maxEntities).map((r) => r.entity);
|
|
1615
|
-
const previewStageStats = {};
|
|
1616
|
-
Object.entries(stage_stats).forEach(([stageId, stats]) => {
|
|
1617
|
-
previewStageStats[stageId] = {
|
|
1618
|
-
stage_id: stats.stage_id,
|
|
1619
|
-
stage_name: stats.stage_name,
|
|
1620
|
-
input_count: stats.input_count,
|
|
1621
|
-
excluded_count: stats.excluded_count,
|
|
1622
|
-
remaining_count: stats.input_count - stats.excluded_count
|
|
1623
|
-
};
|
|
1624
|
-
});
|
|
1625
|
-
return {
|
|
1626
|
-
totalMatched: total_matched,
|
|
1627
|
-
totalExcluded: total_excluded,
|
|
1628
|
-
matchPercentage,
|
|
1629
|
-
previewEntities,
|
|
1630
|
-
stageStats: previewStageStats
|
|
1631
|
-
};
|
|
1632
|
-
}
|
|
1633
|
-
function FunnelPreview({
|
|
1634
|
-
funnel,
|
|
1635
|
-
sampleEntities,
|
|
1636
|
-
onPreview,
|
|
1637
|
-
renderEntity,
|
|
1638
|
-
maxPreviewEntities = 10,
|
|
1639
|
-
className = ""
|
|
1640
|
-
}) {
|
|
1641
|
-
const [result, setResult] = react.useState(null);
|
|
1642
|
-
const [isComputing, setIsComputing] = react.useState(false);
|
|
1643
|
-
const debouncedFunnel = useDebouncedValue(funnel, 300);
|
|
1644
|
-
react.useEffect(() => {
|
|
1645
|
-
async function compute() {
|
|
1646
|
-
setIsComputing(true);
|
|
1647
|
-
try {
|
|
1648
|
-
const engine = new FunnelEngine();
|
|
1649
|
-
const execResult = engine.execute(debouncedFunnel, sampleEntities);
|
|
1650
|
-
const previewResult = convertToPreviewResult(
|
|
1651
|
-
execResult,
|
|
1652
|
-
maxPreviewEntities
|
|
1653
|
-
);
|
|
1654
|
-
setResult(previewResult);
|
|
1655
|
-
if (onPreview) {
|
|
1656
|
-
onPreview(previewResult);
|
|
1657
|
-
}
|
|
1658
|
-
} catch (error) {
|
|
1659
|
-
console.error("Preview computation failed:", error);
|
|
1660
|
-
setResult({
|
|
1661
|
-
totalMatched: 0,
|
|
1662
|
-
totalExcluded: sampleEntities.length,
|
|
1663
|
-
matchPercentage: 0,
|
|
1664
|
-
previewEntities: [],
|
|
1665
|
-
stageStats: {}
|
|
1666
|
-
});
|
|
1667
|
-
} finally {
|
|
1668
|
-
setIsComputing(false);
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
compute();
|
|
1672
|
-
}, [debouncedFunnel, sampleEntities, maxPreviewEntities, onPreview]);
|
|
1673
|
-
if (isComputing && !result) {
|
|
1674
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsx(LoadingPreview, {}) });
|
|
1675
|
-
}
|
|
1676
|
-
if (!result) {
|
|
1677
|
-
return null;
|
|
1678
|
-
}
|
|
1679
|
-
const { totalMatched, totalExcluded, matchPercentage, previewEntities, stageStats } = result;
|
|
1680
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, role: "region", "aria-label": "Funnel preview", children: [
|
|
1681
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-4", children: "Preview Results" }),
|
|
1682
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1683
|
-
PreviewStats,
|
|
1684
|
-
{
|
|
1685
|
-
totalMatched,
|
|
1686
|
-
totalExcluded,
|
|
1687
|
-
matchPercentage,
|
|
1688
|
-
className: "mb-6"
|
|
1689
|
-
}
|
|
1690
|
-
),
|
|
1691
|
-
funnel.stages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1692
|
-
StageBreakdown,
|
|
1693
|
-
{
|
|
1694
|
-
stageStats,
|
|
1695
|
-
stages: funnel.stages,
|
|
1696
|
-
className: "mb-6"
|
|
1697
|
-
}
|
|
1698
|
-
),
|
|
1699
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1700
|
-
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: [
|
|
1701
|
-
"Sample Matches (",
|
|
1702
|
-
Math.min(previewEntities.length, maxPreviewEntities),
|
|
1703
|
-
" of",
|
|
1704
|
-
" ",
|
|
1705
|
-
totalMatched.toLocaleString(),
|
|
1706
|
-
")"
|
|
1707
|
-
] }),
|
|
1708
|
-
previewEntities.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-8 text-center bg-gray-50 rounded-lg border-2 border-dashed border-gray-300", children: [
|
|
1709
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600", children: "No entities matched this funnel" }),
|
|
1710
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mt-1", children: "Try adjusting your filter rules" })
|
|
1711
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
1712
|
-
previewEntities.map((entity, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1713
|
-
EntityCard,
|
|
1714
|
-
{
|
|
1715
|
-
entity,
|
|
1716
|
-
renderEntity
|
|
1717
|
-
},
|
|
1718
|
-
index
|
|
1719
|
-
)),
|
|
1720
|
-
totalMatched > maxPreviewEntities && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-2 text-sm text-gray-500", children: [
|
|
1721
|
-
"+ ",
|
|
1722
|
-
(totalMatched - maxPreviewEntities).toLocaleString(),
|
|
1723
|
-
" more..."
|
|
1724
|
-
] })
|
|
1725
|
-
] })
|
|
1726
|
-
] }),
|
|
1727
|
-
isComputing && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-gray-600", children: [
|
|
1728
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-5 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin" }),
|
|
1729
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "Updating preview..." })
|
|
1730
|
-
] }) })
|
|
1731
|
-
] });
|
|
1732
|
-
}
|
|
1733
|
-
var statusConfig = {
|
|
1734
|
-
active: {
|
|
1735
|
-
color: "text-green-800",
|
|
1736
|
-
bgColor: "bg-green-100",
|
|
1737
|
-
label: "ACTIVE"
|
|
1738
|
-
},
|
|
1739
|
-
draft: {
|
|
1740
|
-
color: "text-yellow-800",
|
|
1741
|
-
bgColor: "bg-yellow-100",
|
|
1742
|
-
label: "DRAFT"
|
|
1743
|
-
},
|
|
1744
|
-
paused: {
|
|
1745
|
-
color: "text-gray-800",
|
|
1746
|
-
bgColor: "bg-gray-100",
|
|
1747
|
-
label: "PAUSED"
|
|
1748
|
-
},
|
|
1749
|
-
archived: {
|
|
1750
|
-
color: "text-red-800",
|
|
1751
|
-
bgColor: "bg-red-100",
|
|
1752
|
-
label: "ARCHIVED"
|
|
1753
|
-
}
|
|
1754
|
-
};
|
|
1755
|
-
function StatusBadge({ status, className = "" }) {
|
|
1756
|
-
const config = statusConfig[status];
|
|
1757
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1758
|
-
"span",
|
|
1759
|
-
{
|
|
1760
|
-
className: `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color} ${className}`,
|
|
1761
|
-
children: config.label
|
|
1762
|
-
}
|
|
1763
|
-
);
|
|
1764
|
-
}
|
|
1765
|
-
function StageIndicator({
|
|
1766
|
-
order,
|
|
1767
|
-
name,
|
|
1768
|
-
ruleCount,
|
|
1769
|
-
isLast = false,
|
|
1770
|
-
className = ""
|
|
1771
|
-
}) {
|
|
1772
|
-
const circledNumber = order < 20 ? String.fromCharCode(9312 + order) : `(${order + 1})`;
|
|
1773
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-start gap-2 ${className}`, children: [
|
|
1774
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center", children: [
|
|
1775
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center text-sm font-medium", children: circledNumber }),
|
|
1776
|
-
!isLast && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-0.5 h-6 bg-gray-200 mt-1" })
|
|
1777
|
-
] }),
|
|
1778
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 pt-0.5 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
1779
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900 truncate", children: name }),
|
|
1780
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-500 whitespace-nowrap", children: [
|
|
1781
|
-
ruleCount,
|
|
1782
|
-
" ",
|
|
1783
|
-
ruleCount === 1 ? "rule" : "rules"
|
|
1784
|
-
] })
|
|
1785
|
-
] }) })
|
|
1786
|
-
] });
|
|
1787
|
-
}
|
|
1788
|
-
function MatchBar({ matched, total, className = "" }) {
|
|
1789
|
-
const percentage = total > 0 ? Math.round(matched / total * 100) : 0;
|
|
1790
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-1 ${className}`, children: [
|
|
1791
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-6 bg-gray-200 rounded-md overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1792
|
-
"div",
|
|
1793
|
-
{
|
|
1794
|
-
className: "absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300",
|
|
1795
|
-
style: { width: `${percentage}%` },
|
|
1796
|
-
role: "progressbar",
|
|
1797
|
-
"aria-valuenow": percentage,
|
|
1798
|
-
"aria-valuemin": 0,
|
|
1799
|
-
"aria-valuemax": 100,
|
|
1800
|
-
"aria-label": `${matched} of ${total} matched`
|
|
1801
|
-
}
|
|
1802
|
-
) }),
|
|
1803
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-right", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium text-gray-700", children: [
|
|
1804
|
-
matched.toLocaleString(),
|
|
1805
|
-
" matched"
|
|
1806
|
-
] }) })
|
|
1807
|
-
] });
|
|
1808
|
-
}
|
|
1809
|
-
function FunnelStats({
|
|
1810
|
-
input,
|
|
1811
|
-
matched,
|
|
1812
|
-
excluded,
|
|
1813
|
-
className = ""
|
|
1814
|
-
}) {
|
|
1815
|
-
const stats = [
|
|
1816
|
-
{
|
|
1817
|
-
label: "INPUT",
|
|
1818
|
-
value: input,
|
|
1819
|
-
color: "text-blue-600",
|
|
1820
|
-
bgColor: "bg-blue-50"
|
|
1821
|
-
},
|
|
1822
|
-
{
|
|
1823
|
-
label: "MATCHED",
|
|
1824
|
-
value: matched,
|
|
1825
|
-
color: "text-green-600",
|
|
1826
|
-
bgColor: "bg-green-50"
|
|
1827
|
-
},
|
|
1828
|
-
{
|
|
1829
|
-
label: "EXCLUDED",
|
|
1830
|
-
value: excluded,
|
|
1831
|
-
color: "text-red-600",
|
|
1832
|
-
bgColor: "bg-red-50"
|
|
1833
|
-
}
|
|
1834
|
-
];
|
|
1835
|
-
return /* @__PURE__ */ jsxRuntime.jsx("dl", { className: `grid grid-cols-3 gap-2 ${className}`, children: stats.map(({ label, value, color, bgColor }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1836
|
-
"div",
|
|
1837
|
-
{
|
|
1838
|
-
className: `${bgColor} rounded-lg px-3 py-2.5 text-center`,
|
|
1839
|
-
children: [
|
|
1840
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-xs font-medium text-gray-600 mb-1", children: label }),
|
|
1841
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: `text-lg font-bold ${color}`, children: value.toLocaleString() })
|
|
1842
|
-
]
|
|
1843
|
-
},
|
|
1844
|
-
label
|
|
1845
|
-
)) });
|
|
1846
|
-
}
|
|
1847
|
-
function FunnelCard({
|
|
1848
|
-
funnel,
|
|
1849
|
-
latestRun,
|
|
1850
|
-
onViewFlow,
|
|
1851
|
-
onEdit,
|
|
1852
|
-
className = ""
|
|
1853
|
-
}) {
|
|
1854
|
-
const stats = latestRun ? {
|
|
1855
|
-
input: latestRun.total_input,
|
|
1856
|
-
matched: latestRun.total_matched,
|
|
1857
|
-
excluded: latestRun.total_excluded
|
|
1858
|
-
} : {
|
|
1859
|
-
input: 0,
|
|
1860
|
-
matched: 0,
|
|
1861
|
-
excluded: 0
|
|
1862
|
-
};
|
|
1863
|
-
const handleViewFlow = () => {
|
|
1864
|
-
if (onViewFlow) {
|
|
1865
|
-
onViewFlow(funnel);
|
|
1866
|
-
}
|
|
1867
|
-
};
|
|
1868
|
-
const hasRun = latestRun && latestRun.status === "completed";
|
|
1869
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1870
|
-
"article",
|
|
1871
|
-
{
|
|
1872
|
-
className: `bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200 ${className}`,
|
|
1873
|
-
"aria-label": `Funnel: ${funnel.name}`,
|
|
1874
|
-
children: [
|
|
1875
|
-
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "px-6 pt-5 pb-3 border-b border-gray-100", children: [
|
|
1876
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
1877
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900 flex-1 min-w-0", children: funnel.name }),
|
|
1878
|
-
/* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: funnel.status })
|
|
1879
|
-
] }),
|
|
1880
|
-
funnel.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-gray-600 line-clamp-2", children: funnel.description })
|
|
1881
|
-
] }),
|
|
1882
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1883
|
-
"section",
|
|
1884
|
-
{
|
|
1885
|
-
className: "px-6 py-4 space-y-0",
|
|
1886
|
-
"aria-label": "Funnel stages",
|
|
1887
|
-
children: funnel.stages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-500 italic py-4 text-center", children: "No stages defined" }) : funnel.stages.map((stage, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1888
|
-
StageIndicator,
|
|
1889
|
-
{
|
|
1890
|
-
order: index,
|
|
1891
|
-
name: stage.name,
|
|
1892
|
-
ruleCount: stage.rules.length,
|
|
1893
|
-
isLast: index === funnel.stages.length - 1
|
|
1894
|
-
},
|
|
1895
|
-
stage.id
|
|
1896
|
-
))
|
|
1897
|
-
}
|
|
1898
|
-
),
|
|
1899
|
-
hasRun && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1900
|
-
"section",
|
|
1901
|
-
{
|
|
1902
|
-
className: "px-6 py-4 border-t border-gray-100",
|
|
1903
|
-
"aria-label": "Match results",
|
|
1904
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1905
|
-
MatchBar,
|
|
1906
|
-
{
|
|
1907
|
-
matched: stats.matched,
|
|
1908
|
-
total: stats.input
|
|
1909
|
-
}
|
|
1910
|
-
)
|
|
1911
|
-
}
|
|
1912
|
-
),
|
|
1913
|
-
hasRun && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1914
|
-
"section",
|
|
1915
|
-
{
|
|
1916
|
-
className: "px-6 py-4 border-t border-gray-100",
|
|
1917
|
-
"aria-label": "Funnel statistics",
|
|
1918
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1919
|
-
FunnelStats,
|
|
1920
|
-
{
|
|
1921
|
-
input: stats.input,
|
|
1922
|
-
matched: stats.matched,
|
|
1923
|
-
excluded: stats.excluded
|
|
1924
|
-
}
|
|
1925
|
-
)
|
|
1926
|
-
}
|
|
1927
|
-
),
|
|
1928
|
-
!hasRun && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1929
|
-
"section",
|
|
1930
|
-
{
|
|
1931
|
-
className: "px-6 py-4 border-t border-gray-100 text-center",
|
|
1932
|
-
"aria-label": "Funnel status",
|
|
1933
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500", children: latestRun?.status === "failed" ? "Last run failed" : latestRun?.status === "running" ? "Running..." : "No runs yet" })
|
|
1934
|
-
}
|
|
1935
|
-
),
|
|
1936
|
-
/* @__PURE__ */ jsxRuntime.jsx("footer", { className: "px-6 py-4 border-t border-gray-100", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1937
|
-
"button",
|
|
1938
|
-
{
|
|
1939
|
-
onClick: handleViewFlow,
|
|
1940
|
-
className: "w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-gray-50 hover:bg-gray-100 text-gray-900 text-sm font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
|
|
1941
|
-
"aria-label": `View flow details for ${funnel.name}`,
|
|
1942
|
-
children: [
|
|
1943
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "View Flow" }),
|
|
1944
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1945
|
-
"svg",
|
|
1946
|
-
{
|
|
1947
|
-
className: "w-4 h-4 transition-transform group-hover:translate-x-0.5",
|
|
1948
|
-
fill: "none",
|
|
1949
|
-
viewBox: "0 0 24 24",
|
|
1950
|
-
stroke: "currentColor",
|
|
1951
|
-
"aria-hidden": "true",
|
|
1952
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1953
|
-
"path",
|
|
1954
|
-
{
|
|
1955
|
-
strokeLinecap: "round",
|
|
1956
|
-
strokeLinejoin: "round",
|
|
1957
|
-
strokeWidth: 2,
|
|
1958
|
-
d: "M13 7l5 5m0 0l-5 5m5-5H6"
|
|
1959
|
-
}
|
|
1960
|
-
)
|
|
1961
|
-
}
|
|
1962
|
-
)
|
|
1963
|
-
]
|
|
1964
|
-
}
|
|
1965
|
-
) })
|
|
1966
|
-
]
|
|
1967
|
-
}
|
|
1968
|
-
);
|
|
1969
|
-
}
|
|
1970
|
-
function getStageColor(stage) {
|
|
1971
|
-
const matchAction = stage.match_action;
|
|
1972
|
-
const noMatchAction = stage.no_match_action;
|
|
1973
|
-
if (matchAction === "output") {
|
|
1974
|
-
return "#22c55e";
|
|
1975
|
-
}
|
|
1976
|
-
if (noMatchAction === "exclude" || noMatchAction === "tag_exclude") {
|
|
1977
|
-
return "#ef4444";
|
|
1978
|
-
}
|
|
1979
|
-
if (matchAction === "tag" || matchAction === "tag_continue") {
|
|
1980
|
-
return "#eab308";
|
|
1981
|
-
}
|
|
1982
|
-
return "#3b82f6";
|
|
1983
|
-
}
|
|
1984
|
-
function getActionLabel(stage) {
|
|
1985
|
-
const matchAction = stage.match_action;
|
|
1986
|
-
const noMatchAction = stage.no_match_action;
|
|
1987
|
-
if (matchAction === "output") return "Output";
|
|
1988
|
-
if (noMatchAction === "exclude") return "Exclude Non-Matches";
|
|
1989
|
-
if (noMatchAction === "tag_exclude") return "Tag & Exclude";
|
|
1990
|
-
if (matchAction === "tag") return "Tag Matches";
|
|
1991
|
-
if (matchAction === "tag_continue") return "Tag & Continue";
|
|
1992
|
-
return "Continue";
|
|
1993
|
-
}
|
|
1994
|
-
function StageNode({ data }) {
|
|
1995
|
-
const { stage, stats, onStageClick } = data;
|
|
1996
|
-
const color = getStageColor(stage);
|
|
1997
|
-
const actionLabel = getActionLabel(stage);
|
|
1998
|
-
const handleClick = () => {
|
|
1999
|
-
if (onStageClick) {
|
|
2000
|
-
onStageClick(stage);
|
|
2001
|
-
}
|
|
2002
|
-
};
|
|
2003
|
-
const handleKeyDown = (event) => {
|
|
2004
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
2005
|
-
event.preventDefault();
|
|
2006
|
-
handleClick();
|
|
2007
|
-
}
|
|
2008
|
-
};
|
|
2009
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2010
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2011
|
-
react$1.Handle,
|
|
2012
|
-
{
|
|
2013
|
-
type: "target",
|
|
2014
|
-
position: react$1.Position.Top,
|
|
2015
|
-
style: { background: color, opacity: 0 },
|
|
2016
|
-
isConnectable: false
|
|
2017
|
-
}
|
|
2018
|
-
),
|
|
2019
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2020
|
-
"div",
|
|
2021
|
-
{
|
|
2022
|
-
className: "stage-node",
|
|
2023
|
-
onClick: handleClick,
|
|
2024
|
-
onKeyDown: handleKeyDown,
|
|
2025
|
-
role: "button",
|
|
2026
|
-
tabIndex: 0,
|
|
2027
|
-
"aria-label": `Stage ${stage.order + 1}: ${stage.name}`,
|
|
2028
|
-
style: {
|
|
2029
|
-
borderColor: color,
|
|
2030
|
-
borderWidth: "2px",
|
|
2031
|
-
borderStyle: "solid"
|
|
2032
|
-
},
|
|
2033
|
-
children: [
|
|
2034
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-number", style: { color }, children: getCircledNumber(stage.order + 1) }),
|
|
2035
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-name", title: stage.name, children: stage.name }),
|
|
2036
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-rules", children: [
|
|
2037
|
-
stage.rules.length,
|
|
2038
|
-
" ",
|
|
2039
|
-
stage.rules.length === 1 ? "rule" : "rules"
|
|
2040
|
-
] }),
|
|
2041
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-action", style: { color }, children: actionLabel }),
|
|
2042
|
-
stats && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-stats", children: [
|
|
2043
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
|
|
2044
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Input:" }),
|
|
2045
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value", children: stats.input_count })
|
|
2046
|
-
] }),
|
|
2047
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
|
|
2048
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Matched:" }),
|
|
2049
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value text-green-600", children: stats.matched_count })
|
|
2050
|
-
] }),
|
|
2051
|
-
stats.excluded_count > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
|
|
2052
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Excluded:" }),
|
|
2053
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value text-red-600", children: stats.excluded_count })
|
|
2054
|
-
] })
|
|
2055
|
-
] }),
|
|
2056
|
-
stage.description && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-description", title: stage.description, children: stage.description.length > 50 ? `${stage.description.substring(0, 50)}...` : stage.description })
|
|
2057
|
-
]
|
|
2058
|
-
}
|
|
2059
|
-
),
|
|
2060
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2061
|
-
react$1.Handle,
|
|
2062
|
-
{
|
|
2063
|
-
type: "source",
|
|
2064
|
-
position: react$1.Position.Bottom,
|
|
2065
|
-
style: { background: color, opacity: 0 },
|
|
2066
|
-
isConnectable: false
|
|
2067
|
-
}
|
|
2068
|
-
)
|
|
2069
|
-
] });
|
|
2070
|
-
}
|
|
2071
|
-
function FlowLegend() {
|
|
2072
|
-
const [isExpanded, setIsExpanded] = react.useState(true);
|
|
2073
|
-
const legendItems = [
|
|
2074
|
-
{ color: "#3b82f6", label: "Continue" },
|
|
2075
|
-
{ color: "#ef4444", label: "Exclude" },
|
|
2076
|
-
{ color: "#eab308", label: "Tag" },
|
|
2077
|
-
{ color: "#22c55e", label: "Output" }
|
|
2078
|
-
];
|
|
2079
|
-
return /* @__PURE__ */ jsxRuntime.jsx(react$1.Panel, { position: "bottom-right", className: "flow-legend-panel", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flow-legend", children: [
|
|
2080
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2081
|
-
"button",
|
|
2082
|
-
{
|
|
2083
|
-
className: "legend-toggle",
|
|
2084
|
-
onClick: () => setIsExpanded(!isExpanded),
|
|
2085
|
-
"aria-label": isExpanded ? "Collapse legend" : "Expand legend",
|
|
2086
|
-
children: [
|
|
2087
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "legend-title", children: "Legend" }),
|
|
2088
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2089
|
-
"svg",
|
|
2090
|
-
{
|
|
2091
|
-
className: `legend-chevron ${isExpanded ? "expanded" : ""}`,
|
|
2092
|
-
width: "12",
|
|
2093
|
-
height: "12",
|
|
2094
|
-
viewBox: "0 0 12 12",
|
|
2095
|
-
fill: "none",
|
|
2096
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
2097
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2098
|
-
"path",
|
|
2099
|
-
{
|
|
2100
|
-
d: "M3 4.5L6 7.5L9 4.5",
|
|
2101
|
-
stroke: "currentColor",
|
|
2102
|
-
strokeWidth: "1.5",
|
|
2103
|
-
strokeLinecap: "round",
|
|
2104
|
-
strokeLinejoin: "round"
|
|
2105
|
-
}
|
|
2106
|
-
)
|
|
2107
|
-
}
|
|
2108
|
-
)
|
|
2109
|
-
]
|
|
2110
|
-
}
|
|
2111
|
-
),
|
|
2112
|
-
isExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "legend-items", children: legendItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "legend-item", children: [
|
|
2113
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2114
|
-
"div",
|
|
2115
|
-
{
|
|
2116
|
-
className: "legend-color",
|
|
2117
|
-
style: {
|
|
2118
|
-
backgroundColor: item.color,
|
|
2119
|
-
border: `2px solid ${item.color}`
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
),
|
|
2123
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "legend-label", children: item.label })
|
|
2124
|
-
] }, item.label)) })
|
|
2125
|
-
] }) });
|
|
2126
|
-
}
|
|
2127
|
-
function getExcludedCount(runData, fromStageId, toStageId) {
|
|
2128
|
-
const fromStats = runData.stage_stats[fromStageId];
|
|
2129
|
-
const toStats = runData.stage_stats[toStageId];
|
|
2130
|
-
if (!fromStats || !toStats) return 0;
|
|
2131
|
-
return fromStats.continued_count - toStats.input_count;
|
|
2132
|
-
}
|
|
2133
|
-
function getCircledNumber(num) {
|
|
2134
|
-
const circledNumbers = ["\u2460", "\u2461", "\u2462", "\u2463", "\u2464", "\u2465", "\u2466", "\u2467", "\u2468", "\u2469"];
|
|
2135
|
-
return num <= 10 ? circledNumbers[num - 1] : `${num}`;
|
|
2136
|
-
}
|
|
2137
|
-
var VERTICAL_SPACING = 180;
|
|
2138
|
-
var HORIZONTAL_CENTER = 250;
|
|
2139
|
-
function FunnelVisualFlow({
|
|
2140
|
-
funnel,
|
|
2141
|
-
runData,
|
|
2142
|
-
onStageClick,
|
|
2143
|
-
onEdgeClick,
|
|
2144
|
-
className = "",
|
|
2145
|
-
height = 600
|
|
2146
|
-
}) {
|
|
2147
|
-
const nodeTypes = react.useMemo(
|
|
2148
|
-
() => ({
|
|
2149
|
-
stageNode: StageNode
|
|
2150
|
-
}),
|
|
2151
|
-
[]
|
|
2152
|
-
);
|
|
2153
|
-
const initialNodes = react.useMemo(() => {
|
|
2154
|
-
return funnel.stages.map((stage, index) => {
|
|
2155
|
-
const stats = runData?.stage_stats?.[stage.id];
|
|
2156
|
-
return {
|
|
2157
|
-
id: stage.id,
|
|
2158
|
-
type: "stageNode",
|
|
2159
|
-
position: { x: HORIZONTAL_CENTER, y: index * VERTICAL_SPACING },
|
|
2160
|
-
data: {
|
|
2161
|
-
stage,
|
|
2162
|
-
stats,
|
|
2163
|
-
onStageClick
|
|
2164
|
-
}
|
|
2165
|
-
};
|
|
2166
|
-
});
|
|
2167
|
-
}, [funnel.stages, runData, onStageClick]);
|
|
2168
|
-
const initialEdges = react.useMemo(() => {
|
|
2169
|
-
if (funnel.stages.length < 2) return [];
|
|
2170
|
-
return funnel.stages.slice(0, -1).map((stage, index) => {
|
|
2171
|
-
const nextStage = funnel.stages[index + 1];
|
|
2172
|
-
const excludedCount = runData ? getExcludedCount(runData, stage.id, nextStage.id) : void 0;
|
|
2173
|
-
return {
|
|
2174
|
-
id: `${stage.id}-${nextStage.id}`,
|
|
2175
|
-
source: stage.id,
|
|
2176
|
-
target: nextStage.id,
|
|
2177
|
-
label: excludedCount !== void 0 ? `-${excludedCount}` : "",
|
|
2178
|
-
animated: true,
|
|
2179
|
-
style: { stroke: "#94a3b8", strokeWidth: 2 },
|
|
2180
|
-
labelStyle: { fill: "#ef4444", fontWeight: 600 },
|
|
2181
|
-
labelBgStyle: { fill: "#fef2f2", fillOpacity: 0.9 }
|
|
2182
|
-
};
|
|
2183
|
-
});
|
|
2184
|
-
}, [funnel.stages, runData]);
|
|
2185
|
-
const [nodes, , onNodesChange] = react$1.useNodesState(initialNodes);
|
|
2186
|
-
const [edges, , onEdgesChange] = react$1.useEdgesState(initialEdges);
|
|
2187
|
-
const handleEdgeClick = react.useCallback(
|
|
2188
|
-
(event, edge) => {
|
|
2189
|
-
if (onEdgeClick) {
|
|
2190
|
-
onEdgeClick(edge.source, edge.target);
|
|
2191
|
-
}
|
|
2192
|
-
},
|
|
2193
|
-
[onEdgeClick]
|
|
2194
|
-
);
|
|
2195
|
-
if (funnel.stages.length === 0) {
|
|
2196
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2197
|
-
"div",
|
|
2198
|
-
{
|
|
2199
|
-
className: `funnel-visual-flow-empty ${className}`,
|
|
2200
|
-
style: { height },
|
|
2201
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "empty-state", children: [
|
|
2202
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: "No stages to visualize" }),
|
|
2203
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-400 text-xs mt-1", children: "Add stages to see the funnel flow" })
|
|
2204
|
-
] })
|
|
2205
|
-
}
|
|
2206
|
-
);
|
|
2207
|
-
}
|
|
2208
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `funnel-visual-flow ${className}`, style: { height }, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2209
|
-
react$1.ReactFlow,
|
|
2210
|
-
{
|
|
2211
|
-
nodes,
|
|
2212
|
-
edges,
|
|
2213
|
-
onNodesChange,
|
|
2214
|
-
onEdgesChange,
|
|
2215
|
-
onEdgeClick: handleEdgeClick,
|
|
2216
|
-
nodeTypes,
|
|
2217
|
-
fitView: true,
|
|
2218
|
-
fitViewOptions: {
|
|
2219
|
-
padding: 0.2,
|
|
2220
|
-
includeHiddenNodes: false
|
|
2221
|
-
},
|
|
2222
|
-
minZoom: 0.5,
|
|
2223
|
-
maxZoom: 1.5,
|
|
2224
|
-
defaultViewport: { x: 0, y: 0, zoom: 1 },
|
|
2225
|
-
nodesDraggable: false,
|
|
2226
|
-
nodesConnectable: false,
|
|
2227
|
-
elementsSelectable: true,
|
|
2228
|
-
children: [
|
|
2229
|
-
/* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { variant: react$1.BackgroundVariant.Dots, gap: 16, size: 1 }),
|
|
2230
|
-
/* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false }),
|
|
2231
|
-
/* @__PURE__ */ jsxRuntime.jsx(FlowLegend, {})
|
|
2232
|
-
]
|
|
2233
|
-
}
|
|
2234
|
-
) });
|
|
2235
|
-
}
|
|
2236
|
-
function LogicToggle({ logic, onChange, className = "" }) {
|
|
2237
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center gap-4 ${className}`, children: [
|
|
2238
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700", children: "Logic:" }),
|
|
2239
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
2240
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2241
|
-
"input",
|
|
2242
|
-
{
|
|
2243
|
-
type: "radio",
|
|
2244
|
-
name: "filter-logic",
|
|
2245
|
-
value: "AND",
|
|
2246
|
-
checked: logic === "AND",
|
|
2247
|
-
onChange: (e) => onChange(e.target.value),
|
|
2248
|
-
className: "w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
|
|
2249
|
-
}
|
|
2250
|
-
),
|
|
2251
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-700", children: [
|
|
2252
|
-
"AND ",
|
|
2253
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500", children: "(all must match)" })
|
|
2254
|
-
] })
|
|
2255
|
-
] }),
|
|
2256
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
2257
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2258
|
-
"input",
|
|
2259
|
-
{
|
|
2260
|
-
type: "radio",
|
|
2261
|
-
name: "filter-logic",
|
|
2262
|
-
value: "OR",
|
|
2263
|
-
checked: logic === "OR",
|
|
2264
|
-
onChange: (e) => onChange(e.target.value),
|
|
2265
|
-
className: "w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
|
|
2266
|
-
}
|
|
2267
|
-
),
|
|
2268
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-700", children: [
|
|
2269
|
-
"OR ",
|
|
2270
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500", children: "(any can match)" })
|
|
2271
|
-
] })
|
|
2272
|
-
] })
|
|
2273
|
-
] });
|
|
2274
|
-
}
|
|
2275
|
-
function FieldSelector({
|
|
2276
|
-
fields,
|
|
2277
|
-
value,
|
|
2278
|
-
onChange,
|
|
2279
|
-
error,
|
|
2280
|
-
className = ""
|
|
2281
|
-
}) {
|
|
2282
|
-
const grouped = fields.reduce((acc, field) => {
|
|
2283
|
-
const category = field.category || "Other";
|
|
2284
|
-
if (!acc[category]) {
|
|
2285
|
-
acc[category] = [];
|
|
2286
|
-
}
|
|
2287
|
-
acc[category].push(field);
|
|
2288
|
-
return acc;
|
|
2289
|
-
}, {});
|
|
2290
|
-
const categories = Object.keys(grouped).sort();
|
|
2291
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
|
|
2292
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "field-selector", className: "text-xs font-medium text-gray-700", children: "Field" }),
|
|
2293
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2294
|
-
"select",
|
|
2295
|
-
{
|
|
2296
|
-
id: "field-selector",
|
|
2297
|
-
value,
|
|
2298
|
-
onChange: (e) => onChange(e.target.value),
|
|
2299
|
-
className: `
|
|
2300
|
-
w-full px-3 py-2 text-sm border rounded-md
|
|
2301
|
-
bg-white
|
|
2302
|
-
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
2303
|
-
${error ? "border-red-500" : "border-gray-300"}
|
|
2304
|
-
`,
|
|
2305
|
-
children: [
|
|
2306
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select a field..." }),
|
|
2307
|
-
categories.map((category) => /* @__PURE__ */ jsxRuntime.jsx("optgroup", { label: category, children: grouped[category].map((field) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: field.name, children: field.label }, field.name)) }, category))
|
|
2308
|
-
]
|
|
2309
|
-
}
|
|
2310
|
-
),
|
|
2311
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
|
|
2312
|
-
] });
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
// src/components/FilterRuleEditor/constants.ts
|
|
2316
|
-
var OPERATOR_LABELS = {
|
|
2317
|
-
// Equality
|
|
2318
|
-
eq: "equals",
|
|
2319
|
-
ne: "not equals",
|
|
2320
|
-
// Comparison
|
|
2321
|
-
gt: "greater than",
|
|
2322
|
-
lt: "less than",
|
|
2323
|
-
gte: "greater or equal",
|
|
2324
|
-
lte: "less or equal",
|
|
2325
|
-
// String operations
|
|
2326
|
-
contains: "contains",
|
|
2327
|
-
not_contains: "does not contain",
|
|
2328
|
-
startswith: "starts with",
|
|
2329
|
-
endswith: "ends with",
|
|
2330
|
-
matches: "matches regex",
|
|
2331
|
-
// Array/Set operations
|
|
2332
|
-
in: "is one of",
|
|
2333
|
-
not_in: "is not one of",
|
|
2334
|
-
has_any: "has any of",
|
|
2335
|
-
has_all: "has all of",
|
|
2336
|
-
// Null checks
|
|
2337
|
-
isnull: "is empty",
|
|
2338
|
-
isnotnull: "is not empty",
|
|
2339
|
-
// Tag operations
|
|
2340
|
-
has_tag: "has tag",
|
|
2341
|
-
not_has_tag: "does not have tag",
|
|
2342
|
-
// Boolean
|
|
2343
|
-
is_true: "is true",
|
|
2344
|
-
is_false: "is false"
|
|
2345
|
-
};
|
|
2346
|
-
var NULL_VALUE_OPERATORS = [
|
|
2347
|
-
"isnull",
|
|
2348
|
-
"isnotnull",
|
|
2349
|
-
"is_true",
|
|
2350
|
-
"is_false"
|
|
2351
|
-
];
|
|
2352
|
-
var MULTI_VALUE_OPERATORS = [
|
|
2353
|
-
"in",
|
|
2354
|
-
"not_in",
|
|
2355
|
-
"has_any",
|
|
2356
|
-
"has_all"
|
|
2357
|
-
];
|
|
2358
|
-
function OperatorSelector({
|
|
2359
|
-
operators,
|
|
2360
|
-
value,
|
|
2361
|
-
onChange,
|
|
2362
|
-
disabled = false,
|
|
2363
|
-
error,
|
|
2364
|
-
className = ""
|
|
2365
|
-
}) {
|
|
2366
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
|
|
2367
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "operator-selector", className: "text-xs font-medium text-gray-700", children: "Operator" }),
|
|
2368
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2369
|
-
"select",
|
|
2370
|
-
{
|
|
2371
|
-
id: "operator-selector",
|
|
2372
|
-
value,
|
|
2373
|
-
onChange: (e) => onChange(e.target.value),
|
|
2374
|
-
disabled,
|
|
2375
|
-
className: `
|
|
2376
|
-
w-full px-3 py-2 text-sm border rounded-md
|
|
2377
|
-
bg-white
|
|
2378
|
-
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
2379
|
-
disabled:bg-gray-100 disabled:cursor-not-allowed disabled:text-gray-500
|
|
2380
|
-
${error ? "border-red-500" : "border-gray-300"}
|
|
2381
|
-
`,
|
|
2382
|
-
children: [
|
|
2383
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select operator..." }),
|
|
2384
|
-
operators.map((op) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: op, children: OPERATOR_LABELS[op] }, op))
|
|
2385
|
-
]
|
|
2386
|
-
}
|
|
2387
|
-
),
|
|
2388
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
|
|
2389
|
-
] });
|
|
2390
|
-
}
|
|
2391
|
-
function TextValueInput({
|
|
2392
|
-
value,
|
|
2393
|
-
onChange,
|
|
2394
|
-
placeholder = "Enter text...",
|
|
2395
|
-
error,
|
|
2396
|
-
className = ""
|
|
2397
|
-
}) {
|
|
2398
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
|
|
2399
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "text-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
|
|
2400
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2401
|
-
"input",
|
|
2402
|
-
{
|
|
2403
|
-
id: "text-value",
|
|
2404
|
-
type: "text",
|
|
2405
|
-
value: value || "",
|
|
2406
|
-
onChange: (e) => onChange(e.target.value),
|
|
2407
|
-
placeholder,
|
|
2408
|
-
className: `
|
|
2409
|
-
w-full px-3 py-2 text-sm border rounded-md
|
|
2410
|
-
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
2411
|
-
${error ? "border-red-500" : "border-gray-300"}
|
|
2412
|
-
`
|
|
2413
|
-
}
|
|
2414
|
-
),
|
|
2415
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
|
|
2416
|
-
] });
|
|
2417
|
-
}
|
|
2418
|
-
function NumberValueInput({
|
|
2419
|
-
value,
|
|
2420
|
-
onChange,
|
|
2421
|
-
min,
|
|
2422
|
-
max,
|
|
2423
|
-
placeholder = "Enter number...",
|
|
2424
|
-
error,
|
|
2425
|
-
className = ""
|
|
2426
|
-
}) {
|
|
2427
|
-
const handleChange = (e) => {
|
|
2428
|
-
const val = e.target.value;
|
|
2429
|
-
if (val === "") {
|
|
2430
|
-
onChange(null);
|
|
2431
|
-
} else {
|
|
2432
|
-
const num = parseFloat(val);
|
|
2433
|
-
if (!isNaN(num)) {
|
|
2434
|
-
onChange(num);
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
};
|
|
2438
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
|
|
2439
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "number-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
|
|
2440
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2441
|
-
"input",
|
|
2442
|
-
{
|
|
2443
|
-
id: "number-value",
|
|
2444
|
-
type: "number",
|
|
2445
|
-
value: value ?? "",
|
|
2446
|
-
onChange: handleChange,
|
|
2447
|
-
min,
|
|
2448
|
-
max,
|
|
2449
|
-
placeholder,
|
|
2450
|
-
className: `
|
|
2451
|
-
w-full px-3 py-2 text-sm border rounded-md
|
|
2452
|
-
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
2453
|
-
${error ? "border-red-500" : "border-gray-300"}
|
|
2454
|
-
`
|
|
2455
|
-
}
|
|
2456
|
-
),
|
|
2457
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
|
|
2458
|
-
] });
|
|
2459
|
-
}
|
|
2460
|
-
function DateValueInput({
|
|
2461
|
-
value,
|
|
2462
|
-
onChange,
|
|
2463
|
-
min,
|
|
2464
|
-
max,
|
|
2465
|
-
placeholder = "Select date...",
|
|
2466
|
-
error,
|
|
2467
|
-
className = ""
|
|
2468
|
-
}) {
|
|
2469
|
-
const handleChange = (e) => {
|
|
2470
|
-
const val = e.target.value;
|
|
2471
|
-
onChange(val || null);
|
|
2472
|
-
};
|
|
2473
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
|
|
2474
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "date-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
|
|
2475
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2476
|
-
"input",
|
|
2477
|
-
{
|
|
2478
|
-
id: "date-value",
|
|
2479
|
-
type: "date",
|
|
2480
|
-
value: value || "",
|
|
2481
|
-
onChange: handleChange,
|
|
2482
|
-
min,
|
|
2483
|
-
max,
|
|
2484
|
-
placeholder,
|
|
2485
|
-
className: `
|
|
2486
|
-
w-full px-3 py-2 text-sm border rounded-md
|
|
2487
|
-
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
2488
|
-
${error ? "border-red-500" : "border-gray-300"}
|
|
2489
|
-
`
|
|
2490
|
-
}
|
|
2491
|
-
),
|
|
2492
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
|
|
2493
|
-
] });
|
|
2494
|
-
}
|
|
2495
|
-
function BooleanValueInput({
|
|
2496
|
-
value,
|
|
2497
|
-
onChange,
|
|
2498
|
-
label = "True",
|
|
2499
|
-
error,
|
|
2500
|
-
className = ""
|
|
2501
|
-
}) {
|
|
2502
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
|
|
2503
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-gray-700", children: "Value" }),
|
|
2504
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
2505
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2506
|
-
"input",
|
|
2507
|
-
{
|
|
2508
|
-
type: "checkbox",
|
|
2509
|
-
checked: value,
|
|
2510
|
-
onChange: (e) => onChange(e.target.checked),
|
|
2511
|
-
className: "w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
2512
|
-
}
|
|
2513
|
-
),
|
|
2514
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700", children: label })
|
|
2515
|
-
] }),
|
|
2516
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
|
|
2517
|
-
] });
|
|
2518
|
-
}
|
|
2519
|
-
function ChoiceValueInput({
|
|
2520
|
-
value,
|
|
2521
|
-
onChange,
|
|
2522
|
-
choices,
|
|
2523
|
-
placeholder = "Select option...",
|
|
2524
|
-
error,
|
|
2525
|
-
className = ""
|
|
2526
|
-
}) {
|
|
2527
|
-
const getChoiceValue = (choice) => {
|
|
2528
|
-
if (typeof choice === "string") return choice;
|
|
2529
|
-
return choice.value || choice;
|
|
2530
|
-
};
|
|
2531
|
-
const getChoiceLabel = (choice) => {
|
|
2532
|
-
if (typeof choice === "string") return choice;
|
|
2533
|
-
return choice.label || choice.value || String(choice);
|
|
2534
|
-
};
|
|
2535
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
|
|
2536
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "choice-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
|
|
2537
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2538
|
-
"select",
|
|
2539
|
-
{
|
|
2540
|
-
id: "choice-value",
|
|
2541
|
-
value: value || "",
|
|
2542
|
-
onChange: (e) => onChange(e.target.value),
|
|
2543
|
-
className: `
|
|
2544
|
-
w-full px-3 py-2 text-sm border rounded-md
|
|
2545
|
-
bg-white
|
|
2546
|
-
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
2547
|
-
${error ? "border-red-500" : "border-gray-300"}
|
|
2548
|
-
`,
|
|
2549
|
-
children: [
|
|
2550
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: placeholder }),
|
|
2551
|
-
choices.map((choice, index) => {
|
|
2552
|
-
const val = getChoiceValue(choice);
|
|
2553
|
-
const label = getChoiceLabel(choice);
|
|
2554
|
-
return /* @__PURE__ */ jsxRuntime.jsx("option", { value: val, children: label }, `${val}-${index}`);
|
|
2555
|
-
})
|
|
2556
|
-
]
|
|
2557
|
-
}
|
|
2558
|
-
),
|
|
2559
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
|
|
2560
|
-
] });
|
|
2561
|
-
}
|
|
2562
|
-
function MultiChoiceValueInput({
|
|
2563
|
-
value = [],
|
|
2564
|
-
onChange,
|
|
2565
|
-
choices,
|
|
2566
|
-
placeholder = "Select options...",
|
|
2567
|
-
error,
|
|
2568
|
-
className = ""
|
|
2569
|
-
}) {
|
|
2570
|
-
const getChoiceValue = (choice) => {
|
|
2571
|
-
if (typeof choice === "string") return choice;
|
|
2572
|
-
return choice.value || choice;
|
|
2573
|
-
};
|
|
2574
|
-
const getChoiceLabel = (choice) => {
|
|
2575
|
-
if (typeof choice === "string") return choice;
|
|
2576
|
-
return choice.label || choice.value || String(choice);
|
|
2577
|
-
};
|
|
2578
|
-
const handleAdd = (newValue) => {
|
|
2579
|
-
if (newValue && !value.includes(newValue)) {
|
|
2580
|
-
onChange([...value, newValue]);
|
|
2581
|
-
}
|
|
2582
|
-
};
|
|
2583
|
-
const handleRemove = (removeValue) => {
|
|
2584
|
-
onChange(value.filter((v) => v !== removeValue));
|
|
2585
|
-
};
|
|
2586
|
-
const getValueLabel = (val) => {
|
|
2587
|
-
const choice = choices.find((c) => getChoiceValue(c) === val);
|
|
2588
|
-
return choice ? getChoiceLabel(choice) : val;
|
|
2589
|
-
};
|
|
2590
|
-
const availableChoices = choices.filter(
|
|
2591
|
-
(choice) => !value.includes(getChoiceValue(choice))
|
|
2592
|
-
);
|
|
2593
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-2 ${className}`, children: [
|
|
2594
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "multi-choice-value", className: "text-xs font-medium text-gray-700", children: "Values" }),
|
|
2595
|
-
value.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: value.map((val) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2596
|
-
"span",
|
|
2597
|
-
{
|
|
2598
|
-
className: "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded",
|
|
2599
|
-
children: [
|
|
2600
|
-
getValueLabel(val),
|
|
2601
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2602
|
-
"button",
|
|
2603
|
-
{
|
|
2604
|
-
type: "button",
|
|
2605
|
-
onClick: () => handleRemove(val),
|
|
2606
|
-
className: "hover:text-blue-900 focus:outline-none",
|
|
2607
|
-
"aria-label": `Remove ${getValueLabel(val)}`,
|
|
2608
|
-
children: "\xD7"
|
|
2609
|
-
}
|
|
2610
|
-
)
|
|
2611
|
-
]
|
|
2612
|
-
},
|
|
2613
|
-
val
|
|
2614
|
-
)) }),
|
|
2615
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2616
|
-
"select",
|
|
2617
|
-
{
|
|
2618
|
-
id: "multi-choice-value",
|
|
2619
|
-
value: "",
|
|
2620
|
-
onChange: (e) => handleAdd(e.target.value),
|
|
2621
|
-
className: `
|
|
2622
|
-
w-full px-3 py-2 text-sm border rounded-md
|
|
2623
|
-
bg-white
|
|
2624
|
-
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
2625
|
-
${error ? "border-red-500" : "border-gray-300"}
|
|
2626
|
-
`,
|
|
2627
|
-
children: [
|
|
2628
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: placeholder }),
|
|
2629
|
-
availableChoices.map((choice, index) => {
|
|
2630
|
-
const val = getChoiceValue(choice);
|
|
2631
|
-
const label = getChoiceLabel(choice);
|
|
2632
|
-
return /* @__PURE__ */ jsxRuntime.jsx("option", { value: val, children: label }, `${val}-${index}`);
|
|
2633
|
-
})
|
|
2634
|
-
]
|
|
2635
|
-
}
|
|
2636
|
-
),
|
|
2637
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
|
|
2638
|
-
] });
|
|
2639
|
-
}
|
|
2640
|
-
function RuleRow({
|
|
2641
|
-
rule,
|
|
2642
|
-
onChange,
|
|
2643
|
-
onRemove,
|
|
2644
|
-
fieldRegistry,
|
|
2645
|
-
className = ""
|
|
2646
|
-
}) {
|
|
2647
|
-
const selectedField = fieldRegistry.find((f) => f.name === rule.field_path);
|
|
2648
|
-
const availableOperators = selectedField?.operators || [];
|
|
2649
|
-
const needsValue = rule.operator && !NULL_VALUE_OPERATORS.includes(rule.operator);
|
|
2650
|
-
const needsMultiValue = rule.operator && MULTI_VALUE_OPERATORS.includes(rule.operator);
|
|
2651
|
-
const handleFieldChange = (fieldName) => {
|
|
2652
|
-
const field = fieldRegistry.find((f) => f.name === fieldName);
|
|
2653
|
-
onChange({
|
|
2654
|
-
...rule,
|
|
2655
|
-
field_path: fieldName,
|
|
2656
|
-
operator: field?.operators[0] || "",
|
|
2657
|
-
value: null
|
|
2658
|
-
});
|
|
2659
|
-
};
|
|
2660
|
-
const handleOperatorChange = (operator) => {
|
|
2661
|
-
onChange({
|
|
2662
|
-
...rule,
|
|
2663
|
-
operator,
|
|
2664
|
-
value: MULTI_VALUE_OPERATORS.includes(operator) ? [] : null
|
|
2665
|
-
});
|
|
2666
|
-
};
|
|
2667
|
-
const handleValueChange = (value) => {
|
|
2668
|
-
onChange({
|
|
2669
|
-
...rule,
|
|
2670
|
-
value
|
|
2671
|
-
});
|
|
2672
|
-
};
|
|
2673
|
-
const renderValueInput = () => {
|
|
2674
|
-
if (!needsValue) return null;
|
|
2675
|
-
if (!selectedField) return null;
|
|
2676
|
-
const { type, constraints } = selectedField;
|
|
2677
|
-
if (needsMultiValue) {
|
|
2678
|
-
if (constraints?.choices) {
|
|
2679
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2680
|
-
MultiChoiceValueInput,
|
|
2681
|
-
{
|
|
2682
|
-
value: Array.isArray(rule.value) ? rule.value : [],
|
|
2683
|
-
onChange: handleValueChange,
|
|
2684
|
-
choices: constraints.choices
|
|
2685
|
-
}
|
|
2686
|
-
);
|
|
2687
|
-
}
|
|
2688
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2689
|
-
TextValueInput,
|
|
2690
|
-
{
|
|
2691
|
-
value: Array.isArray(rule.value) ? rule.value.join(", ") : "",
|
|
2692
|
-
onChange: (val) => handleValueChange(val.split(",").map((v) => v.trim())),
|
|
2693
|
-
placeholder: "Enter values, comma-separated..."
|
|
2694
|
-
}
|
|
2695
|
-
);
|
|
2696
|
-
}
|
|
2697
|
-
switch (type) {
|
|
2698
|
-
case "string":
|
|
2699
|
-
if (constraints?.choices && rule.operator === "eq") {
|
|
2700
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2701
|
-
ChoiceValueInput,
|
|
2702
|
-
{
|
|
2703
|
-
value: rule.value || "",
|
|
2704
|
-
onChange: handleValueChange,
|
|
2705
|
-
choices: constraints.choices
|
|
2706
|
-
}
|
|
2707
|
-
);
|
|
2708
|
-
}
|
|
2709
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2710
|
-
TextValueInput,
|
|
2711
|
-
{
|
|
2712
|
-
value: rule.value || "",
|
|
2713
|
-
onChange: handleValueChange
|
|
2714
|
-
}
|
|
2715
|
-
);
|
|
2716
|
-
case "number":
|
|
2717
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2718
|
-
NumberValueInput,
|
|
2719
|
-
{
|
|
2720
|
-
value: rule.value,
|
|
2721
|
-
onChange: handleValueChange,
|
|
2722
|
-
min: constraints?.min_value,
|
|
2723
|
-
max: constraints?.max_value
|
|
2724
|
-
}
|
|
2725
|
-
);
|
|
2726
|
-
case "date":
|
|
2727
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2728
|
-
DateValueInput,
|
|
2729
|
-
{
|
|
2730
|
-
value: rule.value || null,
|
|
2731
|
-
onChange: handleValueChange,
|
|
2732
|
-
min: constraints?.min_value,
|
|
2733
|
-
max: constraints?.max_value
|
|
2734
|
-
}
|
|
2735
|
-
);
|
|
2736
|
-
case "boolean":
|
|
2737
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2738
|
-
BooleanValueInput,
|
|
2739
|
-
{
|
|
2740
|
-
value: rule.value || false,
|
|
2741
|
-
onChange: handleValueChange
|
|
2742
|
-
}
|
|
2743
|
-
);
|
|
2744
|
-
case "tag":
|
|
2745
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2746
|
-
TextValueInput,
|
|
2747
|
-
{
|
|
2748
|
-
value: rule.value || "",
|
|
2749
|
-
onChange: handleValueChange,
|
|
2750
|
-
placeholder: "Enter tag name..."
|
|
2751
|
-
}
|
|
2752
|
-
);
|
|
2753
|
-
default:
|
|
2754
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2755
|
-
TextValueInput,
|
|
2756
|
-
{
|
|
2757
|
-
value: rule.value || "",
|
|
2758
|
-
onChange: handleValueChange
|
|
2759
|
-
}
|
|
2760
|
-
);
|
|
2761
|
-
}
|
|
2762
|
-
};
|
|
2763
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2764
|
-
"div",
|
|
2765
|
-
{
|
|
2766
|
-
className: `
|
|
2767
|
-
relative group
|
|
2768
|
-
border border-gray-200 rounded-lg p-4
|
|
2769
|
-
bg-white hover:shadow-sm transition-shadow
|
|
2770
|
-
${className}
|
|
2771
|
-
`,
|
|
2772
|
-
children: [
|
|
2773
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2774
|
-
"button",
|
|
2775
|
-
{
|
|
2776
|
-
type: "button",
|
|
2777
|
-
onClick: onRemove,
|
|
2778
|
-
className: "\n absolute top-2 right-2\n w-6 h-6 flex items-center justify-center\n text-gray-400 hover:text-red-600 hover:bg-red-50\n rounded transition-colors\n focus:outline-none focus:ring-2 focus:ring-red-500\n ",
|
|
2779
|
-
"aria-label": "Remove rule",
|
|
2780
|
-
children: "\xD7"
|
|
2781
|
-
}
|
|
2782
|
-
),
|
|
2783
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 pr-8", children: [
|
|
2784
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2785
|
-
FieldSelector,
|
|
2786
|
-
{
|
|
2787
|
-
fields: fieldRegistry,
|
|
2788
|
-
value: rule.field_path,
|
|
2789
|
-
onChange: handleFieldChange
|
|
2790
|
-
}
|
|
2791
|
-
),
|
|
2792
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2793
|
-
OperatorSelector,
|
|
2794
|
-
{
|
|
2795
|
-
operators: availableOperators,
|
|
2796
|
-
value: rule.operator || "",
|
|
2797
|
-
onChange: handleOperatorChange,
|
|
2798
|
-
disabled: !rule.field_path
|
|
2799
|
-
}
|
|
2800
|
-
),
|
|
2801
|
-
needsValue && renderValueInput()
|
|
2802
|
-
] })
|
|
2803
|
-
]
|
|
2804
|
-
}
|
|
2805
|
-
);
|
|
2806
|
-
}
|
|
2807
|
-
function FilterRuleEditor({
|
|
2808
|
-
rules,
|
|
2809
|
-
onChange,
|
|
2810
|
-
fieldRegistry,
|
|
2811
|
-
logic = "AND",
|
|
2812
|
-
onLogicChange,
|
|
2813
|
-
className = ""
|
|
2814
|
-
}) {
|
|
2815
|
-
const handleAddRule = () => {
|
|
2816
|
-
const newRule = {
|
|
2817
|
-
field_path: "",
|
|
2818
|
-
operator: "eq",
|
|
2819
|
-
value: null
|
|
2820
|
-
};
|
|
2821
|
-
onChange([...rules, newRule]);
|
|
2822
|
-
};
|
|
2823
|
-
const handleUpdateRule = (index, updatedRule) => {
|
|
2824
|
-
const newRules = [...rules];
|
|
2825
|
-
newRules[index] = updatedRule;
|
|
2826
|
-
onChange(newRules);
|
|
2827
|
-
};
|
|
2828
|
-
const handleRemoveRule = (index) => {
|
|
2829
|
-
const newRules = rules.filter((_, i) => i !== index);
|
|
2830
|
-
onChange(newRules);
|
|
2831
|
-
};
|
|
2832
|
-
const ruleErrors = rules.map((rule) => validateFilterRule(rule));
|
|
2833
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-4 ${className}`, children: [
|
|
2834
|
-
onLogicChange && /* @__PURE__ */ jsxRuntime.jsx(LogicToggle, { logic, onChange: onLogicChange }),
|
|
2835
|
-
rules.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50", children: [
|
|
2836
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2837
|
-
"svg",
|
|
2838
|
-
{
|
|
2839
|
-
className: "w-12 h-12 text-gray-400 mb-3",
|
|
2840
|
-
fill: "none",
|
|
2841
|
-
viewBox: "0 0 24 24",
|
|
2842
|
-
stroke: "currentColor",
|
|
2843
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2844
|
-
"path",
|
|
2845
|
-
{
|
|
2846
|
-
strokeLinecap: "round",
|
|
2847
|
-
strokeLinejoin: "round",
|
|
2848
|
-
strokeWidth: 2,
|
|
2849
|
-
d: "M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
|
2850
|
-
}
|
|
2851
|
-
)
|
|
2852
|
-
}
|
|
2853
|
-
),
|
|
2854
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600 mb-4", children: "No filter rules yet" }),
|
|
2855
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2856
|
-
"button",
|
|
2857
|
-
{
|
|
2858
|
-
type: "button",
|
|
2859
|
-
onClick: handleAddRule,
|
|
2860
|
-
className: "\n inline-flex items-center gap-2\n px-4 py-2\n text-sm font-medium text-white\n bg-blue-600 hover:bg-blue-700\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\n transition-colors\n ",
|
|
2861
|
-
children: [
|
|
2862
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-lg", children: "+" }),
|
|
2863
|
-
"Add First Rule"
|
|
2864
|
-
]
|
|
2865
|
-
}
|
|
2866
|
-
)
|
|
2867
|
-
] }),
|
|
2868
|
-
rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3", children: rules.map((rule, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2869
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2870
|
-
RuleRow,
|
|
2871
|
-
{
|
|
2872
|
-
rule,
|
|
2873
|
-
onChange: (updatedRule) => handleUpdateRule(index, updatedRule),
|
|
2874
|
-
onRemove: () => handleRemoveRule(index),
|
|
2875
|
-
fieldRegistry
|
|
2876
|
-
}
|
|
2877
|
-
),
|
|
2878
|
-
index < rules.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center py-2", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-3 py-1 text-xs font-semibold text-gray-700 bg-gray-100 border border-gray-300 rounded-full", children: logic }) }),
|
|
2879
|
-
ruleErrors[index].length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 px-4 py-2 bg-red-50 border border-red-200 rounded text-sm text-red-700", children: ruleErrors[index].map((error, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: error }, i)) })
|
|
2880
|
-
] }, index)) }),
|
|
2881
|
-
rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2882
|
-
"button",
|
|
2883
|
-
{
|
|
2884
|
-
type: "button",
|
|
2885
|
-
onClick: handleAddRule,
|
|
2886
|
-
className: "\n w-full\n flex items-center justify-center gap-2\n px-4 py-3\n text-sm font-medium text-blue-600\n bg-white hover:bg-blue-50\n border-2 border-dashed border-blue-300 hover:border-blue-400\n rounded-lg\n focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\n transition-colors\n ",
|
|
2887
|
-
"aria-label": `Add rule ${rules.length + 1}`,
|
|
2888
|
-
children: [
|
|
2889
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xl", children: "+" }),
|
|
2890
|
-
"Add Rule"
|
|
2891
|
-
]
|
|
2892
|
-
}
|
|
2893
|
-
),
|
|
2894
|
-
rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs text-gray-500", children: [
|
|
2895
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
2896
|
-
rules.length,
|
|
2897
|
-
" ",
|
|
2898
|
-
rules.length === 1 ? "rule" : "rules"
|
|
2899
|
-
] }),
|
|
2900
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: logic === "AND" ? "All rules must match" : "Any rule can match" })
|
|
2901
|
-
] })
|
|
2902
|
-
] });
|
|
2903
|
-
}
|
|
2904
|
-
var MATCH_ACTIONS = [
|
|
2905
|
-
{
|
|
2906
|
-
value: "continue",
|
|
2907
|
-
label: "Continue",
|
|
2908
|
-
description: "Continue to next stage without tagging"
|
|
2909
|
-
},
|
|
2910
|
-
{
|
|
2911
|
-
value: "tag",
|
|
2912
|
-
label: "Tag & Stop",
|
|
2913
|
-
description: "Add tags and stop processing"
|
|
2914
|
-
},
|
|
2915
|
-
{
|
|
2916
|
-
value: "tag_continue",
|
|
2917
|
-
label: "Tag & Continue",
|
|
2918
|
-
description: "Add tags and continue to next stage"
|
|
2919
|
-
},
|
|
2920
|
-
{
|
|
2921
|
-
value: "output",
|
|
2922
|
-
label: "Output",
|
|
2923
|
-
description: "Add to output and stop processing"
|
|
2924
|
-
}
|
|
2925
|
-
];
|
|
2926
|
-
var NO_MATCH_ACTIONS = [
|
|
2927
|
-
{
|
|
2928
|
-
value: "continue",
|
|
2929
|
-
label: "Continue",
|
|
2930
|
-
description: "Continue to next stage"
|
|
2931
|
-
},
|
|
2932
|
-
{
|
|
2933
|
-
value: "exclude",
|
|
2934
|
-
label: "Exclude",
|
|
2935
|
-
description: "Exclude from output and stop processing"
|
|
2936
|
-
},
|
|
2937
|
-
{
|
|
2938
|
-
value: "tag_exclude",
|
|
2939
|
-
label: "Tag & Exclude",
|
|
2940
|
-
description: "Add tags, exclude from output, and stop"
|
|
2941
|
-
}
|
|
2942
|
-
];
|
|
2943
|
-
function StageActions({
|
|
2944
|
-
stage,
|
|
2945
|
-
onMatchActionChange,
|
|
2946
|
-
onNoMatchActionChange
|
|
2947
|
-
}) {
|
|
2948
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-actions", children: [
|
|
2949
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
|
|
2950
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `match-action-${stage.id}`, className: "form-label", children: "Action on Match" }),
|
|
2951
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2952
|
-
"select",
|
|
2953
|
-
{
|
|
2954
|
-
id: `match-action-${stage.id}`,
|
|
2955
|
-
value: stage.match_action,
|
|
2956
|
-
onChange: (e) => onMatchActionChange(e.target.value),
|
|
2957
|
-
className: "form-select",
|
|
2958
|
-
children: MATCH_ACTIONS.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
|
|
2959
|
-
}
|
|
2960
|
-
),
|
|
2961
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: MATCH_ACTIONS.find((a) => a.value === stage.match_action)?.description })
|
|
2962
|
-
] }),
|
|
2963
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
|
|
2964
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `no-match-action-${stage.id}`, className: "form-label", children: "Action on No Match" }),
|
|
2965
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2966
|
-
"select",
|
|
2967
|
-
{
|
|
2968
|
-
id: `no-match-action-${stage.id}`,
|
|
2969
|
-
value: stage.no_match_action,
|
|
2970
|
-
onChange: (e) => onNoMatchActionChange(e.target.value),
|
|
2971
|
-
className: "form-select",
|
|
2972
|
-
children: NO_MATCH_ACTIONS.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
|
|
2973
|
-
}
|
|
2974
|
-
),
|
|
2975
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: NO_MATCH_ACTIONS.find((a) => a.value === stage.no_match_action)?.description })
|
|
2976
|
-
] })
|
|
2977
|
-
] });
|
|
2978
|
-
}
|
|
2979
|
-
function TagInput({
|
|
2980
|
-
tags,
|
|
2981
|
-
onChange,
|
|
2982
|
-
placeholder = "Add tag...",
|
|
2983
|
-
className = ""
|
|
2984
|
-
}) {
|
|
2985
|
-
const [inputValue, setInputValue] = react.useState("");
|
|
2986
|
-
const addTag = react.useCallback((tag) => {
|
|
2987
|
-
const trimmed = tag.trim().toLowerCase();
|
|
2988
|
-
if (!trimmed) {
|
|
2989
|
-
return;
|
|
2990
|
-
}
|
|
2991
|
-
if (tags.includes(trimmed)) {
|
|
2992
|
-
return;
|
|
2993
|
-
}
|
|
2994
|
-
onChange([...tags, trimmed]);
|
|
2995
|
-
setInputValue("");
|
|
2996
|
-
}, [tags, onChange]);
|
|
2997
|
-
const removeTag = react.useCallback((index) => {
|
|
2998
|
-
onChange(tags.filter((_, i) => i !== index));
|
|
2999
|
-
}, [tags, onChange]);
|
|
3000
|
-
const handleKeyDown = react.useCallback((e) => {
|
|
3001
|
-
if (e.key === "Enter" || e.key === ",") {
|
|
3002
|
-
e.preventDefault();
|
|
3003
|
-
addTag(inputValue);
|
|
3004
|
-
} else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
|
|
3005
|
-
removeTag(tags.length - 1);
|
|
3006
|
-
}
|
|
3007
|
-
}, [inputValue, tags, addTag, removeTag]);
|
|
3008
|
-
const handleBlur = react.useCallback(() => {
|
|
3009
|
-
if (inputValue) {
|
|
3010
|
-
addTag(inputValue);
|
|
3011
|
-
}
|
|
3012
|
-
}, [inputValue, addTag]);
|
|
3013
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `tag-input ${className}`, children: [
|
|
3014
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tag-input-container", children: [
|
|
3015
|
-
tags.map((tag, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tag-chip", children: [
|
|
3016
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "tag-text", children: tag }),
|
|
3017
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3018
|
-
"button",
|
|
3019
|
-
{
|
|
3020
|
-
type: "button",
|
|
3021
|
-
onClick: () => removeTag(index),
|
|
3022
|
-
className: "tag-remove",
|
|
3023
|
-
"aria-label": `Remove tag ${tag}`,
|
|
3024
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3025
|
-
"path",
|
|
3026
|
-
{
|
|
3027
|
-
d: "M4 4l6 6M10 4l-6 6",
|
|
3028
|
-
stroke: "currentColor",
|
|
3029
|
-
strokeWidth: "1.5",
|
|
3030
|
-
strokeLinecap: "round"
|
|
3031
|
-
}
|
|
3032
|
-
) })
|
|
3033
|
-
}
|
|
3034
|
-
)
|
|
3035
|
-
] }, index)),
|
|
3036
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3037
|
-
"input",
|
|
3038
|
-
{
|
|
3039
|
-
type: "text",
|
|
3040
|
-
value: inputValue,
|
|
3041
|
-
onChange: (e) => setInputValue(e.target.value),
|
|
3042
|
-
onKeyDown: handleKeyDown,
|
|
3043
|
-
onBlur: handleBlur,
|
|
3044
|
-
placeholder: tags.length === 0 ? placeholder : "",
|
|
3045
|
-
className: "tag-input-field"
|
|
3046
|
-
}
|
|
3047
|
-
)
|
|
3048
|
-
] }),
|
|
3049
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "tag-input-hint", children: "Press Enter or comma to add tags" })
|
|
3050
|
-
] });
|
|
3051
|
-
}
|
|
3052
|
-
function useDebounce(callback, delay) {
|
|
3053
|
-
const [timeoutId, setTimeoutId] = react.useState(null);
|
|
3054
|
-
return react.useCallback(
|
|
3055
|
-
((...args) => {
|
|
3056
|
-
if (timeoutId) {
|
|
3057
|
-
clearTimeout(timeoutId);
|
|
3058
|
-
}
|
|
3059
|
-
const newTimeoutId = setTimeout(() => {
|
|
3060
|
-
callback(...args);
|
|
3061
|
-
}, delay);
|
|
3062
|
-
setTimeoutId(newTimeoutId);
|
|
3063
|
-
}),
|
|
3064
|
-
[callback, delay, timeoutId]
|
|
3065
|
-
);
|
|
3066
|
-
}
|
|
3067
|
-
function StageForm({
|
|
3068
|
-
stage,
|
|
3069
|
-
onUpdate,
|
|
3070
|
-
fieldRegistry
|
|
3071
|
-
}) {
|
|
3072
|
-
const [name, setName] = react.useState(stage.name);
|
|
3073
|
-
const [description, setDescription] = react.useState(stage.description || "");
|
|
3074
|
-
const debouncedUpdateName = useDebounce((value) => {
|
|
3075
|
-
onUpdate({ ...stage, name: value });
|
|
3076
|
-
}, 300);
|
|
3077
|
-
const debouncedUpdateDescription = useDebounce((value) => {
|
|
3078
|
-
onUpdate({ ...stage, description: value });
|
|
3079
|
-
}, 500);
|
|
3080
|
-
const handleNameChange = react.useCallback((e) => {
|
|
3081
|
-
const value = e.target.value;
|
|
3082
|
-
setName(value);
|
|
3083
|
-
debouncedUpdateName(value);
|
|
3084
|
-
}, [debouncedUpdateName]);
|
|
3085
|
-
const handleDescriptionChange = react.useCallback((e) => {
|
|
3086
|
-
const value = e.target.value;
|
|
3087
|
-
setDescription(value);
|
|
3088
|
-
debouncedUpdateDescription(value);
|
|
3089
|
-
}, [debouncedUpdateDescription]);
|
|
3090
|
-
const handleFilterLogicChange = react.useCallback((logic) => {
|
|
3091
|
-
onUpdate({ ...stage, filter_logic: logic });
|
|
3092
|
-
}, [stage, onUpdate]);
|
|
3093
|
-
const handleMatchActionChange = react.useCallback((action) => {
|
|
3094
|
-
onUpdate({ ...stage, match_action: action });
|
|
3095
|
-
}, [stage, onUpdate]);
|
|
3096
|
-
const handleNoMatchActionChange = react.useCallback((action) => {
|
|
3097
|
-
onUpdate({ ...stage, no_match_action: action });
|
|
3098
|
-
}, [stage, onUpdate]);
|
|
3099
|
-
const handleMatchTagsChange = react.useCallback((tags) => {
|
|
3100
|
-
onUpdate({ ...stage, match_tags: tags });
|
|
3101
|
-
}, [stage, onUpdate]);
|
|
3102
|
-
const handleNoMatchTagsChange = react.useCallback((tags) => {
|
|
3103
|
-
onUpdate({ ...stage, no_match_tags: tags });
|
|
3104
|
-
}, [stage, onUpdate]);
|
|
3105
|
-
const handleRulesChange = react.useCallback((rules) => {
|
|
3106
|
-
onUpdate({ ...stage, rules });
|
|
3107
|
-
}, [stage, onUpdate]);
|
|
3108
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-form", children: [
|
|
3109
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
|
|
3110
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `stage-name-${stage.id}`, className: "form-label", children: "Stage Name" }),
|
|
3111
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3112
|
-
"input",
|
|
3113
|
-
{
|
|
3114
|
-
id: `stage-name-${stage.id}`,
|
|
3115
|
-
type: "text",
|
|
3116
|
-
value: name,
|
|
3117
|
-
onChange: handleNameChange,
|
|
3118
|
-
className: "form-input",
|
|
3119
|
-
placeholder: "e.g., High ICP Score",
|
|
3120
|
-
required: true
|
|
3121
|
-
}
|
|
3122
|
-
)
|
|
3123
|
-
] }),
|
|
3124
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
|
|
3125
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `stage-desc-${stage.id}`, className: "form-label", children: "Description" }),
|
|
3126
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3127
|
-
"textarea",
|
|
3128
|
-
{
|
|
3129
|
-
id: `stage-desc-${stage.id}`,
|
|
3130
|
-
value: description,
|
|
3131
|
-
onChange: handleDescriptionChange,
|
|
3132
|
-
className: "form-textarea",
|
|
3133
|
-
placeholder: "Describe the purpose of this stage...",
|
|
3134
|
-
rows: 3
|
|
3135
|
-
}
|
|
3136
|
-
)
|
|
3137
|
-
] }),
|
|
3138
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
|
|
3139
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Filter Logic" }),
|
|
3140
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "filter-logic-toggle", children: [
|
|
3141
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3142
|
-
"button",
|
|
3143
|
-
{
|
|
3144
|
-
type: "button",
|
|
3145
|
-
onClick: () => handleFilterLogicChange("AND"),
|
|
3146
|
-
className: `toggle-button ${stage.filter_logic === "AND" ? "active" : ""}`,
|
|
3147
|
-
children: [
|
|
3148
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3149
|
-
"input",
|
|
3150
|
-
{
|
|
3151
|
-
type: "radio",
|
|
3152
|
-
name: `filter-logic-${stage.id}`,
|
|
3153
|
-
value: "AND",
|
|
3154
|
-
checked: stage.filter_logic === "AND",
|
|
3155
|
-
onChange: () => handleFilterLogicChange("AND"),
|
|
3156
|
-
className: "sr-only"
|
|
3157
|
-
}
|
|
3158
|
-
),
|
|
3159
|
-
"AND"
|
|
3160
|
-
]
|
|
3161
|
-
}
|
|
3162
|
-
),
|
|
3163
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3164
|
-
"button",
|
|
3165
|
-
{
|
|
3166
|
-
type: "button",
|
|
3167
|
-
onClick: () => handleFilterLogicChange("OR"),
|
|
3168
|
-
className: `toggle-button ${stage.filter_logic === "OR" ? "active" : ""}`,
|
|
3169
|
-
children: [
|
|
3170
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3171
|
-
"input",
|
|
3172
|
-
{
|
|
3173
|
-
type: "radio",
|
|
3174
|
-
name: `filter-logic-${stage.id}`,
|
|
3175
|
-
value: "OR",
|
|
3176
|
-
checked: stage.filter_logic === "OR",
|
|
3177
|
-
onChange: () => handleFilterLogicChange("OR"),
|
|
3178
|
-
className: "sr-only"
|
|
3179
|
-
}
|
|
3180
|
-
),
|
|
3181
|
-
"OR"
|
|
3182
|
-
]
|
|
3183
|
-
}
|
|
3184
|
-
)
|
|
3185
|
-
] }),
|
|
3186
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: stage.filter_logic === "AND" ? "All rules must match for this stage to pass" : "At least one rule must match for this stage to pass" })
|
|
3187
|
-
] }),
|
|
3188
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3189
|
-
StageActions,
|
|
3190
|
-
{
|
|
3191
|
-
stage,
|
|
3192
|
-
onMatchActionChange: handleMatchActionChange,
|
|
3193
|
-
onNoMatchActionChange: handleNoMatchActionChange
|
|
3194
|
-
}
|
|
3195
|
-
),
|
|
3196
|
-
(stage.match_action === "tag" || stage.match_action === "tag_continue") && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
|
|
3197
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Tags on Match" }),
|
|
3198
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3199
|
-
TagInput,
|
|
3200
|
-
{
|
|
3201
|
-
tags: stage.match_tags || [],
|
|
3202
|
-
onChange: handleMatchTagsChange,
|
|
3203
|
-
placeholder: "Add tag..."
|
|
3204
|
-
}
|
|
3205
|
-
),
|
|
3206
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: "Tags to add when rules match" })
|
|
3207
|
-
] }),
|
|
3208
|
-
stage.no_match_action === "tag_exclude" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
|
|
3209
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Tags on No Match" }),
|
|
3210
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3211
|
-
TagInput,
|
|
3212
|
-
{
|
|
3213
|
-
tags: stage.no_match_tags || [],
|
|
3214
|
-
onChange: handleNoMatchTagsChange,
|
|
3215
|
-
placeholder: "Add tag..."
|
|
3216
|
-
}
|
|
3217
|
-
),
|
|
3218
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: "Tags to add when rules don't match" })
|
|
3219
|
-
] }),
|
|
3220
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
|
|
3221
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rules-header", children: /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "form-label", children: [
|
|
3222
|
-
"Filter Rules (",
|
|
3223
|
-
stage.rules.length,
|
|
3224
|
-
")"
|
|
3225
|
-
] }) }),
|
|
3226
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3227
|
-
FilterRuleEditor,
|
|
3228
|
-
{
|
|
3229
|
-
rules: stage.rules,
|
|
3230
|
-
onChange: handleRulesChange,
|
|
3231
|
-
fieldRegistry
|
|
3232
|
-
}
|
|
3233
|
-
)
|
|
3234
|
-
] })
|
|
3235
|
-
] });
|
|
3236
|
-
}
|
|
3237
|
-
function StageCard({
|
|
3238
|
-
stage,
|
|
3239
|
-
expanded,
|
|
3240
|
-
onToggleExpanded,
|
|
3241
|
-
onUpdate,
|
|
3242
|
-
onRemove,
|
|
3243
|
-
fieldRegistry,
|
|
3244
|
-
error,
|
|
3245
|
-
showWarnings = false
|
|
3246
|
-
}) {
|
|
3247
|
-
const {
|
|
3248
|
-
attributes,
|
|
3249
|
-
listeners,
|
|
3250
|
-
setNodeRef,
|
|
3251
|
-
transform,
|
|
3252
|
-
transition,
|
|
3253
|
-
isDragging
|
|
3254
|
-
} = sortable.useSortable({ id: stage.id });
|
|
3255
|
-
const style = {
|
|
3256
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
3257
|
-
transition,
|
|
3258
|
-
opacity: isDragging ? 0.5 : 1
|
|
3259
|
-
};
|
|
3260
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3261
|
-
"div",
|
|
3262
|
-
{
|
|
3263
|
-
ref: setNodeRef,
|
|
3264
|
-
style,
|
|
3265
|
-
className: `stage-card ${isDragging ? "dragging" : ""} ${error ? "error" : ""}`,
|
|
3266
|
-
children: [
|
|
3267
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-header", children: [
|
|
3268
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3269
|
-
"button",
|
|
3270
|
-
{
|
|
3271
|
-
...attributes,
|
|
3272
|
-
...listeners,
|
|
3273
|
-
className: "drag-handle",
|
|
3274
|
-
"aria-label": "Drag to reorder",
|
|
3275
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3276
|
-
"path",
|
|
3277
|
-
{
|
|
3278
|
-
d: "M7 4h6M7 10h6M7 16h6",
|
|
3279
|
-
stroke: "currentColor",
|
|
3280
|
-
strokeWidth: "2",
|
|
3281
|
-
strokeLinecap: "round"
|
|
3282
|
-
}
|
|
3283
|
-
) })
|
|
3284
|
-
}
|
|
3285
|
-
),
|
|
3286
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3287
|
-
"button",
|
|
3288
|
-
{
|
|
3289
|
-
onClick: onToggleExpanded,
|
|
3290
|
-
className: "stage-title-button",
|
|
3291
|
-
"aria-expanded": expanded,
|
|
3292
|
-
children: [
|
|
3293
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "stage-number", children: [
|
|
3294
|
-
"Stage ",
|
|
3295
|
-
stage.order + 1,
|
|
3296
|
-
":"
|
|
3297
|
-
] }),
|
|
3298
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "stage-name", children: stage.name || "Untitled Stage" }),
|
|
3299
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3300
|
-
"svg",
|
|
3301
|
-
{
|
|
3302
|
-
width: "20",
|
|
3303
|
-
height: "20",
|
|
3304
|
-
viewBox: "0 0 20 20",
|
|
3305
|
-
fill: "none",
|
|
3306
|
-
className: `expand-icon ${expanded ? "expanded" : ""}`,
|
|
3307
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3308
|
-
"path",
|
|
3309
|
-
{
|
|
3310
|
-
d: "M6 8l4 4 4-4",
|
|
3311
|
-
stroke: "currentColor",
|
|
3312
|
-
strokeWidth: "2",
|
|
3313
|
-
strokeLinecap: "round",
|
|
3314
|
-
strokeLinejoin: "round"
|
|
3315
|
-
}
|
|
3316
|
-
)
|
|
3317
|
-
}
|
|
3318
|
-
)
|
|
3319
|
-
]
|
|
3320
|
-
}
|
|
3321
|
-
),
|
|
3322
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3323
|
-
"button",
|
|
3324
|
-
{
|
|
3325
|
-
onClick: onRemove,
|
|
3326
|
-
className: "delete-button",
|
|
3327
|
-
"aria-label": "Delete stage",
|
|
3328
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3329
|
-
"path",
|
|
3330
|
-
{
|
|
3331
|
-
d: "M6 6l8 8M14 6l-8 8",
|
|
3332
|
-
stroke: "currentColor",
|
|
3333
|
-
strokeWidth: "2",
|
|
3334
|
-
strokeLinecap: "round"
|
|
3335
|
-
}
|
|
3336
|
-
) })
|
|
3337
|
-
}
|
|
3338
|
-
)
|
|
3339
|
-
] }),
|
|
3340
|
-
error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "error-message", children: [
|
|
3341
|
-
/* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: [
|
|
3342
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3343
|
-
"path",
|
|
3344
|
-
{
|
|
3345
|
-
d: "M8 1l7 13H1L8 1z",
|
|
3346
|
-
stroke: "currentColor",
|
|
3347
|
-
strokeWidth: "2",
|
|
3348
|
-
strokeLinejoin: "round"
|
|
3349
|
-
}
|
|
3350
|
-
),
|
|
3351
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 6v3M8 11h.01", stroke: "currentColor", strokeWidth: "2" })
|
|
3352
|
-
] }),
|
|
3353
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
|
|
3354
|
-
] }),
|
|
3355
|
-
showWarnings && !error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "warning-message", children: [
|
|
3356
|
-
/* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: [
|
|
3357
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "7", stroke: "currentColor", strokeWidth: "2" }),
|
|
3358
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v3M8 10h.01", stroke: "currentColor", strokeWidth: "2" })
|
|
3359
|
-
] }),
|
|
3360
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Stage has no filter rules" })
|
|
3361
|
-
] }),
|
|
3362
|
-
!expanded && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-summary", children: [
|
|
3363
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
|
|
3364
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "Rules:" }),
|
|
3365
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.rules.length })
|
|
3366
|
-
] }),
|
|
3367
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
|
|
3368
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "Logic:" }),
|
|
3369
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.filter_logic })
|
|
3370
|
-
] }),
|
|
3371
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
|
|
3372
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "On Match:" }),
|
|
3373
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.match_action })
|
|
3374
|
-
] }),
|
|
3375
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
|
|
3376
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "On No Match:" }),
|
|
3377
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.no_match_action })
|
|
3378
|
-
] })
|
|
3379
|
-
] }),
|
|
3380
|
-
expanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-form-wrapper", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3381
|
-
StageForm,
|
|
3382
|
-
{
|
|
3383
|
-
stage,
|
|
3384
|
-
onUpdate,
|
|
3385
|
-
fieldRegistry
|
|
3386
|
-
}
|
|
3387
|
-
) })
|
|
3388
|
-
]
|
|
3389
|
-
}
|
|
3390
|
-
);
|
|
3391
|
-
}
|
|
3392
|
-
function AddStageButton({
|
|
3393
|
-
onClick,
|
|
3394
|
-
position,
|
|
3395
|
-
className = ""
|
|
3396
|
-
}) {
|
|
3397
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3398
|
-
"button",
|
|
3399
|
-
{
|
|
3400
|
-
type: "button",
|
|
3401
|
-
onClick,
|
|
3402
|
-
className: `add-stage-button ${position} ${className}`,
|
|
3403
|
-
"aria-label": `Add stage ${position === "top" ? "at top" : position === "bottom" ? "at bottom" : "below"}`,
|
|
3404
|
-
children: [
|
|
3405
|
-
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3406
|
-
"path",
|
|
3407
|
-
{
|
|
3408
|
-
d: "M10 5v10M5 10h10",
|
|
3409
|
-
stroke: "currentColor",
|
|
3410
|
-
strokeWidth: "2",
|
|
3411
|
-
strokeLinecap: "round"
|
|
3412
|
-
}
|
|
3413
|
-
) }),
|
|
3414
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
3415
|
-
position === "top" && "Add Stage",
|
|
3416
|
-
position === "bottom" && "Add Stage Below",
|
|
3417
|
-
position === "inline" && "Add Stage"
|
|
3418
|
-
] })
|
|
3419
|
-
]
|
|
3420
|
-
}
|
|
3421
|
-
);
|
|
3422
|
-
}
|
|
3423
|
-
function generateStageId() {
|
|
3424
|
-
return `stage-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
3425
|
-
}
|
|
3426
|
-
function createEmptyStage(order) {
|
|
3427
|
-
return {
|
|
3428
|
-
id: generateStageId(),
|
|
3429
|
-
order,
|
|
3430
|
-
name: `Stage ${order + 1}`,
|
|
3431
|
-
description: "",
|
|
3432
|
-
filter_logic: "AND",
|
|
3433
|
-
rules: [],
|
|
3434
|
-
match_action: "continue",
|
|
3435
|
-
no_match_action: "continue",
|
|
3436
|
-
match_tags: [],
|
|
3437
|
-
no_match_tags: []
|
|
3438
|
-
};
|
|
3439
|
-
}
|
|
3440
|
-
function validateStageName(name, stages, currentStageId) {
|
|
3441
|
-
const trimmedName = name.trim();
|
|
3442
|
-
if (!trimmedName) {
|
|
3443
|
-
return "Stage name is required";
|
|
3444
|
-
}
|
|
3445
|
-
const duplicate = stages.find(
|
|
3446
|
-
(s) => s.id !== currentStageId && s.name.trim().toLowerCase() === trimmedName.toLowerCase()
|
|
3447
|
-
);
|
|
3448
|
-
if (duplicate) {
|
|
3449
|
-
return "Stage name must be unique";
|
|
3450
|
-
}
|
|
3451
|
-
return null;
|
|
3452
|
-
}
|
|
3453
|
-
function FunnelStageBuilder({
|
|
3454
|
-
funnel,
|
|
3455
|
-
onUpdate,
|
|
3456
|
-
fieldRegistry,
|
|
3457
|
-
className = ""
|
|
3458
|
-
}) {
|
|
3459
|
-
const [expandedStages, setExpandedStages] = react.useState(
|
|
3460
|
-
new Set(funnel.stages.map((s) => s.id))
|
|
3461
|
-
);
|
|
3462
|
-
const [errors, setErrors] = react.useState(/* @__PURE__ */ new Map());
|
|
3463
|
-
const sensors = core.useSensors(
|
|
3464
|
-
core.useSensor(core.PointerSensor),
|
|
3465
|
-
core.useSensor(core.KeyboardSensor, {
|
|
3466
|
-
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
3467
|
-
})
|
|
3468
|
-
);
|
|
3469
|
-
const toggleExpanded = react.useCallback((stageId) => {
|
|
3470
|
-
setExpandedStages((prev) => {
|
|
3471
|
-
const next = new Set(prev);
|
|
3472
|
-
if (next.has(stageId)) {
|
|
3473
|
-
next.delete(stageId);
|
|
3474
|
-
} else {
|
|
3475
|
-
next.add(stageId);
|
|
3476
|
-
}
|
|
3477
|
-
return next;
|
|
3478
|
-
});
|
|
3479
|
-
}, []);
|
|
3480
|
-
const handleAddStage = react.useCallback((insertAfterIndex) => {
|
|
3481
|
-
const newOrder = insertAfterIndex !== void 0 ? insertAfterIndex + 1 : funnel.stages.length;
|
|
3482
|
-
const newStage = createEmptyStage(newOrder);
|
|
3483
|
-
const updatedStages = funnel.stages.map((stage) => {
|
|
3484
|
-
if (stage.order >= newOrder) {
|
|
3485
|
-
return { ...stage, order: stage.order + 1 };
|
|
3486
|
-
}
|
|
3487
|
-
return stage;
|
|
3488
|
-
});
|
|
3489
|
-
updatedStages.splice(newOrder, 0, newStage);
|
|
3490
|
-
setExpandedStages((prev) => new Set(prev).add(newStage.id));
|
|
3491
|
-
onUpdate({
|
|
3492
|
-
...funnel,
|
|
3493
|
-
stages: updatedStages
|
|
3494
|
-
});
|
|
3495
|
-
}, [funnel, onUpdate]);
|
|
3496
|
-
const handleRemoveStage = react.useCallback((stageId) => {
|
|
3497
|
-
const stageIndex = funnel.stages.findIndex((s) => s.id === stageId);
|
|
3498
|
-
if (stageIndex === -1) return;
|
|
3499
|
-
const updatedStages = funnel.stages.filter((s) => s.id !== stageId);
|
|
3500
|
-
updatedStages.forEach((stage, index) => {
|
|
3501
|
-
stage.order = index;
|
|
3502
|
-
});
|
|
3503
|
-
setExpandedStages((prev) => {
|
|
3504
|
-
const next = new Set(prev);
|
|
3505
|
-
next.delete(stageId);
|
|
3506
|
-
return next;
|
|
3507
|
-
});
|
|
3508
|
-
setErrors((prev) => {
|
|
3509
|
-
const next = new Map(prev);
|
|
3510
|
-
next.delete(stageId);
|
|
3511
|
-
return next;
|
|
3512
|
-
});
|
|
3513
|
-
onUpdate({
|
|
3514
|
-
...funnel,
|
|
3515
|
-
stages: updatedStages
|
|
3516
|
-
});
|
|
3517
|
-
}, [funnel, onUpdate]);
|
|
3518
|
-
const handleUpdateStage = react.useCallback((updatedStage) => {
|
|
3519
|
-
const nameError = validateStageName(updatedStage.name, funnel.stages, updatedStage.id);
|
|
3520
|
-
setErrors((prev) => {
|
|
3521
|
-
const next = new Map(prev);
|
|
3522
|
-
if (nameError) {
|
|
3523
|
-
next.set(updatedStage.id, nameError);
|
|
3524
|
-
} else {
|
|
3525
|
-
next.delete(updatedStage.id);
|
|
3526
|
-
}
|
|
3527
|
-
return next;
|
|
3528
|
-
});
|
|
3529
|
-
const updatedStages = funnel.stages.map(
|
|
3530
|
-
(stage) => stage.id === updatedStage.id ? updatedStage : stage
|
|
3531
|
-
);
|
|
3532
|
-
onUpdate({
|
|
3533
|
-
...funnel,
|
|
3534
|
-
stages: updatedStages
|
|
3535
|
-
});
|
|
3536
|
-
}, [funnel, onUpdate]);
|
|
3537
|
-
const handleDragEnd = react.useCallback((event) => {
|
|
3538
|
-
const { active, over } = event;
|
|
3539
|
-
if (!over || active.id === over.id) {
|
|
3540
|
-
return;
|
|
3541
|
-
}
|
|
3542
|
-
const oldIndex = funnel.stages.findIndex((s) => s.id === active.id);
|
|
3543
|
-
const newIndex = funnel.stages.findIndex((s) => s.id === over.id);
|
|
3544
|
-
if (oldIndex === -1 || newIndex === -1) {
|
|
3545
|
-
return;
|
|
3546
|
-
}
|
|
3547
|
-
const reorderedStages = sortable.arrayMove(funnel.stages, oldIndex, newIndex);
|
|
3548
|
-
reorderedStages.forEach((stage, index) => {
|
|
3549
|
-
stage.order = index;
|
|
3550
|
-
});
|
|
3551
|
-
onUpdate({
|
|
3552
|
-
...funnel,
|
|
3553
|
-
stages: reorderedStages
|
|
3554
|
-
});
|
|
3555
|
-
}, [funnel, onUpdate]);
|
|
3556
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `funnel-stage-builder ${className}`, children: [
|
|
3557
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(AddStageButton, { onClick: () => handleAddStage(), position: "top" }) }),
|
|
3558
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3559
|
-
core.DndContext,
|
|
3560
|
-
{
|
|
3561
|
-
sensors,
|
|
3562
|
-
collisionDetection: core.closestCenter,
|
|
3563
|
-
onDragEnd: handleDragEnd,
|
|
3564
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3565
|
-
sortable.SortableContext,
|
|
3566
|
-
{
|
|
3567
|
-
items: funnel.stages.map((s) => s.id),
|
|
3568
|
-
strategy: sortable.verticalListSortingStrategy,
|
|
3569
|
-
children: funnel.stages.map((stage, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-wrapper", children: [
|
|
3570
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3571
|
-
StageCard,
|
|
3572
|
-
{
|
|
3573
|
-
stage,
|
|
3574
|
-
expanded: expandedStages.has(stage.id),
|
|
3575
|
-
onToggleExpanded: () => toggleExpanded(stage.id),
|
|
3576
|
-
onUpdate: handleUpdateStage,
|
|
3577
|
-
onRemove: () => handleRemoveStage(stage.id),
|
|
3578
|
-
fieldRegistry,
|
|
3579
|
-
error: errors.get(stage.id),
|
|
3580
|
-
showWarnings: stage.rules.length === 0
|
|
3581
|
-
}
|
|
3582
|
-
),
|
|
3583
|
-
index < funnel.stages.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-arrow", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3584
|
-
"path",
|
|
3585
|
-
{
|
|
3586
|
-
d: "M12 5v14m0 0l-4-4m4 4l4-4",
|
|
3587
|
-
stroke: "currentColor",
|
|
3588
|
-
strokeWidth: "2",
|
|
3589
|
-
strokeLinecap: "round",
|
|
3590
|
-
strokeLinejoin: "round"
|
|
3591
|
-
}
|
|
3592
|
-
) }) })
|
|
3593
|
-
] }, stage.id))
|
|
3594
|
-
}
|
|
3595
|
-
)
|
|
3596
|
-
}
|
|
3597
|
-
),
|
|
3598
|
-
funnel.stages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3599
|
-
AddStageButton,
|
|
3600
|
-
{
|
|
3601
|
-
onClick: () => handleAddStage(funnel.stages.length - 1),
|
|
3602
|
-
position: "bottom"
|
|
3603
|
-
}
|
|
3604
|
-
) }),
|
|
3605
|
-
funnel.stages.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "empty-state", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-center py-8", children: "No stages yet. Add your first stage to get started." }) })
|
|
3606
|
-
] });
|
|
3607
|
-
}
|
|
3608
|
-
function RunFilters({
|
|
3609
|
-
filters,
|
|
3610
|
-
onFiltersChange,
|
|
3611
|
-
className = ""
|
|
3612
|
-
}) {
|
|
3613
|
-
const updateFilter = (key, value) => {
|
|
3614
|
-
onFiltersChange({ ...filters, [key]: value });
|
|
3615
|
-
};
|
|
3616
|
-
const clearFilters = () => {
|
|
3617
|
-
onFiltersChange({
|
|
3618
|
-
status: "all",
|
|
3619
|
-
trigger_type: "all",
|
|
3620
|
-
date_range: "month"
|
|
3621
|
-
});
|
|
3622
|
-
};
|
|
3623
|
-
const hasActiveFilters = filters.status !== "all" || filters.trigger_type !== "all" || filters.date_range !== "month";
|
|
3624
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3625
|
-
"div",
|
|
3626
|
-
{
|
|
3627
|
-
className: `flex items-center gap-3 p-3 bg-gray-50 border-b border-gray-200 ${className}`,
|
|
3628
|
-
children: [
|
|
3629
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3630
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "status-filter", className: "text-sm font-medium text-gray-700", children: "Status:" }),
|
|
3631
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3632
|
-
"select",
|
|
3633
|
-
{
|
|
3634
|
-
id: "status-filter",
|
|
3635
|
-
value: filters.status || "all",
|
|
3636
|
-
onChange: (e) => updateFilter("status", e.target.value),
|
|
3637
|
-
className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
3638
|
-
children: [
|
|
3639
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All" }),
|
|
3640
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "completed", children: "Complete" }),
|
|
3641
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "running", children: "Running" }),
|
|
3642
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "failed", children: "Failed" }),
|
|
3643
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "pending", children: "Pending" }),
|
|
3644
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "cancelled", children: "Cancelled" })
|
|
3645
|
-
]
|
|
3646
|
-
}
|
|
3647
|
-
)
|
|
3648
|
-
] }),
|
|
3649
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3650
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "trigger-filter", className: "text-sm font-medium text-gray-700", children: "Trigger:" }),
|
|
3651
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3652
|
-
"select",
|
|
3653
|
-
{
|
|
3654
|
-
id: "trigger-filter",
|
|
3655
|
-
value: filters.trigger_type || "all",
|
|
3656
|
-
onChange: (e) => updateFilter("trigger_type", e.target.value),
|
|
3657
|
-
className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
3658
|
-
children: [
|
|
3659
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All" }),
|
|
3660
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "manual", children: "Manual" }),
|
|
3661
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "scheduled", children: "Scheduled" }),
|
|
3662
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "webhook", children: "Webhook" }),
|
|
3663
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "api", children: "API" })
|
|
3664
|
-
]
|
|
3665
|
-
}
|
|
3666
|
-
)
|
|
3667
|
-
] }),
|
|
3668
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3669
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "date-filter", className: "text-sm font-medium text-gray-700", children: "Date:" }),
|
|
3670
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3671
|
-
"select",
|
|
3672
|
-
{
|
|
3673
|
-
id: "date-filter",
|
|
3674
|
-
value: filters.date_range || "month",
|
|
3675
|
-
onChange: (e) => updateFilter("date_range", e.target.value),
|
|
3676
|
-
className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
3677
|
-
children: [
|
|
3678
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All time" }),
|
|
3679
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "today", children: "Today" }),
|
|
3680
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "week", children: "Last 7 days" }),
|
|
3681
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "month", children: "Last 30 days" })
|
|
3682
|
-
]
|
|
3683
|
-
}
|
|
3684
|
-
)
|
|
3685
|
-
] }),
|
|
3686
|
-
hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3687
|
-
"button",
|
|
3688
|
-
{
|
|
3689
|
-
onClick: clearFilters,
|
|
3690
|
-
className: "ml-auto px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
3691
|
-
children: "Clear filters"
|
|
3692
|
-
}
|
|
3693
|
-
)
|
|
3694
|
-
]
|
|
3695
|
-
}
|
|
3696
|
-
);
|
|
3697
|
-
}
|
|
3698
|
-
var statusConfig2 = {
|
|
3699
|
-
completed: {
|
|
3700
|
-
icon: "\u2713",
|
|
3701
|
-
label: "Complete",
|
|
3702
|
-
color: "text-green-800",
|
|
3703
|
-
bgColor: "bg-green-100"
|
|
3704
|
-
},
|
|
3705
|
-
running: {
|
|
3706
|
-
icon: "\u23F8",
|
|
3707
|
-
label: "Running",
|
|
3708
|
-
color: "text-blue-800",
|
|
3709
|
-
bgColor: "bg-blue-100",
|
|
3710
|
-
spinning: true
|
|
3711
|
-
},
|
|
3712
|
-
failed: {
|
|
3713
|
-
icon: "\u2717",
|
|
3714
|
-
label: "Failed",
|
|
3715
|
-
color: "text-red-800",
|
|
3716
|
-
bgColor: "bg-red-100"
|
|
3717
|
-
},
|
|
3718
|
-
pending: {
|
|
3719
|
-
icon: "\u25CB",
|
|
3720
|
-
label: "Pending",
|
|
3721
|
-
color: "text-yellow-800",
|
|
3722
|
-
bgColor: "bg-yellow-100"
|
|
3723
|
-
},
|
|
3724
|
-
cancelled: {
|
|
3725
|
-
icon: "\xD7",
|
|
3726
|
-
label: "Cancelled",
|
|
3727
|
-
color: "text-gray-800",
|
|
3728
|
-
bgColor: "bg-gray-100"
|
|
3729
|
-
}
|
|
3730
|
-
};
|
|
3731
|
-
function RunStatusBadge({ status, className = "" }) {
|
|
3732
|
-
const config = statusConfig2[status];
|
|
3733
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3734
|
-
"span",
|
|
3735
|
-
{
|
|
3736
|
-
className: `inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color} ${className}`,
|
|
3737
|
-
children: [
|
|
3738
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: config.spinning ? "animate-spin" : "", "aria-hidden": "true", children: config.icon }),
|
|
3739
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: config.label })
|
|
3740
|
-
]
|
|
3741
|
-
}
|
|
3742
|
-
);
|
|
3743
|
-
}
|
|
3744
|
-
function RunActions({
|
|
3745
|
-
run,
|
|
3746
|
-
onViewDetails,
|
|
3747
|
-
onViewResults,
|
|
3748
|
-
onReRun,
|
|
3749
|
-
onCancel,
|
|
3750
|
-
className = ""
|
|
3751
|
-
}) {
|
|
3752
|
-
const [isOpen, setIsOpen] = react.useState(false);
|
|
3753
|
-
const dropdownRef = react.useRef(null);
|
|
3754
|
-
react.useEffect(() => {
|
|
3755
|
-
const handleClickOutside = (event) => {
|
|
3756
|
-
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
3757
|
-
setIsOpen(false);
|
|
3758
|
-
}
|
|
3759
|
-
};
|
|
3760
|
-
if (isOpen) {
|
|
3761
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
3762
|
-
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
3763
|
-
}
|
|
3764
|
-
}, [isOpen]);
|
|
3765
|
-
react.useEffect(() => {
|
|
3766
|
-
const handleEscape = (event) => {
|
|
3767
|
-
if (event.key === "Escape" && isOpen) {
|
|
3768
|
-
setIsOpen(false);
|
|
3769
|
-
}
|
|
3770
|
-
};
|
|
3771
|
-
if (isOpen) {
|
|
3772
|
-
document.addEventListener("keydown", handleEscape);
|
|
3773
|
-
return () => document.removeEventListener("keydown", handleEscape);
|
|
3774
|
-
}
|
|
3775
|
-
}, [isOpen]);
|
|
3776
|
-
const canCancel = run.status === "pending" || run.status === "running";
|
|
3777
|
-
const canViewResults = run.status === "completed";
|
|
3778
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative ${className}`, ref: dropdownRef, children: [
|
|
3779
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3780
|
-
"button",
|
|
3781
|
-
{
|
|
3782
|
-
onClick: () => setIsOpen(!isOpen),
|
|
3783
|
-
className: "p-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
3784
|
-
"aria-label": "Run actions",
|
|
3785
|
-
"aria-haspopup": "true",
|
|
3786
|
-
"aria-expanded": isOpen,
|
|
3787
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3788
|
-
"svg",
|
|
3789
|
-
{
|
|
3790
|
-
className: "w-5 h-5",
|
|
3791
|
-
fill: "none",
|
|
3792
|
-
stroke: "currentColor",
|
|
3793
|
-
viewBox: "0 0 24 24",
|
|
3794
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3795
|
-
"path",
|
|
3796
|
-
{
|
|
3797
|
-
strokeLinecap: "round",
|
|
3798
|
-
strokeLinejoin: "round",
|
|
3799
|
-
strokeWidth: 2,
|
|
3800
|
-
d: "M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
|
|
3801
|
-
}
|
|
3802
|
-
)
|
|
3803
|
-
}
|
|
3804
|
-
)
|
|
3805
|
-
}
|
|
3806
|
-
),
|
|
3807
|
-
isOpen && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3808
|
-
"div",
|
|
3809
|
-
{
|
|
3810
|
-
className: "absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-10",
|
|
3811
|
-
role: "menu",
|
|
3812
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-1", children: [
|
|
3813
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3814
|
-
"button",
|
|
3815
|
-
{
|
|
3816
|
-
onClick: () => {
|
|
3817
|
-
onViewDetails(run);
|
|
3818
|
-
setIsOpen(false);
|
|
3819
|
-
},
|
|
3820
|
-
className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2",
|
|
3821
|
-
role: "menuitem",
|
|
3822
|
-
children: [
|
|
3823
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u{1F441}" }),
|
|
3824
|
-
"View Details"
|
|
3825
|
-
]
|
|
3826
|
-
}
|
|
3827
|
-
),
|
|
3828
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3829
|
-
"button",
|
|
3830
|
-
{
|
|
3831
|
-
onClick: () => {
|
|
3832
|
-
onViewResults(run);
|
|
3833
|
-
setIsOpen(false);
|
|
3834
|
-
},
|
|
3835
|
-
disabled: !canViewResults,
|
|
3836
|
-
className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white",
|
|
3837
|
-
role: "menuitem",
|
|
3838
|
-
children: [
|
|
3839
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u{1F4CA}" }),
|
|
3840
|
-
"View Results"
|
|
3841
|
-
]
|
|
3842
|
-
}
|
|
3843
|
-
),
|
|
3844
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3845
|
-
"button",
|
|
3846
|
-
{
|
|
3847
|
-
onClick: () => {
|
|
3848
|
-
onReRun(run);
|
|
3849
|
-
setIsOpen(false);
|
|
3850
|
-
},
|
|
3851
|
-
className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2",
|
|
3852
|
-
role: "menuitem",
|
|
3853
|
-
children: [
|
|
3854
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u21BB" }),
|
|
3855
|
-
"Re-run"
|
|
3856
|
-
]
|
|
3857
|
-
}
|
|
3858
|
-
),
|
|
3859
|
-
canCancel && onCancel && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3860
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-gray-200 my-1" }),
|
|
3861
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3862
|
-
"button",
|
|
3863
|
-
{
|
|
3864
|
-
onClick: () => {
|
|
3865
|
-
if (confirm("Are you sure you want to cancel this run?")) {
|
|
3866
|
-
onCancel(run);
|
|
3867
|
-
setIsOpen(false);
|
|
3868
|
-
}
|
|
3869
|
-
},
|
|
3870
|
-
className: "w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2",
|
|
3871
|
-
role: "menuitem",
|
|
3872
|
-
children: [
|
|
3873
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" }),
|
|
3874
|
-
"Cancel Run"
|
|
3875
|
-
]
|
|
3876
|
-
}
|
|
3877
|
-
)
|
|
3878
|
-
] })
|
|
3879
|
-
] })
|
|
3880
|
-
}
|
|
3881
|
-
)
|
|
3882
|
-
] });
|
|
3883
|
-
}
|
|
3884
|
-
|
|
3885
|
-
// src/components/FunnelRunHistory/utils.ts
|
|
3886
|
-
function formatDuration(ms) {
|
|
3887
|
-
if (ms === void 0 || ms === null) return "-";
|
|
3888
|
-
if (ms === 0) return "0ms";
|
|
3889
|
-
const seconds = Math.floor(ms / 1e3);
|
|
3890
|
-
const minutes = Math.floor(seconds / 60);
|
|
3891
|
-
const hours = Math.floor(minutes / 60);
|
|
3892
|
-
if (hours > 0) {
|
|
3893
|
-
const remainingMinutes = minutes % 60;
|
|
3894
|
-
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
3895
|
-
}
|
|
3896
|
-
if (minutes > 0) {
|
|
3897
|
-
const remainingSeconds = seconds % 60;
|
|
3898
|
-
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
3899
|
-
}
|
|
3900
|
-
if (seconds > 0) {
|
|
3901
|
-
return `${seconds}s`;
|
|
3902
|
-
}
|
|
3903
|
-
return `${ms}ms`;
|
|
3904
|
-
}
|
|
3905
|
-
function formatRelativeTime(date) {
|
|
3906
|
-
const now = /* @__PURE__ */ new Date();
|
|
3907
|
-
const then = new Date(date);
|
|
3908
|
-
const diffMs = now.getTime() - then.getTime();
|
|
3909
|
-
const diffSeconds = Math.floor(diffMs / 1e3);
|
|
3910
|
-
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
3911
|
-
const diffHours = Math.floor(diffMinutes / 60);
|
|
3912
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
3913
|
-
if (diffDays > 0) {
|
|
3914
|
-
return `${diffDays}d ago`;
|
|
3915
|
-
}
|
|
3916
|
-
if (diffHours > 0) {
|
|
3917
|
-
return `${diffHours}h ago`;
|
|
3918
|
-
}
|
|
3919
|
-
if (diffMinutes > 0) {
|
|
3920
|
-
return `${diffMinutes}m ago`;
|
|
3921
|
-
}
|
|
3922
|
-
return "Just now";
|
|
3923
|
-
}
|
|
3924
|
-
function calculateMatchRate(matched, total) {
|
|
3925
|
-
if (total === 0) return 0;
|
|
3926
|
-
return Math.round(matched / total * 100);
|
|
3927
|
-
}
|
|
3928
|
-
function formatNumber(num) {
|
|
3929
|
-
return num.toLocaleString();
|
|
3930
|
-
}
|
|
3931
|
-
function formatFullTimestamp(date) {
|
|
3932
|
-
const d = new Date(date);
|
|
3933
|
-
return d.toLocaleString("en-US", {
|
|
3934
|
-
year: "numeric",
|
|
3935
|
-
month: "short",
|
|
3936
|
-
day: "numeric",
|
|
3937
|
-
hour: "2-digit",
|
|
3938
|
-
minute: "2-digit",
|
|
3939
|
-
second: "2-digit"
|
|
3940
|
-
});
|
|
3941
|
-
}
|
|
3942
|
-
function RunRow({
|
|
3943
|
-
run,
|
|
3944
|
-
onViewDetails,
|
|
3945
|
-
onViewResults,
|
|
3946
|
-
onReRun,
|
|
3947
|
-
onCancel
|
|
3948
|
-
}) {
|
|
3949
|
-
const matchRate = run.status === "completed" ? calculateMatchRate(run.total_matched, run.total_input) : null;
|
|
3950
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3951
|
-
"tr",
|
|
3952
|
-
{
|
|
3953
|
-
onClick: () => onViewDetails(run),
|
|
3954
|
-
onKeyDown: (e) => {
|
|
3955
|
-
if (e.key === "Enter") {
|
|
3956
|
-
onViewDetails(run);
|
|
3957
|
-
}
|
|
3958
|
-
},
|
|
3959
|
-
tabIndex: 0,
|
|
3960
|
-
className: "border-b border-gray-200 hover:bg-gray-50 cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500",
|
|
3961
|
-
children: [
|
|
3962
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3963
|
-
"span",
|
|
3964
|
-
{
|
|
3965
|
-
className: "text-sm text-gray-900",
|
|
3966
|
-
title: formatFullTimestamp(run.started_at),
|
|
3967
|
-
children: formatRelativeTime(run.started_at)
|
|
3968
|
-
}
|
|
3969
|
-
) }),
|
|
3970
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: run.status }) }),
|
|
3971
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700 capitalize", children: run.trigger_type }) }),
|
|
3972
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-900", children: formatDuration(run.duration_ms) }) }),
|
|
3973
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: formatNumber(run.total_input) }) }),
|
|
3974
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-green-600", children: run.status === "completed" ? formatNumber(run.total_matched) : "-" }) }),
|
|
3975
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: matchRate !== null ? `${matchRate}%` : "-" }) }),
|
|
3976
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3977
|
-
RunActions,
|
|
3978
|
-
{
|
|
3979
|
-
run,
|
|
3980
|
-
onViewDetails,
|
|
3981
|
-
onViewResults,
|
|
3982
|
-
onReRun,
|
|
3983
|
-
onCancel
|
|
3984
|
-
}
|
|
3985
|
-
) })
|
|
3986
|
-
]
|
|
3987
|
-
}
|
|
3988
|
-
);
|
|
3989
|
-
}
|
|
3990
|
-
function StageBreakdownList({
|
|
3991
|
-
stages,
|
|
3992
|
-
className = ""
|
|
3993
|
-
}) {
|
|
3994
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `space-y-3 ${className}`, children: stages.map((stage, index) => {
|
|
3995
|
-
const delta = stage.matched_count - stage.input_count;
|
|
3996
|
-
const matchRate = stage.input_count > 0 ? Math.round(stage.matched_count / stage.input_count * 100) : 0;
|
|
3997
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3998
|
-
"div",
|
|
3999
|
-
{
|
|
4000
|
-
className: "p-3 bg-gray-50 rounded-lg border border-gray-200",
|
|
4001
|
-
children: [
|
|
4002
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
|
|
4003
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-blue-600 rounded-full", children: index + 1 }),
|
|
4004
|
-
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold text-gray-900", children: stage.stage_name })
|
|
4005
|
-
] }),
|
|
4006
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-2 text-center", children: [
|
|
4007
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4008
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Input" }),
|
|
4009
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-bold text-blue-600", children: formatNumber(stage.input_count) })
|
|
4010
|
-
] }),
|
|
4011
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4012
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Matched" }),
|
|
4013
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-bold text-green-600", children: formatNumber(stage.matched_count) })
|
|
4014
|
-
] }),
|
|
4015
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4016
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Rate" }),
|
|
4017
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-lg font-bold text-gray-700", children: [
|
|
4018
|
-
matchRate,
|
|
4019
|
-
"%"
|
|
4020
|
-
] })
|
|
4021
|
-
] })
|
|
4022
|
-
] }),
|
|
4023
|
-
delta !== 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 pt-2 border-t border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1 text-sm", children: [
|
|
4024
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4025
|
-
"span",
|
|
4026
|
-
{
|
|
4027
|
-
className: `font-medium ${delta > 0 ? "text-green-600" : "text-red-600"}`,
|
|
4028
|
-
children: [
|
|
4029
|
-
delta > 0 ? "\u25B2" : "\u25BC",
|
|
4030
|
-
" ",
|
|
4031
|
-
formatNumber(Math.abs(delta))
|
|
4032
|
-
]
|
|
4033
|
-
}
|
|
4034
|
-
),
|
|
4035
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500", children: delta > 0 ? "added" : "excluded" })
|
|
4036
|
-
] }) }),
|
|
4037
|
-
stage.error_count && stage.error_count > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 pt-2 border-t border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-red-600 text-center", children: [
|
|
4038
|
-
"\u26A0 ",
|
|
4039
|
-
formatNumber(stage.error_count),
|
|
4040
|
-
" errors"
|
|
4041
|
-
] }) })
|
|
4042
|
-
]
|
|
4043
|
-
},
|
|
4044
|
-
stage.stage_id
|
|
4045
|
-
);
|
|
4046
|
-
}) });
|
|
4047
|
-
}
|
|
4048
|
-
function RunDetailsModal({
|
|
4049
|
-
run,
|
|
4050
|
-
onClose,
|
|
4051
|
-
onViewResults,
|
|
4052
|
-
onReRun
|
|
4053
|
-
}) {
|
|
4054
|
-
const modalRef = react.useRef(null);
|
|
4055
|
-
react.useEffect(() => {
|
|
4056
|
-
const handleEscape = (e) => {
|
|
4057
|
-
if (e.key === "Escape") {
|
|
4058
|
-
onClose();
|
|
4059
|
-
}
|
|
4060
|
-
};
|
|
4061
|
-
if (run) {
|
|
4062
|
-
document.addEventListener("keydown", handleEscape);
|
|
4063
|
-
document.body.style.overflow = "hidden";
|
|
4064
|
-
return () => {
|
|
4065
|
-
document.removeEventListener("keydown", handleEscape);
|
|
4066
|
-
document.body.style.overflow = "unset";
|
|
4067
|
-
};
|
|
4068
|
-
}
|
|
4069
|
-
}, [run, onClose]);
|
|
4070
|
-
react.useEffect(() => {
|
|
4071
|
-
if (run && modalRef.current) {
|
|
4072
|
-
const focusableElements = modalRef.current.querySelectorAll(
|
|
4073
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
4074
|
-
);
|
|
4075
|
-
const firstElement = focusableElements[0];
|
|
4076
|
-
const lastElement = focusableElements[focusableElements.length - 1];
|
|
4077
|
-
firstElement?.focus();
|
|
4078
|
-
const handleTab = (e) => {
|
|
4079
|
-
if (e.key !== "Tab") return;
|
|
4080
|
-
if (e.shiftKey && document.activeElement === firstElement) {
|
|
4081
|
-
e.preventDefault();
|
|
4082
|
-
lastElement?.focus();
|
|
4083
|
-
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
4084
|
-
e.preventDefault();
|
|
4085
|
-
firstElement?.focus();
|
|
4086
|
-
}
|
|
4087
|
-
};
|
|
4088
|
-
document.addEventListener("keydown", handleTab);
|
|
4089
|
-
return () => document.removeEventListener("keydown", handleTab);
|
|
4090
|
-
}
|
|
4091
|
-
}, [run]);
|
|
4092
|
-
if (!run) return null;
|
|
4093
|
-
const stageStatsArray = Object.values(run.stage_stats);
|
|
4094
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4095
|
-
"div",
|
|
4096
|
-
{
|
|
4097
|
-
className: "fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50",
|
|
4098
|
-
onClick: onClose,
|
|
4099
|
-
role: "dialog",
|
|
4100
|
-
"aria-modal": "true",
|
|
4101
|
-
"aria-labelledby": "modal-title",
|
|
4102
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4103
|
-
"div",
|
|
4104
|
-
{
|
|
4105
|
-
ref: modalRef,
|
|
4106
|
-
onClick: (e) => e.stopPropagation(),
|
|
4107
|
-
className: "bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col",
|
|
4108
|
-
children: [
|
|
4109
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
4110
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { id: "modal-title", className: "text-lg font-semibold text-gray-900", children: "Run Details" }),
|
|
4111
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4112
|
-
"button",
|
|
4113
|
-
{
|
|
4114
|
-
onClick: onClose,
|
|
4115
|
-
className: "p-1 text-gray-400 hover:text-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
4116
|
-
"aria-label": "Close modal",
|
|
4117
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4118
|
-
"path",
|
|
4119
|
-
{
|
|
4120
|
-
fillRule: "evenodd",
|
|
4121
|
-
d: "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z",
|
|
4122
|
-
clipRule: "evenodd"
|
|
4123
|
-
}
|
|
4124
|
-
) })
|
|
4125
|
-
}
|
|
4126
|
-
)
|
|
4127
|
-
] }) }),
|
|
4128
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 overflow-y-auto flex-1", children: [
|
|
4129
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6", children: [
|
|
4130
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4 mb-4", children: [
|
|
4131
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4132
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Status" }),
|
|
4133
|
-
/* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: run.status })
|
|
4134
|
-
] }),
|
|
4135
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4136
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Duration" }),
|
|
4137
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-900", children: formatDuration(run.duration_ms) })
|
|
4138
|
-
] }),
|
|
4139
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4140
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Started" }),
|
|
4141
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-900", children: /* @__PURE__ */ jsxRuntime.jsx("span", { title: formatFullTimestamp(run.started_at), children: formatRelativeTime(run.started_at) }) })
|
|
4142
|
-
] }),
|
|
4143
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4144
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Triggered by" }),
|
|
4145
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm font-medium text-gray-900", children: [
|
|
4146
|
-
run.triggered_by || "System",
|
|
4147
|
-
" (",
|
|
4148
|
-
run.trigger_type,
|
|
4149
|
-
")"
|
|
4150
|
-
] })
|
|
4151
|
-
] })
|
|
4152
|
-
] }),
|
|
4153
|
-
run.status === "failed" && run.error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-red-50 border border-red-200 rounded-lg", children: [
|
|
4154
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-red-800 mb-1", children: "Error" }),
|
|
4155
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-red-700", children: run.error })
|
|
4156
|
-
] })
|
|
4157
|
-
] }),
|
|
4158
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4159
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 mb-3", children: "Stage Breakdown" }),
|
|
4160
|
-
stageStatsArray.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(StageBreakdownList, { stages: stageStatsArray }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-500 text-center py-4", children: "No stage data available" })
|
|
4161
|
-
] })
|
|
4162
|
-
] }),
|
|
4163
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-t border-gray-200 flex items-center justify-end gap-3", children: [
|
|
4164
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4165
|
-
"button",
|
|
4166
|
-
{
|
|
4167
|
-
onClick: onClose,
|
|
4168
|
-
className: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
4169
|
-
children: "Close"
|
|
4170
|
-
}
|
|
4171
|
-
),
|
|
4172
|
-
onReRun && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4173
|
-
"button",
|
|
4174
|
-
{
|
|
4175
|
-
onClick: () => {
|
|
4176
|
-
onReRun(run);
|
|
4177
|
-
onClose();
|
|
4178
|
-
},
|
|
4179
|
-
className: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
4180
|
-
children: "\u21BB Re-run"
|
|
4181
|
-
}
|
|
4182
|
-
),
|
|
4183
|
-
onViewResults && run.status === "completed" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4184
|
-
"button",
|
|
4185
|
-
{
|
|
4186
|
-
onClick: () => {
|
|
4187
|
-
onViewResults(run);
|
|
4188
|
-
onClose();
|
|
4189
|
-
},
|
|
4190
|
-
className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
4191
|
-
children: "View Results"
|
|
4192
|
-
}
|
|
4193
|
-
)
|
|
4194
|
-
] })
|
|
4195
|
-
]
|
|
4196
|
-
}
|
|
4197
|
-
)
|
|
4198
|
-
}
|
|
4199
|
-
);
|
|
4200
|
-
}
|
|
4201
|
-
function FunnelRunHistory({
|
|
4202
|
-
funnelId,
|
|
4203
|
-
apiClient,
|
|
4204
|
-
onViewResults,
|
|
4205
|
-
className = ""
|
|
4206
|
-
}) {
|
|
4207
|
-
const [runs, setRuns] = react.useState([]);
|
|
4208
|
-
const [isLoading, setIsLoading] = react.useState(true);
|
|
4209
|
-
const [error, setError] = react.useState(null);
|
|
4210
|
-
const [selectedRun, setSelectedRun] = react.useState(null);
|
|
4211
|
-
const [filters, setFilters] = react.useState({
|
|
4212
|
-
status: "all",
|
|
4213
|
-
trigger_type: "all",
|
|
4214
|
-
date_range: "month"
|
|
4215
|
-
});
|
|
4216
|
-
const [pagination, setPagination] = react.useState({
|
|
4217
|
-
page: 1,
|
|
4218
|
-
page_size: 10,
|
|
4219
|
-
total: 0
|
|
4220
|
-
});
|
|
4221
|
-
const [isRefreshing, setIsRefreshing] = react.useState(false);
|
|
4222
|
-
const loadRuns = react.useCallback(async () => {
|
|
4223
|
-
try {
|
|
4224
|
-
setError(null);
|
|
4225
|
-
const params = {
|
|
4226
|
-
page: pagination.page,
|
|
4227
|
-
page_size: pagination.page_size,
|
|
4228
|
-
ordering: "-started_at"
|
|
4229
|
-
// Most recent first
|
|
4230
|
-
};
|
|
4231
|
-
if (filters.status && filters.status !== "all") {
|
|
4232
|
-
params.status = filters.status;
|
|
4233
|
-
}
|
|
4234
|
-
if (filters.trigger_type && filters.trigger_type !== "all") {
|
|
4235
|
-
params.trigger_type = filters.trigger_type;
|
|
4236
|
-
}
|
|
4237
|
-
const response = await apiClient.getFunnelRuns(funnelId, params);
|
|
4238
|
-
setRuns(response.results);
|
|
4239
|
-
setPagination((prev) => ({
|
|
4240
|
-
...prev,
|
|
4241
|
-
total: response.count
|
|
4242
|
-
}));
|
|
4243
|
-
} catch (err) {
|
|
4244
|
-
setError(err instanceof Error ? err.message : "Failed to load runs");
|
|
4245
|
-
console.error("Failed to load funnel runs:", err);
|
|
4246
|
-
} finally {
|
|
4247
|
-
setIsLoading(false);
|
|
4248
|
-
setIsRefreshing(false);
|
|
4249
|
-
}
|
|
4250
|
-
}, [funnelId, apiClient, pagination.page, pagination.page_size, filters]);
|
|
4251
|
-
react.useEffect(() => {
|
|
4252
|
-
loadRuns();
|
|
4253
|
-
}, [loadRuns]);
|
|
4254
|
-
react.useEffect(() => {
|
|
4255
|
-
const hasActiveRuns = runs.some(
|
|
4256
|
-
(r) => r.status === "pending" || r.status === "running"
|
|
4257
|
-
);
|
|
4258
|
-
if (hasActiveRuns) {
|
|
4259
|
-
const interval = setInterval(() => {
|
|
4260
|
-
setIsRefreshing(true);
|
|
4261
|
-
loadRuns();
|
|
4262
|
-
}, 5e3);
|
|
4263
|
-
return () => clearInterval(interval);
|
|
4264
|
-
}
|
|
4265
|
-
}, [runs, loadRuns]);
|
|
4266
|
-
const handleRefresh = () => {
|
|
4267
|
-
setIsRefreshing(true);
|
|
4268
|
-
loadRuns();
|
|
4269
|
-
};
|
|
4270
|
-
const handleReRun = async (run) => {
|
|
4271
|
-
try {
|
|
4272
|
-
await apiClient.runFunnel(funnelId, {
|
|
4273
|
-
trigger_type: "manual",
|
|
4274
|
-
metadata: { re_run_of: run.id }
|
|
4275
|
-
});
|
|
4276
|
-
loadRuns();
|
|
4277
|
-
} catch (err) {
|
|
4278
|
-
console.error("Failed to re-run funnel:", err);
|
|
4279
|
-
alert("Failed to re-run funnel. Please try again.");
|
|
4280
|
-
}
|
|
4281
|
-
};
|
|
4282
|
-
const handleCancel = async (run) => {
|
|
4283
|
-
try {
|
|
4284
|
-
await apiClient.cancelFunnelRun(run.id);
|
|
4285
|
-
loadRuns();
|
|
4286
|
-
} catch (err) {
|
|
4287
|
-
console.error("Failed to cancel run:", err);
|
|
4288
|
-
alert("Failed to cancel run. Please try again.");
|
|
4289
|
-
}
|
|
4290
|
-
};
|
|
4291
|
-
const handleViewResults = (run) => {
|
|
4292
|
-
if (onViewResults) {
|
|
4293
|
-
onViewResults(run);
|
|
4294
|
-
} else {
|
|
4295
|
-
setSelectedRun(run);
|
|
4296
|
-
}
|
|
4297
|
-
};
|
|
4298
|
-
const totalPages = Math.ceil(pagination.total / pagination.page_size);
|
|
4299
|
-
const canGoBack = pagination.page > 1;
|
|
4300
|
-
const canGoForward = pagination.page < totalPages;
|
|
4301
|
-
const handlePreviousPage = () => {
|
|
4302
|
-
if (canGoBack) {
|
|
4303
|
-
setPagination((prev) => ({ ...prev, page: prev.page - 1 }));
|
|
4304
|
-
}
|
|
4305
|
-
};
|
|
4306
|
-
const handleNextPage = () => {
|
|
4307
|
-
if (canGoForward) {
|
|
4308
|
-
setPagination((prev) => ({ ...prev, page: prev.page + 1 }));
|
|
4309
|
-
}
|
|
4310
|
-
};
|
|
4311
|
-
const startIndex = (pagination.page - 1) * pagination.page_size + 1;
|
|
4312
|
-
const endIndex = Math.min(
|
|
4313
|
-
pagination.page * pagination.page_size,
|
|
4314
|
-
pagination.total
|
|
4315
|
-
);
|
|
4316
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `bg-white rounded-lg border border-gray-200 ${className}`, children: [
|
|
4317
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between", children: [
|
|
4318
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900", children: "Funnel Run History" }),
|
|
4319
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4320
|
-
"button",
|
|
4321
|
-
{
|
|
4322
|
-
onClick: handleRefresh,
|
|
4323
|
-
disabled: isRefreshing,
|
|
4324
|
-
className: "px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
4325
|
-
"aria-label": "Refresh run history",
|
|
4326
|
-
children: [
|
|
4327
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: isRefreshing ? "animate-spin inline-block" : "", children: "\u21BB" }),
|
|
4328
|
-
" ",
|
|
4329
|
-
"Refresh"
|
|
4330
|
-
]
|
|
4331
|
-
}
|
|
4332
|
-
)
|
|
4333
|
-
] }),
|
|
4334
|
-
/* @__PURE__ */ jsxRuntime.jsx(RunFilters, { filters, onFiltersChange: setFilters }),
|
|
4335
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full", children: [
|
|
4336
|
-
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-gray-50 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
4337
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Date" }),
|
|
4338
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Status" }),
|
|
4339
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Trigger" }),
|
|
4340
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Duration" }),
|
|
4341
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Input" }),
|
|
4342
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Matched" }),
|
|
4343
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "%" }),
|
|
4344
|
-
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Actions" }) })
|
|
4345
|
-
] }) }),
|
|
4346
|
-
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: isLoading && runs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 8, className: "px-4 py-12 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500", children: "Loading runs..." }) }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: 8, className: "px-4 py-12 text-center", children: [
|
|
4347
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-red-600", children: [
|
|
4348
|
-
"Error: ",
|
|
4349
|
-
error
|
|
4350
|
-
] }),
|
|
4351
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4352
|
-
"button",
|
|
4353
|
-
{
|
|
4354
|
-
onClick: loadRuns,
|
|
4355
|
-
className: "mt-2 text-sm text-blue-600 hover:text-blue-700 underline",
|
|
4356
|
-
children: "Try again"
|
|
4357
|
-
}
|
|
4358
|
-
)
|
|
4359
|
-
] }) }) : runs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 8, className: "px-4 py-12 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500", children: "No runs found. Run this funnel to see history." }) }) }) : runs.map((run) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4360
|
-
RunRow,
|
|
4361
|
-
{
|
|
4362
|
-
run,
|
|
4363
|
-
onViewDetails: setSelectedRun,
|
|
4364
|
-
onViewResults: handleViewResults,
|
|
4365
|
-
onReRun: handleReRun,
|
|
4366
|
-
onCancel: handleCancel
|
|
4367
|
-
},
|
|
4368
|
-
run.id
|
|
4369
|
-
)) })
|
|
4370
|
-
] }) }),
|
|
4371
|
-
runs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-t border-gray-200 flex items-center justify-between", children: [
|
|
4372
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-gray-600", children: [
|
|
4373
|
-
"Showing ",
|
|
4374
|
-
startIndex,
|
|
4375
|
-
"-",
|
|
4376
|
-
endIndex,
|
|
4377
|
-
" of ",
|
|
4378
|
-
pagination.total
|
|
4379
|
-
] }),
|
|
4380
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4381
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4382
|
-
"button",
|
|
4383
|
-
{
|
|
4384
|
-
onClick: handlePreviousPage,
|
|
4385
|
-
disabled: !canGoBack,
|
|
4386
|
-
className: "px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
4387
|
-
"aria-label": "Previous page",
|
|
4388
|
-
children: "\u2039"
|
|
4389
|
-
}
|
|
4390
|
-
),
|
|
4391
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-600", children: [
|
|
4392
|
-
"Page ",
|
|
4393
|
-
pagination.page,
|
|
4394
|
-
" of ",
|
|
4395
|
-
totalPages
|
|
4396
|
-
] }),
|
|
4397
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4398
|
-
"button",
|
|
4399
|
-
{
|
|
4400
|
-
onClick: handleNextPage,
|
|
4401
|
-
disabled: !canGoForward,
|
|
4402
|
-
className: "px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
4403
|
-
"aria-label": "Next page",
|
|
4404
|
-
children: "\u203A"
|
|
4405
|
-
}
|
|
4406
|
-
)
|
|
4407
|
-
] })
|
|
4408
|
-
] }),
|
|
4409
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4410
|
-
RunDetailsModal,
|
|
4411
|
-
{
|
|
4412
|
-
run: selectedRun,
|
|
4413
|
-
onClose: () => setSelectedRun(null),
|
|
4414
|
-
onViewResults,
|
|
4415
|
-
onReRun: handleReRun
|
|
4416
|
-
}
|
|
4417
|
-
)
|
|
4418
|
-
] });
|
|
4419
|
-
}
|
|
4420
|
-
|
|
4421
|
-
exports.AddStageButton = AddStageButton;
|
|
4422
|
-
exports.BooleanValueInput = BooleanValueInput;
|
|
4423
|
-
exports.ChoiceValueInput = ChoiceValueInput;
|
|
4424
|
-
exports.DateValueInput = DateValueInput;
|
|
4425
|
-
exports.EntityCard = EntityCard;
|
|
4426
|
-
exports.FetchAdapter = FetchAdapter;
|
|
4427
|
-
exports.FieldSelector = FieldSelector;
|
|
4428
|
-
exports.FilterRuleEditor = FilterRuleEditor;
|
|
4429
|
-
exports.FlowLegend = FlowLegend;
|
|
4430
|
-
exports.FunnelApiClient = FunnelApiClient;
|
|
4431
|
-
exports.FunnelCard = FunnelCard;
|
|
4432
|
-
exports.FunnelEngine = FunnelEngine;
|
|
4433
|
-
exports.FunnelPreview = FunnelPreview;
|
|
4434
|
-
exports.FunnelRunHistory = FunnelRunHistory;
|
|
4435
|
-
exports.FunnelStageBuilder = FunnelStageBuilder;
|
|
4436
|
-
exports.FunnelStats = FunnelStats;
|
|
4437
|
-
exports.FunnelVisualFlow = FunnelVisualFlow;
|
|
4438
|
-
exports.LoadingPreview = LoadingPreview;
|
|
4439
|
-
exports.LogicToggle = LogicToggle;
|
|
4440
|
-
exports.MULTI_VALUE_OPERATORS = MULTI_VALUE_OPERATORS;
|
|
4441
|
-
exports.MatchBar = MatchBar;
|
|
4442
|
-
exports.MultiChoiceValueInput = MultiChoiceValueInput;
|
|
4443
|
-
exports.NULL_VALUE_OPERATORS = NULL_VALUE_OPERATORS;
|
|
4444
|
-
exports.NumberValueInput = NumberValueInput;
|
|
4445
|
-
exports.OPERATOR_LABELS = OPERATOR_LABELS;
|
|
4446
|
-
exports.OperatorSelector = OperatorSelector;
|
|
4447
|
-
exports.PreviewStats = PreviewStats;
|
|
4448
|
-
exports.RuleRow = RuleRow;
|
|
4449
|
-
exports.RunActions = RunActions;
|
|
4450
|
-
exports.RunDetailsModal = RunDetailsModal;
|
|
4451
|
-
exports.RunFilters = RunFilters;
|
|
4452
|
-
exports.RunRow = RunRow;
|
|
4453
|
-
exports.RunStatusBadge = RunStatusBadge;
|
|
4454
|
-
exports.StageActions = StageActions;
|
|
4455
|
-
exports.StageBreakdown = StageBreakdown;
|
|
4456
|
-
exports.StageBreakdownList = StageBreakdownList;
|
|
4457
|
-
exports.StageCard = StageCard;
|
|
4458
|
-
exports.StageForm = StageForm;
|
|
4459
|
-
exports.StageIndicator = StageIndicator;
|
|
4460
|
-
exports.StageNode = StageNode;
|
|
4461
|
-
exports.StatusBadge = StatusBadge;
|
|
4462
|
-
exports.TagInput = TagInput;
|
|
4463
|
-
exports.TextValueInput = TextValueInput;
|
|
4464
|
-
exports.applyOperator = applyOperator;
|
|
4465
|
-
exports.calculateMatchRate = calculateMatchRate;
|
|
4466
|
-
exports.createApiError = createApiError;
|
|
4467
|
-
exports.createFunnelStore = createFunnelStore;
|
|
4468
|
-
exports.createInitialState = createInitialState;
|
|
4469
|
-
exports.evaluateRule = evaluateRule;
|
|
4470
|
-
exports.evaluateRuleWithResult = evaluateRuleWithResult;
|
|
4471
|
-
exports.evaluateRules = evaluateRules;
|
|
4472
|
-
exports.evaluateRulesAND = evaluateRulesAND;
|
|
4473
|
-
exports.evaluateRulesOR = evaluateRulesOR;
|
|
4474
|
-
exports.evaluateRulesWithResults = evaluateRulesWithResults;
|
|
4475
|
-
exports.filterEntities = filterEntities;
|
|
4476
|
-
exports.formatDuration = formatDuration;
|
|
4477
|
-
exports.formatFullTimestamp = formatFullTimestamp;
|
|
4478
|
-
exports.formatNumber = formatNumber;
|
|
4479
|
-
exports.formatRelativeTime = formatRelativeTime;
|
|
4480
|
-
exports.getCircledNumber = getCircledNumber;
|
|
4481
|
-
exports.getFields = getFields;
|
|
4482
|
-
exports.getValidOperators = getValidOperators;
|
|
4483
|
-
exports.hasField = hasField;
|
|
4484
|
-
exports.isApiError = isApiError;
|
|
4485
|
-
exports.isFieldDefinition = isFieldDefinition;
|
|
4486
|
-
exports.isFilterRule = isFilterRule;
|
|
4487
|
-
exports.isFunnel = isFunnel;
|
|
4488
|
-
exports.isFunnelResult = isFunnelResult;
|
|
4489
|
-
exports.isFunnelRun = isFunnelRun;
|
|
4490
|
-
exports.isStage = isStage;
|
|
4491
|
-
exports.isValidOperator = isValidOperator;
|
|
4492
|
-
exports.resolveField = resolveField;
|
|
4493
|
-
exports.setField = setField;
|
|
4494
|
-
exports.useDebouncedValue = useDebouncedValue;
|
|
4495
|
-
exports.validateFilterRule = validateFilterRule;
|
|
4496
|
-
exports.validateFunnel = validateFunnel;
|
|
4497
|
-
exports.validateStage = validateStage;
|
|
4498
|
-
//# sourceMappingURL=index.cjs.map
|
|
4499
|
-
//# sourceMappingURL=index.cjs.map
|