@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.
- package/package.json +11 -37
- package/patterns/crud-screen-set/PATTERN.md +88 -0
- package/patterns/crud-screen-set/example/books/BookEditPage.vue +54 -0
- package/patterns/crud-screen-set/example/books/BookFormFields.vue +52 -0
- package/patterns/crud-screen-set/example/books/BookListPage.vue +54 -0
- package/patterns/crud-screen-set/example/books/BookNewPage.vue +45 -0
- package/patterns/crud-screen-set/example/books/BookViewPage.vue +50 -0
- package/patterns/crud-screen-set/example/books/formFields.js +31 -0
- package/patterns/crud-screen-set/example/books/listExtensions.js +9 -0
- package/src/client/components/CrudAddEditScreen.vue +29 -31
- package/src/client/components/CrudDeleteAction.vue +1 -2
- package/src/client/components/CrudListBulkActionSurface.vue +2 -3
- package/src/client/components/CrudListRecordActionMenu.vue +3 -9
- package/src/client/components/CrudListScreen.vue +80 -63
- package/src/client/components/CrudViewScreen.vue +32 -33
- package/src/client/composables/crud/crudSchemaFormHelpers.js +1 -1
- package/src/client/composables/support/errorMessageHelpers.js +4 -2
- package/src/client/composables/useCrudDeleteAction.js +2 -2
- package/src/client/composables/useCrudListScreen.js +2 -2
- package/src/client/composables/useCrudViewScreen.js +2 -2
- package/test/crudScreenComponents.test.js +31 -14
- package/test/crudScreenPattern.test.js +60 -0
- package/test/errorMessageHelpers.test.js +13 -2
- package/test/useCrudAddEdit.test.js +20 -0
- 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
|
|
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="
|
|
28
|
-
assert.match(listSource, /class="
|
|
29
|
-
assert.match(listSource, /class="
|
|
30
|
-
assert.doesNotMatch(listSource, /
|
|
31
|
-
assert.doesNotMatch(listSource, /
|
|
32
|
-
assert.doesNotMatch(listSource, /class="
|
|
33
|
-
assert.match(listSource, /\.
|
|
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]*\.
|
|
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
|
-
/\.
|
|
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, /
|
|
50
|
-
assert.match(viewSource, /
|
|
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, /
|
|
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
|
|
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
|
|
26
|
-
assert.equal(
|
|
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: ["
|
|
109
|
+
{ queryKey: ["crud", "notes"] }
|
|
110
110
|
]);
|
|
111
111
|
});
|
|
112
112
|
|