@jskit-ai/http-web 0.1.79 → 0.1.80

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": "@jskit-ai/http-web",
3
- "version": "0.1.79",
3
+ "version": "0.1.80",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -44,14 +44,14 @@
44
44
  "./client/support/contractGuards": "./src/client/support/contractGuards.js"
45
45
  },
46
46
  "dependencies": {
47
- "@jskit-ai/resource-crud-core": "0.1.176",
47
+ "@jskit-ai/resource-crud-core": "0.1.177",
48
48
  "@mdi/js": "^7.4.47"
49
49
  },
50
50
  "peerDependencies": {
51
- "@jskit-ai/http-runtime": "0.1.232",
52
- "@jskit-ai/kernel": "0.1.234",
53
- "@jskit-ai/realtime": "0.1.231",
54
- "@jskit-ai/shell-web": "0.1.238",
51
+ "@jskit-ai/http-runtime": "0.1.233",
52
+ "@jskit-ai/kernel": "0.1.235",
53
+ "@jskit-ai/realtime": "0.1.232",
54
+ "@jskit-ai/shell-web": "0.1.239",
55
55
  "@tanstack/vue-query": "^5.90.5",
56
56
  "vue": "^3.5.13",
57
57
  "vue-router": "^5.0.4",
@@ -42,6 +42,13 @@ questionnaire.
42
42
  - Resource-load errors stay inside the screen with retry. Command success and
43
43
  failure use JSKIT's shared toast/snackbar feedback path.
44
44
  - URLs are route templates or route objects resolved through the screen runtime.
45
+ - Browser URL templates include the configured surface prefix; unlike API
46
+ suffixes, `/new` and `/` are absolute browser paths. Use the complete route or
47
+ `paths.page()` for a link without record placeholders, and the owning CRUD
48
+ runtime's `resolveParams()` for record placeholders.
49
+ - `CrudListScreen` applies `createLabel` to all create actions. Its responsive
50
+ card and table layouts both remain in the DOM; Playwright text assertions
51
+ should filter to visible matches rather than match a hidden layout too.
45
52
  - Product field components are ordinary application source.
46
53
  - Query keys and placement sources describe the resource, not an authoring tool.
47
54
  - No generated-file markers, overwrite contracts, receipts, or provenance remain.
@@ -69,6 +76,14 @@ id (or a distinct new-record marker) so form cache entries stay separate.
69
76
  The New/Edit examples supply it; list/view screen wrappers derive their
70
77
  standard keys from `resourceNamespace`.
71
78
 
79
+ Save navigation is configured in the top-level `saveSuccess` object. The default
80
+ prefers a configured record-view URL, then the list. A create-and-list variation
81
+ sets `navigateToView: false` and `listUrlTemplate` to its list route. Set both
82
+ `navigateToView` and `navigateToList` to `false` to stay on the form. A custom
83
+ `addEditOptions.onSaveSuccess` replaces default invalidation and navigation;
84
+ it is unnecessary for ordinary list navigation. The generic CRUD guide explains
85
+ this contract under **Save navigation**.
86
+
72
87
  Ownership and permission-bootstrap mode are different options. For a public
73
88
  resource on a public surface, use `ownershipFilter: "public"` in form options
74
89
  or lower-level request composables. List/view screen wrappers derive request
@@ -314,7 +314,7 @@ function setSelectableRowsSelected(selected = true) {
314
314
  variant="flat"
315
315
  :to="listPrimaryAction"
316
316
  >
317
- New
317
+ {{ createLabel }}
318
318
  </v-btn>
319
319
  </section>
320
320
  </template>
@@ -3,6 +3,12 @@ import path from "node:path";
3
3
  import test from "node:test";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { readFileSync } from "node:fs";
7
+ import { registerHooks } from "node:module";
8
+ import { compileScript, parse } from "@vue/compiler-sfc";
9
+ import { createSSRApp, defineComponent, h } from "vue";
10
+ import { renderToString } from "vue/server-renderer";
11
+ import { createMemoryHistory, createRouter } from "vue-router";
6
12
 
7
13
  const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
14
  const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
@@ -15,6 +21,58 @@ async function readClientFile(...parts) {
15
21
  return readFile(path.join(PACKAGE_DIR, "src", "client", ...parts), "utf8");
16
22
  }
17
23
 
24
+ test("every CRUD list create action retains its configured label and destination", async () => {
25
+ const componentUrl = new URL("../src/client/components/CrudListScreen.vue", import.meta.url).href;
26
+ const hooks = registerHooks({
27
+ load(url, context, nextLoad) {
28
+ if (!url.endsWith(".vue")) return nextLoad(url, context);
29
+ if (url !== componentUrl) {
30
+ return { format: "module", shortCircuit: true, source: "export default { render: () => null };" };
31
+ }
32
+ const { descriptor } = parse(readFileSync(new URL(url), "utf8"));
33
+ return {
34
+ format: "module", shortCircuit: true,
35
+ source: compileScript(descriptor, { id: "crud-list-create-label", inlineTemplate: true }).content
36
+ };
37
+ }
38
+ });
39
+ let CrudListScreen;
40
+ try {
41
+ ({ default: CrudListScreen } = await import(componentUrl));
42
+ } finally {
43
+ hooks.deregister();
44
+ }
45
+
46
+ for (const populated of [false, true]) {
47
+ for (const createLabel of [undefined, "Add book"]) {
48
+ const screen = {
49
+ records: {}, listPrimaryAction: "/books/new",
50
+ displayRows: populated ? [{ key: "1", recordKey: "1", record: { title: "Kindred" } }] : [],
51
+ resolveRecordTitle: (record) => record.title
52
+ };
53
+ const app = createSSRApp({ render: () => h(CrudListScreen, {
54
+ screen, ...(createLabel === undefined ? {} : { createLabel })
55
+ }) });
56
+ const router = createRouter({
57
+ history: createMemoryHistory(), routes: [{ path: "/books", component: { render: () => null } }]
58
+ });
59
+ await router.push("/books");
60
+ app.use(router);
61
+ app.component("VBtn", defineComponent({
62
+ props: { to: { type: [String, Object], default: undefined } },
63
+ setup: (props, { slots }) => () => h("a", { href: props.to }, slots.default?.())
64
+ }));
65
+ for (const name of ["VSheet", "VTable", "VTextField", "VSkeletonLoader", "VCheckboxBtn"]) {
66
+ app.component(name, defineComponent({ setup: (_props, { slots }) => () => h("div", slots.default?.()) }));
67
+ }
68
+ const html = await renderToString(app);
69
+ const labels = [...html.matchAll(/<a[^>]*href="\/books\/new"[^>]*>(.*?)<\/a>/gs)]
70
+ .map((match) => match[1].trim());
71
+ assert.deepEqual(labels, Array(populated ? 2 : 3).fill(createLabel ?? "New record"));
72
+ }
73
+ }
74
+ });
75
+
18
76
  test("CRUD screen components own list/view/form chrome centrally", async () => {
19
77
  const listSource = await readComponent("CrudListScreen.vue");
20
78
  const viewSource = await readComponent("CrudViewScreen.vue");
@@ -88,6 +88,7 @@ test("the human guide's public create/list configuration works without bootstrap
88
88
  form.formState.title = "Public book";
89
89
  await form.addEdit.submit();
90
90
  assert.deepEqual(writes, [{ title: "Public book" }]);
91
+ assert.equal(router.currentRoute.value.path, "/books");
91
92
  await list.records.reload();
92
93
  assert.deepEqual(list.records.items, [{ id: "1", title: "Public book" }]);
93
94
  assert.deepEqual(Object.keys(resource.operations), ["list", "view", "create"]);