@jskit-ai/users-web 0.1.165 → 0.1.168
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 +494 -8
- package/src/client/components/CrudDeleteAction.vue +93 -0
- package/src/client/components/CrudListScreen.vue +1 -1
- package/src/client/components/CrudViewScreen.vue +1 -0
- package/src/client/composables/crud/crudJsonApiTransportSupport.js +8 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +58 -7
- package/src/client/composables/useCrudDeleteAction.js +160 -0
- package/src/client/index.js +2 -0
- package/test/crudJsonApiTransportSupport.test.js +13 -0
- package/test/crudListDateFilterSurface.browser.test.js +12 -103
- package/test/crudScreenComponents.test.js +16 -3
- package/test/exportsContract.test.js +2 -0
- package/test/{packageDescriptorOwnership.test.js → packageMetadataOwnership.test.js} +4 -2
- package/test/settingsPlacementContract.test.js +10 -8
- package/test/useCrudAddEdit.test.js +50 -4
- package/test/useCrudDeleteAction.test.js +139 -0
- package/package.descriptor.mjs +0 -468
|
@@ -108,6 +108,29 @@ test("buildCrudFormPayload and applyCrudPayloadToForm round-trip date-time field
|
|
|
108
108
|
assert.equal(form.scheduledAt, "2024-01-02T03:04");
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
test("date-time form values honor strict temporal precision without losing fractional tails", () => {
|
|
112
|
+
const noFraction = buildCrudFormPayload(
|
|
113
|
+
[{ key: "scheduledAt", type: "string", format: "date-time", temporalPrecision: 0 }],
|
|
114
|
+
{ scheduledAt: "2024-01-02T03:04" }
|
|
115
|
+
);
|
|
116
|
+
assert.match(noFraction.scheduledAt, /T\d{2}:\d{2}:00Z$/u);
|
|
117
|
+
assert.doesNotMatch(noFraction.scheduledAt, /\.000Z$/u);
|
|
118
|
+
|
|
119
|
+
const microseconds = buildCrudFormPayload(
|
|
120
|
+
[{ key: "scheduledAt", type: "string", format: "date-time", temporalPrecision: 6 }],
|
|
121
|
+
{ scheduledAt: "2024-01-02T03:04:05.123456" }
|
|
122
|
+
);
|
|
123
|
+
assert.match(microseconds.scheduledAt, /T\d{2}:\d{2}:05\.123456Z$/u);
|
|
124
|
+
|
|
125
|
+
const form = reactive({ scheduledAt: "" });
|
|
126
|
+
applyCrudPayloadToForm(
|
|
127
|
+
[{ key: "scheduledAt", type: "string", format: "date-time", temporalPrecision: 6 }],
|
|
128
|
+
form,
|
|
129
|
+
{ scheduledAt: "2024-01-02T03:04:05.123456Z" }
|
|
130
|
+
);
|
|
131
|
+
assert.match(form.scheduledAt, /T\d{2}:\d{2}:05\.123456$/u);
|
|
132
|
+
});
|
|
133
|
+
|
|
111
134
|
test("applyCrudPayloadToForm normalizes date values for HTML date inputs", () => {
|
|
112
135
|
const fields = [
|
|
113
136
|
{ key: "publishedOn", type: "string", format: "date" },
|
|
@@ -137,7 +160,7 @@ test("applyCrudPayloadToForm normalizes date values for HTML date inputs", () =>
|
|
|
137
160
|
});
|
|
138
161
|
});
|
|
139
162
|
|
|
140
|
-
test("buildCrudFormPayload
|
|
163
|
+
test("buildCrudFormPayload preserves strict time precision", () => {
|
|
141
164
|
const fields = [
|
|
142
165
|
{ key: "fromTime", type: "string", format: "time" },
|
|
143
166
|
{ key: "toTime", type: "string", format: "time" }
|
|
@@ -150,7 +173,7 @@ test("buildCrudFormPayload normalizes time fields to canonical HH:MM", () => {
|
|
|
150
173
|
|
|
151
174
|
assert.deepEqual(payload, {
|
|
152
175
|
fromTime: "18:13",
|
|
153
|
-
toTime: "18:45"
|
|
176
|
+
toTime: "18:45:00"
|
|
154
177
|
});
|
|
155
178
|
});
|
|
156
179
|
|
|
@@ -224,7 +247,7 @@ test("buildCrudFormPayload preserves nullable booleans while keeping non-nullabl
|
|
|
224
247
|
);
|
|
225
248
|
});
|
|
226
249
|
|
|
227
|
-
test("applyCrudPayloadToForm
|
|
250
|
+
test("applyCrudPayloadToForm preserves strict time values for form inputs", () => {
|
|
228
251
|
const fields = [
|
|
229
252
|
{ key: "fromTime", type: "string", format: "time" },
|
|
230
253
|
{ key: "toTime", type: "string", format: "time" }
|
|
@@ -240,11 +263,34 @@ test("applyCrudPayloadToForm normalizes time fields for form inputs", () => {
|
|
|
240
263
|
});
|
|
241
264
|
|
|
242
265
|
assert.deepEqual(form, {
|
|
243
|
-
fromTime: "18:13",
|
|
266
|
+
fromTime: "18:13:00",
|
|
244
267
|
toTime: "18:45"
|
|
245
268
|
});
|
|
246
269
|
});
|
|
247
270
|
|
|
271
|
+
test("strict temporal form values round-trip seconds and fractions", () => {
|
|
272
|
+
const fields = [
|
|
273
|
+
{ key: "fromTime", type: "string", format: "time", temporalPrecision: 6 },
|
|
274
|
+
{ key: "scheduledAt", type: "string", format: "date-time", temporalPrecision: 3 }
|
|
275
|
+
];
|
|
276
|
+
const form = reactive({
|
|
277
|
+
fromTime: "",
|
|
278
|
+
scheduledAt: ""
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
applyCrudPayloadToForm(fields, form, {
|
|
282
|
+
fromTime: "18:13:14.123456",
|
|
283
|
+
scheduledAt: "2026-08-13T07:08:09.123Z"
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
assert.equal(form.fromTime, "18:13:14.123456");
|
|
287
|
+
assert.match(form.scheduledAt, /^2026-08-13T\d{2}:\d{2}:09\.123$/u);
|
|
288
|
+
assert.deepEqual(buildCrudFormPayload(fields, form), {
|
|
289
|
+
fromTime: "18:13:14.123456",
|
|
290
|
+
scheduledAt: "2026-08-13T07:08:09.123Z"
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
248
294
|
test("applyCrudPayloadToForm maps payload values into reactive form model", () => {
|
|
249
295
|
const form = reactive({
|
|
250
296
|
name: "",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
|
|
4
|
+
import { createSSRApp, h, nextTick, ref } from "vue";
|
|
5
|
+
import { renderToString } from "vue/server-renderer";
|
|
6
|
+
import { createMemoryHistory, createRouter } from "vue-router";
|
|
7
|
+
import {
|
|
8
|
+
requireCrudDeleteOperation,
|
|
9
|
+
useCrudDeleteAction
|
|
10
|
+
} from "../src/client/composables/useCrudDeleteAction.js";
|
|
11
|
+
|
|
12
|
+
const deleteResource = Object.freeze({
|
|
13
|
+
namespace: "notes",
|
|
14
|
+
operations: Object.freeze({
|
|
15
|
+
delete: Object.freeze({
|
|
16
|
+
method: "DELETE"
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function createTestRouter() {
|
|
22
|
+
return createRouter({
|
|
23
|
+
history: createMemoryHistory(),
|
|
24
|
+
routes: [
|
|
25
|
+
{ path: "/notes", component: { render: () => h("div") } },
|
|
26
|
+
{ path: "/notes/:noteId", component: { render: () => h("div") } }
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createScreen() {
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
view: Object.freeze({
|
|
34
|
+
recordId: ref("42"),
|
|
35
|
+
record: ref({ id: "42", title: "Keep it simple" }),
|
|
36
|
+
isLoading: ref(false),
|
|
37
|
+
isNotFound: ref(false),
|
|
38
|
+
resolveParams(template = "") {
|
|
39
|
+
return String(template || "").replace(":noteId", "42");
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
listLocation: ref({ path: "/notes" })
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function renderDeleteAction({ client, queryClient = new QueryClient() } = {}) {
|
|
47
|
+
const router = createTestRouter();
|
|
48
|
+
await router.push("/notes/42");
|
|
49
|
+
await router.isReady();
|
|
50
|
+
|
|
51
|
+
let deleteAction = null;
|
|
52
|
+
const app = createSSRApp({
|
|
53
|
+
setup() {
|
|
54
|
+
deleteAction = useCrudDeleteAction({
|
|
55
|
+
screen: createScreen(),
|
|
56
|
+
resource: deleteResource,
|
|
57
|
+
resourceNamespace: "notes",
|
|
58
|
+
apiUrlTemplate: "/notes/:noteId",
|
|
59
|
+
client
|
|
60
|
+
});
|
|
61
|
+
return () => h("div");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
app.use(router);
|
|
65
|
+
app.use(VueQueryPlugin, { queryClient });
|
|
66
|
+
await renderToString(app);
|
|
67
|
+
|
|
68
|
+
return { deleteAction, queryClient, router };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test("useCrudDeleteAction confirms one DELETE, invalidates CRUD state, and returns to the list", async () => {
|
|
72
|
+
const requests = [];
|
|
73
|
+
const invalidations = [];
|
|
74
|
+
let finishRequest = null;
|
|
75
|
+
const queryClient = new QueryClient();
|
|
76
|
+
queryClient.invalidateQueries = async function invalidateQueries(options = {}) {
|
|
77
|
+
invalidations.push(options);
|
|
78
|
+
};
|
|
79
|
+
const pendingRequest = new Promise((resolve) => {
|
|
80
|
+
finishRequest = resolve;
|
|
81
|
+
});
|
|
82
|
+
const client = {
|
|
83
|
+
async request(path, options = {}) {
|
|
84
|
+
requests.push({ path, options });
|
|
85
|
+
return pendingRequest;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const { deleteAction, router } = await renderDeleteAction({ client, queryClient });
|
|
89
|
+
|
|
90
|
+
assert.equal(deleteAction.canDelete, true);
|
|
91
|
+
assert.equal(deleteAction.request(), true);
|
|
92
|
+
assert.equal(deleteAction.isOpen, true);
|
|
93
|
+
|
|
94
|
+
const confirmation = deleteAction.confirm();
|
|
95
|
+
await nextTick();
|
|
96
|
+
assert.equal(deleteAction.isDeleting, true);
|
|
97
|
+
assert.equal(requests.length, 1);
|
|
98
|
+
assert.match(requests[0].path, /\/notes\/42$/);
|
|
99
|
+
assert.equal(requests[0].options.method, "DELETE");
|
|
100
|
+
assert.equal(Object.hasOwn(requests[0].options, "body"), false);
|
|
101
|
+
|
|
102
|
+
finishRequest(null);
|
|
103
|
+
await confirmation;
|
|
104
|
+
|
|
105
|
+
assert.equal(deleteAction.isDeleting, false);
|
|
106
|
+
assert.equal(deleteAction.isOpen, false);
|
|
107
|
+
assert.equal(router.currentRoute.value.path, "/notes");
|
|
108
|
+
assert.deepEqual(invalidations, [
|
|
109
|
+
{ queryKey: ["ui-generator", "notes"] }
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("useCrudDeleteAction keeps the record dialog open with useful error feedback", async () => {
|
|
114
|
+
const client = {
|
|
115
|
+
async request() {
|
|
116
|
+
throw new Error("Delete service unavailable.");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const { deleteAction, router } = await renderDeleteAction({ client });
|
|
120
|
+
|
|
121
|
+
deleteAction.request();
|
|
122
|
+
await deleteAction.confirm();
|
|
123
|
+
|
|
124
|
+
assert.equal(deleteAction.isOpen, true);
|
|
125
|
+
assert.equal(deleteAction.isDeleting, false);
|
|
126
|
+
assert.equal(deleteAction.error, "Delete service unavailable.");
|
|
127
|
+
assert.equal(router.currentRoute.value.path, "/notes/42");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("useCrudDeleteAction rejects resources without the canonical DELETE operation", () => {
|
|
131
|
+
assert.throws(
|
|
132
|
+
() => requireCrudDeleteOperation({ operations: {} }),
|
|
133
|
+
/requires resource\.operations\.delete/
|
|
134
|
+
);
|
|
135
|
+
assert.throws(
|
|
136
|
+
() => requireCrudDeleteOperation({ operations: { delete: { method: "POST" } } }),
|
|
137
|
+
/method to be DELETE/
|
|
138
|
+
);
|
|
139
|
+
});
|