ابنِ مسار موافقة على المصروفات من حالة واحدة إلى أربع، مع شرط يفرض قاعدة إنفاق.
يبني هذا الدرس موافقة على المصروفات: تُكتب المطالبة مسودةً، ثم تُقدَّم، ثم يُوافَق عليها أو تُرفض. وفي الطريق يضيف الحالات والشروط والشاشات لكل خطوة. يعمل المداريّ المكتمل في أسفل الصفحة.
ابدأ بالبيانات. للمطالبة مبلغ وملاحظة، وتعيش في الذاكرة ما دامت الصفحة مفتوحة.
1entity Expense [runtime] {
2 id : string!
3 amount : number = 120
4 note : string = ""
5}تحتاج كل سمة إلى حالة واحدة على الأقل وشاشة لها. يُطلق INIT حين تُفتح الصفحة:
1app expense-step-2 "1.0.0"
2"Step 2: one state and its screen."
3orbital ExpenseOrbital {
4 entity Expense [runtime] {
5 id : string!
6 amount : number = 120
7 }
8
9 trait Approval -> Expense [interaction, instance] {
10 initial: draft
11
12 state draft {
13 INIT -> draft
14 (set @entity.amount 120)
15 (render-ui main { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 })
16 }
17 }
18
19 page "/expenses" -> Approval
20}تحقق منه بـ orb validate. ينجح: حالة واحدة، وشاشة، وصفحة تعرض شيئًا. لاحظ (set @entity.amount 120): كل حقل تقرؤه السمة يجب أن يعيّنه انتقال ما. لا شيء يُفترض ضمنيًا.
تُقدَّم المطالبة ثم يُوافَق عليها أو تُرفض. كل خطوة حالة، وكل زر يرسل حدثًا تقبله حالة ما. إن أضفت زرًا لا تتعامل أي حالة مع حدثه، يرفضه orb validate. جرّب ذلك.
1state draft {
2 SUBMIT -> submitted
3 (render-ui main { type: stack, direction: horizontal, gap: sm, children: [
4 { type: button, label: "Approve", action: APPROVE, variant: primary },
5 { type: button, label: "Reject", action: REJECT, variant: secondary }
6 ] })
7}
8
9state submitted {
10 APPROVE -> approved
11 (set @entity.note "Approved")
12 REJECT -> rejected
13 (set @entity.note "Rejected")
14}لاحظ أن approved وrejected تحتاجان أيضًا إلى مخرج: الحالة التي لا انتقال خارجًا منها طريق مسدود، والمدقق يقول ذلك. تضيف النسخة المكتملة NEW_CLAIM وREVISE.
قاعدة الشركة: المطالبات فوق 500 يورو لا يُوافَق عليها هنا. اكتبها على الانتقال:
APPROVE -> approved when (<= @entity.amount 500)
(set @entity.note "Approved")حين يكون الشرط خاطئًا يُرفض APPROVE وتبقى المطالبة مقدَّمة. تعيش القاعدة في مكان واحد بالضبط، والمختبِر يجرّب جانبيها.
Approval
on Expense · states: 4 · transitions: 7البرنامج أعلاه كآلات حالات، واحدة لكل سمة: كل مربع حالة، وكل سهم حدث ينقلها، والأيقونات على السهم هي التأثيرات التي يشغّلها ذلك الحدث. الرمز ◇ على السهم شرط حارس؛ مرّر المؤشر فوقه لتقرأ الشرط. كيف تقرأ AVL
1app std-orb-docex-expense "1.0.0"
2"std-orb-docex-expense — an approval flow: four states, a guard on the amount, and a rule the shape of the program enforces."
3orbital ExpenseOrbital {
4 entity Expense [runtime] {
5 id : string!
6 amount : number = 120
7 note : string = ""
8 }
9
10 trait Approval -> Expense [interaction, instance] {
11 initial: draft
12
13 state draft {
14 INIT -> draft
15 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
16 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
17 { type: stack, direction: horizontal, gap: sm, children: [
18 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
19 { type: button, label: "Submit", action: SUBMIT, variant: primary }
20 ] }
21 ] })
22 RAISE -> draft
23 (emit MOVED { event: "RAISE" })
24 (set @entity.amount (+ @entity.amount 50))
25 SUBMIT -> submitted when (> @entity.amount 0)
26 (emit MOVED { event: "SUBMIT" })
27 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
28 { type: typography, content: (str/concat "Submitted: " @entity.amount " EUR"), variant: h4 },
29 { type: typography, content: "Claims over 500 EUR cannot be approved here.", variant: small, color: muted },
30 { type: stack, direction: horizontal, gap: sm, children: [
31 { type: button, label: "Approve", action: APPROVE, variant: primary },
32 { type: button, label: "Reject", action: REJECT, variant: secondary }
33 ] }
34 ] })
35 }
36
37 state submitted {
38 APPROVE -> approved when (<= @entity.amount 500)
39 (emit MOVED { event: "APPROVE" })
40 (set @entity.note "Approved")
41 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
42 { type: badge, label: "Approved", variant: success },
43 { type: button, label: "New claim", action: NEW_CLAIM, variant: ghost }
44 ] })
45 REJECT -> rejected
46 (emit MOVED { event: "REJECT" })
47 (set @entity.note "Rejected")
48 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
49 { type: badge, label: "Rejected", variant: error },
50 { type: button, label: "Revise", action: REVISE, variant: primary }
51 ] })
52 }
53
54 state approved {
55 NEW_CLAIM -> draft
56 (emit MOVED { event: "NEW_CLAIM" })
57 (set @entity.amount 120)
58 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
59 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
60 { type: stack, direction: horizontal, gap: sm, children: [
61 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
62 { type: button, label: "Submit", action: SUBMIT, variant: primary }
63 ] }
64 ] })
65 }
66
67 state rejected {
68 REVISE -> draft
69 (emit MOVED { event: "REVISE" })
70 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
71 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
72 { type: stack, direction: horizontal, gap: sm, children: [
73 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
74 { type: button, label: "Submit", action: SUBMIT, variant: primary }
75 ] }
76 ] })
77 }
78
79 emits {
80 MOVED -> external {
81 event : string!
82 }
83 @description "Each move names the event it took, so the diagram beside the demo can follow it."
84 }
85 }
86
87 page "/expenses" -> Approval
88}ما الذي يحدث عندما يقع الحدث APPROVE:
(<= @entity.amount 500)(set @entity.note "Approved")شغّل orb verify عليه: يُجرَّب كل انتقال، ويُختبر الشرط بمبلغ فوق الحد وتحته، وتُفحص كل شاشة.
ابنِ مدير مهام يضيف بيانات مخزّنة وقوائم.