33import { defineFlow } from '@objectstack/spec' ;
44
55/**
6- * Convert Lead to Opportunity — Screen Flow
6+ * Convert Lead → Customer + Opportunity — Master-Detail Screen Flow
77 *
8- * A user-triggered wizard that walks a sales rep through converting a
9- * qualified lead into an Opportunity record. Demonstrates every key
10- * screen-flow node type:
8+ * A user-triggered wizard that walks a sales rep through converting a qualified
9+ * lead into a full Account + Opportunity. Unlike a flat field-list wizard, each
10+ * step renders the target object's COMPLETE create form via an `object-form`
11+ * screen node (`config.objectName`): the client renders the real ObjectForm —
12+ * including inline master-detail child grids — persists the record (and its
13+ * children, atomically), and resumes the run with the new record's id bound to
14+ * `config.idVariable`.
1115 *
1216 * start → get_lead → decision (already converted?)
13- * → already_converted_screen (abort path)
14- * → screen_qualify (collect opportunity details from rep)
15- * → screen_confirm (review + confirm before writing)
16- * → create_opp (create crm_opportunity)
17- * → update_lead (mark lead as converted)
18- * → screen_success (celebrate + navigate)
17+ * → screen_already_converted (abort path)
18+ * → screen_account (Step 1 — full Customer form → account_id)
19+ * → screen_opportunity (Step 2 — full Opportunity form WITH product
20+ * line-items grid, prefilled account → opportunity_id)
21+ * → update_lead (mark converted + link account & opportunity)
1922 * → end
2023 */
2124export const ConvertLeadScreenFlow = defineFlow ( {
2225 name : 'crm_convert_lead_wizard' ,
23- label : 'Convert Lead to Opportunity' ,
26+ label : 'Convert Lead to Customer + Opportunity' ,
2427 description :
25- 'Screen flow wizard that converts a qualified CRM lead into an Opportunity, marks the lead as converted, and links the two records .' ,
28+ 'Screen- flow wizard that walks the rep through a full Customer form then a full Opportunity form (with product line items), creates both, and marks the lead converted .' ,
2629 type : 'screen' ,
2730 status : 'active' ,
2831 runAs : 'user' ,
2932
3033 variables : [
31- // ── inputs (from action trigger) ───────────────────────────────────
32- { name : 'recordId' , type : 'text' , isInput : true , isOutput : false } ,
33- // ── screen-collected inputs (filled by wizard steps) ──────────────
34- // Marked isInput:true so the trigger API can pre-populate them for
35- // automated testing or when the Studio wizard submits all values at once.
36- { name : 'opp_name' , type : 'text' , isInput : true , isOutput : false } ,
37- { name : 'opp_account' , type : 'text' , isInput : true , isOutput : false } ,
38- { name : 'opp_amount' , type : 'number' , isInput : true , isOutput : false } ,
39- { name : 'opp_close_date' , type : 'date' , isInput : true , isOutput : false } ,
40- { name : 'opp_stage' , type : 'text' , isInput : true , isOutput : false } ,
41- // ── intermediate (populated at runtime) ───────────────────────────
42- { name : 'lead_record' , type : 'object' , isInput : false , isOutput : false } ,
43- // ── outputs ───────────────────────────────────────────────────────
44- { name : 'opportunity_id' , type : 'text' , isInput : false , isOutput : true } ,
34+ // ── input (from the action trigger) ───────────────────────────────────
35+ { name : 'recordId' , type : 'text' , isInput : true , isOutput : false } ,
36+ // ── intermediate (populated at runtime) ───────────────────────────────
37+ { name : 'lead_record' , type : 'object' , isInput : false , isOutput : false } ,
38+ // ── produced by the object-form steps (the saved record ids) ──────────
39+ // `isInput: true` lets the trigger API pre-populate them for automated
40+ // testing; at runtime each is bound by its step's `idVariable` on resume.
41+ { name : 'account_id' , type : 'text' , isInput : true , isOutput : true } ,
42+ { name : 'opportunity_id' , type : 'text' , isInput : true , isOutput : true } ,
4543 ] ,
4644
4745 nodes : [
48- // ── 1. Start ──────────────────────────────────────────────────────
46+ // ── 1. Start ──────────────────────────────────────────────────────────
4947 { id : 'start' , type : 'start' , label : 'Start' } ,
5048
51- // ── 2. Load the lead record ───────────────────────────────────────
49+ // ── 2. Load the lead record ───────────────────────────────────────────
5250 {
5351 id : 'get_lead' ,
5452 type : 'get_record' ,
5553 label : 'Load Lead' ,
5654 config : {
5755 objectName : 'crm_lead' ,
58- recordId : '{recordId}' ,
56+ // get_record filters via `filter` (it ignores a bare `recordId`), so
57+ // load THIS lead by id — otherwise findOne with an empty filter returns
58+ // the first lead in the table.
59+ filter : { id : '{recordId}' } ,
5960 outputVariable : 'lead_record' ,
6061 } ,
6162 } ,
6263
63- // ── 3. Guard: already converted? ──────────────────────────────────
64+ // ── 3. Guard: already converted? ──────────────────────────────────────
6465 {
6566 id : 'check_converted' ,
6667 type : 'decision' ,
@@ -73,102 +74,60 @@ export const ConvertLeadScreenFlow = defineFlow({
7374 } ,
7475 } ,
7576
76- // ── 3a. Already-converted abort screen ────────────────────────────
77+ // ── 3a. Already-converted abort screen ────────────────────────────────
7778 {
7879 id : 'screen_already_converted' ,
7980 type : 'screen' ,
8081 label : 'Already Converted' ,
8182 config : {
82- message : 'This lead has already been converted to an opportunity.' ,
83- buttons : [
84- { label : 'Close' , action : 'finish' } ,
85- ] ,
83+ waitForInput : true ,
84+ title : 'Already Converted' ,
85+ description : 'This lead has already been converted to an opportunity.' ,
8686 } ,
8787 } ,
8888
89- // ── 4. Screen 1: Collect qualification + opportunity details ───────
89+ // ── 4. Step 1 — full Customer (Account) form ──────────────────────────
90+ // `objectName` ⇒ object-form screen: the client renders the real
91+ // crm_account create form, persists it, and resumes with the new id under
92+ // `account_id`.
9093 {
91- id : 'screen_qualify ' ,
94+ id : 'screen_account ' ,
9295 type : 'screen' ,
93- label : 'Opportunity Details ' ,
96+ label : 'Customer ' ,
9497 config : {
95- fields : [
96- {
97- name : 'opp_name' ,
98- label : 'Opportunity Name' ,
99- type : 'text' ,
100- required : true ,
101- defaultValue : '{lead_record.name}' ,
102- helpText : 'Descriptive name for the new opportunity' ,
103- } ,
104- {
105- name : 'opp_account' ,
106- label : 'Account' ,
107- type : 'lookup' ,
108- object : 'crm_account' ,
109- required : false ,
110- defaultValue : '{lead_record.account}' ,
111- helpText : 'The account this opportunity belongs to' ,
112- } ,
113- {
114- name : 'opp_amount' ,
115- label : 'Estimated Value ($)' ,
116- type : 'number' ,
117- required : false ,
118- min : 0 ,
119- } ,
120- {
121- name : 'opp_close_date' ,
122- label : 'Expected Close Date' ,
123- type : 'date' ,
124- required : false ,
125- } ,
126- {
127- name : 'opp_stage' ,
128- label : 'Stage' ,
129- type : 'select' ,
130- required : true ,
131- options : [ 'prospecting' , 'qualification' , 'proposal' , 'closed_won' , 'closed_lost' ] ,
132- defaultValue : 'qualification' ,
133- } ,
134- ] ,
98+ objectName : 'crm_account' ,
99+ idVariable : 'account_id' ,
100+ title : 'Step 1 of 2 · Customer' ,
101+ description : 'Review and complete the customer record carried over from the lead.' ,
102+ defaults : {
103+ name : '{lead_record.company}' ,
104+ } ,
135105 } ,
136106 } ,
137107
138- // ── 5. Screen 2: Confirm before write ─────────────────────────────
108+ // ── 5. Step 2 — full Opportunity form WITH product line items ──────────
109+ // crm_opportunity_line_item.opportunity declares inlineEdit: 'grid', so the
110+ // standard Opportunity form (and therefore this step) renders an editable
111+ // product line-item grid below the header — the master-detail entry the
112+ // user asked for. The account FK is prefilled with Step 1's new id.
139113 {
140- id : 'screen_confirm ' ,
114+ id : 'screen_opportunity ' ,
141115 type : 'screen' ,
142- label : 'Confirm Conversion' ,
143- config : {
144- message : 'Create opportunity "{opp_name}" for lead "{lead_record.name}"?\n\nStage: {opp_stage} · Amount: ${opp_amount} · Close: {opp_close_date}' ,
145- buttons : [
146- { label : 'Convert' , action : 'next' } ,
147- { label : 'Back' , action : 'back' } ,
148- { label : 'Cancel' , action : 'finish' } ,
149- ] ,
150- } ,
151- } ,
152-
153- // ── 6. Create the Opportunity ─────────────────────────────────────
154- {
155- id : 'create_opp' ,
156- type : 'create_record' ,
157- label : 'Create Opportunity' ,
116+ label : 'Opportunity' ,
158117 config : {
159118 objectName : 'crm_opportunity' ,
160- fields : {
161- name : '{opp_name}' ,
162- account : '{opp_account}' ,
163- amount : '{opp_amount}' ,
164- close_date : '{opp_close_date}' ,
165- stage : '{opp_stage}' ,
119+ idVariable : 'opportunity_id' ,
120+ title : 'Step 2 of 2 · Opportunity' ,
121+ description : 'Add the opportunity and its product line items. Saved together in one transaction.' ,
122+ defaults : {
123+ name : '{lead_record.name}' ,
124+ account : '{account_id}' ,
125+ stage : 'qualification' ,
166126 } ,
167- outputVariable : 'opportunity_id' ,
168127 } ,
169128 } ,
170129
171- // ── 7 . Mark the lead as converted ───────────────── ────────────────
130+ // ── 6 . Mark the lead converted + link both new records ────────────────
172131 {
173132 id : 'update_lead' ,
174133 type : 'update_record' ,
@@ -177,47 +136,28 @@ export const ConvertLeadScreenFlow = defineFlow({
177136 objectName : 'crm_lead' ,
178137 filter : { id : '{recordId}' } ,
179138 fields : {
180- status : 'converted' ,
181- converted_opportunity : '{opportunity_id }',
182- is_closed : true ,
139+ status : 'converted' ,
140+ account : '{account_id }',
141+ converted_opportunity : '{opportunity_id}' ,
183142 } ,
184143 } ,
185144 } ,
186145
187- // ── 8. Success screen ─────────────────────────────────────────────
188- {
189- id : 'screen_success' ,
190- type : 'screen' ,
191- label : 'Conversion Complete' ,
192- config : {
193- message : '✅ Lead converted! Opportunity "{opp_name}" has been created.' ,
194- buttons : [
195- { label : 'View Opportunity' , action : 'navigate' , target : '/crm_opportunity/{opportunity_id}' } ,
196- { label : 'Done' , action : 'finish' } ,
197- ] ,
198- } ,
199- } ,
200-
201- // ── 9. End ────────────────────────────────────────────────────────
146+ // ── 7. End ────────────────────────────────────────────────────────────
202147 { id : 'end' , type : 'end' , label : 'End' } ,
203148 ] ,
204149
205150 edges : [
206- { id : 'e1' , source : 'start' , target : 'get_lead' , type : 'default' } ,
207- { id : 'e2' , source : 'get_lead' , target : 'check_converted' , type : 'default' } ,
151+ { id : 'e1' , source : 'start' , target : 'get_lead' , type : 'default' } ,
152+ { id : 'e2' , source : 'get_lead' , target : 'check_converted' , type : 'default' } ,
208153 // guard branches
209- { id : 'e3a' , source : 'check_converted' , target : 'screen_already_converted' , type : 'default' , condition : "lead_record.status == 'converted'" , label : 'Yes' } ,
210- { id : 'e3b' , source : 'check_converted' , target : 'screen_qualify' , type : 'default' , label : 'No' } ,
211- { id : 'e3c' , source : 'screen_already_converted' , target : 'end' , type : 'default' } ,
212- // main path
213- { id : 'e4' , source : 'screen_qualify' , target : 'screen_confirm' , type : 'default' } ,
214- // "Convert" proceeds to create the record; "Back" is handled client-side
215- // by the screen-flow runner's history stack — no server back-edge needed
216- // (a back-edge would create a cycle and fail DAG validation).
217- { id : 'e5' , source : 'screen_confirm' , target : 'create_opp' , type : 'default' } ,
218- { id : 'e7' , source : 'create_opp' , target : 'update_lead' , type : 'default' } ,
219- { id : 'e8' , source : 'update_lead' , target : 'screen_success' , type : 'default' } ,
220- { id : 'e9' , source : 'screen_success' , target : 'end' , type : 'default' } ,
154+ { id : 'e3a' , source : 'check_converted' , target : 'screen_already_converted' , type : 'default' , condition : "lead_record.status == 'converted'" , label : 'Yes' } ,
155+ { id : 'e3b' , source : 'check_converted' , target : 'screen_account' , type : 'default' , label : 'No' } ,
156+ { id : 'e3c' , source : 'screen_already_converted' , target : 'end' , type : 'default' } ,
157+ // main path — full Customer form → full Opportunity form → link
158+ { id : 'e4' , source : 'screen_account' , target : 'screen_opportunity' , type : 'default' } ,
159+ { id : 'e5' , source : 'screen_opportunity' , target : 'update_lead' , type : 'default' } ,
160+ { id : 'e6' , source : 'update_lead' , target : 'end' , type : 'default' } ,
221161 ] ,
222162
223163 errorHandling : {
0 commit comments