@jskit-ai/users-web 0.1.70 → 0.1.71

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 (29) hide show
  1. package/package.descriptor.mjs +7 -7
  2. package/package.json +11 -8
  3. package/src/client/bootstrap/user-bootstrap-handler.js +53 -0
  4. package/src/client/composables/crud/crudSchemaFormHelpers.js +1 -22
  5. package/src/client/composables/internal/crudListParentTitleSupport.js +14 -15
  6. package/src/client/composables/records/useAddEdit.js +23 -4
  7. package/src/client/composables/records/useCrudAddEdit.js +5 -20
  8. package/src/client/composables/records/useList.js +22 -22
  9. package/src/client/composables/records/useView.js +13 -27
  10. package/src/client/composables/runtime/operationValidationHelpers.js +18 -19
  11. package/src/client/composables/runtime/useAddEditCore.js +6 -10
  12. package/src/client/composables/runtime/useCommandCore.js +3 -10
  13. package/src/client/composables/runtime/useEndpointResource.js +75 -14
  14. package/src/client/composables/runtime/useListCore.js +45 -17
  15. package/src/client/composables/support/requestQueryRuntimeSupport.js +100 -0
  16. package/src/client/composables/useAccountSettingsRuntime.js +26 -19
  17. package/src/client/composables/useCommand.js +4 -2
  18. package/src/client/composables/useCrudListFilters.js +58 -255
  19. package/src/client/providers/UsersWebClientProvider.js +4 -2
  20. package/test/bootstrap.test.js +130 -0
  21. package/test/operationValidationHelpers.test.js +64 -0
  22. package/test/requestTransportOptions.test.js +107 -0
  23. package/test/useAddEditCore.test.js +124 -0
  24. package/test/useAddEditRequestQueryParams.test.js +162 -0
  25. package/test/useCrudAddEdit.test.js +1 -51
  26. package/test/useCrudListFilters.test.js +33 -4
  27. package/test/useCrudListParentTitle.test.js +40 -27
  28. package/test/useViewRequestQueryParams.test.js +10 -10
  29. package/src/client/composables/support/requestQueryPathSupport.js +0 -31
@@ -9,7 +9,7 @@ function useCommandCore({
9
9
  canRun,
10
10
  fieldBag,
11
11
  feedback,
12
- parseInput,
12
+ input,
13
13
  buildRawPayload,
14
14
  buildCommandPayload,
15
15
  buildCommandOptions,
@@ -43,13 +43,8 @@ function useCommandCore({
43
43
  : {};
44
44
 
45
45
  const validationResult = validateOperationInput({
46
- parseInput,
46
+ input,
47
47
  rawPayload,
48
- context: {
49
- queryClient,
50
- resource,
51
- context
52
- },
53
48
  fieldBag,
54
49
  feedback,
55
50
  validationMessage: String(messages.validation || "Validation failed.")
@@ -58,7 +53,7 @@ function useCommandCore({
58
53
  return null;
59
54
  }
60
55
 
61
- const { parseResult, parsedInput } = validationResult;
56
+ const { parsedInput } = validationResult;
62
57
  const payload = typeof buildCommandPayload === "function"
63
58
  ? buildCommandPayload(parsedInput, {
64
59
  rawPayload,
@@ -83,7 +78,6 @@ function useCommandCore({
83
78
  if (typeof onRunSuccess === "function") {
84
79
  await onRunSuccess(response, {
85
80
  parsed: parsedInput,
86
- parseResult,
87
81
  rawPayload,
88
82
  payload,
89
83
  options,
@@ -103,7 +97,6 @@ function useCommandCore({
103
97
  if (typeof onRunError === "function") {
104
98
  await onRunError(error, {
105
99
  parsed: parsedInput,
106
- parseResult,
107
100
  rawPayload,
108
101
  payload,
109
102
  options,
@@ -1,4 +1,4 @@
1
- import { computed } from "vue";
1
+ import { computed, unref } from "vue";
2
2
  import { useMutation, useQuery } from "@tanstack/vue-query";
3
3
  import { usersWebHttpClient } from "../../lib/httpClient.js";
4
4
  import { asPlainObject } from "../support/scopeHelpers.js";
@@ -6,6 +6,58 @@ import { resolveEnabledRef, resolveTextRef } from "../support/refValueHelpers.js
6
6
  import { toQueryErrorMessage } from "../support/errorMessageHelpers.js";
7
7
  import { hasResolvedQueryData } from "../support/resourceLoadStateHelpers.js";
8
8
 
9
+ function buildEndpointReadRequestOptions({
10
+ method = "GET",
11
+ query = null,
12
+ transport = null
13
+ } = {}) {
14
+ const requestOptions = {
15
+ method: String(method || "GET").toUpperCase()
16
+ };
17
+
18
+ const normalizedQuery = resolveRequestQuery(query);
19
+ if (normalizedQuery) {
20
+ requestOptions.query = normalizedQuery;
21
+ }
22
+
23
+ if (transport && typeof transport === "object") {
24
+ requestOptions.transport = transport;
25
+ }
26
+
27
+ return requestOptions;
28
+ }
29
+
30
+ function resolveRequestQuery(value = null) {
31
+ const source = unref(value);
32
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
33
+ return null;
34
+ }
35
+
36
+ return source;
37
+ }
38
+
39
+ function buildEndpointWriteRequestOptions({
40
+ method = "PATCH",
41
+ body = undefined,
42
+ options = null,
43
+ transport = null
44
+ } = {}) {
45
+ const requestOptions = {
46
+ method: String(method || "PATCH").toUpperCase(),
47
+ ...(asPlainObject(options))
48
+ };
49
+
50
+ if (body !== undefined) {
51
+ requestOptions.body = body;
52
+ }
53
+
54
+ if (transport && typeof transport === "object") {
55
+ requestOptions.transport = transport;
56
+ }
57
+
58
+ return requestOptions;
59
+ }
60
+
9
61
  function useEndpointResource({
10
62
  queryKey,
11
63
  path = "",
@@ -13,6 +65,8 @@ function useEndpointResource({
13
65
  client = usersWebHttpClient,
14
66
  readMethod = "GET",
15
67
  writeMethod = "PATCH",
68
+ readQuery = null,
69
+ transport = null,
16
70
  queryOptions = null,
17
71
  mutationOptions = null,
18
72
  fallbackLoadError = "Unable to load resource.",
@@ -34,7 +88,11 @@ function useEndpointResource({
34
88
  }
35
89
 
36
90
  return client.request(requestPath, {
37
- method: String(readMethod || "GET").toUpperCase()
91
+ ...buildEndpointReadRequestOptions({
92
+ method: readMethod,
93
+ query: readQuery,
94
+ transport
95
+ })
38
96
  });
39
97
  },
40
98
  enabled: queryEnabled,
@@ -50,18 +108,17 @@ function useEndpointResource({
50
108
  }
51
109
 
52
110
  const method = String(options.method || writeMethod || "PATCH").toUpperCase();
53
- const hasBody = Object.prototype.hasOwnProperty.call(options, "body");
111
+ const hasBody = Object.hasOwn(options, "body");
54
112
  const body = hasBody ? options.body : options.payload;
55
- const requestOptions = {
56
- method,
57
- ...(asPlainObject(options.options))
58
- };
59
-
60
- if (body !== undefined) {
61
- requestOptions.body = body;
62
- }
63
-
64
- return client.request(requestPath, requestOptions);
113
+ return client.request(
114
+ requestPath,
115
+ buildEndpointWriteRequestOptions({
116
+ method,
117
+ body,
118
+ options: options.options,
119
+ transport
120
+ })
121
+ );
65
122
  },
66
123
  ...(asPlainObject(mutationOptions))
67
124
  });
@@ -106,4 +163,8 @@ function useEndpointResource({
106
163
  });
107
164
  }
108
165
 
109
- export { useEndpointResource };
166
+ export {
167
+ buildEndpointReadRequestOptions,
168
+ buildEndpointWriteRequestOptions,
169
+ useEndpointResource
170
+ };
@@ -1,22 +1,42 @@
1
- import { computed } from "vue";
2
- import { appendQueryString } from "@jskit-ai/kernel/shared/support";
1
+ import { computed, unref } from "vue";
3
2
  import { usersWebHttpClient } from "../../lib/httpClient.js";
4
3
  import { asPlainObject } from "../support/scopeHelpers.js";
5
4
  import { resolveEnabledRef, resolveTextRef } from "../support/refValueHelpers.js";
6
5
  import { usePagedCollection } from "../usePagedCollection.js";
7
6
 
8
- function appendPageParam(path, pageParam) {
9
- const normalizedPath = String(path || "").trim();
10
- if (!normalizedPath) {
11
- return "";
7
+ function buildListRequestOptions({
8
+ requestOptions = null,
9
+ transport = null,
10
+ pageParam = null
11
+ } = {}) {
12
+ const resolvedOptions = {
13
+ method: "GET",
14
+ ...(resolveRequestOptionsObject(requestOptions))
15
+ };
16
+
17
+ const sourceQuery =
18
+ resolvedOptions.query && typeof resolvedOptions.query === "object" && !Array.isArray(resolvedOptions.query)
19
+ ? { ...resolvedOptions.query }
20
+ : {};
21
+ if (pageParam !== null && pageParam !== undefined && pageParam !== "") {
22
+ sourceQuery.cursor = String(pageParam);
12
23
  }
13
- if (pageParam === null || pageParam === undefined || pageParam === "") {
14
- return normalizedPath;
24
+ if (Object.keys(sourceQuery).length > 0) {
25
+ resolvedOptions.query = sourceQuery;
26
+ } else {
27
+ delete resolvedOptions.query;
15
28
  }
16
29
 
17
- const query = new URLSearchParams();
18
- query.set("cursor", String(pageParam));
19
- return appendQueryString(normalizedPath, query.toString());
30
+ if (transport && typeof transport === "object") {
31
+ resolvedOptions.transport = transport;
32
+ }
33
+
34
+ return resolvedOptions;
35
+ }
36
+
37
+ function resolveRequestOptionsObject(value = null) {
38
+ const source = unref(value);
39
+ return asPlainObject(source);
20
40
  }
21
41
 
22
42
  function useListCore({
@@ -24,6 +44,7 @@ function useListCore({
24
44
  path = "",
25
45
  enabled = true,
26
46
  client = usersWebHttpClient,
47
+ transport = null,
27
48
  initialPageParam = null,
28
49
  getNextPageParam,
29
50
  selectItems,
@@ -43,15 +64,19 @@ function useListCore({
43
64
  initialPageParam,
44
65
  enabled: queryEnabled,
45
66
  queryFn: async ({ pageParam }) => {
46
- const requestPath = appendPageParam(normalizedPath.value, pageParam);
67
+ const requestPath = normalizedPath.value;
47
68
  if (!requestPath) {
48
69
  throw new Error("List path is required.");
49
70
  }
50
71
 
51
- return client.request(requestPath, {
52
- method: "GET",
53
- ...(asPlainObject(requestOptions))
54
- });
72
+ return client.request(
73
+ requestPath,
74
+ buildListRequestOptions({
75
+ requestOptions,
76
+ transport,
77
+ pageParam
78
+ })
79
+ );
55
80
  },
56
81
  getNextPageParam,
57
82
  selectItems,
@@ -62,4 +87,7 @@ function useListCore({
62
87
  return collection;
63
88
  }
64
89
 
65
- export { useListCore };
90
+ export {
91
+ buildListRequestOptions,
92
+ useListCore
93
+ };
@@ -0,0 +1,100 @@
1
+ import { computed, unref } from "vue";
2
+ import {
3
+ resolveQueryParamDescriptors,
4
+ resolveActiveQueryParamEntries,
5
+ buildQueryParamEntriesToken
6
+ } from "./listQueryParamSupport.js";
7
+
8
+ function resolveRequestQueryContext(context = null) {
9
+ const source = unref(context);
10
+ return source && typeof source === "object" && !Array.isArray(source) ? source : {};
11
+ }
12
+
13
+ function resolveRequestQueryBaseKey(sourceQueryKey = null) {
14
+ const source = unref(sourceQueryKey);
15
+ if (Array.isArray(source)) {
16
+ return [...source];
17
+ }
18
+ if (source == null) {
19
+ return [];
20
+ }
21
+ return [source];
22
+ }
23
+
24
+ function appendRequestQueryValue(target = {}, key = "", values = []) {
25
+ const normalizedKey = String(key || "").trim();
26
+ const normalizedValues = (Array.isArray(values) ? values : [])
27
+ .map((value) => String(value ?? "").trim())
28
+ .filter(Boolean);
29
+ if (!normalizedKey || normalizedValues.length < 1) {
30
+ return;
31
+ }
32
+
33
+ const currentValue = target[normalizedKey];
34
+ if (currentValue === undefined) {
35
+ target[normalizedKey] = normalizedValues.length === 1 ? normalizedValues[0] : [...normalizedValues];
36
+ return;
37
+ }
38
+
39
+ const currentValues = Array.isArray(currentValue) ? [...currentValue] : [currentValue];
40
+ target[normalizedKey] = [...currentValues, ...normalizedValues];
41
+ }
42
+
43
+ function buildRequestQueryObject(entries = []) {
44
+ const sourceEntries = Array.isArray(entries) ? entries : [];
45
+ const query = {};
46
+
47
+ for (const entry of sourceEntries) {
48
+ appendRequestQueryValue(query, entry?.key, entry?.values);
49
+ }
50
+
51
+ return Object.freeze(query);
52
+ }
53
+
54
+ function createRequestQueryRuntime({
55
+ requestQueryParams = null,
56
+ context = null,
57
+ sourceQueryKey = null
58
+ } = {}) {
59
+ const requestQueryParamDescriptors = computed(() => {
60
+ return resolveQueryParamDescriptors(requestQueryParams, resolveRequestQueryContext(context));
61
+ });
62
+ const activeRequestQueryParamEntries = computed(() => {
63
+ return resolveActiveQueryParamEntries(requestQueryParamDescriptors.value);
64
+ });
65
+ const activeRequestQueryParamsToken = computed(() => {
66
+ return buildQueryParamEntriesToken(activeRequestQueryParamEntries.value);
67
+ });
68
+ const queryKey = computed(() => {
69
+ if (!activeRequestQueryParamsToken.value) {
70
+ return unref(sourceQueryKey);
71
+ }
72
+
73
+ const next = resolveRequestQueryBaseKey(sourceQueryKey);
74
+ next.push("__request_query__", activeRequestQueryParamsToken.value);
75
+ return next;
76
+ });
77
+ const requestQuery = computed(() => {
78
+ if (activeRequestQueryParamEntries.value.length < 1) {
79
+ return null;
80
+ }
81
+
82
+ return buildRequestQueryObject(activeRequestQueryParamEntries.value);
83
+ });
84
+
85
+ return Object.freeze({
86
+ requestQueryParamDescriptors,
87
+ activeRequestQueryParamEntries,
88
+ activeRequestQueryParamsToken,
89
+ queryKey,
90
+ requestQuery
91
+ });
92
+ }
93
+
94
+ export {
95
+ appendRequestQueryValue,
96
+ buildRequestQueryObject,
97
+ createRequestQueryRuntime,
98
+ resolveRequestQueryBaseKey,
99
+ resolveRequestQueryContext
100
+ };
@@ -7,7 +7,6 @@ import {
7
7
  resolveSurfaceNavigationTargetFromPlacementContext
8
8
  } from "@jskit-ai/shell-web/client/placement";
9
9
  import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
10
- import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
11
10
  import { ROUTE_VISIBILITY_PUBLIC } from "@jskit-ai/kernel/shared/support/visibility";
12
11
  import { userProfileResource } from "@jskit-ai/users-core/shared/resources/userProfileResource";
13
12
  import { userSettingsResource } from "@jskit-ai/users-core/shared/resources/userSettingsResource";
@@ -52,6 +51,24 @@ function useAccountSettingsRuntime() {
52
51
  const accountNotificationsWriteQueryKey = ["users-web", "settings", "account-notifications-write"];
53
52
  const sessionQueryKey = Object.freeze(["users-web", "session", "csrf"]);
54
53
  const OWNERSHIP_PUBLIC = ROUTE_VISIBILITY_PUBLIC;
54
+ const USER_SETTINGS_TRANSPORT = Object.freeze({
55
+ kind: "jsonapi-resource",
56
+ responseType: "user-settings",
57
+ responseKind: "record"
58
+ });
59
+ const USER_PROFILE_UPDATE_TRANSPORT = Object.freeze({
60
+ ...USER_SETTINGS_TRANSPORT,
61
+ requestType: "user-profiles"
62
+ });
63
+ const USER_PREFERENCES_UPDATE_TRANSPORT = Object.freeze({
64
+ ...USER_SETTINGS_TRANSPORT,
65
+ requestType: "user-preferences"
66
+ });
67
+ const USER_NOTIFICATION_SETTINGS_UPDATE_TRANSPORT = Object.freeze({
68
+ ...USER_SETTINGS_TRANSPORT,
69
+ requestType: "user-notification-settings"
70
+ });
71
+ const USER_PROFILE_AVATAR_DELETE_TRANSPORT = USER_SETTINGS_TRANSPORT;
55
72
 
56
73
  const accountSettingsPath = computed(() =>
57
74
  resolveSurfacePathFromPlacementContext(placementContext.value, "account", "/")
@@ -184,6 +201,7 @@ function useAccountSettingsRuntime() {
184
201
  ownershipFilter: OWNERSHIP_PUBLIC,
185
202
  apiSuffix: "/settings",
186
203
  queryKeyFactory: () => accountSettingsQueryKey,
204
+ transport: USER_SETTINGS_TRANSPORT,
187
205
  realtime: {
188
206
  event: "account.settings.changed"
189
207
  },
@@ -198,16 +216,12 @@ function useAccountSettingsRuntime() {
198
216
  queryKeyFactory: () => accountProfileWriteQueryKey,
199
217
  readEnabled: false,
200
218
  writeMethod: "PATCH",
219
+ transport: USER_PROFILE_UPDATE_TRANSPORT,
201
220
  fallbackSaveError: "Unable to update profile.",
202
221
  fieldErrorKeys: ["displayName"],
203
222
  model: profileForm,
204
223
  mapLoadedToModel: mapAccountSettingsPayload,
205
- parseInput: (rawPayload) =>
206
- validateOperationSection({
207
- operation: userProfileResource.operations.patch,
208
- section: "bodyValidator",
209
- value: rawPayload
210
- }),
224
+ input: userProfileResource.operations.patch.body,
211
225
  buildRawPayload: (model) => ({
212
226
  displayName: String(model.displayName || "").trim()
213
227
  }),
@@ -221,6 +235,7 @@ function useAccountSettingsRuntime() {
221
235
  ownershipFilter: OWNERSHIP_PUBLIC,
222
236
  apiSuffix: "/settings/profile/avatar",
223
237
  writeMethod: "DELETE",
238
+ transport: USER_PROFILE_AVATAR_DELETE_TRANSPORT,
224
239
  fallbackRunError: "Unable to remove avatar.",
225
240
  model: profileForm,
226
241
  onRunSuccess: (payload, { queryClient: commandQueryClient }) => {
@@ -240,16 +255,12 @@ function useAccountSettingsRuntime() {
240
255
  queryKeyFactory: () => accountPreferencesWriteQueryKey,
241
256
  readEnabled: false,
242
257
  writeMethod: "PATCH",
258
+ transport: USER_PREFERENCES_UPDATE_TRANSPORT,
243
259
  fallbackSaveError: "Unable to update preferences.",
244
260
  fieldErrorKeys: ["theme", "locale", "timeZone", "dateFormat", "numberFormat", "currencyCode", "avatarSize"],
245
261
  model: preferencesForm,
246
262
  mapLoadedToModel: mapAccountSettingsPayload,
247
- parseInput: (rawPayload) =>
248
- validateOperationSection({
249
- operation: userSettingsResource.operations.preferencesUpdate,
250
- section: "bodyValidator",
251
- value: rawPayload
252
- }),
263
+ input: userSettingsResource.operations.preferencesUpdate.body,
253
264
  buildRawPayload: (model) => ({
254
265
  theme: model.theme,
255
266
  locale: model.locale,
@@ -272,15 +283,11 @@ function useAccountSettingsRuntime() {
272
283
  queryKeyFactory: () => accountNotificationsWriteQueryKey,
273
284
  readEnabled: false,
274
285
  writeMethod: "PATCH",
286
+ transport: USER_NOTIFICATION_SETTINGS_UPDATE_TRANSPORT,
275
287
  fallbackSaveError: "Unable to update notifications.",
276
288
  model: notificationsForm,
277
289
  mapLoadedToModel: mapAccountSettingsPayload,
278
- parseInput: (rawPayload) =>
279
- validateOperationSection({
280
- operation: userSettingsResource.operations.notificationsUpdate,
281
- section: "bodyValidator",
282
- value: rawPayload
283
- }),
290
+ input: userSettingsResource.operations.notificationsUpdate.body,
284
291
  buildRawPayload: (model) => ({
285
292
  productUpdates: Boolean(model.productUpdates),
286
293
  accountActivity: Boolean(model.accountActivity),
@@ -17,12 +17,13 @@ function useCommand({
17
17
  apiSuffix = "",
18
18
  runPermissions = [],
19
19
  writeMethod = "POST",
20
+ transport = null,
20
21
  placementSource = "users-web.command",
21
22
  fallbackRunError = "Unable to complete action.",
22
23
  fieldErrorKeys = [],
23
24
  clearOnRouteChange = true,
24
25
  model,
25
- parseInput,
26
+ input,
26
27
  buildRawPayload,
27
28
  buildCommandPayload,
28
29
  buildCommandOptions,
@@ -52,6 +53,7 @@ function useCommand({
52
53
  path: operationScope.apiPath,
53
54
  enabled: false,
54
55
  writeMethod,
56
+ transport,
55
57
  fallbackSaveError: fallbackRunError
56
58
  });
57
59
 
@@ -67,7 +69,7 @@ function useCommand({
67
69
  canRun,
68
70
  fieldBag,
69
71
  feedback,
70
- parseInput,
72
+ input,
71
73
  buildRawPayload,
72
74
  buildCommandPayload,
73
75
  buildCommandOptions,