Adding a New Step — End-to-End
เพิ่ม step ใหม่เข้า Lego Engine ทั้งฝั่ง backend (Handler + StepCatalog + FlowStep + DI) และ frontend (component + STEP_REGISTRY) โดยยึด stepType เป็น contract
อัปเดต: 2026-06-04
การเพิ่ม step ใหม่แตะ ทั้ง 2 ฝั่ง และทั้งสองฝั่งผูกกันด้วยสิ่งเดียว: stepType (string)
stepType คือ contract
StepCatalogStepTypeCode
≡
Backend HandlerStepType
≡
FE STEP_REGISTRYkey
'AddressVerificationStep' — string เดียวกันทั้ง 3 ที่
ฝั่ง Backend (4 ขั้น)
ใช้ ExampleAddressVerificationStepHandler เป็น template (เป็น reference ไม่ได้ register จริง)
1) เขียน Handler (IFlowStepHandler)
public sealed class AddressVerificationStepHandler : IFlowStepHandler
{
// ตรงเป๊ะกับ StepCatalog.StepTypeCode และ FE component key
public string StepType => "AddressVerificationStep";
public Task<FlowStepExecutionResult> ExecuteAsync(FlowStepExecutionContext ctx, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(ctx.InputJson))
return Fail("Step.NoInput", "กรุณากรอกที่อยู่บริษัท");
// parse: รองรับทั้ง { stepData: {...} } และ flat {...}
// อ่านค่า "ก่อน" doc ถูก dispose (กัน ObjectDisposedException)
using var doc = JsonDocument.Parse(ctx.InputJson);
var root = doc.RootElement.TryGetProperty("stepData", out var sd) ? sd : doc.RootElement;
var address = GetStr(root, "address");
// validate ฝั่ง server เสมอ (อย่าเชื่อ FE)
if (string.IsNullOrWhiteSpace(address))
return Fail("Step.RequiredField", "ที่อยู่จำเป็นต้องกรอก");
// อ่าน data ของ step อื่นผ่าน StepType (ไม่ใช่ index)
var companyId = ReadCompanyId(ctx.StepData.GetValueOrDefault("JuristicBindingStep"));
var output = JsonSerializer.Serialize(new { address, companyId, verifiedAt = DateTime.UtcNow.ToString("O") });
return Ok(output); // engine จะ upsert เป็น FlowInstanceStepData
}
}
2) INSERT StepCatalog
INSERT INTO "StepCatalog" ("StepTypeCode","ExecutionMode","SkipScope","DependenciesJson")
VALUES ('AddressVerificationStep', 0 /*Interactive*/, 2 /*PerUserPerCompany*/,
'[{"StepTypeCode":"JuristicBindingStep","Mode":"Required"}]');
3) ผูกเข้า Flow (FlowStep)
INSERT INTO "FlowSteps" ("FlowDefinitionId","StepCatalogId","Order","StepTypeCode", ...)
VALUES (<flowDefId>, <stepCatalogId>, <order หลัง JuristicBinding>, 'AddressVerificationStep', ...);
4) register DI
// UserService02.Infrastructure/DependencyInjection.cs
services.AddScoped<IFlowStepHandler, AddressVerificationStepHandler>();
ฝั่ง Frontend (เฉพาะ Interactive step)
5) สร้าง component ตาม contract
component conform contract ของ StepHostDirective: input canEdit/context, output dataChange/validChange/action
@Component({ selector: 'app-address-verification', standalone: true, /* ... */ })
export class AddressVerificationComponent {
canEdit = input<boolean>(true);
dataChange = output<object>();
validChange = output<boolean>();
action = output<StepActionEmit>();
onSubmit() {
if (this.form.invalid) return;
this.action.emit({ code: 'Next', payload: this.form.value });
}
}
6) register ใน STEP_REGISTRY (1 บรรทัด)
// step-registry.ts — key ต้องตรงกับ StepType ฝั่ง BE
export const STEP_REGISTRY: Record<string, Type<unknown>> = {
// ...
AddressVerificationStep: AddressVerificationComponent,
};
7) (ถ้า component คุมปุ่มเอง) เพิ่มใน SELF_ACTION_STEPS
const SELF_ACTION_STEPS = new Set([... , 'AddressVerificationStep']);
Checklist
| # | ที่ | ทำอะไร |
|---|---|---|
| 1 | BE Handler | IFlowStepHandler + StepType ตรง |
| 2 | DB StepCatalog | INSERT row (ExecutionMode/SkipScope/Deps) |
| 3 | DB FlowStep | ผูกเข้า flow + Order |
| 4 | BE DI | AddScoped<IFlowStepHandler, X>() |
| 5 | FE component | conform contract (Interactive เท่านั้น) |
| 6 | FE registry | เพิ่มใน STEP_REGISTRY |
| 7 | FE (ถ้าจำเป็น) | SELF_ACTION_STEPS / sidebar progress |