一键导入
migrate-workflow
Migrate multi-step wizard workflows from legacy MVC to Angular with workflow progress tracking, step components, and state transitions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Migrate multi-step wizard workflows from legacy MVC to Angular with workflow progress tracking, step components, and state transitions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Create or flesh out a Jira story with structured sections for POs, Devs, and Claude. Accepts an optional Jira key to update an existing card.
Export Water Quality Management Plans for one jurisdiction from the local Neptune database to an Esri File Geodatabase in native projection. Use when a client requests WQMP spatial data for a specific city/jurisdiction.
Extract rework items from a Jira card's Acceptance Criteria and produce a fix plan. Auto-detects issue key from branch or accepts one as argument.
Fetch a Jira card by key (e.g. NPT-100) and produce a detailed implementation plan. Use this when starting work on a new story.
Build the SQL Server DACPAC and scaffold EF Core entities. Run this after finishing all SQL schema edits in Neptune.Database/.
Add a scrollspy Table of Contents sidebar to a page component with signal-based scroll tracking.
| name | migrate-workflow |
| description | Migrate multi-step wizard workflows from legacy MVC to Angular with workflow progress tracking, step components, and state transitions. |
| allowed-tools | ["Read","Glob","Grep","Edit","Write","Bash(dotnet build:*)","Bash(npm run gen-model:*)","Bash(dotnet test:*)","Bash(npm test:*)"] |
| argument-hint | <EntityName> |
When the user invokes /migrate-workflow <EntityName>:
This skill guides migration of multi-step wizard workflows from legacy MVC to Angular + ASP.NET Core API. Use it for entities with sequential data entry steps, such as projects, assessments, or multi-page forms.
First, thoroughly examine the existing MVC implementation:
Neptune.WebMvc/Controllers/{Entity}Controller.csNeptune.WebMvc/Views/{Entity}/ (look for step-specific views)Step1, Step2, Submit)Document for each step:
| Step Type | Pattern | Examples |
|---|---|---|
| Form entry | Reactive form, save to entity | Project Information, BMP Details |
| Map selection | Map component + geometry binding | Delineation Area, OVTA Boundary |
| File upload | [FromForm] + file processing | Document Attachments |
| Grid/list management | Add/remove items from collection | Add Treatment BMPs |
| Review/submit | Read-only summary + submit action | Review and Finalize |
Create Neptune.EFModels/Workflows/{Entity}WorkflowProgress.cs:
using Neptune.EFModels.Entities;
using Neptune.Models.DataTransferObjects;
using Microsoft.EntityFrameworkCore;
namespace Neptune.EFModels.Workflows;
public static class {Entity}WorkflowProgress
{
public enum {Entity}WorkflowStep
{
Step1Name,
Step2Name,
Step3Name,
// ... all steps
}
public static async Task<{Entity}WorkflowProgressDto?> GetProgressAsync(
NeptuneDbContext dbContext,
int {entity}ID)
{
var ctx = await LoadWorkflowContextAsync(dbContext, {entity}ID);
if (ctx == null) return null;
return new {Entity}WorkflowProgressDto
{
{Entity}ID = ctx.{Entity}ID,
Name = ctx.Name,
Steps = Enum.GetValuesAsUnderlyingType<{Entity}WorkflowStep>()
.Cast<{Entity}WorkflowStep>()
.ToDictionary(
step => step,
step => new WorkflowStepStatus
{
Completed = IsStepComplete(ctx, step),
Disabled = !IsStepActive(ctx, step)
})
};
}
public static async Task<bool> CanSubmitAsync(NeptuneDbContext dbContext, int {entity}ID)
{
var ctx = await LoadWorkflowContextAsync(dbContext, {entity}ID);
if (ctx == null) return false;
return Enum.GetValuesAsUnderlyingType<{Entity}WorkflowStep>()
.Cast<{Entity}WorkflowStep>()
.All(step => IsStepComplete(ctx, step));
}
private static bool IsStepActive({Entity}WorkflowContext ctx, {Entity}WorkflowStep step)
{
return step switch
{
{Entity}WorkflowStep.Step1Name => true,
_ => ctx.{Entity}ID > 0
};
}
private static bool IsStepComplete({Entity}WorkflowContext ctx, {Entity}WorkflowStep step)
{
return step switch
{
{Entity}WorkflowStep.Step1Name => true,
{Entity}WorkflowStep.Step2Name => ctx.SomeCount > 0,
_ => throw new ArgumentOutOfRangeException(nameof(step))
};
}
internal sealed class {Entity}WorkflowContext
{
public int {Entity}ID { get; init; }
public string? Name { get; init; }
// ... counts and flags for step completion checks
}
internal static async Task<{Entity}WorkflowContext?> LoadWorkflowContextAsync(
NeptuneDbContext dbContext,
int {entity}ID)
{
return await dbContext.{Entities}
.AsNoTracking()
.Where(x => x.{Entity}ID == {entity}ID)
.Select(x => new {Entity}WorkflowContext
{
{Entity}ID = x.{Entity}ID,
Name = x.Name,
})
.SingleOrDefaultAsync();
}
}
Add workflow endpoints to Neptune.API/Controllers/{Entity}Controller.cs:
#region Workflow
[HttpGet("{id}/workflow/progress")]
[JurisdictionEditFeature]
public async Task<ActionResult<{Entity}WorkflowProgressDto>> GetWorkflowProgress([FromRoute] int id)
{
var progress = await {Entity}WorkflowProgress.GetProgressAsync(DbContext, id);
if (progress == null) return NotFound();
return Ok(progress);
}
[HttpPut("{id}/workflow/steps/{step-name}")]
[JurisdictionEditFeature]
public async Task<IActionResult> Save{StepName}([FromRoute] int id, [FromBody] StepUpsertDto request)
{
var entity = Entities.GetByIDWithChangeTracking(DbContext, id);
Entities.Update{StepName}(DbContext, entity, request);
return NoContent();
}
[HttpPost("{id}/workflow/submit")]
[JurisdictionEditFeature]
public async Task<IActionResult> SubmitWorkflow([FromRoute] int id)
{
var canSubmit = await {Entity}WorkflowProgress.CanSubmitAsync(DbContext, id);
if (!canSubmit) return BadRequest("Not all steps are complete.");
var entity = Entities.GetByIDWithChangeTracking(DbContext, id);
Entities.SubmitWorkflow(DbContext, entity);
return NoContent();
}
#endregion
Workflows typically have a status lifecycle. Neptune's existing workflows use these patterns:
ProjectStatusEnum: Draft, PendingReview, Submitted, ApprovedOnlandVisualTrashAssessmentStatusEnum: InProgress, Complete, UnfinishedAdd to Neptune.EFModels/Entities/{Entities}.cs:
#region State Transitions
public static void Submit(NeptuneDbContext dbContext, int {entity}ID, int callingPersonID)
{
var entity = GetByIDWithChangeTracking(dbContext, {entity}ID);
entity.{Entity}StatusID = (int){Entity}StatusEnum.Submitted;
entity.UpdateDate = DateTime.UtcNow;
entity.UpdatePersonID = callingPersonID;
dbContext.SaveChanges();
}
public static void Approve(NeptuneDbContext dbContext, int {entity}ID, int callingPersonID)
{
var entity = GetByIDWithChangeTracking(dbContext, {entity}ID);
entity.{Entity}StatusID = (int){Entity}StatusEnum.Approved;
entity.UpdateDate = DateTime.UtcNow;
entity.UpdatePersonID = callingPersonID;
dbContext.SaveChanges();
}
public static void Return(NeptuneDbContext dbContext, int {entity}ID, int callingPersonID)
{
var entity = GetByIDWithChangeTracking(dbContext, {entity}ID);
entity.{Entity}StatusID = (int){Entity}StatusEnum.Draft;
entity.UpdateDate = DateTime.UtcNow;
entity.UpdatePersonID = callingPersonID;
dbContext.SaveChanges();
}
#endregion
Neptune has shared workflow components at src/app/shared/components/:
| Component | Purpose |
|---|---|
workflow-body | Main content area for workflow steps |
workflow-nav | Navigation sidebar for workflow steps |
workflow-nav-item | Individual step in workflow nav (shows completion state) |
workflow-help | Help/info panel for workflow steps |
Create Neptune.Web/src/app/pages/{entity}/workflow/{entity}-workflow-outlet/:
import { Component, Input, OnInit } from "@angular/core";
import { RouterOutlet } from "@angular/router";
import { Observable } from "rxjs";
import { AsyncPipe } from "@angular/common";
import { WorkflowNavComponent } from "src/app/shared/components/workflow-nav/workflow-nav.component";
import { WorkflowNavItemComponent } from "src/app/shared/components/workflow-nav/workflow-nav-item/workflow-nav-item.component";
import { {Entity}Service } from "src/app/shared/generated/api/{entity}.service";
@Component({
selector: "{entity}-workflow-outlet",
standalone: true,
imports: [RouterOutlet, AsyncPipe, WorkflowNavComponent, WorkflowNavItemComponent],
templateUrl: "./{entity}-workflow-outlet.component.html",
styleUrls: ["./{entity}-workflow-outlet.component.scss"],
})
export class {Entity}WorkflowOutletComponent implements OnInit {
@Input() {entity}ID: number | null = null;
public progress$: Observable<{Entity}WorkflowProgressDto>;
constructor(private {entity}Service: {Entity}Service) {}
ngOnInit() {
if (this.{entity}ID) {
this.progress$ = this.{entity}Service.getWorkflowProgress(this.{entity}ID);
}
}
}
<div class="workflow dashboard">
<aside class="sidebar">
@if (progress$ | async; as progressDto) {
<workflow-nav>
<workflow-nav-item
[navRouterLink]="['step-1']"
[complete]="progressDto?.Steps?.Step1Name?.Completed"
[disabled]="progressDto?.Steps?.Step1Name?.Disabled">
Step 1 Title
</workflow-nav-item>
<workflow-nav-item
[navRouterLink]="['step-2']"
[complete]="progressDto?.Steps?.Step2Name?.Completed"
[disabled]="progressDto?.Steps?.Step2Name?.Disabled">
Step 2 Title
</workflow-nav-item>
</workflow-nav>
}
</aside>
<main class="main">
<div class="outlet-container">
<router-outlet></router-outlet>
</div>
</main>
</div>
Create a component for each step in Neptune.Web/src/app/pages/{entity}/workflow/{step-name}/:
import { Component, Input, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { FormGroup, ReactiveFormsModule } from "@angular/forms";
import { FormFieldComponent, FormFieldType } from "src/app/shared/components/forms/form-field/form-field.component";
import { WorkflowBodyComponent } from "src/app/shared/components/workflow-body/workflow-body.component";
import { PageHeaderComponent } from "src/app/shared/components/page-header/page-header.component";
@Component({
selector: "{entity}-{step-name}",
standalone: true,
imports: [ReactiveFormsModule, PageHeaderComponent, WorkflowBodyComponent, FormFieldComponent],
templateUrl: "./{step-name}.component.html",
})
export class {StepName}Component implements OnInit {
@Input() {entity}ID: number | null = null;
public isLoadingSubmit = false;
public FormFieldType = FormFieldType;
constructor(
private router: Router,
private {entity}Service: {Entity}Service
) {}
ngOnInit(): void {
if (this.{entity}ID) {
this.{entity}Service.get{Entity}(this.{entity}ID).subscribe((detail) => {
this.formGroup.patchValue(detail);
});
}
}
public save(andContinue: boolean = false): void {
this.isLoadingSubmit = true;
const request = this.formGroup.value;
this.{entity}Service.update{Entity}(this.{entity}ID, request).subscribe({
next: () => {
this.isLoadingSubmit = false;
if (andContinue) {
this.router.navigate(["/{entities}/edit", this.{entity}ID, "next-step"]);
}
},
error: () => {
this.isLoadingSubmit = false;
}
});
}
}
<page-header pageTitle="{Step Title}"></page-header>
<workflow-body>
<div class="card">
<div class="card-header">
<span class="card-title">{Section Title}</span>
</div>
<div class="card-body">
<form [formGroup]="formGroup">
<div class="grid-12">
<div class="g-col-6">
<form-field
[formGroup]="formGroup"
fieldLabel="Field Label"
formControlName="FieldName"
[type]="FormFieldType.Text">
</form-field>
</div>
</div>
</form>
</div>
</div>
</workflow-body>
<div class="page-footer">
<button class="btn btn-secondary" (click)="save(false)" [disabled]="isLoadingSubmit || formGroup.invalid">
Save
</button>
<button class="btn btn-primary" (click)="save(true)" [disabled]="isLoadingSubmit || formGroup.invalid">
Save & Continue
</button>
</div>
Add routes to Neptune.Web/src/app/app.routes.ts:
// Create flow
{
path: "{entities}/new",
title: "New {Entity}",
loadComponent: () =>
import("./pages/{entity}/workflow/{entity}-workflow-outlet/{entity}-workflow-outlet.component").then(
(m) => m.{Entity}WorkflowOutletComponent
),
children: [
{ path: "", redirectTo: "step-1", pathMatch: "full" },
{
path: "step-1",
loadComponent: () =>
import("./pages/{entity}/workflow/step-1/step-1.component").then(
(m) => m.Step1Component
),
},
],
},
// Edit flow - all steps available
{
path: "{entities}/edit/:{entity}ID",
title: "Edit {Entity}",
loadComponent: () =>
import("./pages/{entity}/workflow/{entity}-workflow-outlet/{entity}-workflow-outlet.component").then(
(m) => m.{Entity}WorkflowOutletComponent
),
children: [
{ path: "", redirectTo: "step-1", pathMatch: "full" },
{
path: "step-1",
loadComponent: () =>
import("./pages/{entity}/workflow/step-1/step-1.component").then(
(m) => m.Step1Component
),
},
{
path: "step-2",
loadComponent: () =>
import("./pages/{entity}/workflow/step-2/step-2.component").then(
(m) => m.Step2Component
),
},
// ... all remaining steps
],
},
{Entity}WorkflowProgress.cs with step enum and completion logicGetWorkflowProgress endpoint/new) with first step only/edit/:id) with all step childrennpm run gen-model)