@jskit-ai/users-web 0.1.162 → 0.1.164
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.descriptor.mjs +7 -7
- package/package.json +8 -7
- package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +36 -0
- package/src/client/composables/records/useCrudAddEdit.js +4 -0
- package/src/client/composables/records/useCrudList.js +4 -0
- package/src/client/composables/records/useCrudView.js +4 -0
- package/src/client/composables/useCommand.js +6 -4
- package/src/client/composables/useCrudListScreen.js +19 -9
- package/src/client/index.js +4 -0
- package/test/crudHttpClientSupport.test.js +77 -0
- package/test/exportsContract.test.js +1 -0
- package/test/requestTransportOptions.test.js +52 -0
- package/test/useCrudAddEdit.test.js +29 -0
package/package.descriptor.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { HOME_COG_OUTLET } from "./src/shared/toolsOutletContracts.js";
|
|
|
3
3
|
export default Object.freeze({
|
|
4
4
|
packageVersion: 1,
|
|
5
5
|
packageId: "@jskit-ai/users-web",
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.164",
|
|
7
7
|
kind: "runtime",
|
|
8
8
|
description: "Users web module: account/profile UI plus shared users web widgets.",
|
|
9
9
|
dependsOn: [
|
|
@@ -286,12 +286,12 @@ export default Object.freeze({
|
|
|
286
286
|
dependencies: {
|
|
287
287
|
runtime: {
|
|
288
288
|
"@mdi/js": "^7.4.47",
|
|
289
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
290
|
-
"@jskit-ai/realtime": "0.1.
|
|
291
|
-
"@jskit-ai/kernel": "0.1.
|
|
292
|
-
"@jskit-ai/shell-web": "0.1.
|
|
293
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
294
|
-
"@jskit-ai/users-core": "0.1.
|
|
289
|
+
"@jskit-ai/http-runtime": "0.1.145",
|
|
290
|
+
"@jskit-ai/realtime": "0.1.143",
|
|
291
|
+
"@jskit-ai/kernel": "0.1.146",
|
|
292
|
+
"@jskit-ai/shell-web": "0.1.147",
|
|
293
|
+
"@jskit-ai/uploads-image-web": "0.1.122",
|
|
294
|
+
"@jskit-ai/users-core": "0.1.160"
|
|
295
295
|
},
|
|
296
296
|
dev: {}
|
|
297
297
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.164",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"./client/composables/useList": "./src/client/composables/records/useList.js",
|
|
32
32
|
"./client/composables/useCrudList": "./src/client/composables/records/useCrudList.js",
|
|
33
33
|
"./client/composables/useCrudListScreen": "./src/client/composables/useCrudListScreen.js",
|
|
34
|
+
"./client/crudHttpClient": "./src/client/composables/crud/crudHttpClientSupport.js",
|
|
34
35
|
"./client/composables/useCrudListParentTitle": "./src/client/composables/useCrudListParentTitle.js",
|
|
35
36
|
"./client/composables/useRealtimeQueryInvalidation": "./src/client/composables/useRealtimeQueryInvalidation.js",
|
|
36
37
|
"./client/composables/useSurfaceRouteContext": "./src/client/composables/useSurfaceRouteContext.js",
|
|
@@ -46,12 +47,12 @@
|
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@mdi/js": "^7.4.47",
|
|
49
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
50
|
-
"@jskit-ai/kernel": "0.1.
|
|
51
|
-
"@jskit-ai/realtime": "0.1.
|
|
52
|
-
"@jskit-ai/shell-web": "0.1.
|
|
53
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
54
|
-
"@jskit-ai/users-core": "0.1.
|
|
50
|
+
"@jskit-ai/http-runtime": "0.1.145",
|
|
51
|
+
"@jskit-ai/kernel": "0.1.146",
|
|
52
|
+
"@jskit-ai/realtime": "0.1.143",
|
|
53
|
+
"@jskit-ai/shell-web": "0.1.147",
|
|
54
|
+
"@jskit-ai/uploads-image-web": "0.1.122",
|
|
55
|
+
"@jskit-ai/users-core": "0.1.160"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"@tanstack/vue-query": "^5.90.5",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { getUsersWebHttpClient } from "../../lib/httpClient.js";
|
|
3
|
+
|
|
4
|
+
const CRUD_API_ACCESS_AUTHENTICATED = "authenticated";
|
|
5
|
+
const CRUD_API_ACCESS_PUBLIC = "public";
|
|
6
|
+
const CRUD_API_ACCESS_VALUES = Object.freeze([
|
|
7
|
+
CRUD_API_ACCESS_AUTHENTICATED,
|
|
8
|
+
CRUD_API_ACCESS_PUBLIC
|
|
9
|
+
]);
|
|
10
|
+
const publicCrudClients = new WeakSet();
|
|
11
|
+
|
|
12
|
+
function normalizeCrudApiAccess(value = "") {
|
|
13
|
+
const normalized = normalizeText(value).toLowerCase() || CRUD_API_ACCESS_AUTHENTICATED;
|
|
14
|
+
if (CRUD_API_ACCESS_VALUES.includes(normalized)) {
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new TypeError(
|
|
19
|
+
`CRUD resource apiAccess must be one of: ${CRUD_API_ACCESS_VALUES.join(", ")}. ` +
|
|
20
|
+
`Received: ${String(value || "") || "(empty)"}`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveCrudHttpClient(resource = null, { client = null } = {}) {
|
|
25
|
+
const activeClient = client || getUsersWebHttpClient();
|
|
26
|
+
if (!activeClient || typeof activeClient.request !== "function") {
|
|
27
|
+
throw new TypeError("resolveCrudHttpClient requires a client with request().");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (normalizeCrudApiAccess(resource?.apiAccess) !== CRUD_API_ACCESS_PUBLIC) {
|
|
31
|
+
return activeClient;
|
|
32
|
+
}
|
|
33
|
+
if (publicCrudClients.has(activeClient)) {
|
|
34
|
+
return activeClient;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const publicClient = Object.freeze({
|
|
38
|
+
request(url, options = {}) {
|
|
39
|
+
const requestOptions = options && typeof options === "object" && !Array.isArray(options)
|
|
40
|
+
? options
|
|
41
|
+
: {};
|
|
42
|
+
return activeClient.request(url, {
|
|
43
|
+
...requestOptions,
|
|
44
|
+
csrf: false
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
publicCrudClients.add(publicClient);
|
|
49
|
+
return publicClient;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
CRUD_API_ACCESS_AUTHENTICATED,
|
|
54
|
+
CRUD_API_ACCESS_PUBLIC,
|
|
55
|
+
normalizeCrudApiAccess,
|
|
56
|
+
resolveCrudHttpClient
|
|
57
|
+
};
|
|
@@ -115,6 +115,37 @@ function toDateTimeLocalInputValue(value) {
|
|
|
115
115
|
].join("-") + `T${padDateTimePart(date.getHours())}:${padDateTimePart(date.getMinutes())}`;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function toDateInputValue(value) {
|
|
119
|
+
if (value == null || value === "") {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (value instanceof Date) {
|
|
124
|
+
return Number.isNaN(value.getTime())
|
|
125
|
+
? String(value)
|
|
126
|
+
: value.toISOString().slice(0, 10);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const normalized = String(value).trim();
|
|
130
|
+
if (!normalized) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const calendarDateMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})(?:$|[T\s])/u);
|
|
135
|
+
if (calendarDateMatch) {
|
|
136
|
+
const calendarDate = calendarDateMatch[1];
|
|
137
|
+
const parsedCalendarDate = new Date(`${calendarDate}T00:00:00.000Z`);
|
|
138
|
+
if (
|
|
139
|
+
!Number.isNaN(parsedCalendarDate.getTime()) &&
|
|
140
|
+
parsedCalendarDate.toISOString().slice(0, 10) === calendarDate
|
|
141
|
+
) {
|
|
142
|
+
return calendarDate;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return normalized;
|
|
147
|
+
}
|
|
148
|
+
|
|
118
149
|
function toIsoUtcDateTimeValue(value) {
|
|
119
150
|
const normalized = String(value ?? "").trim();
|
|
120
151
|
if (!normalized) {
|
|
@@ -299,6 +330,11 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
|
|
|
299
330
|
continue;
|
|
300
331
|
}
|
|
301
332
|
|
|
333
|
+
if (fieldFormat === "date") {
|
|
334
|
+
targetModel[fieldKey] = toDateInputValue(rawValue);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
302
338
|
if (fieldFormat === "date-time") {
|
|
303
339
|
targetModel[fieldKey] = toDateTimeLocalInputValue(rawValue);
|
|
304
340
|
continue;
|
|
@@ -2,6 +2,7 @@ import { computed, proxyRefs, reactive, watch } from "vue";
|
|
|
2
2
|
import { useRoute, useRouter } from "vue-router";
|
|
3
3
|
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
4
4
|
import { resolveCrudJsonApiTransport } from "../crud/crudJsonApiTransportSupport.js";
|
|
5
|
+
import { resolveCrudHttpClient } from "../crud/crudHttpClientSupport.js";
|
|
5
6
|
import { useAddEdit } from "./useAddEdit.js";
|
|
6
7
|
import {
|
|
7
8
|
resolveCrudBoundValues,
|
|
@@ -186,6 +187,9 @@ function useCrudAddEdit({
|
|
|
186
187
|
const addEdit = useAddEdit({
|
|
187
188
|
...normalizedAddEditOptions,
|
|
188
189
|
resource: resolvedResource,
|
|
190
|
+
client: resolveCrudHttpClient(resolvedResource, {
|
|
191
|
+
client: normalizedAddEditOptions.client
|
|
192
|
+
}),
|
|
189
193
|
transport: resolvedTransport,
|
|
190
194
|
model: form,
|
|
191
195
|
fieldErrorKeys,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed, unref } from "vue";
|
|
2
2
|
import { useRoute } from "vue-router";
|
|
3
3
|
import { resolveCrudJsonApiTransport } from "../crud/crudJsonApiTransportSupport.js";
|
|
4
|
+
import { resolveCrudHttpClient } from "../crud/crudHttpClientSupport.js";
|
|
4
5
|
import {
|
|
5
6
|
resolveLookupFieldDisplayValue,
|
|
6
7
|
resolveRecordTitle
|
|
@@ -71,6 +72,9 @@ function useCrudList({
|
|
|
71
72
|
});
|
|
72
73
|
const records = useList({
|
|
73
74
|
...listOptions,
|
|
75
|
+
client: resolveCrudHttpClient(resource, {
|
|
76
|
+
client: listOptions.client
|
|
77
|
+
}),
|
|
74
78
|
transport: resolveCrudJsonApiTransport(listOptions.transport, resource, {
|
|
75
79
|
mode: "list"
|
|
76
80
|
}),
|
|
@@ -3,6 +3,7 @@ import { useRoute } from "vue-router";
|
|
|
3
3
|
import {
|
|
4
4
|
resolveCrudJsonApiTransport
|
|
5
5
|
} from "../crud/crudJsonApiTransportSupport.js";
|
|
6
|
+
import { resolveCrudHttpClient } from "../crud/crudHttpClientSupport.js";
|
|
6
7
|
import {
|
|
7
8
|
resolveLookupFieldDisplayValue,
|
|
8
9
|
resolveRecordTitle
|
|
@@ -30,6 +31,9 @@ function useCrudView({
|
|
|
30
31
|
const view = useView({
|
|
31
32
|
...viewOptions,
|
|
32
33
|
resource,
|
|
34
|
+
client: resolveCrudHttpClient(resource, {
|
|
35
|
+
client: viewOptions.client
|
|
36
|
+
}),
|
|
33
37
|
transport: resolveCrudJsonApiTransport(viewOptions.transport, resource, {
|
|
34
38
|
mode: "view"
|
|
35
39
|
}),
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
setupRouteChangeCleanup,
|
|
10
10
|
setupOperationErrorReporting
|
|
11
11
|
} from "./runtime/operationUiHelpers.js";
|
|
12
|
+
import { resolveCrudHttpClient } from "./crud/crudHttpClientSupport.js";
|
|
12
13
|
|
|
13
14
|
function useCommand({
|
|
14
15
|
ownershipFilter = ROUTE_VISIBILITY_WORKSPACE,
|
|
@@ -18,6 +19,7 @@ function useCommand({
|
|
|
18
19
|
runPermissions = [],
|
|
19
20
|
writeMethod = "POST",
|
|
20
21
|
client = null,
|
|
22
|
+
resource: commandResource = null,
|
|
21
23
|
transport = null,
|
|
22
24
|
placementSource = "users-web.command",
|
|
23
25
|
fallbackRunError = "Unable to complete action.",
|
|
@@ -50,10 +52,10 @@ function useCommand({
|
|
|
50
52
|
const routeContext = operationScope.routeContext;
|
|
51
53
|
const canRun = operationScope.permissionGate("run");
|
|
52
54
|
|
|
53
|
-
const
|
|
55
|
+
const endpointResource = useEndpointResource({
|
|
54
56
|
path: operationScope.apiPath,
|
|
55
57
|
enabled: false,
|
|
56
|
-
client,
|
|
58
|
+
client: resolveCrudHttpClient(commandResource, { client }),
|
|
57
59
|
writeMethod,
|
|
58
60
|
transport,
|
|
59
61
|
fallbackSaveError: fallbackRunError
|
|
@@ -66,7 +68,7 @@ function useCommand({
|
|
|
66
68
|
|
|
67
69
|
const command = useCommandCore({
|
|
68
70
|
model,
|
|
69
|
-
resource,
|
|
71
|
+
resource: endpointResource,
|
|
70
72
|
writeMethod,
|
|
71
73
|
canRun,
|
|
72
74
|
fieldBag,
|
|
@@ -109,7 +111,7 @@ function useCommand({
|
|
|
109
111
|
message: command.message,
|
|
110
112
|
messageType: command.messageType,
|
|
111
113
|
run: command.run,
|
|
112
|
-
resource
|
|
114
|
+
resource: endpointResource
|
|
113
115
|
});
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -3,6 +3,15 @@ import { useCrudList } from "./records/useCrudList.js";
|
|
|
3
3
|
import { useCrudListBulkActions } from "./useCrudListBulkActions.js";
|
|
4
4
|
import { useCrudListFilters } from "./useCrudListFilters.js";
|
|
5
5
|
import { useCrudListRowActions } from "./useCrudListRowActions.js";
|
|
6
|
+
import { resolveCrudHttpClient } from "./crud/crudHttpClientSupport.js";
|
|
7
|
+
|
|
8
|
+
function buildCrudListActionContext(records, client) {
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
records,
|
|
11
|
+
reload: records.reload,
|
|
12
|
+
client
|
|
13
|
+
});
|
|
14
|
+
}
|
|
6
15
|
|
|
7
16
|
function formatCrudListCardValue(value) {
|
|
8
17
|
if (value === null || value === undefined || value === "") {
|
|
@@ -102,6 +111,7 @@ function createCrudListDisplayRow(record = {}, index = 0, records = {}) {
|
|
|
102
111
|
|
|
103
112
|
function useCrudListScreen({
|
|
104
113
|
adapter = null,
|
|
114
|
+
client = null,
|
|
105
115
|
resource = null,
|
|
106
116
|
resourceNamespace = "resource",
|
|
107
117
|
apiSuffix = "",
|
|
@@ -123,6 +133,7 @@ function useCrudListScreen({
|
|
|
123
133
|
requestRecoveryLabel = "Records",
|
|
124
134
|
fallbackLoadError = "Unable to load records."
|
|
125
135
|
} = {}) {
|
|
136
|
+
const crudHttpClient = resolveCrudHttpClient(resource, { client });
|
|
126
137
|
const filterRuntime = useCrudListFilters(listFilters);
|
|
127
138
|
const normalizedRecordChangedEvents = Array.isArray(recordChangedEvents)
|
|
128
139
|
? recordChangedEvents
|
|
@@ -130,6 +141,7 @@ function useCrudListScreen({
|
|
|
130
141
|
const normalizedResourceNamespace = String(resourceNamespace || "resource").trim() || "resource";
|
|
131
142
|
const records = useCrudList({
|
|
132
143
|
adapter: adapter || undefined,
|
|
144
|
+
client: crudHttpClient,
|
|
133
145
|
resource,
|
|
134
146
|
apiSuffix,
|
|
135
147
|
queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
|
|
@@ -184,17 +196,11 @@ function useCrudListScreen({
|
|
|
184
196
|
);
|
|
185
197
|
const bulkActions = useCrudListBulkActions(listBulkActions, {
|
|
186
198
|
resolveRecordId: (record, index) => records.resolveRowKey(record, index),
|
|
187
|
-
resolveContext: () => (
|
|
188
|
-
records,
|
|
189
|
-
reload: records.reload
|
|
190
|
-
})
|
|
199
|
+
resolveContext: () => buildCrudListActionContext(records, crudHttpClient)
|
|
191
200
|
});
|
|
192
201
|
const rowActions = useCrudListRowActions(listRowActions, {
|
|
193
202
|
resolveRecordId: (record, index) => records.resolveRowKey(record, index),
|
|
194
|
-
resolveContext: () => (
|
|
195
|
-
records,
|
|
196
|
-
reload: records.reload
|
|
197
|
-
})
|
|
203
|
+
resolveContext: () => buildCrudListActionContext(records, crudHttpClient)
|
|
198
204
|
});
|
|
199
205
|
const listPrimaryAction = computed(() =>
|
|
200
206
|
newUrlTemplate ? records.resolveParams(newUrlTemplate) : ""
|
|
@@ -224,4 +230,8 @@ function useCrudListScreen({
|
|
|
224
230
|
});
|
|
225
231
|
}
|
|
226
232
|
|
|
227
|
-
|
|
233
|
+
const __testables = Object.freeze({
|
|
234
|
+
buildCrudListActionContext
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
export { __testables, useCrudListScreen };
|
package/src/client/index.js
CHANGED
|
@@ -7,6 +7,10 @@ export { default as CrudListBulkActionSurface } from "./components/CrudListBulkA
|
|
|
7
7
|
export { default as CrudListFilterSurface } from "./components/CrudListFilterSurface.vue";
|
|
8
8
|
export { default as CrudListScreen } from "./components/CrudListScreen.vue";
|
|
9
9
|
export { default as CrudViewScreen } from "./components/CrudViewScreen.vue";
|
|
10
|
+
export {
|
|
11
|
+
normalizeCrudApiAccess,
|
|
12
|
+
resolveCrudHttpClient
|
|
13
|
+
} from "./composables/crud/crudHttpClientSupport.js";
|
|
10
14
|
|
|
11
15
|
const clientProviders = Object.freeze([UsersWebClientProvider]);
|
|
12
16
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
normalizeCrudApiAccess,
|
|
6
|
+
resolveCrudHttpClient
|
|
7
|
+
} from "../src/client/composables/crud/crudHttpClientSupport.js";
|
|
8
|
+
import {
|
|
9
|
+
__testables as crudListScreenTestables
|
|
10
|
+
} from "../src/client/composables/useCrudListScreen.js";
|
|
11
|
+
|
|
12
|
+
test("resolveCrudHttpClient injects per-request CSRF opt-out for public resources", async () => {
|
|
13
|
+
const calls = [];
|
|
14
|
+
const configuredClient = {
|
|
15
|
+
async request(url, options) {
|
|
16
|
+
calls.push([url, options]);
|
|
17
|
+
return { ok: true };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const client = resolveCrudHttpClient({ apiAccess: "public" }, {
|
|
21
|
+
client: configuredClient
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
for (const method of ["POST", "PATCH", "DELETE"]) {
|
|
25
|
+
await client.request("/api/books/1", {
|
|
26
|
+
method,
|
|
27
|
+
csrf: true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
calls.map(([url, options]) => [url, options.method, options.csrf]),
|
|
33
|
+
[
|
|
34
|
+
["/api/books/1", "POST", false],
|
|
35
|
+
["/api/books/1", "PATCH", false],
|
|
36
|
+
["/api/books/1", "DELETE", false]
|
|
37
|
+
]
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("resolveCrudHttpClient preserves the configured client for authenticated resources", () => {
|
|
42
|
+
const configuredClient = {
|
|
43
|
+
request() {}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
assert.equal(
|
|
47
|
+
resolveCrudHttpClient({ apiAccess: "authenticated" }, { client: configuredClient }),
|
|
48
|
+
configuredClient
|
|
49
|
+
);
|
|
50
|
+
assert.equal(
|
|
51
|
+
resolveCrudHttpClient({}, { client: configuredClient }),
|
|
52
|
+
configuredClient
|
|
53
|
+
);
|
|
54
|
+
assert.equal(normalizeCrudApiAccess(""), "authenticated");
|
|
55
|
+
assert.throws(
|
|
56
|
+
() => resolveCrudHttpClient({ apiAccess: "always" }, { client: configuredClient }),
|
|
57
|
+
/apiAccess must be one of: authenticated, public/
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("CRUD list row and bulk action context carries the resource-aware client", () => {
|
|
62
|
+
const records = {
|
|
63
|
+
reload() {}
|
|
64
|
+
};
|
|
65
|
+
const client = {
|
|
66
|
+
request() {}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(
|
|
70
|
+
crudListScreenTestables.buildCrudListActionContext(records, client),
|
|
71
|
+
{
|
|
72
|
+
records,
|
|
73
|
+
reload: records.reload,
|
|
74
|
+
client
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
});
|
|
@@ -39,6 +39,7 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
39
39
|
"./client/composables/useCrudListFilters",
|
|
40
40
|
"./client/composables/useCrudList",
|
|
41
41
|
"./client/composables/useCrudListScreen",
|
|
42
|
+
"./client/crudHttpClient",
|
|
42
43
|
"./client/composables/useCrudView",
|
|
43
44
|
"./client/composables/useCrudViewScreen",
|
|
44
45
|
"./client/lib/httpClient"
|
|
@@ -403,3 +403,55 @@ test("commands use the configured users-web HTTP client by default", async () =>
|
|
|
403
403
|
resetUsersWebHttpClientForTests();
|
|
404
404
|
}
|
|
405
405
|
});
|
|
406
|
+
|
|
407
|
+
test("commands use public CRUD resource access without a CSRF session preflight", async () => {
|
|
408
|
+
const queryClient = new QueryClient();
|
|
409
|
+
const calls = [];
|
|
410
|
+
let command = null;
|
|
411
|
+
configureUsersWebHttpClient({
|
|
412
|
+
fetchImpl: async (url, options) => {
|
|
413
|
+
calls.push([url, options]);
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
status: 204,
|
|
417
|
+
headers: {
|
|
418
|
+
get() {
|
|
419
|
+
return "";
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
async json() {
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const app = createSSRApp({
|
|
431
|
+
setup() {
|
|
432
|
+
command = useCommand({
|
|
433
|
+
access: "always",
|
|
434
|
+
apiSuffix: "/books/1",
|
|
435
|
+
writeMethod: "DELETE",
|
|
436
|
+
resource: {
|
|
437
|
+
apiAccess: "public"
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return () => h("div");
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
app.use(VueQueryPlugin, {
|
|
444
|
+
queryClient
|
|
445
|
+
});
|
|
446
|
+
await renderToString(app);
|
|
447
|
+
|
|
448
|
+
await command.run();
|
|
449
|
+
|
|
450
|
+
assert.equal(calls.length, 1);
|
|
451
|
+
assert.equal(calls[0][0], "/api/books/1");
|
|
452
|
+
assert.equal(calls[0][1].method, "DELETE");
|
|
453
|
+
assert.equal(Object.hasOwn(calls[0][1], "csrf"), false);
|
|
454
|
+
} finally {
|
|
455
|
+
resetUsersWebHttpClientForTests();
|
|
456
|
+
}
|
|
457
|
+
});
|
|
@@ -108,6 +108,35 @@ 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("applyCrudPayloadToForm normalizes date values for HTML date inputs", () => {
|
|
112
|
+
const fields = [
|
|
113
|
+
{ key: "publishedOn", type: "string", format: "date" },
|
|
114
|
+
{ key: "reviewedOn", type: "string", format: "date" },
|
|
115
|
+
{ key: "archivedOn", type: "string", format: "date" },
|
|
116
|
+
{ key: "invalidOn", type: "string", format: "date" }
|
|
117
|
+
];
|
|
118
|
+
const form = reactive({
|
|
119
|
+
publishedOn: "",
|
|
120
|
+
reviewedOn: "",
|
|
121
|
+
archivedOn: "",
|
|
122
|
+
invalidOn: ""
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
applyCrudPayloadToForm(fields, form, {
|
|
126
|
+
publishedOn: "2026-08-10",
|
|
127
|
+
reviewedOn: "2026-08-11T00:00:00.000Z",
|
|
128
|
+
archivedOn: new Date("2026-08-12T00:00:00.000Z"),
|
|
129
|
+
invalidOn: "not-a-date"
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.deepEqual(form, {
|
|
133
|
+
publishedOn: "2026-08-10",
|
|
134
|
+
reviewedOn: "2026-08-11",
|
|
135
|
+
archivedOn: "2026-08-12",
|
|
136
|
+
invalidOn: "not-a-date"
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
111
140
|
test("buildCrudFormPayload normalizes time fields to canonical HH:MM", () => {
|
|
112
141
|
const fields = [
|
|
113
142
|
{ key: "fromTime", type: "string", format: "time" },
|