@startsimpli/funnels 0.1.3 → 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 -3243
- 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 -3196
- 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 -21
- 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 -19
- 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 -391
- 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 -388
- package/dist/store/index.js.map +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StageActions tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
7
|
+
import { StageActions } from './StageActions';
|
|
8
|
+
import { FunnelStage } from '../../types';
|
|
9
|
+
|
|
10
|
+
// Create test stage
|
|
11
|
+
function createTestStage(): FunnelStage {
|
|
12
|
+
return {
|
|
13
|
+
id: 'stage-1',
|
|
14
|
+
order: 0,
|
|
15
|
+
name: 'Test Stage',
|
|
16
|
+
filter_logic: 'AND',
|
|
17
|
+
rules: [],
|
|
18
|
+
match_action: 'continue',
|
|
19
|
+
no_match_action: 'continue',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('StageActions', () => {
|
|
24
|
+
it('renders match action dropdown', () => {
|
|
25
|
+
const stage = createTestStage();
|
|
26
|
+
const onMatchActionChange = vi.fn();
|
|
27
|
+
const onNoMatchActionChange = vi.fn();
|
|
28
|
+
|
|
29
|
+
render(
|
|
30
|
+
<StageActions
|
|
31
|
+
stage={stage}
|
|
32
|
+
onMatchActionChange={onMatchActionChange}
|
|
33
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(screen.getByLabelText(/Action on Match/i)).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders no match action dropdown', () => {
|
|
41
|
+
const stage = createTestStage();
|
|
42
|
+
const onMatchActionChange = vi.fn();
|
|
43
|
+
const onNoMatchActionChange = vi.fn();
|
|
44
|
+
|
|
45
|
+
render(
|
|
46
|
+
<StageActions
|
|
47
|
+
stage={stage}
|
|
48
|
+
onMatchActionChange={onMatchActionChange}
|
|
49
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(screen.getByLabelText(/Action on No Match/i)).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('displays all match action options', () => {
|
|
57
|
+
const stage = createTestStage();
|
|
58
|
+
const onMatchActionChange = vi.fn();
|
|
59
|
+
const onNoMatchActionChange = vi.fn();
|
|
60
|
+
|
|
61
|
+
render(
|
|
62
|
+
<StageActions
|
|
63
|
+
stage={stage}
|
|
64
|
+
onMatchActionChange={onMatchActionChange}
|
|
65
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const matchSelect = screen.getByLabelText(/Action on Match/i) as HTMLSelectElement;
|
|
70
|
+
const options = Array.from(matchSelect.options).map(opt => opt.value);
|
|
71
|
+
|
|
72
|
+
expect(options).toEqual(['continue', 'tag', 'tag_continue', 'output']);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('displays all no match action options', () => {
|
|
76
|
+
const stage = createTestStage();
|
|
77
|
+
const onMatchActionChange = vi.fn();
|
|
78
|
+
const onNoMatchActionChange = vi.fn();
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
<StageActions
|
|
82
|
+
stage={stage}
|
|
83
|
+
onMatchActionChange={onMatchActionChange}
|
|
84
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const noMatchSelect = screen.getByLabelText(/Action on No Match/i) as HTMLSelectElement;
|
|
89
|
+
const options = Array.from(noMatchSelect.options).map(opt => opt.value);
|
|
90
|
+
|
|
91
|
+
expect(options).toEqual(['continue', 'exclude', 'tag_exclude']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('calls onMatchActionChange when match action changes', () => {
|
|
95
|
+
const stage = createTestStage();
|
|
96
|
+
const onMatchActionChange = vi.fn();
|
|
97
|
+
const onNoMatchActionChange = vi.fn();
|
|
98
|
+
|
|
99
|
+
render(
|
|
100
|
+
<StageActions
|
|
101
|
+
stage={stage}
|
|
102
|
+
onMatchActionChange={onMatchActionChange}
|
|
103
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const matchSelect = screen.getByLabelText(/Action on Match/i);
|
|
108
|
+
fireEvent.change(matchSelect, { target: { value: 'output' } });
|
|
109
|
+
|
|
110
|
+
expect(onMatchActionChange).toHaveBeenCalledWith('output');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('calls onNoMatchActionChange when no match action changes', () => {
|
|
114
|
+
const stage = createTestStage();
|
|
115
|
+
const onMatchActionChange = vi.fn();
|
|
116
|
+
const onNoMatchActionChange = vi.fn();
|
|
117
|
+
|
|
118
|
+
render(
|
|
119
|
+
<StageActions
|
|
120
|
+
stage={stage}
|
|
121
|
+
onMatchActionChange={onMatchActionChange}
|
|
122
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const noMatchSelect = screen.getByLabelText(/Action on No Match/i);
|
|
127
|
+
fireEvent.change(noMatchSelect, { target: { value: 'exclude' } });
|
|
128
|
+
|
|
129
|
+
expect(onNoMatchActionChange).toHaveBeenCalledWith('exclude');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('displays current match action', () => {
|
|
133
|
+
const stage = createTestStage();
|
|
134
|
+
stage.match_action = 'tag_continue';
|
|
135
|
+
|
|
136
|
+
const onMatchActionChange = vi.fn();
|
|
137
|
+
const onNoMatchActionChange = vi.fn();
|
|
138
|
+
|
|
139
|
+
render(
|
|
140
|
+
<StageActions
|
|
141
|
+
stage={stage}
|
|
142
|
+
onMatchActionChange={onMatchActionChange}
|
|
143
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const matchSelect = screen.getByLabelText(/Action on Match/i) as HTMLSelectElement;
|
|
148
|
+
expect(matchSelect.value).toBe('tag_continue');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('displays current no match action', () => {
|
|
152
|
+
const stage = createTestStage();
|
|
153
|
+
stage.no_match_action = 'tag_exclude';
|
|
154
|
+
|
|
155
|
+
const onMatchActionChange = vi.fn();
|
|
156
|
+
const onNoMatchActionChange = vi.fn();
|
|
157
|
+
|
|
158
|
+
render(
|
|
159
|
+
<StageActions
|
|
160
|
+
stage={stage}
|
|
161
|
+
onMatchActionChange={onMatchActionChange}
|
|
162
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const noMatchSelect = screen.getByLabelText(/Action on No Match/i) as HTMLSelectElement;
|
|
167
|
+
expect(noMatchSelect.value).toBe('tag_exclude');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('shows description for match action', () => {
|
|
171
|
+
const stage = createTestStage();
|
|
172
|
+
stage.match_action = 'output';
|
|
173
|
+
|
|
174
|
+
const onMatchActionChange = vi.fn();
|
|
175
|
+
const onNoMatchActionChange = vi.fn();
|
|
176
|
+
|
|
177
|
+
render(
|
|
178
|
+
<StageActions
|
|
179
|
+
stage={stage}
|
|
180
|
+
onMatchActionChange={onMatchActionChange}
|
|
181
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(screen.getByText(/Add to output and stop processing/i)).toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('shows description for no match action', () => {
|
|
189
|
+
const stage = createTestStage();
|
|
190
|
+
stage.no_match_action = 'exclude';
|
|
191
|
+
|
|
192
|
+
const onMatchActionChange = vi.fn();
|
|
193
|
+
const onNoMatchActionChange = vi.fn();
|
|
194
|
+
|
|
195
|
+
render(
|
|
196
|
+
<StageActions
|
|
197
|
+
stage={stage}
|
|
198
|
+
onMatchActionChange={onMatchActionChange}
|
|
199
|
+
onNoMatchActionChange={onNoMatchActionChange}
|
|
200
|
+
/>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(screen.getByText(/Exclude from output and stop processing/i)).toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StageActions - Action dropdowns and toggles
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Match action dropdown
|
|
6
|
+
* - No match action dropdown
|
|
7
|
+
* - Clear labels and descriptions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { FunnelStage, MatchAction, NoMatchAction } from '../../types';
|
|
12
|
+
|
|
13
|
+
export interface StageActionsProps {
|
|
14
|
+
/** The stage being configured */
|
|
15
|
+
stage: FunnelStage;
|
|
16
|
+
|
|
17
|
+
/** Match action change handler */
|
|
18
|
+
onMatchActionChange: (action: MatchAction) => void;
|
|
19
|
+
|
|
20
|
+
/** No match action change handler */
|
|
21
|
+
onNoMatchActionChange: (action: NoMatchAction) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Match action options with descriptions
|
|
26
|
+
*/
|
|
27
|
+
const MATCH_ACTIONS: Array<{ value: MatchAction; label: string; description: string }> = [
|
|
28
|
+
{
|
|
29
|
+
value: 'continue',
|
|
30
|
+
label: 'Continue',
|
|
31
|
+
description: 'Continue to next stage without tagging',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
value: 'tag',
|
|
35
|
+
label: 'Tag & Stop',
|
|
36
|
+
description: 'Add tags and stop processing',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: 'tag_continue',
|
|
40
|
+
label: 'Tag & Continue',
|
|
41
|
+
description: 'Add tags and continue to next stage',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
value: 'output',
|
|
45
|
+
label: 'Output',
|
|
46
|
+
description: 'Add to output and stop processing',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* No match action options with descriptions
|
|
52
|
+
*/
|
|
53
|
+
const NO_MATCH_ACTIONS: Array<{ value: NoMatchAction; label: string; description: string }> = [
|
|
54
|
+
{
|
|
55
|
+
value: 'continue',
|
|
56
|
+
label: 'Continue',
|
|
57
|
+
description: 'Continue to next stage',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: 'exclude',
|
|
61
|
+
label: 'Exclude',
|
|
62
|
+
description: 'Exclude from output and stop processing',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
value: 'tag_exclude',
|
|
66
|
+
label: 'Tag & Exclude',
|
|
67
|
+
description: 'Add tags, exclude from output, and stop',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* StageActions component
|
|
73
|
+
*/
|
|
74
|
+
export function StageActions({
|
|
75
|
+
stage,
|
|
76
|
+
onMatchActionChange,
|
|
77
|
+
onNoMatchActionChange,
|
|
78
|
+
}: StageActionsProps) {
|
|
79
|
+
return (
|
|
80
|
+
<div className="stage-actions">
|
|
81
|
+
{/* Match action */}
|
|
82
|
+
<div className="form-group">
|
|
83
|
+
<label htmlFor={`match-action-${stage.id}`} className="form-label">
|
|
84
|
+
Action on Match
|
|
85
|
+
</label>
|
|
86
|
+
<select
|
|
87
|
+
id={`match-action-${stage.id}`}
|
|
88
|
+
value={stage.match_action}
|
|
89
|
+
onChange={(e) => onMatchActionChange(e.target.value as MatchAction)}
|
|
90
|
+
className="form-select"
|
|
91
|
+
>
|
|
92
|
+
{MATCH_ACTIONS.map((option) => (
|
|
93
|
+
<option key={option.value} value={option.value}>
|
|
94
|
+
{option.label}
|
|
95
|
+
</option>
|
|
96
|
+
))}
|
|
97
|
+
</select>
|
|
98
|
+
<p className="form-hint">
|
|
99
|
+
{MATCH_ACTIONS.find((a) => a.value === stage.match_action)?.description}
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* No match action */}
|
|
104
|
+
<div className="form-group">
|
|
105
|
+
<label htmlFor={`no-match-action-${stage.id}`} className="form-label">
|
|
106
|
+
Action on No Match
|
|
107
|
+
</label>
|
|
108
|
+
<select
|
|
109
|
+
id={`no-match-action-${stage.id}`}
|
|
110
|
+
value={stage.no_match_action}
|
|
111
|
+
onChange={(e) => onNoMatchActionChange(e.target.value as NoMatchAction)}
|
|
112
|
+
className="form-select"
|
|
113
|
+
>
|
|
114
|
+
{NO_MATCH_ACTIONS.map((option) => (
|
|
115
|
+
<option key={option.value} value={option.value}>
|
|
116
|
+
{option.label}
|
|
117
|
+
</option>
|
|
118
|
+
))}
|
|
119
|
+
</select>
|
|
120
|
+
<p className="form-hint">
|
|
121
|
+
{NO_MATCH_ACTIONS.find((a) => a.value === stage.no_match_action)?.description}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StageCard - Individual stage card with drag handle
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Drag handle for reordering
|
|
6
|
+
* - Expand/collapse
|
|
7
|
+
* - Delete button
|
|
8
|
+
* - Stage configuration form when expanded
|
|
9
|
+
* - Validation errors and warnings
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { useSortable } from '@dnd-kit/sortable';
|
|
14
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
15
|
+
import { FunnelStage, FieldDefinition } from '../../types';
|
|
16
|
+
import { StageForm } from './StageForm';
|
|
17
|
+
|
|
18
|
+
export interface StageCardProps {
|
|
19
|
+
/** The stage to display */
|
|
20
|
+
stage: FunnelStage;
|
|
21
|
+
|
|
22
|
+
/** Whether stage is expanded */
|
|
23
|
+
expanded: boolean;
|
|
24
|
+
|
|
25
|
+
/** Toggle expanded state */
|
|
26
|
+
onToggleExpanded: () => void;
|
|
27
|
+
|
|
28
|
+
/** Update stage */
|
|
29
|
+
onUpdate: (stage: FunnelStage) => void;
|
|
30
|
+
|
|
31
|
+
/** Remove stage */
|
|
32
|
+
onRemove: () => void;
|
|
33
|
+
|
|
34
|
+
/** Available fields */
|
|
35
|
+
fieldRegistry: FieldDefinition[];
|
|
36
|
+
|
|
37
|
+
/** Validation error */
|
|
38
|
+
error?: string;
|
|
39
|
+
|
|
40
|
+
/** Show warnings */
|
|
41
|
+
showWarnings?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* StageCard component
|
|
46
|
+
*/
|
|
47
|
+
export function StageCard({
|
|
48
|
+
stage,
|
|
49
|
+
expanded,
|
|
50
|
+
onToggleExpanded,
|
|
51
|
+
onUpdate,
|
|
52
|
+
onRemove,
|
|
53
|
+
fieldRegistry,
|
|
54
|
+
error,
|
|
55
|
+
showWarnings = false,
|
|
56
|
+
}: StageCardProps) {
|
|
57
|
+
const {
|
|
58
|
+
attributes,
|
|
59
|
+
listeners,
|
|
60
|
+
setNodeRef,
|
|
61
|
+
transform,
|
|
62
|
+
transition,
|
|
63
|
+
isDragging,
|
|
64
|
+
} = useSortable({ id: stage.id });
|
|
65
|
+
|
|
66
|
+
const style = {
|
|
67
|
+
transform: CSS.Transform.toString(transform),
|
|
68
|
+
transition,
|
|
69
|
+
opacity: isDragging ? 0.5 : 1,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
ref={setNodeRef}
|
|
75
|
+
style={style}
|
|
76
|
+
className={`stage-card ${isDragging ? 'dragging' : ''} ${error ? 'error' : ''}`}
|
|
77
|
+
>
|
|
78
|
+
{/* Header */}
|
|
79
|
+
<div className="stage-header">
|
|
80
|
+
{/* Drag handle */}
|
|
81
|
+
<button
|
|
82
|
+
{...attributes}
|
|
83
|
+
{...listeners}
|
|
84
|
+
className="drag-handle"
|
|
85
|
+
aria-label="Drag to reorder"
|
|
86
|
+
>
|
|
87
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
88
|
+
<path
|
|
89
|
+
d="M7 4h6M7 10h6M7 16h6"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
strokeWidth="2"
|
|
92
|
+
strokeLinecap="round"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
{/* Stage title (clickable to expand/collapse) */}
|
|
98
|
+
<button
|
|
99
|
+
onClick={onToggleExpanded}
|
|
100
|
+
className="stage-title-button"
|
|
101
|
+
aria-expanded={expanded}
|
|
102
|
+
>
|
|
103
|
+
<span className="stage-number">Stage {stage.order + 1}:</span>
|
|
104
|
+
<span className="stage-name">{stage.name || 'Untitled Stage'}</span>
|
|
105
|
+
|
|
106
|
+
{/* Expand/collapse icon */}
|
|
107
|
+
<svg
|
|
108
|
+
width="20"
|
|
109
|
+
height="20"
|
|
110
|
+
viewBox="0 0 20 20"
|
|
111
|
+
fill="none"
|
|
112
|
+
className={`expand-icon ${expanded ? 'expanded' : ''}`}
|
|
113
|
+
>
|
|
114
|
+
<path
|
|
115
|
+
d="M6 8l4 4 4-4"
|
|
116
|
+
stroke="currentColor"
|
|
117
|
+
strokeWidth="2"
|
|
118
|
+
strokeLinecap="round"
|
|
119
|
+
strokeLinejoin="round"
|
|
120
|
+
/>
|
|
121
|
+
</svg>
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
{/* Delete button */}
|
|
125
|
+
<button
|
|
126
|
+
onClick={onRemove}
|
|
127
|
+
className="delete-button"
|
|
128
|
+
aria-label="Delete stage"
|
|
129
|
+
>
|
|
130
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
131
|
+
<path
|
|
132
|
+
d="M6 6l8 8M14 6l-8 8"
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
strokeWidth="2"
|
|
135
|
+
strokeLinecap="round"
|
|
136
|
+
/>
|
|
137
|
+
</svg>
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Validation error */}
|
|
142
|
+
{error && (
|
|
143
|
+
<div className="error-message">
|
|
144
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
145
|
+
<path
|
|
146
|
+
d="M8 1l7 13H1L8 1z"
|
|
147
|
+
stroke="currentColor"
|
|
148
|
+
strokeWidth="2"
|
|
149
|
+
strokeLinejoin="round"
|
|
150
|
+
/>
|
|
151
|
+
<path d="M8 6v3M8 11h.01" stroke="currentColor" strokeWidth="2" />
|
|
152
|
+
</svg>
|
|
153
|
+
<span>{error}</span>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* Warning (no rules) */}
|
|
158
|
+
{showWarnings && !error && (
|
|
159
|
+
<div className="warning-message">
|
|
160
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
161
|
+
<circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="2" />
|
|
162
|
+
<path d="M8 5v3M8 10h.01" stroke="currentColor" strokeWidth="2" />
|
|
163
|
+
</svg>
|
|
164
|
+
<span>Stage has no filter rules</span>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{/* Collapsed summary */}
|
|
169
|
+
{!expanded && (
|
|
170
|
+
<div className="stage-summary">
|
|
171
|
+
<div className="summary-item">
|
|
172
|
+
<span className="summary-label">Rules:</span>
|
|
173
|
+
<span className="summary-value">{stage.rules.length}</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="summary-item">
|
|
176
|
+
<span className="summary-label">Logic:</span>
|
|
177
|
+
<span className="summary-value">{stage.filter_logic}</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="summary-item">
|
|
180
|
+
<span className="summary-label">On Match:</span>
|
|
181
|
+
<span className="summary-value">{stage.match_action}</span>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="summary-item">
|
|
184
|
+
<span className="summary-label">On No Match:</span>
|
|
185
|
+
<span className="summary-value">{stage.no_match_action}</span>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Expanded form */}
|
|
191
|
+
{expanded && (
|
|
192
|
+
<div className="stage-form-wrapper">
|
|
193
|
+
<StageForm
|
|
194
|
+
stage={stage}
|
|
195
|
+
onUpdate={onUpdate}
|
|
196
|
+
fieldRegistry={fieldRegistry}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|