Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4e92004
Add workflow management with SpiffWorkflow and basic frontend UI
cursoragent Aug 27, 2025
9effddc
Move WorkflowList component from components to view directory
cursoragent Aug 27, 2025
fa7c2c7
Checkpoint before follow-up message
cursoragent Aug 27, 2025
8ded55d
Update SpiffWorkflow to version 3.1.1
cursoragent Aug 27, 2025
4880422
Simplify SpiffWorkflow imports in workflow service
cursoragent Aug 27, 2025
ccfb613
Checkpoint before follow-up message
cursoragent Aug 27, 2025
70b4557
Checkpoint before follow-up message
cursoragent Aug 27, 2025
206cb41
Update SpiffWorkflow parsing to use 3.x method for loading BPMN specs
cursoragent Aug 28, 2025
b72aabf
Fix workflow status check for SpiffWorkflow 3.x compatibility
cursoragent Aug 28, 2025
d0e7a7e
Fix user task name retrieval in workflow instance serialization
cursoragent Aug 28, 2025
2853948
Refactor workflow task completion with explicit approval data model
cursoragent Aug 28, 2025
f9de3ad
Update type hints to use Python 3.10+ syntax in state-related modules
cursoragent Aug 28, 2025
88e0034
Refactor states endpoint and service with improved code formatting
cursoragent Aug 28, 2025
e204056
Fix task ID type handling in workflow task completion
cursoragent Aug 28, 2025
d855f83
Improve task ID validation with more descriptive error message
cursoragent Aug 28, 2025
be72c0e
Update type hints for optional types in workflow service
cursoragent Aug 28, 2025
0281ca1
Fix task ID handling in complete_user_task to support string and int IDs
cursoragent Aug 28, 2025
baa62df
Update workflow task completion method to pass task object directly
cursoragent Aug 28, 2025
9c9325c
Improve task completion logic with direct lookup and simplified compl…
cursoragent Aug 28, 2025
4298b73
Update simple approval workflow with default rejection path and condi…
cursoragent Aug 28, 2025
26884cf
Update workflow data and simplify gateway condition expression
cursoragent Aug 28, 2025
6188955
Improve task readiness check with dynamic state transition attempt
cursoragent Aug 28, 2025
b24b497
Update workflow condition to use data.get() for safer approval check
cursoragent Aug 28, 2025
3b270fb
Checkpoint before follow-up message
cursoragent Aug 28, 2025
81b0523
Fix workflow condition expression to use task.data instead of data
cursoragent Aug 28, 2025
871b9b3
Improve user task detection in workflow service without direct UserTa…
cursoragent Aug 28, 2025
25efd7c
Optimize workflow spec loading with caching and improved error handling
cursoragent Aug 28, 2025
9d4d65f
Improve task retrieval by including waiting tasks in workflow service
cursoragent Aug 28, 2025
16c9ce2
Normalize approved flag to string for BPMN gateway condition
cursoragent Aug 28, 2025
4722e9b
Refactor workflow service to simplify spec loading and handle boolean…
cursoragent Aug 28, 2025
12d2efb
Fix workflow condition expression for approval gateway
cursoragent Aug 28, 2025
0e3bf76
Update BPMN workflow with BPMN2 namespace and improved flow definitions
cursoragent Aug 28, 2025
95075a1
lint fix
ann-aot Aug 28, 2025
29a5401
Update .gitignore, eslint config, and package.json for improved linti…
ann-aot Aug 28, 2025
113b29e
Refactor workflow schemas into separate module
cursoragent Aug 29, 2025
6b37e1d
Remove unused workflow schema imports
cursoragent Aug 29, 2025
befb0a3
Merge pull request #23 from ann-aot/cursor/move-workflow-request-mode…
ann-aot Aug 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/app/api/v1/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from fastapi import APIRouter
from app.api.v1.endpoints import states
from app.api.v1.endpoints import states, workflows

api_router = APIRouter()

# Include all endpoint routers
api_router.include_router(states.router, prefix="/states", tags=["states"])
api_router.include_router(
workflows.router, prefix="/workflows", tags=["workflows"])
71 changes: 38 additions & 33 deletions backend/app/api/v1/endpoints/states.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List
from app.database import get_db
from app.schemas.state import StateCreate, StateUpdate, StateResponse
from app.services.state_service import StateService

router = APIRouter()


@router.get("/", response_model=List[StateResponse], summary="Get all states")
@router.get("/", response_model=list[StateResponse], summary="Get all states")
async def get_states(
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100,
ge=1,
le=1000,
description="Maximum number of records to return"
),
db: Session = Depends(get_db)
limit: int = Query(
100,
ge=1,
le=1000,
description="Maximum number of records to return",
),
db: Session = Depends(get_db),
):
"""
Retrieve all states with pagination support, ordered by sort_order.
Expand All @@ -27,10 +27,11 @@ async def get_states(
return states


@router.get("/active",
response_model=List[StateResponse],
summary="Get active states"
)
@router.get(
"/active",
response_model=list[StateResponse],
summary="Get active states",
)
async def get_active_states(db: Session = Depends(get_db)):
"""
Retrieve all active states, ordered by sort_order.
Expand All @@ -39,13 +40,14 @@ async def get_active_states(db: Session = Depends(get_db)):
return states


@router.get("/{state_id}",
response_model=StateResponse,
summary="Get state by ID"
)
@router.get(
"/{state_id}",
response_model=StateResponse,
summary="Get state by ID",
)
async def get_state(
state_id: int,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
):
"""
Retrieve a specific state by its ID.
Expand All @@ -57,14 +59,15 @@ async def get_state(
return state


@router.post("/",
response_model=StateResponse,
status_code=201,
summary="Create new state"
)
@router.post(
"/",
response_model=StateResponse,
status_code=201,
summary="Create new state",
)
async def create_state(
state: StateCreate,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
):
"""
Create a new state.
Expand All @@ -77,14 +80,15 @@ async def create_state(
return StateService.create_state(db, state)


@router.put("/{state_id}",
response_model=StateResponse,
summary="Update state"
)
@router.put(
"/{state_id}",
response_model=StateResponse,
summary="Update state",
)
async def update_state(
state_id: int,
state_update: StateUpdate,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
):
"""
Update an existing state.
Expand All @@ -98,7 +102,7 @@ async def update_state(
@router.delete("/{state_id}", summary="Delete state")
async def delete_state(
state_id: int,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
):
"""
Delete a state.
Expand All @@ -108,10 +112,11 @@ async def delete_state(
return {"message": f"State {state_id} deleted successfully"}


@router.post("/initialize",
response_model=List[StateResponse],
summary="Initialize default states"
)
@router.post(
"/initialize",
response_model=list[StateResponse],
summary="Initialize default states",
)
async def initialize_default_states(db: Session = Depends(get_db)):
"""
Initialize default states (New, In Progress, Done) if they don't exist.
Expand Down
52 changes: 52 additions & 0 deletions backend/app/api/v1/endpoints/workflows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from fastapi import APIRouter, HTTPException

from app.services.workflow_service import WorkflowService
from app.schemas.workflow import StartWorkflowRequest, CompleteTaskRequest


router = APIRouter()





@router.post("/start", summary="Start a new workflow instance")
async def start_workflow(payload: StartWorkflowRequest):
try:
return WorkflowService.start_workflow(
name=payload.name,
bpmn_filename=payload.bpmn_filename,
process_id=payload.process_id,
)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="BPMN file not found")
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))


@router.get("/", summary="List workflow instances")
async def list_workflows():
return WorkflowService.list_workflows()


@router.post(
"/{instance_id}/tasks/{task_id}/complete",
summary="Complete a user task",
)
async def complete_task(
instance_id: str,
task_id: str,
payload: CompleteTaskRequest,
):
try:
return WorkflowService.complete_user_task(
instance_id,
task_id,
payload.data.model_dump(),
)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))
2 changes: 1 addition & 1 deletion backend/app/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# Schemas Package
21 changes: 10 additions & 11 deletions backend/app/schemas/state.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pydantic import BaseModel, Field, ConfigDict, field_serializer
from typing import Optional
from datetime import datetime


Expand All @@ -13,10 +12,10 @@ class StateBase(CustomBaseModel):
max_length=50,
description="State name"
)
description: Optional[str] = Field(None,
max_length=200,
description="State description"
)
description: str | None = Field(None,
max_length=200,
description="State description"
)
is_active: bool = Field(True, description="Whether the state is active")
sort_order: int = Field(0, ge=0, description="Sort order for display")

Expand All @@ -26,17 +25,17 @@ class StateCreate(StateBase):


class StateUpdate(CustomBaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=50)
description: Optional[str] = Field(None, max_length=200)
is_active: Optional[bool] = None
sort_order: Optional[int] = Field(None, ge=0)
name: str | None = Field(None, min_length=1, max_length=50)
description: str | None = Field(None, max_length=200)
is_active: bool | None = None
sort_order: int | None = Field(None, ge=0)


class StateResponse(StateBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
updated_at: datetime | None = None

@field_serializer("created_at", "updated_at", when_used="json")
def serialize_dt(self, dt: Optional[datetime]) -> Optional[str]:
def serialize_dt(self, dt: datetime | None) -> str | None:
return dt.isoformat() if dt else None
15 changes: 15 additions & 0 deletions backend/app/schemas/workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pydantic import BaseModel, Field


class StartWorkflowRequest(BaseModel):
name: str = Field(default="Simple Approval")
bpmn_filename: str = Field(default="simple_approval.bpmn")
process_id: str = Field(default="SimpleApproval")


class CompleteTaskRequest(BaseModel):
class ApprovalData(BaseModel):
approved: bool

data: ApprovalData

27 changes: 14 additions & 13 deletions backend/app/services/state_service.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from typing import List, Optional
from app.models.state import State
from app.schemas.state import StateCreate, StateUpdate
from fastapi import HTTPException


class StateService:
@staticmethod
def get_states(db: Session,
skip: int = 0,
limit: int = 100
) -> List[State]:
def get_states(
db: Session,
skip: int = 0,
limit: int = 100,
) -> list[State]:
"""Get all states with pagination, ordered by sort_order"""
return (
db.query(State)
Expand All @@ -22,17 +22,17 @@ def get_states(db: Session,
)

@staticmethod
def get_state_by_id(db: Session, state_id: int) -> Optional[State]:
def get_state_by_id(db: Session, state_id: int) -> State | None:
"""Get state by ID"""
return db.query(State).filter(State.id == state_id).first()

@staticmethod
def get_state_by_name(db: Session, name: str) -> Optional[State]:
def get_state_by_name(db: Session, name: str) -> State | None:
"""Get state by name"""
return db.query(State).filter(State.name == name).first()

@staticmethod
def get_active_states(db: Session) -> List[State]:
def get_active_states(db: Session) -> list[State]:
"""Get all active states, ordered by sort_order"""
return (
db.query(State)
Expand Down Expand Up @@ -65,10 +65,11 @@ def create_state(db: Session, state: StateCreate) -> State:
)

@staticmethod
def update_state(db: Session,
state_id: int,
state_update: StateUpdate
) -> State:
def update_state(
db: Session,
state_id: int,
state_update: StateUpdate,
) -> State:
"""Update an existing state"""
db_state = StateService.get_state_by_id(db, state_id)
if not db_state:
Expand Down Expand Up @@ -117,7 +118,7 @@ def delete_state(db: Session, state_id: int) -> bool:
)

@staticmethod
def initialize_default_states(db: Session) -> List[State]:
def initialize_default_states(db: Session) -> list[State]:
"""Initialize default states if they don't exist"""
default_states = [
{"name": "New",
Expand Down
Loading
Loading