@nanobpm/urban 0.90.2 → 0.91.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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.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",
|
|
@@ -1955,9 +1955,17 @@ function buildEngineForm(cfg, row, onSuccess) {
|
|
|
1955
1955
|
const formKey = row[cfg.formKeyField || "form_key"];
|
|
1956
1956
|
const box = el("div", { class: "pc-subform" });
|
|
1957
1957
|
const msg = el("p", { class: "njf-msg", role: "status", "aria-live": "polite" });
|
|
1958
|
+
// Optional page-author knob: route completion through the app's own canonical
|
|
1959
|
+
// door instead of the runtime's generic seam. Defaults to /app/actions/complete
|
|
1960
|
+
// when unset (existing pages unchanged). Same trust class as detail.form's
|
|
1961
|
+
// action.path; rebased through apiUrl() by getJSON like every other page fetch.
|
|
1962
|
+
const completePath =
|
|
1963
|
+
typeof cfg.completePath === "string" && cfg.completePath.trim() !== ""
|
|
1964
|
+
? cfg.completePath.trim()
|
|
1965
|
+
: "/app/actions/complete";
|
|
1958
1966
|
/** @param {Record<string, any>} variables */
|
|
1959
1967
|
const complete = (variables) =>
|
|
1960
|
-
getJSON(
|
|
1968
|
+
getJSON(completePath, {
|
|
1961
1969
|
method: "POST",
|
|
1962
1970
|
headers: { "content-type": "application/json" },
|
|
1963
1971
|
body: JSON.stringify({ userTaskKey, variables }),
|
|
@@ -84,6 +84,7 @@ test("buildEngineForm resolves the row's formKey and completes with the entered
|
|
|
84
84
|
assert.ok(input, "the deployed form-js schema was rendered by the shared renderer");
|
|
85
85
|
input!.value = "done";
|
|
86
86
|
const form = created.find((n) => n.tagName === "FORM");
|
|
87
|
+
assert.ok(form, "the deployed schema rendered a form element to submit");
|
|
87
88
|
await form!.fire("submit");
|
|
88
89
|
await new Promise((r) => setTimeout(r, 0));
|
|
89
90
|
const completeReq = calls.find((c) => c.url.includes("/app/actions/complete"));
|
|
@@ -116,6 +117,7 @@ test("buildEngineForm surfaces a completion failure in the rendered-form path",
|
|
|
116
117
|
assert.ok(msg, "a failed completion is surfaced to the operator (not silent)");
|
|
117
118
|
assert.notEqual(msg!.textContent, "", "the error message element has text");
|
|
118
119
|
const submit = created.find((n) => n.tagName === "BUTTON" && n.type === "submit");
|
|
120
|
+
assert.ok(submit, "the completion path rendered a submit button");
|
|
119
121
|
assert.equal(submit!.disabled, false, "the submit button is re-enabled so the operator can retry");
|
|
120
122
|
});
|
|
121
123
|
|
|
@@ -156,3 +158,101 @@ test("buildEngineForm degrades to bare completion when the formKey resolves to n
|
|
|
156
158
|
const button = created.find((n) => n.tagName === "BUTTON" && n.textContent === "Complete");
|
|
157
159
|
assert.ok(button, "a 204 (no form) falls back to the bare completion button");
|
|
158
160
|
});
|
|
161
|
+
|
|
162
|
+
test("buildEngineForm routes the rendered-form completion through a configured completePath", async (t) => {
|
|
163
|
+
const created: FakeElement[] = [];
|
|
164
|
+
const calls: FetchCall[] = [];
|
|
165
|
+
t.after(installFakeDom(created));
|
|
166
|
+
t.after(
|
|
167
|
+
installFakeFetch((url) => {
|
|
168
|
+
if (url.includes("/app/actions/form")) {
|
|
169
|
+
return { status: 200, json: { schema: { components: [{ type: "textfield", key: "note" }] } } };
|
|
170
|
+
}
|
|
171
|
+
return { status: 200, json: { ok: true } };
|
|
172
|
+
}, calls),
|
|
173
|
+
);
|
|
174
|
+
const box = buildEngineForm(
|
|
175
|
+
{ ...CFG, completePath: "/app/api/actions/complete-user-task" },
|
|
176
|
+
{ form_key: "form-c", user_task_key: "ut-c" },
|
|
177
|
+
() => {},
|
|
178
|
+
);
|
|
179
|
+
assert.ok(box);
|
|
180
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
181
|
+
const input = created.find((n) => n.tagName === "INPUT");
|
|
182
|
+
assert.ok(input, "the deployed form-js schema was rendered by the shared renderer");
|
|
183
|
+
input!.value = "done";
|
|
184
|
+
const form = created.find((n) => n.tagName === "FORM");
|
|
185
|
+
assert.ok(form, "the deployed schema rendered a form element to submit");
|
|
186
|
+
await form!.fire("submit");
|
|
187
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
188
|
+
const completeReq = calls.find((c) => c.url.includes("/app/api/actions/complete-user-task"));
|
|
189
|
+
assert.ok(completeReq, "completion POSTs to the configured completePath, not the generic seam");
|
|
190
|
+
assert.equal(completeReq!.method, "POST");
|
|
191
|
+
assert.deepEqual(completeReq!.body, { userTaskKey: "ut-c", variables: { note: "done" } });
|
|
192
|
+
assert.equal(
|
|
193
|
+
calls.some((c) => c.url.includes("/app/actions/complete")),
|
|
194
|
+
false,
|
|
195
|
+
"the generic seam is not called when completePath is set",
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("buildEngineForm routes the bare completion through a configured completePath", async (t) => {
|
|
200
|
+
const created: FakeElement[] = [];
|
|
201
|
+
const calls: FetchCall[] = [];
|
|
202
|
+
t.after(installFakeDom(created));
|
|
203
|
+
t.after(installFakeFetch(() => ({ status: 200, json: { ok: true } }), calls));
|
|
204
|
+
const box = buildEngineForm(
|
|
205
|
+
{ ...CFG, completePath: "/app/custom-complete" },
|
|
206
|
+
{ user_task_key: "ut-b" },
|
|
207
|
+
() => {},
|
|
208
|
+
);
|
|
209
|
+
assert.ok(box);
|
|
210
|
+
const button = created.find((n) => n.tagName === "BUTTON");
|
|
211
|
+
assert.ok(button, "the bare completion path rendered a Complete button");
|
|
212
|
+
await button!.fire("click");
|
|
213
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
214
|
+
const completeReq = calls.find((c) => c.url.includes("/app/custom-complete"));
|
|
215
|
+
assert.ok(completeReq, "the bare button completes via the configured completePath");
|
|
216
|
+
assert.equal(completeReq!.body.userTaskKey, "ut-b");
|
|
217
|
+
assert.deepEqual(completeReq!.body.variables, {}, "bare completion sends empty variables");
|
|
218
|
+
assert.equal(
|
|
219
|
+
calls.some((c) => c.url.includes("/app/actions/complete")),
|
|
220
|
+
false,
|
|
221
|
+
"the generic seam is not called when completePath is set",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("buildEngineForm falls back to the generic seam when completePath is empty/absent", async (t) => {
|
|
226
|
+
const created: FakeElement[] = [];
|
|
227
|
+
const calls: FetchCall[] = [];
|
|
228
|
+
t.after(installFakeDom(created));
|
|
229
|
+
t.after(installFakeFetch(() => ({ status: 200, json: { ok: true } }), calls));
|
|
230
|
+
// An empty-string completePath is treated as absent (default seam preserved).
|
|
231
|
+
const box = buildEngineForm({ ...CFG, completePath: "" }, { user_task_key: "ut-d" }, () => {});
|
|
232
|
+
assert.ok(box);
|
|
233
|
+
const button = created.find((n) => n.tagName === "BUTTON");
|
|
234
|
+
assert.ok(button, "the bare completion path rendered a Complete button");
|
|
235
|
+
await button!.fire("click");
|
|
236
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
237
|
+
const completeReq = calls.find((c) => c.url.includes("/app/actions/complete"));
|
|
238
|
+
assert.ok(completeReq, "an empty completePath still targets the default /app/actions/complete seam");
|
|
239
|
+
assert.equal(completeReq!.body.userTaskKey, "ut-d");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("buildEngineForm treats a whitespace-only completePath as absent", async (t) => {
|
|
243
|
+
const created: FakeElement[] = [];
|
|
244
|
+
const calls: FetchCall[] = [];
|
|
245
|
+
t.after(installFakeDom(created));
|
|
246
|
+
t.after(installFakeFetch(() => ({ status: 200, json: { ok: true } }), calls));
|
|
247
|
+
// A whitespace-only completePath is normalized away so it never becomes an
|
|
248
|
+
// unintended relative fetch target — the default seam is preserved.
|
|
249
|
+
const box = buildEngineForm({ ...CFG, completePath: " " }, { user_task_key: "ut-e" }, () => {});
|
|
250
|
+
assert.ok(box);
|
|
251
|
+
const button = created.find((n) => n.tagName === "BUTTON");
|
|
252
|
+
assert.ok(button, "the bare completion path rendered a Complete button");
|
|
253
|
+
await button!.fire("click");
|
|
254
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
255
|
+
const completeReq = calls.find((c) => c.url.includes("/app/actions/complete"));
|
|
256
|
+
assert.ok(completeReq, "a whitespace-only completePath still targets the default /app/actions/complete seam");
|
|
257
|
+
assert.equal(completeReq!.body.userTaskKey, "ut-e");
|
|
258
|
+
});
|