Skip to content

Commit 6141f8f

Browse files
chore: add workflow models and operations for form processing
1 parent 28b8276 commit 6141f8f

2 files changed

Lines changed: 194 additions & 40 deletions

File tree

src/forms/operations.tsp

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ using Http;
22

33
@tag("Forms")
44
namespace CoreSystem.Forms {
5+
@summary("List Forms")
6+
@doc("List all existing forms. Archived forms are excluded by default.")
7+
@route("/forms")
8+
@get
9+
op listForms(): Form[];
10+
11+
@summary("Get Form")
12+
@doc("Get a specific form by its unique identifier.")
13+
@route("/forms/{id}")
14+
@get
15+
op getFormById(@path id: uuid): Form;
16+
517
@summary("Update Form")
618
@doc("Update an existing form by its unique identifier.")
719
@route("/forms/{id}")
@@ -16,46 +28,6 @@ namespace CoreSystem.Forms {
1628
@statusCode statusCode: 204;
1729
};
1830

19-
@summary("Get Form")
20-
@doc("Get a specific form by its unique identifier.")
21-
@route("/forms/{id}")
22-
@get
23-
op getFormById(@path id: uuid): Form;
24-
25-
@summary("List Forms")
26-
@doc("List all existing forms. Archived forms are excluded by default.")
27-
@route("/forms")
28-
@get
29-
op listForms(): Form[];
30-
31-
@summary("List Sections")
32-
@doc("List all the questions of the sections in the form.")
33-
@route("/forms/{id}/sections")
34-
@get
35-
op listSections(@path id: uuid): ListSectionsResponse[];
36-
37-
@summary("Upload Form Cover Image")
38-
@doc("Upload a cover image for a form. The frontend should send images in WebP format with max-width/height: 1800px and quality 80%.")
39-
@route("/forms/{id}/cover")
40-
@post
41-
op uploadFormCoverImage(@path id: uuid, @header("content-type") contentType: "multipart/form-data", @multipartBody body: FormCoverUploadRequest): FormCoverUploadResponse;
42-
43-
@summary("Get Form Cover Image")
44-
@doc("Get the cover image binary data. Returns the image in WebP format.")
45-
@route("/forms/{id}/cover")
46-
@get
47-
op getFormCoverImage(@path id: uuid): {
48-
@header("content-type") contentType: "image/webp";
49-
@body body: bytes;
50-
};
51-
52-
// for form publish
53-
@summary("Preview Recipients")
54-
@doc("Preview recipients by orgIds/unitIds (no duplicated).")
55-
@route("/forms/recipients/preview")
56-
@post
57-
op previewRecipients(@body req: RecipientSelectionRequest): RecipientSelectionResponse;
58-
5931
@summary("Publish Form")
6032
@doc("Publish a form. Set status to 'published'.")
6133
@route("/forms/{id}/publish")
@@ -74,6 +46,30 @@ namespace CoreSystem.Forms {
7446
@body response: Form;
7547
};
7648

49+
// === Cover Image Operations ===
50+
@summary("Get Form Cover Image")
51+
@doc("Get the cover image binary data. Returns the image in WebP format.")
52+
@route("/forms/{id}/cover")
53+
@get
54+
op getFormCoverImage(@path id: uuid): {
55+
@header("content-type") contentType: "image/webp";
56+
@body body: bytes;
57+
};
58+
59+
@summary("Upload Form Cover Image")
60+
@doc("Upload a cover image for a form. The frontend should send images in WebP format with max-width/height: 1800px and quality 80%.")
61+
@route("/forms/{id}/cover")
62+
@post
63+
op uploadFormCoverImage(@path id: uuid, @header("content-type") contentType: "multipart/form-data", @multipartBody body: FormCoverUploadRequest): FormCoverUploadResponse;
64+
65+
// === Section Operations ===
66+
@summary("List Sections")
67+
@doc("List all the questions of the sections in the form.")
68+
@route("/forms/{id}/sections")
69+
@get
70+
op listSections(@path id: uuid): ListSectionsResponse[];
71+
72+
// === Google Sheet Operations ===
7773
@summary("Get Google Sheet Email")
7874
@doc("Get the Gmail address that should be added to Google Sheet permissions. This address needs to be granted access to the Google Sheet so the system can modify the form.")
7975
@route("/forms/google-sheet-email")
@@ -91,4 +87,12 @@ namespace CoreSystem.Forms {
9187
@route("/forms/fonts")
9288
@get
9389
op getFormFonts(): Font[];
90+
91+
// Deprecated
92+
// @summary("Preview Recipients")
93+
// @doc("Preview recipients by orgIds/unitIds (no duplicated).")
94+
// @route("/forms/recipients/preview")
95+
// @post
96+
// op previewRecipients(@body req: RecipientSelectionRequest): RecipientSelectionResponse;
97+
9498
}

src/forms/workflow/models.tsp

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
namespace CoreSystem.FormWorkflow {
2+
@doc("Node type enum for workflow nodes")
3+
enum NodeType {
4+
@doc("Section node - represents a form section/page")
5+
section: "SECTION",
6+
7+
@doc("End node - represents the end of the workflow (thank you page and redirect)")
8+
end: "END",
9+
10+
@doc("Start node - represents the workflow entry point")
11+
start: "START",
12+
13+
@doc("Condition node - represents a condition check node")
14+
condition: "CONDITION",
15+
}
16+
17+
enum CreateNodeRequestType {
18+
@doc("Create a section node - represents a form section")
19+
section: "SECTION",
20+
21+
@doc("Create a condition node - represents a condition check node")
22+
condition: "CONDITION",
23+
}
24+
25+
@doc("Data source for condition rules")
26+
enum ConditionSource {
27+
@doc("Source from choice answer")
28+
choice: "CHOICE",
29+
30+
@doc("Source from non-choice answer")
31+
nonChoice: "NON_CHOICE",
32+
}
33+
34+
@doc("Condition rule for condition nodes")
35+
model ConditionRule {
36+
@doc("Source of data for the condition")
37+
@example(ConditionSource.nonChoice)
38+
source: ConditionSource;
39+
40+
@doc("The question ID to check against")
41+
@example("9a843aa0-8451-4e3b-b6a0-8c0e994e9040")
42+
question: uuid;
43+
44+
@doc("Regex pattern to match against. If source is NON_CHOICE, the regex is matched against the answer value. If source is CHOICE, the answer is an array of selected choice option IDs, and this regex is matched against each element; the condition passes if any element matches (e.g. use an exact-match UUID regex like ^<uuid>$).")
45+
@format("regex")
46+
@example("^([6-9]|[1-9][0-9]+)$") // non-choice source
47+
@example("^f421cfc2-2b84-4da9-9629-011af609935a$") // choice source (any selected option ID equals this UUID)
48+
pattern: string;
49+
}
50+
51+
@doc("Workflow node structure with linked list connections")
52+
model NodeStructure {
53+
@doc("Unique identifier for the node.")
54+
id: uuid;
55+
56+
@doc("Type of the node")
57+
@example(NodeType.start)
58+
type: NodeType;
59+
60+
@doc("The label for the node")
61+
@example("開始表單")
62+
label: string;
63+
}
64+
65+
@doc("Request body for creating a new node for a workflow")
66+
@example(#{ type: CreateNodeRequestType.section })
67+
model CreateNodeRequest {
68+
@doc("Type of the node")
69+
@example(CreateNodeRequestType.section)
70+
type: CreateNodeRequestType;
71+
}
72+
73+
@doc("Workflow node request model with linked list connections")
74+
model NodeRequest {
75+
@doc("Unique identifier for the node.")
76+
id: uuid;
77+
78+
@doc("The label for the node")
79+
@example("開始表單")
80+
label: string;
81+
82+
@doc("The condition rule for the node, only for condition nodes")
83+
conditionRule?: ConditionRule;
84+
85+
@doc("Next node id for sequential flow (for start and section nodes)")
86+
next?: uuid;
87+
88+
@doc("Next node id when condition is true (for condition nodes)")
89+
nextTrue?: uuid;
90+
91+
@doc("Next node id when condition is false (for condition nodes)")
92+
nextFalse?: uuid;
93+
}
94+
95+
@doc("Workflow node response model with linked list connections")
96+
model NodeResponse {
97+
...NodeStructure;
98+
99+
@doc("The condition rule for the node, only for condition nodes")
100+
conditionRule: ConditionRule;
101+
102+
@doc("Next node id for sequential flow (for start and section nodes)")
103+
next: uuid;
104+
105+
@doc("Next node id when condition is true (for condition nodes)")
106+
nextTrue: uuid;
107+
108+
@doc("Next node id when condition is false (for condition nodes)")
109+
nextFalse: uuid;
110+
}
111+
112+
@doc("Validation info type enum for workflow validation errors")
113+
enum ValidationInfoType {
114+
@doc("Workflow-level validation failed")
115+
workflowValidationFailed: "workflow_validation_failed",
116+
117+
@doc("Graph structure validation failed")
118+
graphValidationFailed: "graph_validation_failed",
119+
120+
@doc("Individual node validation failed")
121+
nodeValidationFailed: "node_validation_failed",
122+
123+
@doc("Unknown validation error")
124+
unknownError: "unknown",
125+
}
126+
127+
@doc("Validation information for workflow validation errors")
128+
model ValidationInfo {
129+
@doc("The type/category of validation error")
130+
@example(ValidationInfoType.graphValidationFailed)
131+
type: ValidationInfoType;
132+
133+
@doc("The ID of the node that caused the validation error")
134+
@example("b23f2668-c2dc-4213-88fe-eceedff46398")
135+
nodeId: uuid;
136+
137+
@doc("Human-readable description of the validation error")
138+
@example("section node 'b23f2668-c2dc-4213-88fe-eceedff46398' must have a 'next' field")
139+
message: string;
140+
}
141+
142+
@doc("Response model for getting a workflow, includes workflow nodes and validation info")
143+
model GetWorkflowResponse {
144+
@doc("Array of workflow nodes")
145+
workflow: NodeResponse[];
146+
147+
@doc("Array of validation information/errors for the workflow")
148+
info: ValidationInfo[];
149+
}
150+
}

0 commit comments

Comments
 (0)