@jskit-ai/http-web 0.1.4 → 0.1.6

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.
Files changed (25) hide show
  1. package/package.json +11 -37
  2. package/patterns/crud-screen-set/PATTERN.md +88 -0
  3. package/patterns/crud-screen-set/example/books/BookEditPage.vue +54 -0
  4. package/patterns/crud-screen-set/example/books/BookFormFields.vue +52 -0
  5. package/patterns/crud-screen-set/example/books/BookListPage.vue +54 -0
  6. package/patterns/crud-screen-set/example/books/BookNewPage.vue +45 -0
  7. package/patterns/crud-screen-set/example/books/BookViewPage.vue +50 -0
  8. package/patterns/crud-screen-set/example/books/formFields.js +31 -0
  9. package/patterns/crud-screen-set/example/books/listExtensions.js +9 -0
  10. package/src/client/components/CrudAddEditScreen.vue +29 -31
  11. package/src/client/components/CrudDeleteAction.vue +1 -2
  12. package/src/client/components/CrudListBulkActionSurface.vue +2 -3
  13. package/src/client/components/CrudListRecordActionMenu.vue +3 -9
  14. package/src/client/components/CrudListScreen.vue +80 -63
  15. package/src/client/components/CrudViewScreen.vue +32 -33
  16. package/src/client/composables/crud/crudSchemaFormHelpers.js +1 -1
  17. package/src/client/composables/support/errorMessageHelpers.js +4 -2
  18. package/src/client/composables/useCrudDeleteAction.js +2 -2
  19. package/src/client/composables/useCrudListScreen.js +2 -2
  20. package/src/client/composables/useCrudViewScreen.js +2 -2
  21. package/test/crudScreenComponents.test.js +31 -14
  22. package/test/crudScreenPattern.test.js +60 -0
  23. package/test/errorMessageHelpers.test.js +13 -2
  24. package/test/useCrudAddEdit.test.js +20 -0
  25. package/test/useCrudDeleteAction.test.js +1 -1
@@ -15,39 +15,41 @@ async function readClientFile(...parts) {
15
15
  return readFile(path.join(PACKAGE_DIR, "src", "client", ...parts), "utf8");
16
16
  }
17
17
 
18
- test("CRUD screen components own generated list/view/form chrome centrally", async () => {
18
+ test("CRUD screen components own list/view/form chrome centrally", async () => {
19
19
  const listSource = await readComponent("CrudListScreen.vue");
20
20
  const viewSource = await readComponent("CrudViewScreen.vue");
21
21
  const addEditSource = await readComponent("CrudAddEditScreen.vue");
22
22
  const deleteActionSource = await readComponent("CrudDeleteAction.vue");
23
+ const bulkActionSource = await readComponent("CrudListBulkActionSurface.vue");
24
+ const rowActionSource = await readComponent("CrudListRecordActionMenu.vue");
23
25
 
24
26
  assert.match(listSource, /CrudListBulkActionSurface/);
25
27
  assert.match(listSource, /CrudListFilterSurface/);
26
28
  assert.match(listSource, /CrudListRecordActionMenu/);
27
- assert.match(listSource, /class="ui-generator-list-cards"/);
28
- assert.match(listSource, /class="ui-generator-list-table"/);
29
- assert.match(listSource, /class="ui-generator-list-fab"/);
30
- assert.doesNotMatch(listSource, /ui-generator-list-cards d-md-none/);
31
- assert.doesNotMatch(listSource, /ui-generator-list-table d-none d-md-block/);
32
- assert.doesNotMatch(listSource, /class="ui-generator-list-fab d-md-none"/);
33
- assert.match(listSource, /\.ui-generator-list-table\s*\{\s*display:\s*none;/);
29
+ assert.match(listSource, /class="crud-list-cards"/);
30
+ assert.match(listSource, /class="crud-list-table"/);
31
+ assert.match(listSource, /class="crud-list-fab"/);
32
+ assert.doesNotMatch(listSource, /crud-list-cards d-md-none/);
33
+ assert.doesNotMatch(listSource, /crud-list-table d-none d-md-block/);
34
+ assert.doesNotMatch(listSource, /class="crud-list-fab d-md-none"/);
35
+ assert.match(listSource, /\.crud-list-table\s*\{\s*display:\s*none;/);
34
36
  assert.match(
35
37
  listSource,
36
- /@media \(min-width: 960px\)[\s\S]*\.ui-generator-list-cards\s*\{\s*display:\s*none;[\s\S]*\.ui-generator-list-table\s*\{\s*display:\s*block;[\s\S]*\.ui-generator-list-fab\s*\{\s*display:\s*none;/
38
+ /@media \(min-width: 960px\)[\s\S]*\.crud-list-cards\s*\{\s*display:\s*none;[\s\S]*\.crud-list-table\s*\{\s*display:\s*block;[\s\S]*\.crud-list-fab\s*\{\s*display:\s*none;/
37
39
  );
38
40
  assert.match(listSource, /button-label="More"/);
39
41
  assert.match(listSource, /selectableRows/);
40
42
  assert.match(
41
43
  listSource,
42
- /\.ui-generator-list-element :deep\(\.v-btn\)\s*\{\s*min-height:\s*48px;/
44
+ /\.crud-list-element :deep\(\.v-btn\)\s*\{\s*min-height:\s*48px;/
43
45
  );
44
46
  assert.match(listSource, /<slot[\s\S]*name="card-fields"/);
45
47
  assert.match(listSource, /<slot name="table-header"/);
46
48
  assert.match(listSource, /<slot[\s\S]*name="table-row"/);
47
49
  assert.match(listSource, /:row="row"/);
48
50
 
49
- assert.match(viewSource, /generated-ui-screen generated-ui-screen--operator ui-generator-view-element/);
50
- assert.match(viewSource, /ui-generator-view-panel/);
51
+ assert.match(viewSource, /crud-screen crud-screen--operator crud-view-element/);
52
+ assert.match(viewSource, /crud-view-panel/);
51
53
  assert.match(viewSource, /@click="view\.refresh"/);
52
54
  assert.match(viewSource, /<slot name="actions" :screen="screen" :view="view" \/>/);
53
55
  assert.match(viewSource, /<slot name="before-fields"/);
@@ -55,7 +57,7 @@ test("CRUD screen components own generated list/view/form chrome centrally", asy
55
57
  assert.match(viewSource, /<slot name="after-fields"/);
56
58
  assert.match(viewSource, /supporting-content/);
57
59
 
58
- assert.match(addEditSource, /generated-ui-screen generated-ui-screen--operator ui-generator-add-edit-form/);
60
+ assert.match(addEditSource, /crud-screen crud-screen--operator crud-add-edit-form/);
59
61
  assert.match(addEditSource, /addEdit\.canRetryLoad/);
60
62
  assert.match(addEditSource, /@click="addEdit\.refresh"/);
61
63
  assert.match(addEditSource, /<slot[\s\S]*name="fields"/);
@@ -64,9 +66,23 @@ test("CRUD screen components own generated list/view/form chrome centrally", asy
64
66
  assert.match(deleteActionSource, /@click="action\.confirm"/);
65
67
  assert.match(deleteActionSource, /@click="closeDialog"/);
66
68
  assert.doesNotMatch(deleteActionSource, /\bactivator=/);
69
+
70
+ const sharedSources = [
71
+ listSource,
72
+ viewSource,
73
+ addEditSource,
74
+ deleteActionSource,
75
+ bulkActionSource,
76
+ rowActionSource
77
+ ].join("\n");
78
+ assert.doesNotMatch(sharedSources, /:loading=|v-progress-(?:circular|linear)/u);
79
+ assert.doesNotMatch(sharedSources, /ui-generator|generated-ui/u);
80
+ assert.match(listSource, /v-skeleton-loader/u);
81
+ assert.match(viewSource, /v-skeleton-loader/u);
82
+ assert.match(addEditSource, /v-skeleton-loader/u);
67
83
  });
68
84
 
69
- test("CRUD screen composables expose generated page extension inputs", async () => {
85
+ test("CRUD screen composables expose page extension inputs", async () => {
70
86
  const listSource = await readClientFile("composables", "useCrudListScreen.js");
71
87
  const viewSource = await readClientFile("composables", "useCrudViewScreen.js");
72
88
 
@@ -82,6 +98,7 @@ test("CRUD screen composables expose generated page extension inputs", async ()
82
98
  assert.match(viewSource, /queryKeyFactory = null/);
83
99
  assert.match(viewSource, /requestQueryParams/);
84
100
  assert.match(viewSource, /readEnabled/);
101
+ assert.doesNotMatch(`${listSource}\n${viewSource}`, /ui-generator/u);
85
102
  });
86
103
 
87
104
  test("CRUD screen composables are importable package APIs", async () => {
@@ -0,0 +1,60 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import { readdir, readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { promisify } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+ import { compileScript, compileTemplate, parse } from "@vue/compiler-sfc";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+ const patternRoot = path.resolve(
12
+ path.dirname(fileURLToPath(import.meta.url)),
13
+ "../patterns/crud-screen-set"
14
+ );
15
+ const exampleRoot = path.join(patternRoot, "example", "books");
16
+
17
+ test("CRUD screen pattern is thin app-owned source over shared public screens", async () => {
18
+ const files = (await readdir(exampleRoot)).sort();
19
+ const sources = await Promise.all(files.map((file) => readFile(path.join(exampleRoot, file), "utf8")));
20
+ const combinedSource = sources.join("\n");
21
+
22
+ assert.deepEqual(files, [
23
+ "BookEditPage.vue",
24
+ "BookFormFields.vue",
25
+ "BookListPage.vue",
26
+ "BookNewPage.vue",
27
+ "BookViewPage.vue",
28
+ "formFields.js",
29
+ "listExtensions.js"
30
+ ]);
31
+ assert.match(combinedSource, /useCrudListScreen/u);
32
+ assert.match(combinedSource, /useCrudViewScreen/u);
33
+ assert.match(combinedSource, /useCrudAddEditScreen/u);
34
+ assert.match(combinedSource, /useCrudDeleteAction/u);
35
+ assert.doesNotMatch(combinedSource, /ui-generator|generated-ui|jskit:crud-ui|__JSKIT_/u);
36
+ assert.doesNotMatch(combinedSource, /:loading=|v-progress-(?:circular|linear)/u);
37
+
38
+ for (const file of files) {
39
+ const absolutePath = path.join(exampleRoot, file);
40
+ if (file.endsWith(".js")) {
41
+ await execFileAsync(process.execPath, ["--check", absolutePath]);
42
+ continue;
43
+ }
44
+
45
+ const source = await readFile(absolutePath, "utf8");
46
+ const { descriptor, errors } = parse(source, { filename: absolutePath });
47
+ assert.deepEqual(errors, []);
48
+ if (descriptor.scriptSetup) {
49
+ compileScript(descriptor, { id: `pattern-${file}` });
50
+ }
51
+ if (descriptor.template) {
52
+ const result = compileTemplate({
53
+ id: `pattern-${file}`,
54
+ filename: absolutePath,
55
+ source: descriptor.template.content
56
+ });
57
+ assert.deepEqual(result.errors, []);
58
+ }
59
+ }
60
+ });
@@ -22,7 +22,18 @@ test("toQueryErrorMessage prefers fallback for generic transport messages", () =
22
22
  );
23
23
  });
24
24
 
25
- test("toUiErrorMessage prefers fallback copy before runtime error text", () => {
26
- assert.equal(toUiErrorMessage({ message: "Server exploded" }, "Unable to save."), "Unable to save.");
25
+ test("toUiErrorMessage resolves specific runtime errors before fallback copy", () => {
26
+ assert.equal(
27
+ toUiErrorMessage({ message: "Current password is invalid." }, "Unable to update your password."),
28
+ "Current password is invalid."
29
+ );
27
30
  assert.equal(toUiErrorMessage({ message: "Server exploded" }, ""), "Server exploded");
28
31
  });
32
+
33
+ test("toUiErrorMessage uses fallback copy for generic transport errors", () => {
34
+ assert.equal(
35
+ toUiErrorMessage({ status: 500, message: "Request failed with status 500." }, "Unable to save."),
36
+ "Unable to save."
37
+ );
38
+ assert.equal(toUiErrorMessage({}, "Unable to save."), "Unable to save.");
39
+ });
@@ -213,6 +213,26 @@ test("buildCrudFormPayload serializes cleared nullable typed fields as null", ()
213
213
  });
214
214
  });
215
215
 
216
+ test("buildCrudFormPayload preserves explicit null for nullable string fields", () => {
217
+ const payload = buildCrudFormPayload(
218
+ [
219
+ { key: "sex", type: "string", nullable: true, component: "select" },
220
+ { key: "nickname", type: "string", nullable: true },
221
+ { key: "requiredName", type: "string", nullable: false }
222
+ ],
223
+ {
224
+ sex: null,
225
+ nickname: null,
226
+ requiredName: null
227
+ }
228
+ );
229
+
230
+ assert.deepEqual(payload, {
231
+ sex: null,
232
+ nickname: null
233
+ });
234
+ });
235
+
216
236
  test("buildCrudFormPayload preserves nullable booleans while keeping non-nullable booleans binary", () => {
217
237
  const fields = [
218
238
  { key: "active", type: "boolean" },
@@ -106,7 +106,7 @@ test("useCrudDeleteAction confirms one DELETE, invalidates CRUD state, and retur
106
106
  assert.equal(deleteAction.isOpen, false);
107
107
  assert.equal(router.currentRoute.value.path, "/notes");
108
108
  assert.deepEqual(invalidations, [
109
- { queryKey: ["ui-generator", "notes"] }
109
+ { queryKey: ["crud", "notes"] }
110
110
  ]);
111
111
  });
112
112