@nanobpm/urban 0.75.1 → 0.76.0
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.
|
@@ -47,7 +47,7 @@ function api(path,opts){return fetch(BASE+path,opts).then(r=>{if(!r.ok)return r.
|
|
|
47
47
|
function loadTasks(){
|
|
48
48
|
formEl.replaceChildren();
|
|
49
49
|
tasksEl.replaceChildren(el('li',{},'loading…'));
|
|
50
|
-
return api('/api/tasks').then(ts=>{
|
|
50
|
+
return api('/api/tasks'+location.search).then(ts=>{
|
|
51
51
|
tasksEl.replaceChildren();
|
|
52
52
|
if(!ts.length){tasksEl.appendChild(el('li',{},'No open tasks.'));return;}
|
|
53
53
|
for(const t of ts){
|
|
@@ -202,13 +202,18 @@ export function mountSurfaces(ctx, app) {
|
|
|
202
202
|
source: "surface:taskInbox",
|
|
203
203
|
handler: async (req) => {
|
|
204
204
|
const pik = req.query.get("processInstanceKey") ?? undefined;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
// "
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
const assignee = req.query.get("assignee") ?? undefined;
|
|
206
|
+
const candidateGroup = req.query.get("candidateGroup") ?? undefined;
|
|
207
|
+
// Constrain the inbox to open (answerable) tasks via openUserTasks — the accessor that
|
|
208
|
+
// pins state="CREATED" for us. A bare searchUserTasks returns tasks in every state
|
|
209
|
+
// (CREATED/COMPLETED/CANCELED/...), so already-answered or withdrawn tasks would surface
|
|
210
|
+
// as if actionable — completing one then fails with "User task ... is not active".
|
|
211
|
+
// Deriving the pin from openUserTasks (rather than re-pinning state here) keeps the
|
|
212
|
+
// definition of "open" single-sourced. See nanobpm/nano-ide#248.
|
|
213
|
+
const tasks = await app.engine.openUserTasks({
|
|
211
214
|
...(pik ? { processInstanceKey: pik } : {}),
|
|
215
|
+
...(assignee ? { assignee } : {}),
|
|
216
|
+
...(candidateGroup ? { candidateGroup } : {}),
|
|
212
217
|
});
|
|
213
218
|
return json(tasks);
|
|
214
219
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -185,7 +185,7 @@ test("/api/tasks surfaces the resolved form linkage", async () => {
|
|
|
185
185
|
let seenFilter: unknown;
|
|
186
186
|
const engine: EngineClient = {
|
|
187
187
|
...fakeEngine,
|
|
188
|
-
|
|
188
|
+
openUserTasks: async (filter) => {
|
|
189
189
|
seenFilter = filter;
|
|
190
190
|
return [
|
|
191
191
|
{ userTaskKey: "1", elementId: "approve", formKey: "form-123" },
|
|
@@ -195,26 +195,97 @@ test("/api/tasks surfaces the resolved form linkage", async () => {
|
|
|
195
195
|
};
|
|
196
196
|
const { tasks } = inboxRoutes(engine);
|
|
197
197
|
const res = await call(tasks, { query: "processInstanceKey=pi-9" });
|
|
198
|
-
assert.deepEqual(seenFilter, {
|
|
198
|
+
assert.deepEqual(seenFilter, { processInstanceKey: "pi-9" });
|
|
199
199
|
const parsed = JSON.parse(String(res.body));
|
|
200
200
|
assert.equal(parsed[0].formKey, "form-123");
|
|
201
201
|
assert.equal(parsed[1].formKey, undefined);
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
-
test("/api/tasks constrains the search to open
|
|
204
|
+
test("/api/tasks constrains the search to open tasks via openUserTasks", async () => {
|
|
205
205
|
let seenFilter: unknown;
|
|
206
|
+
let openCalled = false;
|
|
206
207
|
const engine: EngineClient = {
|
|
207
208
|
...fakeEngine,
|
|
208
|
-
searchUserTasks
|
|
209
|
+
// A regression to a bare searchUserTasks (which does not pin state="CREATED") must fail
|
|
210
|
+
// loudly rather than silently surface answered/canceled tasks (nanobpm/nano-ide#248).
|
|
211
|
+
searchUserTasks: async () => {
|
|
212
|
+
throw new Error("inbox must use openUserTasks, not a bare searchUserTasks");
|
|
213
|
+
},
|
|
214
|
+
openUserTasks: async (filter) => {
|
|
215
|
+
openCalled = true;
|
|
209
216
|
seenFilter = filter;
|
|
210
217
|
return [];
|
|
211
218
|
},
|
|
212
219
|
};
|
|
213
220
|
const { tasks } = inboxRoutes(engine);
|
|
214
221
|
await call(tasks);
|
|
215
|
-
// No processInstanceKey supplied
|
|
216
|
-
//
|
|
217
|
-
assert.
|
|
222
|
+
// No processInstanceKey supplied; the inbox routes through openUserTasks, which pins the
|
|
223
|
+
// open (CREATED) invariant for us, so the handler forwards only selectors (none here).
|
|
224
|
+
assert.equal(openCalled, true);
|
|
225
|
+
assert.deepEqual(seenFilter, {});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("/api/tasks forwards assignee and candidateGroup to the engine (nanobpm/nano-ide#438)", async () => {
|
|
229
|
+
let seenFilter: unknown;
|
|
230
|
+
const engine: EngineClient = {
|
|
231
|
+
...fakeEngine,
|
|
232
|
+
openUserTasks: async (filter) => {
|
|
233
|
+
seenFilter = filter;
|
|
234
|
+
return [];
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const { tasks } = inboxRoutes(engine);
|
|
238
|
+
await call(tasks, { query: "assignee=demo-reviewer&candidateGroup=reviewers&processInstanceKey=pi-9" });
|
|
239
|
+
// A manifest ui.path like /tasks?assignee=… must scope the inbox to that reviewer/group on a
|
|
240
|
+
// shared engine — the handler forwards both alongside the processInstanceKey selector (the
|
|
241
|
+
// open/CREATED pin is openUserTasks' responsibility, not repeated here).
|
|
242
|
+
assert.deepEqual(seenFilter, {
|
|
243
|
+
processInstanceKey: "pi-9",
|
|
244
|
+
assignee: "demo-reviewer",
|
|
245
|
+
candidateGroup: "reviewers",
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("/api/tasks omits assignee/candidateGroup when absent (unscoped inbox unchanged)", async () => {
|
|
250
|
+
let seenFilter: unknown;
|
|
251
|
+
const engine: EngineClient = {
|
|
252
|
+
...fakeEngine,
|
|
253
|
+
openUserTasks: async (filter) => {
|
|
254
|
+
seenFilter = filter;
|
|
255
|
+
return [];
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
const { tasks } = inboxRoutes(engine);
|
|
259
|
+
await call(tasks, { query: "assignee=demo-reviewer" });
|
|
260
|
+
// Only the supplied param is forwarded; candidateGroup stays off so its key never appears.
|
|
261
|
+
assert.deepEqual(seenFilter, { assignee: "demo-reviewer" });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("/api/tasks omits assignee when only candidateGroup is present (symmetric scoping)", async () => {
|
|
265
|
+
let seenFilter: unknown;
|
|
266
|
+
const engine: EngineClient = {
|
|
267
|
+
...fakeEngine,
|
|
268
|
+
openUserTasks: async (filter) => {
|
|
269
|
+
seenFilter = filter;
|
|
270
|
+
return [];
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const { tasks } = inboxRoutes(engine);
|
|
274
|
+
await call(tasks, { query: "candidateGroup=reviewers" });
|
|
275
|
+
// Symmetric to the assignee-only case: only the supplied param is forwarded, so assignee's
|
|
276
|
+
// key never appears when scoping the inbox to a candidate group alone.
|
|
277
|
+
assert.deepEqual(seenFilter, { candidateGroup: "reviewers" });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("inbox client forwards the surface query string to /api/tasks (nanobpm/nano-ide#438)", async () => {
|
|
281
|
+
const { page } = inboxRoutes(fakeEngine);
|
|
282
|
+
const rendered = String((await call(page)).body);
|
|
283
|
+
// A scoped surface path (e.g. /tasks?assignee=…) reaches the iframe as location.search; the
|
|
284
|
+
// client must append it so the handler sees the scope. Guard the drift closed.
|
|
285
|
+
assert.ok(
|
|
286
|
+
rendered.includes("'/api/tasks'+location.search") || rendered.includes("'/api/tasks' + location.search"),
|
|
287
|
+
"client forwards location.search onto the /api/tasks fetch",
|
|
288
|
+
);
|
|
218
289
|
});
|
|
219
290
|
|
|
220
291
|
test("/api/form resolves a linked form's schema", async () => {
|
|
@@ -59,7 +59,7 @@ function api(path,opts){return fetch(BASE+path,opts).then(r=>{if(!r.ok)return r.
|
|
|
59
59
|
function loadTasks(){
|
|
60
60
|
formEl.replaceChildren();
|
|
61
61
|
tasksEl.replaceChildren(el('li',{},'loading…'));
|
|
62
|
-
return api('/api/tasks').then(ts=>{
|
|
62
|
+
return api('/api/tasks'+location.search).then(ts=>{
|
|
63
63
|
tasksEl.replaceChildren();
|
|
64
64
|
if(!ts.length){tasksEl.appendChild(el('li',{},'No open tasks.'));return;}
|
|
65
65
|
for(const t of ts){
|
|
@@ -216,13 +216,18 @@ export function mountSurfaces(ctx: RuntimeContext, app: AppApi): SurfacesHandle
|
|
|
216
216
|
source: "surface:taskInbox",
|
|
217
217
|
handler: async (req) => {
|
|
218
218
|
const pik = req.query.get("processInstanceKey") ?? undefined;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
// "
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
const assignee = req.query.get("assignee") ?? undefined;
|
|
220
|
+
const candidateGroup = req.query.get("candidateGroup") ?? undefined;
|
|
221
|
+
// Constrain the inbox to open (answerable) tasks via openUserTasks — the accessor that
|
|
222
|
+
// pins state="CREATED" for us. A bare searchUserTasks returns tasks in every state
|
|
223
|
+
// (CREATED/COMPLETED/CANCELED/...), so already-answered or withdrawn tasks would surface
|
|
224
|
+
// as if actionable — completing one then fails with "User task ... is not active".
|
|
225
|
+
// Deriving the pin from openUserTasks (rather than re-pinning state here) keeps the
|
|
226
|
+
// definition of "open" single-sourced. See nanobpm/nano-ide#248.
|
|
227
|
+
const tasks = await app.engine.openUserTasks({
|
|
225
228
|
...(pik ? { processInstanceKey: pik } : {}),
|
|
229
|
+
...(assignee ? { assignee } : {}),
|
|
230
|
+
...(candidateGroup ? { candidateGroup } : {}),
|
|
226
231
|
});
|
|
227
232
|
return json(tasks);
|
|
228
233
|
},
|