@ram_28/kf-ai-sdk 2.0.15 → 2.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -14
- package/dist/api.cjs +1 -1
- package/dist/api.mjs +1 -1
- package/dist/auth/authConfig.d.ts +1 -1
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth.cjs +1 -1
- package/dist/auth.mjs +1 -1
- package/dist/bdo/core/BaseBdo.d.ts +1 -1
- package/dist/bdo.cjs +1 -1
- package/dist/bdo.mjs +3 -3
- package/dist/components/hooks/useActivityForm/createActivityItemProxy.d.ts +1 -1
- package/dist/components/hooks/useActivityForm/createActivityItemProxy.d.ts.map +1 -1
- package/dist/components/hooks/useActivityForm/types.d.ts +6 -7
- package/dist/components/hooks/useActivityForm/types.d.ts.map +1 -1
- package/dist/components/hooks/useActivityForm/useActivityForm.d.ts.map +1 -1
- package/dist/components/hooks/useActivityTable/types.d.ts +7 -6
- package/dist/components/hooks/useActivityTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useActivityTable/useActivityTable.d.ts +1 -1
- package/dist/components/hooks/useActivityTable/useActivityTable.d.ts.map +1 -1
- package/dist/components/hooks/useBDOForm/createItemProxy.d.ts.map +1 -0
- package/dist/components/hooks/useBDOForm/createResolver.d.ts.map +1 -0
- package/dist/components/hooks/useBDOForm/index.d.ts +6 -0
- package/dist/components/hooks/useBDOForm/index.d.ts.map +1 -0
- package/dist/components/hooks/useBDOForm/shared.d.ts +50 -0
- package/dist/components/hooks/useBDOForm/shared.d.ts.map +1 -0
- package/dist/components/hooks/{useForm → useBDOForm}/types.d.ts +6 -6
- package/dist/components/hooks/useBDOForm/types.d.ts.map +1 -0
- package/dist/components/hooks/{useForm/useForm.d.ts → useBDOForm/useBDOForm.d.ts} +4 -4
- package/dist/components/hooks/useBDOForm/useBDOForm.d.ts.map +1 -0
- package/dist/components/hooks/useBDOTable/types.d.ts +20 -14
- package/dist/components/hooks/useBDOTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useBDOTable/useBDOTable.d.ts +2 -2
- package/dist/components/hooks/useBDOTable/useBDOTable.d.ts.map +1 -1
- package/dist/{constants-ConHc1oS.js → constants-Cyi942Yr.js} +5 -5
- package/dist/constants-DEmYwKfC.cjs +1 -0
- package/dist/filter.cjs +1 -1
- package/dist/filter.mjs +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.d.ts +1 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.mjs +250 -253
- package/dist/form.types.d.ts +1 -1
- package/dist/form.types.d.ts.map +1 -1
- package/dist/shared-5a7UkED1.js +1180 -0
- package/dist/shared-nnmlRVs7.cjs +1 -0
- package/dist/table.cjs +1 -1
- package/dist/table.mjs +14 -14
- package/dist/table.types.d.ts +1 -1
- package/dist/table.types.d.ts.map +1 -1
- package/dist/types/constants.d.ts +4 -4
- package/dist/workflow/Activity.d.ts +22 -7
- package/dist/workflow/Activity.d.ts.map +1 -1
- package/dist/workflow/client.d.ts +2 -2
- package/dist/workflow/client.d.ts.map +1 -1
- package/dist/workflow/types.d.ts +7 -3
- package/dist/workflow/types.d.ts.map +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.mjs +518 -576
- package/docs/README.md +51 -0
- package/docs/bdo/README.md +161 -0
- package/docs/bdo/api_reference.md +281 -0
- package/docs/examples/bdo/create-product.md +69 -0
- package/docs/examples/bdo/edit-product-dialog.md +95 -0
- package/docs/examples/bdo/filtered-product-table.md +100 -0
- package/docs/examples/bdo/product-listing.md +73 -0
- package/docs/examples/bdo/supplier-dropdown.md +60 -0
- package/docs/examples/workflow/approve-leave-request.md +76 -0
- package/docs/examples/workflow/filtered-activity-table.md +101 -0
- package/docs/examples/workflow/my-pending-requests.md +90 -0
- package/docs/examples/workflow/start-new-workflow.md +47 -0
- package/docs/examples/workflow/submit-leave-request.md +72 -0
- package/docs/examples/workflow/workflow-progress.md +49 -0
- package/docs/useActivityForm/README.md +241 -0
- package/docs/useActivityForm/api_reference.md +279 -0
- package/docs/useActivityTable/README.md +263 -0
- package/docs/useActivityTable/api_reference.md +294 -0
- package/docs/useBDOForm/README.md +172 -0
- package/docs/useBDOForm/api_reference.md +244 -0
- package/docs/useBDOTable/README.md +242 -0
- package/docs/useBDOTable/api_reference.md +253 -0
- package/docs/useFilter/README.md +323 -0
- package/docs/useFilter/api_reference.md +228 -0
- package/docs/workflow/README.md +158 -0
- package/docs/workflow/api_reference.md +161 -0
- package/package.json +2 -2
- package/sdk/auth/authConfig.ts +1 -1
- package/sdk/auth/types.ts +1 -1
- package/sdk/bdo/core/BaseBdo.ts +2 -2
- package/sdk/components/hooks/useActivityForm/createActivityItemProxy.ts +1 -1
- package/sdk/components/hooks/useActivityForm/createActivityResolver.ts +1 -1
- package/sdk/components/hooks/useActivityForm/types.ts +8 -10
- package/sdk/components/hooks/useActivityForm/useActivityForm.ts +52 -265
- package/sdk/components/hooks/useActivityTable/types.ts +6 -5
- package/sdk/components/hooks/useActivityTable/useActivityTable.ts +14 -43
- package/sdk/components/hooks/{useForm → useBDOForm}/index.ts +4 -3
- package/sdk/components/hooks/useBDOForm/shared.ts +250 -0
- package/sdk/components/hooks/{useForm → useBDOForm}/types.ts +9 -9
- package/sdk/components/hooks/{useForm/useForm.ts → useBDOForm/useBDOForm.ts} +70 -96
- package/sdk/components/hooks/useBDOTable/types.ts +20 -12
- package/sdk/components/hooks/useBDOTable/useBDOTable.ts +12 -7
- package/sdk/form.ts +2 -2
- package/sdk/form.types.ts +4 -4
- package/sdk/table.types.ts +2 -0
- package/sdk/types/constants.ts +4 -4
- package/sdk/workflow/Activity.ts +68 -13
- package/sdk/workflow/client.ts +65 -25
- package/sdk/workflow/types.ts +10 -2
- package/dist/components/hooks/useForm/createItemProxy.d.ts.map +0 -1
- package/dist/components/hooks/useForm/createResolver.d.ts.map +0 -1
- package/dist/components/hooks/useForm/index.d.ts +0 -5
- package/dist/components/hooks/useForm/index.d.ts.map +0 -1
- package/dist/components/hooks/useForm/types.d.ts.map +0 -1
- package/dist/components/hooks/useForm/useForm.d.ts.map +0 -1
- package/dist/constants-QX2RX-wu.cjs +0 -1
- package/dist/createResolver-AIgUwoS6.cjs +0 -1
- package/dist/createResolver-ZHXQ7QMa.js +0 -1078
- package/docs/api.md +0 -95
- package/docs/bdo.md +0 -224
- package/docs/gaps.md +0 -410
- package/docs/useActivityTable.md +0 -481
- package/docs/useBDOTable.md +0 -317
- package/docs/useFilter.md +0 -188
- package/docs/useForm.md +0 -376
- package/docs/workflow.md +0 -818
- /package/dist/components/hooks/{useForm → useBDOForm}/createItemProxy.d.ts +0 -0
- /package/dist/components/hooks/{useForm → useBDOForm}/createResolver.d.ts +0 -0
- /package/docs/{useAuth.md → useAuth/README.md} +0 -0
- /package/sdk/components/hooks/{useForm → useBDOForm}/createItemProxy.ts +0 -0
- /package/sdk/components/hooks/{useForm → useBDOForm}/createResolver.ts +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Filtered Product Table
|
|
2
|
+
|
|
3
|
+
> Filter products by category and price range using the integrated `table.filter` API.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useState, useMemo } from "react";
|
|
7
|
+
import { useBDOTable } from "@ram_28/kf-ai-sdk/table";
|
|
8
|
+
import type { UseBDOTableReturnType } from "@ram_28/kf-ai-sdk/table/types";
|
|
9
|
+
import { ConditionOperator, RHSType } from "@ram_28/kf-ai-sdk/filter";
|
|
10
|
+
import { BuyerProduct } from "@/bdo/buyer/Product";
|
|
11
|
+
|
|
12
|
+
export default function FilteredProductTable() {
|
|
13
|
+
const product = useMemo(() => new BuyerProduct(), []);
|
|
14
|
+
const table: UseBDOTableReturnType<BuyerProduct> = useBDOTable({ bdo: product });
|
|
15
|
+
|
|
16
|
+
// ── Category filter (single EQ condition) ─────────────────────
|
|
17
|
+
const [categoryFilter, setCategoryFilter] = useState("");
|
|
18
|
+
const [categoryConditionId, setCategoryConditionId] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
const handleCategoryFilter = (value: string) => {
|
|
21
|
+
if (categoryConditionId) {
|
|
22
|
+
table.filter.removeCondition(categoryConditionId);
|
|
23
|
+
setCategoryConditionId(null);
|
|
24
|
+
}
|
|
25
|
+
setCategoryFilter(value);
|
|
26
|
+
if (value && value !== "all") {
|
|
27
|
+
const id = table.filter.addCondition({
|
|
28
|
+
LHSField: "Category",
|
|
29
|
+
Operator: ConditionOperator.EQ,
|
|
30
|
+
RHSType: RHSType.Constant,
|
|
31
|
+
RHSValue: value,
|
|
32
|
+
});
|
|
33
|
+
setCategoryConditionId(id);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ── Price range filter (GTE + LTE conditions) ─────────────────
|
|
38
|
+
const [minPrice, setMinPrice] = useState("");
|
|
39
|
+
const [maxPrice, setMaxPrice] = useState("");
|
|
40
|
+
const [minPriceConditionId, setMinPriceConditionId] = useState<string | null>(null);
|
|
41
|
+
const [maxPriceConditionId, setMaxPriceConditionId] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const applyPriceFilter = () => {
|
|
44
|
+
if (minPriceConditionId) { table.filter.removeCondition(minPriceConditionId); setMinPriceConditionId(null); }
|
|
45
|
+
if (maxPriceConditionId) { table.filter.removeCondition(maxPriceConditionId); setMaxPriceConditionId(null); }
|
|
46
|
+
|
|
47
|
+
if (minPrice !== "") {
|
|
48
|
+
const id = table.filter.addCondition({
|
|
49
|
+
LHSField: "Price", Operator: ConditionOperator.GTE, RHSType: RHSType.Constant, RHSValue: Number(minPrice),
|
|
50
|
+
});
|
|
51
|
+
setMinPriceConditionId(id);
|
|
52
|
+
}
|
|
53
|
+
if (maxPrice !== "") {
|
|
54
|
+
const id = table.filter.addCondition({
|
|
55
|
+
LHSField: "Price", Operator: ConditionOperator.LTE, RHSType: RHSType.Constant, RHSValue: Number(maxPrice),
|
|
56
|
+
});
|
|
57
|
+
setMaxPriceConditionId(id);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (table.isLoading) return <p>Loading...</p>;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div>
|
|
65
|
+
{/* Category dropdown */}
|
|
66
|
+
<select value={categoryFilter} onChange={(e) => handleCategoryFilter(e.target.value)}>
|
|
67
|
+
<option value="all">All categories</option>
|
|
68
|
+
{product.Category.options.map((opt) => (
|
|
69
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
70
|
+
))}
|
|
71
|
+
</select>
|
|
72
|
+
|
|
73
|
+
{/* Price range */}
|
|
74
|
+
<input type="number" placeholder="Min" value={minPrice} onChange={(e) => setMinPrice(e.target.value)} />
|
|
75
|
+
<input type="number" placeholder="Max" value={maxPrice} onChange={(e) => setMaxPrice(e.target.value)} />
|
|
76
|
+
<button onClick={applyPriceFilter}>Apply</button>
|
|
77
|
+
|
|
78
|
+
{/* Table */}
|
|
79
|
+
<table>
|
|
80
|
+
<tbody>
|
|
81
|
+
{table.rows.map((row) => (
|
|
82
|
+
<tr key={row._id}>
|
|
83
|
+
<td>{row.Title.get()}</td>
|
|
84
|
+
<td>${row.Price.get()?.toFixed(2)}</td>
|
|
85
|
+
<td>{row.Category.get()}</td>
|
|
86
|
+
</tr>
|
|
87
|
+
))}
|
|
88
|
+
</tbody>
|
|
89
|
+
</table>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Key Patterns
|
|
96
|
+
|
|
97
|
+
- **`addCondition()` returns an ID** -- store it in state to remove the condition later with `removeCondition(id)`
|
|
98
|
+
- **Category filter** -- single `ConditionOperator.EQ` condition; remove-then-add on every change
|
|
99
|
+
- **Price range** -- two separate conditions (`GTE` and `LTE`), each tracked by its own ID
|
|
100
|
+
- **Filter changes auto-reset pagination** -- when conditions change, the table resets to page 1
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Product Listing
|
|
2
|
+
|
|
3
|
+
> Browse products with search, sortable columns, and pagination using `useBDOTable`.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { useBDOTable } from "@ram_28/kf-ai-sdk/table";
|
|
8
|
+
import type { UseBDOTableReturnType } from "@ram_28/kf-ai-sdk/table/types";
|
|
9
|
+
import { BuyerProduct } from "@/bdo/buyer/Product";
|
|
10
|
+
|
|
11
|
+
export default function ProductListingPage() {
|
|
12
|
+
const product = useMemo(() => new BuyerProduct(), []);
|
|
13
|
+
const table: UseBDOTableReturnType<BuyerProduct> = useBDOTable({
|
|
14
|
+
bdo: product,
|
|
15
|
+
initialState: { sort: [{ _created_at: "DESC" }], pagination: { pageNo: 1, pageSize: 10 } },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (table.isLoading) return <p>Loading...</p>;
|
|
19
|
+
if (table.error) return <p>Error: {table.error.message}</p>;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
{/* Search */}
|
|
24
|
+
<input
|
|
25
|
+
placeholder="Search by title..."
|
|
26
|
+
value={table.search.query}
|
|
27
|
+
onChange={(e) => table.search.set("Title", e.target.value)}
|
|
28
|
+
/>
|
|
29
|
+
{table.search.query && <button onClick={table.search.clear}>Clear</button>}
|
|
30
|
+
|
|
31
|
+
{/* Table with sortable headers */}
|
|
32
|
+
<table>
|
|
33
|
+
<thead>
|
|
34
|
+
<tr>
|
|
35
|
+
<th onClick={() => table.sort.toggle("Title")} style={{ cursor: "pointer" }}>
|
|
36
|
+
{product.Title.label}
|
|
37
|
+
{table.sort.field === "Title" && (table.sort.direction === "ASC" ? " ↑" : " ↓")}
|
|
38
|
+
</th>
|
|
39
|
+
<th onClick={() => table.sort.toggle("Price")} style={{ cursor: "pointer" }}>
|
|
40
|
+
{product.Price.label}
|
|
41
|
+
{table.sort.field === "Price" && (table.sort.direction === "ASC" ? " ↑" : " ↓")}
|
|
42
|
+
</th>
|
|
43
|
+
<th>{product.Category.label}</th>
|
|
44
|
+
</tr>
|
|
45
|
+
</thead>
|
|
46
|
+
<tbody>
|
|
47
|
+
{table.rows.map((row) => (
|
|
48
|
+
<tr key={row._id}>
|
|
49
|
+
<td>{row.Title.get()}</td>
|
|
50
|
+
<td>${row.Price.get()?.toFixed(2)}</td>
|
|
51
|
+
<td>{row.Category.get()}</td>
|
|
52
|
+
</tr>
|
|
53
|
+
))}
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
|
|
57
|
+
{/* Pagination */}
|
|
58
|
+
<div>
|
|
59
|
+
<button onClick={table.pagination.goToPrevious} disabled={!table.pagination.canGoPrevious}>Previous</button>
|
|
60
|
+
<span>Page {table.pagination.pageNo} of {table.pagination.totalPages}</span>
|
|
61
|
+
<button onClick={table.pagination.goToNext} disabled={!table.pagination.canGoNext}>Next</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Key Patterns
|
|
69
|
+
|
|
70
|
+
- **`row.Field.get()`** -- rows are `ItemType` proxies; always use `.get()` to read values
|
|
71
|
+
- **`search.set(field, query)`** -- updates the input instantly, debounces the API call (300ms)
|
|
72
|
+
- **`sort.toggle(field)`** -- cycles ASC → DESC → cleared on each click
|
|
73
|
+
- **Pagination** -- `canGoNext`/`canGoPrevious` disable buttons at boundaries; `pageNo`, `totalPages` for display
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Supplier Dropdown
|
|
2
|
+
|
|
3
|
+
> Lazy-load ReferenceField options when the dropdown opens using `useQuery`.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useState, useMemo } from "react";
|
|
7
|
+
import { useQuery } from "@tanstack/react-query";
|
|
8
|
+
import { useBDOForm } from "@ram_28/kf-ai-sdk/form";
|
|
9
|
+
import type { UseBDOFormReturnType } from "@ram_28/kf-ai-sdk/form/types";
|
|
10
|
+
import { BuyerProduct } from "@/bdo/buyer/Product";
|
|
11
|
+
import type { ProductSupplierRefType } from "@/bdo/buyer/Product";
|
|
12
|
+
|
|
13
|
+
export default function SupplierDropdown({ recordId }: { recordId?: string }) {
|
|
14
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
15
|
+
const product = useMemo(() => new BuyerProduct(), []);
|
|
16
|
+
|
|
17
|
+
const { watch, setValue, item }: UseBDOFormReturnType<BuyerProduct> = useBDOForm({ bdo: product, recordId });
|
|
18
|
+
|
|
19
|
+
// Lazy-load options only when dropdown opens and item has an _id
|
|
20
|
+
const { data: suppliers = [], isFetching } = useQuery<ProductSupplierRefType[]>({
|
|
21
|
+
queryKey: ["supplier-options", item._id],
|
|
22
|
+
queryFn: () => product.SupplierInfo.fetchOptions(item._id!),
|
|
23
|
+
enabled: dropdownOpen && !!item._id,
|
|
24
|
+
staleTime: Infinity,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const currentSupplier = watch(product.SupplierInfo.id);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<label>{product.SupplierInfo.label}</label>
|
|
32
|
+
<select
|
|
33
|
+
value={currentSupplier?._id ?? ""}
|
|
34
|
+
onFocus={() => setDropdownOpen(true)}
|
|
35
|
+
onChange={(e) => {
|
|
36
|
+
const supplier = suppliers.find((s) => s._id === e.target.value);
|
|
37
|
+
if (supplier) setValue(product.SupplierInfo.id, supplier);
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<option value="">{currentSupplier?.SupplierName ?? "Select supplier"}</option>
|
|
41
|
+
{isFetching ? (
|
|
42
|
+
<option disabled>Loading...</option>
|
|
43
|
+
) : (
|
|
44
|
+
suppliers.map((s) => (
|
|
45
|
+
<option key={s._id} value={s._id}>{s.SupplierName}</option>
|
|
46
|
+
))
|
|
47
|
+
)}
|
|
48
|
+
</select>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Key Patterns
|
|
55
|
+
|
|
56
|
+
- **`enabled: dropdownOpen && !!item._id`** -- options aren't fetched until the dropdown opens and the draft/record has an ID
|
|
57
|
+
- **`fetchOptions(instanceId)`** -- loads reference options for the current record context
|
|
58
|
+
- **`watch()` + `setValue()`** -- reads/writes the entire reference object (`{ _id, SupplierName }`)
|
|
59
|
+
- **`referenceFields`** -- `product.SupplierInfo.referenceFields` lists the fields fetched from the referenced BDO
|
|
60
|
+
- **`staleTime: Infinity`** -- options are cached and not refetched on re-focus
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Approve Leave Request
|
|
2
|
+
|
|
3
|
+
> Manager reviews readonly employee data and approves/rejects using `useActivityForm`.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { useActivityForm } from "@ram_28/kf-ai-sdk/workflow";
|
|
8
|
+
import type { UseActivityFormReturn } from "@ram_28/kf-ai-sdk/workflow";
|
|
9
|
+
import { ManagerApprovalActivity } from "@/workflow/leave";
|
|
10
|
+
|
|
11
|
+
export default function ApprovalForm({ instanceId, onClose }: { instanceId: string; onClose: () => void }) {
|
|
12
|
+
const activity = useMemo(() => new ManagerApprovalActivity(), []);
|
|
13
|
+
|
|
14
|
+
const { register, handleSubmit, errors, isLoading, isSubmitting, watch, setValue }: UseActivityFormReturn<ManagerApprovalActivity> =
|
|
15
|
+
useActivityForm(activity, { activity_instance_id: instanceId, mode: "onBlur" });
|
|
16
|
+
|
|
17
|
+
if (isLoading) return <p>Loading...</p>;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
{/* Readonly context fields — auto-disabled by register() */}
|
|
22
|
+
<div>
|
|
23
|
+
<label>{activity.EmployeeName.label}</label>
|
|
24
|
+
<input {...register(activity.EmployeeName.id)} className="bg-gray-100" />
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<label>{activity.StartDate.label}</label>
|
|
28
|
+
<input type="date" {...register(activity.StartDate.id)} className="bg-gray-100" />
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<label>{activity.EndDate.label}</label>
|
|
32
|
+
<input type="date" {...register(activity.EndDate.id)} className="bg-gray-100" />
|
|
33
|
+
</div>
|
|
34
|
+
<div>
|
|
35
|
+
<label>{activity.LeaveDays.label}</label>
|
|
36
|
+
<input type="number" {...register(activity.LeaveDays.id)} className="bg-gray-100" />
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<hr />
|
|
40
|
+
|
|
41
|
+
{/* Editable manager decision fields */}
|
|
42
|
+
<div>
|
|
43
|
+
<input
|
|
44
|
+
type="checkbox"
|
|
45
|
+
checked={watch(activity.ManagerApproved.id) ?? false}
|
|
46
|
+
onChange={(e) => setValue(activity.ManagerApproved.id, e.target.checked)}
|
|
47
|
+
/>
|
|
48
|
+
<label>
|
|
49
|
+
{activity.ManagerApproved.label}
|
|
50
|
+
{activity.ManagerApproved.required && <span> *</span>}
|
|
51
|
+
</label>
|
|
52
|
+
{errors.ManagerApproved && <p>{errors.ManagerApproved.message}</p>}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div>
|
|
56
|
+
<label>{activity.ManagerReason.label}</label>
|
|
57
|
+
<textarea {...register(activity.ManagerReason.id)} rows={3} placeholder="Reason for approval or rejection..." />
|
|
58
|
+
{errors.ManagerReason && <p>{errors.ManagerReason.message}</p>}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<button onClick={onClose}>Cancel</button>
|
|
62
|
+
<button disabled={isSubmitting} onClick={handleSubmit(() => onClose(), console.error)}>
|
|
63
|
+
{watch(activity.ManagerApproved.id) ? "Approve" : "Reject"} & Submit
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Key Patterns
|
|
71
|
+
|
|
72
|
+
- **Readonly context fields** -- fields from the prior employee activity have `ReadOnly: true` and are auto-disabled by `register()`
|
|
73
|
+
- **Editable decision fields** -- `ManagerApproved` and `ManagerReason` are the only editable fields
|
|
74
|
+
- **Checkbox with `watch()` + `setValue()`** -- booleans use `e.target.checked`, not `e.target.value`
|
|
75
|
+
- **Dynamic submit label** -- `watch(activity.ManagerApproved.id)` drives the button text
|
|
76
|
+
- **`handleSubmit` completes the approval** -- validates, syncs remaining fields, then completes the activity
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Filtered Activity Table
|
|
2
|
+
|
|
3
|
+
> Filter activity instances by date range using `useActivityTable` with the integrated `table.filter` API.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useState, useMemo } from "react";
|
|
7
|
+
import { useActivityTable, ActivityTableStatus } from "@ram_28/kf-ai-sdk/workflow";
|
|
8
|
+
import type { UseActivityTableReturnType } from "@ram_28/kf-ai-sdk/workflow";
|
|
9
|
+
import { ConditionOperator, RHSType } from "@ram_28/kf-ai-sdk/filter";
|
|
10
|
+
import { EmployeeInputActivity } from "@/workflow/leave";
|
|
11
|
+
|
|
12
|
+
export default function FilteredActivityTable() {
|
|
13
|
+
const activity = useMemo(() => new EmployeeInputActivity(), []);
|
|
14
|
+
const table: UseActivityTableReturnType<EmployeeInputActivity> = useActivityTable({ activity, status: ActivityTableStatus.InProgress });
|
|
15
|
+
|
|
16
|
+
// ── Date range filter on StartDate ─────────────────────────────
|
|
17
|
+
const [startDateFrom, setStartDateFrom] = useState("");
|
|
18
|
+
const [startDateTo, setStartDateTo] = useState("");
|
|
19
|
+
const [fromConditionId, setFromConditionId] = useState<string | null>(null);
|
|
20
|
+
const [toConditionId, setToConditionId] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
const applyDateFilter = () => {
|
|
23
|
+
if (fromConditionId) { table.filter.removeCondition(fromConditionId); setFromConditionId(null); }
|
|
24
|
+
if (toConditionId) { table.filter.removeCondition(toConditionId); setToConditionId(null); }
|
|
25
|
+
|
|
26
|
+
if (startDateFrom) {
|
|
27
|
+
const id = table.filter.addCondition({
|
|
28
|
+
LHSField: activity.StartDate.id,
|
|
29
|
+
Operator: ConditionOperator.GTE,
|
|
30
|
+
RHSType: RHSType.Constant,
|
|
31
|
+
RHSValue: startDateFrom,
|
|
32
|
+
});
|
|
33
|
+
setFromConditionId(id);
|
|
34
|
+
}
|
|
35
|
+
if (startDateTo) {
|
|
36
|
+
const id = table.filter.addCondition({
|
|
37
|
+
LHSField: activity.StartDate.id,
|
|
38
|
+
Operator: ConditionOperator.LTE,
|
|
39
|
+
RHSType: RHSType.Constant,
|
|
40
|
+
RHSValue: startDateTo,
|
|
41
|
+
});
|
|
42
|
+
setToConditionId(id);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const clearDateFilter = () => {
|
|
47
|
+
if (fromConditionId) { table.filter.removeCondition(fromConditionId); setFromConditionId(null); }
|
|
48
|
+
if (toConditionId) { table.filter.removeCondition(toConditionId); setToConditionId(null); }
|
|
49
|
+
setStartDateFrom("");
|
|
50
|
+
setStartDateTo("");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (table.isLoading) return <p>Loading...</p>;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
{/* Date range filter */}
|
|
58
|
+
<div>
|
|
59
|
+
<label>From</label>
|
|
60
|
+
<input type="date" value={startDateFrom} onChange={(e) => setStartDateFrom(e.target.value)} />
|
|
61
|
+
<label>To</label>
|
|
62
|
+
<input type="date" value={startDateTo} onChange={(e) => setStartDateTo(e.target.value)} />
|
|
63
|
+
<button onClick={applyDateFilter}>Apply</button>
|
|
64
|
+
{table.filter.hasConditions && <button onClick={clearDateFilter}>Clear</button>}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Table */}
|
|
68
|
+
<table>
|
|
69
|
+
<thead>
|
|
70
|
+
<tr>
|
|
71
|
+
<th>{activity.StartDate.label}</th>
|
|
72
|
+
<th>{activity.EndDate.label}</th>
|
|
73
|
+
<th>{activity.LeaveType.label}</th>
|
|
74
|
+
<th>{activity.LeaveDays.label}</th>
|
|
75
|
+
</tr>
|
|
76
|
+
</thead>
|
|
77
|
+
<tbody>
|
|
78
|
+
{table.rows.map((row) => (
|
|
79
|
+
<tr key={row._id}>
|
|
80
|
+
<td>{row.StartDate.get()}</td>
|
|
81
|
+
<td>{row.EndDate.get()}</td>
|
|
82
|
+
<td>{row.LeaveType.get()}</td>
|
|
83
|
+
<td>{row.LeaveDays.get()}</td>
|
|
84
|
+
</tr>
|
|
85
|
+
))}
|
|
86
|
+
</tbody>
|
|
87
|
+
</table>
|
|
88
|
+
|
|
89
|
+
<span>Page {table.pagination.pageNo} of {table.pagination.totalPages}</span>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Key Patterns
|
|
96
|
+
|
|
97
|
+
- **Date range with GTE + LTE** -- two separate conditions tracked by their own IDs
|
|
98
|
+
- **`addCondition()` returns an ID** -- store it to later remove the condition with `removeCondition(id)`
|
|
99
|
+
- **`table.filter.hasConditions`** -- show a "Clear" button only when filters are active
|
|
100
|
+
- **Filter changes auto-reset pagination** -- the table resets to page 1 when conditions change
|
|
101
|
+
- **Same filter API as `useBDOTable`** -- `table.filter` works identically in both BDO and Activity tables
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# My Pending Requests
|
|
2
|
+
|
|
3
|
+
> Employee views leave requests in In Progress / Completed tabs using `useActivityTable`.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useState, useMemo } from "react";
|
|
7
|
+
import { useActivityTable, ActivityTableStatus } from "@ram_28/kf-ai-sdk/workflow";
|
|
8
|
+
import type { UseActivityTableReturnType } from "@ram_28/kf-ai-sdk/workflow";
|
|
9
|
+
import { EmployeeInputActivity } from "@/workflow/leave";
|
|
10
|
+
|
|
11
|
+
export default function MyPendingRequests({ onOpenForm }: { onOpenForm: (instanceId: string, bpInstanceId: string) => void }) {
|
|
12
|
+
const [status, setStatus] = useState(ActivityTableStatus.InProgress);
|
|
13
|
+
const activity = useMemo(() => new EmployeeInputActivity(), []);
|
|
14
|
+
|
|
15
|
+
const table: UseActivityTableReturnType<EmployeeInputActivity> = useActivityTable({
|
|
16
|
+
activity,
|
|
17
|
+
status,
|
|
18
|
+
initialState: { sort: [{ StartDate: "DESC" }], pagination: { pageNo: 1, pageSize: 10 } },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (table.isLoading) return <p>Loading...</p>;
|
|
22
|
+
if (table.error) return <p>Error: {table.error.message}</p>;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
{/* Tab switcher */}
|
|
27
|
+
<div>
|
|
28
|
+
<button onClick={() => setStatus(ActivityTableStatus.InProgress)}
|
|
29
|
+
style={{ fontWeight: status === ActivityTableStatus.InProgress ? "bold" : "normal" }}>
|
|
30
|
+
My Requests
|
|
31
|
+
</button>
|
|
32
|
+
<button onClick={() => setStatus(ActivityTableStatus.Completed)}
|
|
33
|
+
style={{ fontWeight: status === ActivityTableStatus.Completed ? "bold" : "normal" }}>
|
|
34
|
+
Completed
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<table>
|
|
39
|
+
<thead>
|
|
40
|
+
<tr>
|
|
41
|
+
<th onClick={() => table.sort.toggle(activity.StartDate.id)} style={{ cursor: "pointer" }}>
|
|
42
|
+
{activity.StartDate.label}
|
|
43
|
+
</th>
|
|
44
|
+
<th>{activity.EndDate.label}</th>
|
|
45
|
+
<th>{activity.LeaveType.label}</th>
|
|
46
|
+
<th>{activity.LeaveDays.label}</th>
|
|
47
|
+
<th>Status</th>
|
|
48
|
+
{status === ActivityTableStatus.Completed && <th>Completed At</th>}
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
{table.rows.map((row) => (
|
|
53
|
+
<tr
|
|
54
|
+
key={row._id}
|
|
55
|
+
style={{ cursor: status === ActivityTableStatus.InProgress ? "pointer" : "default" }}
|
|
56
|
+
onClick={() => {
|
|
57
|
+
if (status === ActivityTableStatus.InProgress) {
|
|
58
|
+
onOpenForm(row._id, row.BPInstanceId.get());
|
|
59
|
+
}
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<td>{row.StartDate.get()}</td>
|
|
63
|
+
<td>{row.EndDate.get()}</td>
|
|
64
|
+
<td>{row.LeaveType.get()}</td>
|
|
65
|
+
<td>{row.LeaveDays.get()}</td>
|
|
66
|
+
<td>{row.Status.get()}</td>
|
|
67
|
+
{status === ActivityTableStatus.Completed && <td>{row.CompletedAt.get()}</td>}
|
|
68
|
+
</tr>
|
|
69
|
+
))}
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
|
|
73
|
+
{/* Pagination */}
|
|
74
|
+
<div>
|
|
75
|
+
<button onClick={table.pagination.goToPrevious} disabled={!table.pagination.canGoPrevious}>Previous</button>
|
|
76
|
+
<span>Page {table.pagination.pageNo} of {table.pagination.totalPages}</span>
|
|
77
|
+
<button onClick={table.pagination.goToNext} disabled={!table.pagination.canGoNext}>Next</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Key Patterns
|
|
85
|
+
|
|
86
|
+
- **`ActivityTableStatus.InProgress` / `Completed`** -- changing `status` state triggers an automatic refetch
|
|
87
|
+
- **Activity system fields** -- `row.Status.get()`, `row.CompletedAt.get()`, `row.BPInstanceId.get()` are available on every row
|
|
88
|
+
- **Conditional `CompletedAt` column** -- only rendered in the Completed tab
|
|
89
|
+
- **Clickable rows only in InProgress** -- completed rows are read-only and don't open a form dialog
|
|
90
|
+
- **`row.BPInstanceId.get()`** -- passed along when opening a form, needed for workflow progress tracking
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Start New Workflow
|
|
2
|
+
|
|
3
|
+
> "New Request" button calls `workflow.start()` and opens the form with the returned instance ID.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useState, useMemo, useCallback } from "react";
|
|
7
|
+
import { Workflow } from "@ram_28/kf-ai-sdk/workflow";
|
|
8
|
+
import type { WorkflowStartResponseType } from "@ram_28/kf-ai-sdk/workflow";
|
|
9
|
+
import type { EmployeeInputEntityType } from "@/workflow/leave";
|
|
10
|
+
|
|
11
|
+
export default function StartNewWorkflow({ onOpenForm }: { onOpenForm: (instanceId: string, bpInstanceId: string) => void }) {
|
|
12
|
+
const [isStarting, setIsStarting] = useState(false);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const workflow = useMemo(() => new Workflow<EmployeeInputEntityType>("SimpleLeaveProcess"), []);
|
|
16
|
+
|
|
17
|
+
const handleNewRequest = useCallback(async () => {
|
|
18
|
+
try {
|
|
19
|
+
setIsStarting(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
const result: WorkflowStartResponseType = await workflow.start();
|
|
22
|
+
// result: { BPInstanceId, ActivityId, _id }
|
|
23
|
+
onOpenForm(result._id, result.BPInstanceId);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
setError(err instanceof Error ? err.message : "Failed to start workflow");
|
|
26
|
+
} finally {
|
|
27
|
+
setIsStarting(false);
|
|
28
|
+
}
|
|
29
|
+
}, [workflow, onOpenForm]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<button onClick={handleNewRequest} disabled={isStarting}>
|
|
34
|
+
{isStarting ? "Starting..." : "New Request"}
|
|
35
|
+
</button>
|
|
36
|
+
{error && <p>{error}</p>}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Key Patterns
|
|
43
|
+
|
|
44
|
+
- **`workflow.start()`** -- creates a new process instance and returns `{ BPInstanceId, ActivityId, _id }`
|
|
45
|
+
- **`BPInstanceId`** -- identifies the workflow instance; used for `workflow.progress()`
|
|
46
|
+
- **`_id`** -- the first activity instance ID; passed to `useActivityForm` as `activity_instance_id`
|
|
47
|
+
- **Workflow constructor** -- `new Workflow<TEntity>("BusinessProcessId")` takes the process ID string
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Submit Leave Request
|
|
2
|
+
|
|
3
|
+
> Employee fills in dates, leave type, and reason, then submits using `useActivityForm`.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { useActivityForm } from "@ram_28/kf-ai-sdk/workflow";
|
|
8
|
+
import type { UseActivityFormReturn } from "@ram_28/kf-ai-sdk/workflow";
|
|
9
|
+
import { EmployeeInputActivity } from "@/workflow/leave";
|
|
10
|
+
|
|
11
|
+
export default function LeaveRequestForm({ instanceId, onClose }: { instanceId: string; onClose: () => void }) {
|
|
12
|
+
const activity = useMemo(() => new EmployeeInputActivity(), []);
|
|
13
|
+
|
|
14
|
+
const { register, handleSubmit, errors, isLoading, isSubmitting, watch, setValue }: UseActivityFormReturn<EmployeeInputActivity> =
|
|
15
|
+
useActivityForm(activity, { activity_instance_id: instanceId, mode: "onBlur" });
|
|
16
|
+
|
|
17
|
+
if (isLoading) return <p>Loading...</p>;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<form>
|
|
21
|
+
<div>
|
|
22
|
+
<label>{activity.StartDate.label} {activity.StartDate.required && <span>*</span>}</label>
|
|
23
|
+
<input type="date" {...register(activity.StartDate.id)} />
|
|
24
|
+
{errors.StartDate && <p>{errors.StartDate.message}</p>}
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div>
|
|
28
|
+
<label>{activity.EndDate.label} {activity.EndDate.required && <span>*</span>}</label>
|
|
29
|
+
<input type="date" {...register(activity.EndDate.id)} />
|
|
30
|
+
{errors.EndDate && <p>{errors.EndDate.message}</p>}
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Select field — watch/setValue, not register */}
|
|
34
|
+
<div>
|
|
35
|
+
<label>{activity.LeaveType.label} {activity.LeaveType.required && <span>*</span>}</label>
|
|
36
|
+
<select value={watch(activity.LeaveType.id) ?? ""} onChange={(e) => setValue(activity.LeaveType.id, e.target.value)}>
|
|
37
|
+
<option value="">Select leave type</option>
|
|
38
|
+
{activity.LeaveType.options.map((opt) => (
|
|
39
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
40
|
+
))}
|
|
41
|
+
</select>
|
|
42
|
+
{errors.LeaveType && <p>{errors.LeaveType.message}</p>}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div>
|
|
46
|
+
<label>{activity.Reason.label}</label>
|
|
47
|
+
<textarea {...register(activity.Reason.id)} rows={3} placeholder="Reason for leave..." />
|
|
48
|
+
{errors.Reason && <p>{errors.Reason.message}</p>}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Readonly computed field — auto-disabled by register() */}
|
|
52
|
+
<div>
|
|
53
|
+
<label>{activity.LeaveDays.label} (computed)</label>
|
|
54
|
+
<input type="number" {...register(activity.LeaveDays.id)} />
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<button type="button" onClick={onClose}>Cancel</button>
|
|
58
|
+
<button type="button" disabled={isSubmitting} onClick={handleSubmit(() => onClose(), console.error)}>
|
|
59
|
+
Submit Request
|
|
60
|
+
</button>
|
|
61
|
+
</form>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Key Patterns
|
|
67
|
+
|
|
68
|
+
- **`useActivityForm(activity, { activity_instance_id })`** -- the instance ID comes from `workflow.start()` or a table row
|
|
69
|
+
- **`handleSubmit` completes the activity** -- validates, sends dirty fields, then completes to advance the workflow
|
|
70
|
+
- **Readonly fields auto-disabled** -- `LeaveDays` has `ReadOnly: true`, so `register()` returns `{ disabled: true }`
|
|
71
|
+
- **`watch()` + `setValue()` for selects** -- custom components that don't fire native change events
|
|
72
|
+
- **Per-field sync** -- changes are auto-saved on blur/change; no manual save button needed
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Workflow Progress
|
|
2
|
+
|
|
3
|
+
> Display progress badges for each activity in a workflow instance using `workflow.progress()`.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { useQuery } from "@tanstack/react-query";
|
|
8
|
+
import { Workflow } from "@ram_28/kf-ai-sdk/workflow";
|
|
9
|
+
import type { ActivityProgressType } from "@ram_28/kf-ai-sdk/workflow";
|
|
10
|
+
import type { EmployeeInputEntityType } from "@/workflow/leave";
|
|
11
|
+
|
|
12
|
+
export default function WorkflowProgressBadges({ bpInstanceId }: { bpInstanceId: string }) {
|
|
13
|
+
const workflow = useMemo(() => new Workflow<EmployeeInputEntityType>("SimpleLeaveProcess"), []);
|
|
14
|
+
|
|
15
|
+
const { data: progress } = useQuery<ActivityProgressType[]>({
|
|
16
|
+
queryKey: ["workflow-progress", bpInstanceId],
|
|
17
|
+
queryFn: () => workflow.progress(bpInstanceId),
|
|
18
|
+
staleTime: 0,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!progress) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ display: "flex", gap: "8px" }}>
|
|
25
|
+
{progress.map((entry: ActivityProgressType) => (
|
|
26
|
+
<span
|
|
27
|
+
key={entry.ActivityId}
|
|
28
|
+
style={{
|
|
29
|
+
padding: "4px 12px",
|
|
30
|
+
borderRadius: "12px",
|
|
31
|
+
fontSize: "12px",
|
|
32
|
+
backgroundColor: entry.Status === "COMPLETED" ? "#dcfce7" : "#fef9c3",
|
|
33
|
+
color: entry.Status === "COMPLETED" ? "#166534" : "#854d0e",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{entry._name} — {entry.Status === "COMPLETED" ? "Done" : "In Progress"}
|
|
37
|
+
</span>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Key Patterns
|
|
45
|
+
|
|
46
|
+
- **`workflow.progress(bpInstanceId)`** -- returns `ActivityProgressType[]`, one entry per activity in the process
|
|
47
|
+
- **`ActivityProgressType`** -- each entry has `ActivityId`, `_name`, `Status` (`"COMPLETED"` or `"IN_PROGRESS"`), `CompletedAt`, `CompletedBy`
|
|
48
|
+
- **`BPInstanceId`** -- comes from `workflow.start()` (new requests) or `row.BPInstanceId.get()` (existing table rows)
|
|
49
|
+
- **`staleTime: 0`** -- always refetch progress when the component mounts (progress changes as activities complete)
|