@jskit-ai/users-web 0.1.37 → 0.1.38

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 (71) hide show
  1. package/package.descriptor.mjs +7 -7
  2. package/package.json +16 -12
  3. package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
  4. package/src/client/components/WorkspaceMembersClientElement.vue +3 -3
  5. package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
  6. package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
  7. package/src/client/components/WorkspacesClientElement.vue +2 -2
  8. package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +61 -0
  9. package/src/client/composables/{accountSettingsInvitesRuntime.js → account-settings/accountSettingsInvitesRuntime.js} +1 -1
  10. package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +1 -1
  11. package/src/client/composables/crud/crudBindingSupport.js +75 -0
  12. package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +1 -1
  13. package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +6 -2
  14. package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +155 -2
  15. package/src/client/composables/internal/crudListParentTitleSupport.js +168 -0
  16. package/src/client/composables/internal/useOperationScope.js +1 -1
  17. package/src/client/composables/{useAddEdit.js → records/useAddEdit.js} +9 -9
  18. package/src/client/composables/{useCrudSchemaForm.js → records/useCrudAddEdit.js} +32 -15
  19. package/src/client/composables/records/useCrudList.js +83 -0
  20. package/src/client/composables/records/useCrudView.js +35 -0
  21. package/src/client/composables/{useList.js → records/useList.js} +31 -57
  22. package/src/client/composables/{useView.js → records/useView.js} +6 -9
  23. package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +2 -2
  24. package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +2 -2
  25. package/src/client/composables/{operationAdapters.js → runtime/operationAdapters.js} +1 -1
  26. package/src/client/composables/{useEndpointResource.js → runtime/useEndpointResource.js} +5 -5
  27. package/src/client/composables/{useListCore.js → runtime/useListCore.js} +4 -4
  28. package/src/client/composables/{useUiFeedback.js → runtime/useUiFeedback.js} +1 -1
  29. package/src/client/composables/{viewUiRuntime.js → runtime/viewUiRuntime.js} +2 -2
  30. package/src/client/composables/useAccess.js +2 -2
  31. package/src/client/composables/useAccountSettingsRuntime.js +6 -6
  32. package/src/client/composables/useBootstrapQuery.js +1 -1
  33. package/src/client/composables/useCommand.js +5 -5
  34. package/src/client/composables/useCrudListParentTitle.js +131 -0
  35. package/src/client/composables/usePagedCollection.js +3 -3
  36. package/src/client/composables/useScopeRuntime.js +1 -1
  37. package/src/client/support/menuLinkTarget.js +93 -0
  38. package/test/addEditUiRuntime.test.js +1 -1
  39. package/test/crudBindingSupport.test.js +110 -0
  40. package/test/crudLookupFieldRuntime.test.js +1 -1
  41. package/test/errorMessageHelpers.test.js +1 -1
  42. package/test/exportsContract.test.js +10 -1
  43. package/test/listQueryParamSupport.test.js +1 -1
  44. package/test/listUiRuntime.test.js +1 -1
  45. package/test/menuLinkTarget.test.js +116 -0
  46. package/test/permissions.test.js +2 -2
  47. package/test/refValueHelpers.test.js +1 -1
  48. package/test/resourceLoadStateHelpers.test.js +1 -1
  49. package/test/routeTemplateHelpers.test.js +1 -1
  50. package/test/scopeHelpers.test.js +1 -1
  51. package/test/{useCrudSchemaForm.test.js → useCrudAddEdit.test.js} +81 -1
  52. package/test/useCrudListParentTitle.test.js +143 -0
  53. package/test/useListSearchSupport.test.js +1 -1
  54. package/test/viewCoreLoading.test.js +1 -1
  55. package/test/viewUiRuntime.test.js +1 -1
  56. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -95
  57. /package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -0
  58. /package/src/client/composables/{modelStateHelpers.js → runtime/modelStateHelpers.js} +0 -0
  59. /package/src/client/composables/{operationUiHelpers.js → runtime/operationUiHelpers.js} +0 -0
  60. /package/src/client/composables/{operationValidationHelpers.js → runtime/operationValidationHelpers.js} +0 -0
  61. /package/src/client/composables/{useAddEditCore.js → runtime/useAddEditCore.js} +0 -0
  62. /package/src/client/composables/{useCommandCore.js → runtime/useCommandCore.js} +0 -0
  63. /package/src/client/composables/{useFieldErrorBag.js → runtime/useFieldErrorBag.js} +0 -0
  64. /package/src/client/composables/{useViewCore.js → runtime/useViewCore.js} +0 -0
  65. /package/src/client/composables/{errorMessageHelpers.js → support/errorMessageHelpers.js} +0 -0
  66. /package/src/client/composables/{listQueryParamSupport.js → support/listQueryParamSupport.js} +0 -0
  67. /package/src/client/composables/{listSearchSupport.js → support/listSearchSupport.js} +0 -0
  68. /package/src/client/composables/{refValueHelpers.js → support/refValueHelpers.js} +0 -0
  69. /package/src/client/composables/{resourceLoadStateHelpers.js → support/resourceLoadStateHelpers.js} +0 -0
  70. /package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +0 -0
  71. /package/src/client/composables/{scopeHelpers.js → support/scopeHelpers.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-web",
4
- version: "0.1.37",
4
+ version: "0.1.38",
5
5
  kind: "runtime",
6
6
  description: "Users web module: workspace selector shell element plus workspace/profile/members UI elements.",
7
7
  dependsOn: [
@@ -231,12 +231,12 @@ export default Object.freeze({
231
231
  runtime: {
232
232
  "@tanstack/vue-query": "5.92.12",
233
233
  "@mdi/js": "^7.4.47",
234
- "@jskit-ai/http-runtime": "0.1.22",
235
- "@jskit-ai/realtime": "0.1.22",
236
- "@jskit-ai/kernel": "0.1.23",
237
- "@jskit-ai/shell-web": "0.1.22",
238
- "@jskit-ai/uploads-image-web": "0.1.1",
239
- "@jskit-ai/users-core": "0.1.32",
234
+ "@jskit-ai/http-runtime": "0.1.23",
235
+ "@jskit-ai/realtime": "0.1.23",
236
+ "@jskit-ai/kernel": "0.1.24",
237
+ "@jskit-ai/shell-web": "0.1.23",
238
+ "@jskit-ai/uploads-image-web": "0.1.2",
239
+ "@jskit-ai/users-core": "0.1.33",
240
240
  "vuetify": "^4.0.0"
241
241
  },
242
242
  dev: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -8,26 +8,30 @@
8
8
  "exports": {
9
9
  "./client": "./src/client/index.js",
10
10
  "./client/components/WorkspaceMembersClientElement": "./src/client/components/WorkspaceMembersClientElement.vue",
11
- "./client/composables/useAddEdit": "./src/client/composables/useAddEdit.js",
12
- "./client/composables/useCrudSchemaForm": "./src/client/composables/useCrudSchemaForm.js",
13
- "./client/composables/crudLookupFieldRuntime": "./src/client/composables/crudLookupFieldRuntime.js",
14
- "./client/composables/useList": "./src/client/composables/useList.js",
15
- "./client/composables/useView": "./src/client/composables/useView.js",
11
+ "./client/composables/useAddEdit": "./src/client/composables/records/useAddEdit.js",
12
+ "./client/composables/useCrudAddEdit": "./src/client/composables/records/useCrudAddEdit.js",
13
+ "./client/composables/crudLookupFieldRuntime": "./src/client/composables/crud/crudLookupFieldRuntime.js",
14
+ "./client/composables/useList": "./src/client/composables/records/useList.js",
15
+ "./client/composables/useCrudList": "./src/client/composables/records/useCrudList.js",
16
+ "./client/composables/useCrudListParentTitle": "./src/client/composables/useCrudListParentTitle.js",
17
+ "./client/composables/useView": "./src/client/composables/records/useView.js",
18
+ "./client/composables/useCrudView": "./src/client/composables/records/useCrudView.js",
16
19
  "./client/composables/usePagedCollection": "./src/client/composables/usePagedCollection.js",
17
20
  "./client/composables/useAccountSettingsRuntime": "./src/client/composables/useAccountSettingsRuntime.js",
18
21
  "./client/composables/usePaths": "./src/client/composables/usePaths.js",
19
22
  "./client/composables/useWorkspaceRouteContext": "./src/client/composables/useWorkspaceRouteContext.js",
23
+ "./client/support/menuLinkTarget": "./src/client/support/menuLinkTarget.js",
20
24
  "./client/support/realtimeWorkspace": "./src/client/support/realtimeWorkspace.js"
21
25
  },
22
26
  "dependencies": {
23
27
  "@tanstack/vue-query": "5.92.12",
24
28
  "@mdi/js": "^7.4.47",
25
- "@jskit-ai/http-runtime": "0.1.22",
26
- "@jskit-ai/kernel": "0.1.23",
27
- "@jskit-ai/realtime": "0.1.22",
28
- "@jskit-ai/shell-web": "0.1.22",
29
- "@jskit-ai/uploads-image-web": "0.1.1",
30
- "@jskit-ai/users-core": "0.1.32",
29
+ "@jskit-ai/http-runtime": "0.1.23",
30
+ "@jskit-ai/kernel": "0.1.24",
31
+ "@jskit-ai/realtime": "0.1.23",
32
+ "@jskit-ai/shell-web": "0.1.23",
33
+ "@jskit-ai/uploads-image-web": "0.1.2",
34
+ "@jskit-ai/users-core": "0.1.33",
31
35
  "vuetify": "^4.0.0"
32
36
  }
33
37
  }
@@ -1,9 +1,10 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
+ import { useRoute } from "vue-router";
3
4
  import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
4
5
  import { usePaths } from "../composables/usePaths.js";
5
- import { surfaceRequiresWorkspaceFromPlacementContext } from "../lib/workspaceSurfaceContext.js";
6
6
  import { resolveMenuLinkIcon } from "../lib/menuIcons.js";
7
+ import { resolveMenuLinkTarget } from "../support/menuLinkTarget.js";
7
8
 
8
9
  const props = defineProps({
9
10
  label: {
@@ -36,34 +37,22 @@ const props = defineProps({
36
37
  }
37
38
  });
38
39
 
40
+ const route = useRoute();
39
41
  const paths = usePaths();
40
42
  const { context: placementContext } = useWebPlacementContext();
41
43
 
42
- const targetSurfaceId = computed(() => {
43
- const explicitSurface = String(props.surface || "").trim().toLowerCase();
44
- if (explicitSurface && explicitSurface !== "*") {
45
- return explicitSurface;
46
- }
47
-
48
- return String(paths.currentSurfaceId.value || "").trim().toLowerCase();
49
- });
50
-
51
44
  const resolvedTo = computed(() => {
52
- const explicitTo = String(props.to || "").trim();
53
- if (explicitTo) {
54
- return explicitTo;
55
- }
56
-
57
- const workspaceRequired = surfaceRequiresWorkspaceFromPlacementContext(
58
- placementContext.value,
59
- targetSurfaceId.value
60
- );
61
- const suffix = workspaceRequired ? props.workspaceSuffix : props.nonWorkspaceSuffix;
62
- const normalizedSuffix = String(suffix || "/").trim() || "/";
63
-
64
- return paths.page(normalizedSuffix, {
65
- surface: targetSurfaceId.value,
66
- mode: "auto"
45
+ return resolveMenuLinkTarget({
46
+ to: props.to,
47
+ surface: props.surface,
48
+ currentSurfaceId: paths.currentSurfaceId.value,
49
+ placementContext: placementContext.value,
50
+ workspaceSuffix: props.workspaceSuffix,
51
+ nonWorkspaceSuffix: props.nonWorkspaceSuffix,
52
+ routeParams: route.params || {},
53
+ resolvePagePath(relativePath, options = {}) {
54
+ return paths.page(relativePath, options);
55
+ }
67
56
  });
68
57
  });
69
58
 
@@ -23,11 +23,11 @@ import { computed, reactive, ref, watch } from "vue";
23
23
  import { formatDateTime } from "@jskit-ai/kernel/shared/support";
24
24
  import MembersAdminClientElement from "./MembersAdminClientElement.vue";
25
25
  import { useCommand } from "../composables/useCommand.js";
26
- import { useList } from "../composables/useList.js";
27
- import { useView } from "../composables/useView.js";
26
+ import { useList } from "../composables/records/useList.js";
27
+ import { useView } from "../composables/records/useView.js";
28
28
  import { usePaths } from "../composables/usePaths.js";
29
29
  import { useAccess } from "../composables/useAccess.js";
30
- import { useUiFeedback } from "../composables/useUiFeedback.js";
30
+ import { useUiFeedback } from "../composables/runtime/useUiFeedback.js";
31
31
  import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
32
32
  import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
33
33
  import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
@@ -71,7 +71,7 @@ import { computed, reactive } from "vue";
71
71
  import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
72
72
  import { workspaceResource } from "@jskit-ai/users-core/shared/resources/workspaceResource";
73
73
  import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
74
- import { useAddEdit } from "../composables/useAddEdit.js";
74
+ import { useAddEdit } from "../composables/records/useAddEdit.js";
75
75
  import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
76
76
 
77
77
  const emit = defineEmits(["saved"]);
@@ -176,7 +176,7 @@ import {
176
176
  DEFAULT_WORKSPACE_LIGHT_PALETTE,
177
177
  resolveWorkspaceThemePalettes
178
178
  } from "@jskit-ai/users-core/shared/settings";
179
- import { useAddEdit } from "../composables/useAddEdit.js";
179
+ import { useAddEdit } from "../composables/records/useAddEdit.js";
180
180
  import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
181
181
  import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
182
182
  import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
@@ -8,12 +8,12 @@ import {
8
8
  import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
9
9
  import { normalizeWorkspaceList } from "../lib/bootstrap.js";
10
10
  import { useCommand } from "../composables/useCommand.js";
11
- import { useView } from "../composables/useView.js";
11
+ import { useView } from "../composables/records/useView.js";
12
12
  import { usePaths } from "../composables/usePaths.js";
13
13
  import { useRealtimeQueryInvalidation } from "../composables/useRealtimeQueryInvalidation.js";
14
14
  import { useWorkspaceSurfaceId } from "../composables/useWorkspaceSurfaceId.js";
15
15
  import { USERS_ROUTE_VISIBILITY_PUBLIC } from "@jskit-ai/users-core/shared/support/usersVisibility";
16
- import { normalizePendingInvite } from "../composables/accountSettingsRuntimeHelpers.js";
16
+ import { normalizePendingInvite } from "../composables/account-settings/accountSettingsRuntimeHelpers.js";
17
17
 
18
18
  const route = useRoute();
19
19
  const router = useRouter();
@@ -0,0 +1,61 @@
1
+ import "@jskit-ai/uploads-image-web/client/styles";
2
+ import { createManagedImageAssetRuntime } from "@jskit-ai/uploads-image-web/client/composables/createManagedImageAssetRuntime";
3
+ import { resolveFieldErrors } from "@jskit-ai/http-runtime/client";
4
+ import { usersWebHttpClient } from "../../lib/httpClient.js";
5
+
6
+ function createAccountSettingsAvatarUploadRuntime({
7
+ queryClient,
8
+ sessionQueryKey,
9
+ accountSettingsQueryKey,
10
+ selectedAvatarFileName,
11
+ applySettingsData,
12
+ reportAccountFeedback
13
+ } = {}) {
14
+ async function resolveCsrfToken() {
15
+ const sessionPayload = await queryClient.fetchQuery({
16
+ queryKey: sessionQueryKey,
17
+ queryFn: () =>
18
+ usersWebHttpClient.request("/api/session", {
19
+ method: "GET"
20
+ }),
21
+ staleTime: 60_000
22
+ });
23
+
24
+ const csrfToken = String(sessionPayload?.csrfToken || "");
25
+ if (!csrfToken) {
26
+ throw new Error("Unable to prepare secure avatar upload request.");
27
+ }
28
+
29
+ return csrfToken;
30
+ }
31
+
32
+ return createManagedImageAssetRuntime({
33
+ uploadEndpoint: "/api/settings/profile/avatar",
34
+ fieldName: "avatar",
35
+ selectedFileName: selectedAvatarFileName,
36
+ resolveRequestHeaders: async () => ({
37
+ "csrf-token": await resolveCsrfToken()
38
+ }),
39
+ onUploadSuccess: ({ data }) => {
40
+ applySettingsData(data);
41
+ queryClient.setQueryData(accountSettingsQueryKey, data);
42
+ },
43
+ reportFeedback: reportAccountFeedback,
44
+ resolveUploadErrorMessage: ({ error, response, defaultMessage }) => {
45
+ const body = response?.body && typeof response.body === "object" ? response.body : {};
46
+ const fieldErrors = resolveFieldErrors(body);
47
+ return String(fieldErrors.avatar || body?.error || error?.message || defaultMessage);
48
+ },
49
+ messages: {
50
+ uploadSuccess: "Avatar uploaded.",
51
+ uploadInvalidResponse: "Avatar uploaded, but the response payload was invalid.",
52
+ uploadError: "Unable to upload avatar.",
53
+ uploadRestriction: "Selected avatar file does not meet upload restrictions.",
54
+ editorUnavailable: "Avatar editor is unavailable in this environment.",
55
+ changeError: "Avatar uploaded, but the settings view could not refresh."
56
+ },
57
+ source: "users-web.account-settings-runtime:avatar"
58
+ });
59
+ }
60
+
61
+ export { createAccountSettingsAvatarUploadRuntime };
@@ -1,4 +1,4 @@
1
- import { resolveErrorStatusCode } from "../support/runtimeNormalization.js";
1
+ import { resolveErrorStatusCode } from "../../support/runtimeNormalization.js";
2
2
 
3
3
  function createAccountSettingsInvitesRuntime({
4
4
  invitesAvailable,
@@ -3,7 +3,7 @@ import {
3
3
  normalizeReturnToPath as normalizeSharedReturnToPath,
4
4
  resolveAllowedOriginsFromPlacementContext
5
5
  } from "@jskit-ai/kernel/shared/support";
6
- import { normalizeRecord } from "../support/runtimeNormalization.js";
6
+ import { normalizeRecord } from "../../support/runtimeNormalization.js";
7
7
 
8
8
  function normalizeReturnToPath(value, { fallback = "/", accountSettingsPath = "/account", allowedOrigins = [] } = {}) {
9
9
  return normalizeSharedReturnToPath(value, {
@@ -0,0 +1,75 @@
1
+ import { unref } from "vue";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import { asPlainObject } from "../support/scopeHelpers.js";
4
+
5
+ const CRUD_BINDING_MODE_ROUTE = "route";
6
+ const CRUD_BINDING_MODE_MERGE = "merge";
7
+ const CRUD_BINDING_MODE_EXPLICIT = "explicit";
8
+ const CRUD_BINDING_MODE_NONE = "none";
9
+
10
+ function normalizeCrudBindingMode(value = "") {
11
+ const normalizedValue = normalizeText(value).toLowerCase();
12
+ if (normalizedValue === CRUD_BINDING_MODE_MERGE) {
13
+ return CRUD_BINDING_MODE_MERGE;
14
+ }
15
+ if (normalizedValue === CRUD_BINDING_MODE_EXPLICIT) {
16
+ return CRUD_BINDING_MODE_EXPLICIT;
17
+ }
18
+ if (normalizedValue === CRUD_BINDING_MODE_NONE) {
19
+ return CRUD_BINDING_MODE_NONE;
20
+ }
21
+
22
+ return CRUD_BINDING_MODE_ROUTE;
23
+ }
24
+
25
+ function normalizeCrudBindingConfig(binding = {}) {
26
+ const source = asPlainObject(unref(binding));
27
+ return Object.freeze({
28
+ mode: normalizeCrudBindingMode(source.mode),
29
+ values: source.values ?? null
30
+ });
31
+ }
32
+
33
+ function resolveCrudBindingValues(values, context = {}) {
34
+ if (typeof values === "function") {
35
+ return asPlainObject(values(context));
36
+ }
37
+
38
+ return asPlainObject(unref(values));
39
+ }
40
+
41
+ function resolveCrudBoundValues({
42
+ binding = {},
43
+ routeValues = {},
44
+ context = {}
45
+ } = {}) {
46
+ const normalizedBinding = normalizeCrudBindingConfig(binding);
47
+ const normalizedRouteValues = asPlainObject(routeValues);
48
+ const explicitValues = resolveCrudBindingValues(normalizedBinding.values, context);
49
+
50
+ if (normalizedBinding.mode === CRUD_BINDING_MODE_NONE) {
51
+ return {};
52
+ }
53
+ if (normalizedBinding.mode === CRUD_BINDING_MODE_EXPLICIT) {
54
+ return explicitValues;
55
+ }
56
+ if (normalizedBinding.mode === CRUD_BINDING_MODE_MERGE) {
57
+ return {
58
+ ...normalizedRouteValues,
59
+ ...explicitValues
60
+ };
61
+ }
62
+
63
+ return normalizedRouteValues;
64
+ }
65
+
66
+ export {
67
+ CRUD_BINDING_MODE_ROUTE,
68
+ CRUD_BINDING_MODE_MERGE,
69
+ CRUD_BINDING_MODE_EXPLICIT,
70
+ CRUD_BINDING_MODE_NONE,
71
+ normalizeCrudBindingMode,
72
+ normalizeCrudBindingConfig,
73
+ resolveCrudBindingValues,
74
+ resolveCrudBoundValues
75
+ };
@@ -1,6 +1,6 @@
1
1
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
2
  import { normalizeCrudLookupContainerKey } from "@jskit-ai/kernel/shared/support/crudLookup";
3
- import { asPlainObject } from "./scopeHelpers.js";
3
+ import { asPlainObject } from "../support/scopeHelpers.js";
4
4
 
5
5
  const LOOKUP_LABEL_COMPOSITION_CANDIDATES = Object.freeze([
6
6
  Object.freeze(["name", "surname"]),
@@ -5,12 +5,13 @@ import {
5
5
  normalizeCrudLookupContainerKey,
6
6
  resolveCrudLookupApiPathFromNamespace
7
7
  } from "@jskit-ai/kernel/shared/support/crudLookup";
8
- import { useList } from "./useList.js";
8
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
9
+ import { useList } from "../records/useList.js";
9
10
  import {
10
11
  resolveLookupItemLabel,
11
12
  resolveLookupFieldDisplayValue
12
13
  } from "./crudLookupFieldLabelSupport.js";
13
- import { asPlainObject } from "./scopeHelpers.js";
14
+ import { asPlainObject } from "../support/scopeHelpers.js";
14
15
 
15
16
  function normalizeQueryKeyPrefix(value) {
16
17
  const source = Array.isArray(value) ? value : [];
@@ -109,12 +110,14 @@ function createCrudLookupFieldRuntime({
109
110
  defaultValue: defaultLookupContainerKey,
110
111
  context: `createCrudLookupFieldRuntime formFields["${key}"].relation.containerKey`
111
112
  });
113
+ const relationSurfaceId = normalizeSurfaceId(rawRelation.surfaceId);
112
114
  if (!valueKey) {
113
115
  continue;
114
116
  }
115
117
 
116
118
  const runtime = useList({
117
119
  adapter: adapter || undefined,
120
+ ...(relationSurfaceId ? { surfaceId: relationSurfaceId } : {}),
118
121
  apiSuffix: apiPath,
119
122
  queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
120
123
  ...normalizedQueryKeyPrefix,
@@ -147,6 +150,7 @@ function createCrudLookupFieldRuntime({
147
150
  kind: "lookup",
148
151
  namespace,
149
152
  ...(explicitApiPath ? { apiPath: explicitApiPath } : {}),
153
+ ...(relationSurfaceId ? { surfaceId: relationSurfaceId } : {}),
150
154
  containerKey: relationLookupContainerKey,
151
155
  valueKey,
152
156
  ...(labelKey ? { labelKey } : {})
@@ -1,6 +1,6 @@
1
1
  import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
2
- import { asPlainObject } from "./scopeHelpers.js";
3
- import { toRouteParamValue } from "./routeTemplateHelpers.js";
2
+ import { asPlainObject } from "../support/scopeHelpers.js";
3
+ import { toRouteParamValue } from "../support/routeTemplateHelpers.js";
4
4
 
5
5
  const EMPTY_FIELD_ERROR_LIST = Object.freeze([]);
6
6
  const fieldErrorListCache = new Map();
@@ -44,6 +44,84 @@ function resolveFormFieldType(field = {}) {
44
44
  return String(field.type || "").trim().toLowerCase();
45
45
  }
46
46
 
47
+ function resolveFormFieldFormat(field = {}) {
48
+ return String(field.format || "").trim().toLowerCase();
49
+ }
50
+
51
+ function padDateTimePart(value) {
52
+ return String(value).padStart(2, "0");
53
+ }
54
+
55
+ function normalizeTimeWhitespace(value) {
56
+ return String(value ?? "").replaceAll(/\s+/gu, " ").trim();
57
+ }
58
+
59
+ function toTimeInputValue(value) {
60
+ const normalized = normalizeTimeWhitespace(value);
61
+ if (!normalized) {
62
+ return "";
63
+ }
64
+
65
+ const twentyFourHourMatch = normalized.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/u);
66
+ if (twentyFourHourMatch) {
67
+ const hours = Number(twentyFourHourMatch[1]);
68
+ const minutes = Number(twentyFourHourMatch[2]);
69
+ if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
70
+ return `${padDateTimePart(hours)}:${padDateTimePart(minutes)}`;
71
+ }
72
+ return normalized;
73
+ }
74
+
75
+ const meridiemMatch = normalized.match(/^(\d{1,2}):(\d{2})\s*([ap]\.?m\.?)$/iu);
76
+ if (!meridiemMatch) {
77
+ return normalized;
78
+ }
79
+
80
+ const rawHours = Number(meridiemMatch[1]);
81
+ const minutes = Number(meridiemMatch[2]);
82
+ if (rawHours < 1 || rawHours > 12 || minutes < 0 || minutes > 59) {
83
+ return normalized;
84
+ }
85
+
86
+ let hours = rawHours % 12;
87
+ if (String(meridiemMatch[3] || "").toLowerCase().startsWith("p")) {
88
+ hours += 12;
89
+ }
90
+
91
+ return `${padDateTimePart(hours)}:${padDateTimePart(minutes)}`;
92
+ }
93
+
94
+ function toDateTimeLocalInputValue(value) {
95
+ if (value == null || value === "") {
96
+ return "";
97
+ }
98
+
99
+ const date = value instanceof Date ? value : new Date(value);
100
+ if (Number.isNaN(date.getTime())) {
101
+ return String(value);
102
+ }
103
+
104
+ return [
105
+ date.getFullYear(),
106
+ padDateTimePart(date.getMonth() + 1),
107
+ padDateTimePart(date.getDate())
108
+ ].join("-") + `T${padDateTimePart(date.getHours())}:${padDateTimePart(date.getMinutes())}`;
109
+ }
110
+
111
+ function toIsoUtcDateTimeValue(value) {
112
+ const normalized = String(value ?? "").trim();
113
+ if (!normalized) {
114
+ return "";
115
+ }
116
+
117
+ const date = new Date(normalized);
118
+ if (Number.isNaN(date.getTime())) {
119
+ return normalized;
120
+ }
121
+
122
+ return date.toISOString();
123
+ }
124
+
47
125
  function resolveFormFieldInitialValue(field = {}) {
48
126
  if (Object.prototype.hasOwnProperty.call(field, "initialValue")) {
49
127
  return field.initialValue;
@@ -60,6 +138,23 @@ function resolveFormFieldInitialValue(field = {}) {
60
138
  return "";
61
139
  }
62
140
 
141
+ function shouldSerializeClearedFieldAsNull(field = {}) {
142
+ if (field?.nullable !== true) {
143
+ return false;
144
+ }
145
+
146
+ const fieldType = resolveFormFieldType(field);
147
+ const fieldFormat = resolveFormFieldFormat(field);
148
+
149
+ return (
150
+ fieldType === "integer" ||
151
+ fieldType === "number" ||
152
+ fieldFormat === "date" ||
153
+ fieldFormat === "date-time" ||
154
+ fieldFormat === "time"
155
+ );
156
+ }
157
+
63
158
  function createCrudFormModel(fields = []) {
64
159
  const model = {};
65
160
  for (const field of normalizeCrudFormFields(fields)) {
@@ -76,6 +171,8 @@ function buildCrudFormPayload(fields = [], model = {}) {
76
171
  for (const field of normalizeCrudFormFields(fields)) {
77
172
  const fieldKey = field.key;
78
173
  const fieldType = resolveFormFieldType(field);
174
+ const fieldFormat = resolveFormFieldFormat(field);
175
+ const clearAsNull = shouldSerializeClearedFieldAsNull(field);
79
176
  const rawValue = sourceModel[fieldKey];
80
177
 
81
178
  if (fieldType === "boolean") {
@@ -86,6 +183,9 @@ function buildCrudFormPayload(fields = [], model = {}) {
86
183
  if (fieldType === "integer" || fieldType === "number") {
87
184
  const normalizedValue = String(rawValue ?? "").trim();
88
185
  if (!normalizedValue) {
186
+ if (clearAsNull) {
187
+ payload[fieldKey] = null;
188
+ }
89
189
  continue;
90
190
  }
91
191
 
@@ -97,6 +197,48 @@ function buildCrudFormPayload(fields = [], model = {}) {
97
197
  }
98
198
 
99
199
  if (rawValue == null) {
200
+ if (clearAsNull) {
201
+ payload[fieldKey] = null;
202
+ }
203
+ continue;
204
+ }
205
+
206
+ if (fieldFormat === "date") {
207
+ const normalizedValue = String(rawValue).trim();
208
+ if (!normalizedValue) {
209
+ if (clearAsNull) {
210
+ payload[fieldKey] = null;
211
+ }
212
+ continue;
213
+ }
214
+
215
+ payload[fieldKey] = normalizedValue;
216
+ continue;
217
+ }
218
+
219
+ if (fieldFormat === "date-time") {
220
+ const normalizedValue = toIsoUtcDateTimeValue(rawValue);
221
+ if (!normalizedValue) {
222
+ if (clearAsNull) {
223
+ payload[fieldKey] = null;
224
+ }
225
+ continue;
226
+ }
227
+
228
+ payload[fieldKey] = normalizedValue;
229
+ continue;
230
+ }
231
+
232
+ if (fieldFormat === "time") {
233
+ const normalizedValue = toTimeInputValue(rawValue);
234
+ if (!normalizedValue) {
235
+ if (clearAsNull) {
236
+ payload[fieldKey] = null;
237
+ }
238
+ continue;
239
+ }
240
+
241
+ payload[fieldKey] = normalizedValue;
100
242
  continue;
101
243
  }
102
244
 
@@ -112,6 +254,7 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
112
254
  for (const field of normalizeCrudFormFields(fields)) {
113
255
  const fieldKey = field.key;
114
256
  const fieldType = resolveFormFieldType(field);
257
+ const fieldFormat = resolveFormFieldFormat(field);
115
258
  const rawValue = sourcePayload[fieldKey];
116
259
 
117
260
  if (fieldType === "boolean") {
@@ -124,6 +267,16 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
124
267
  continue;
125
268
  }
126
269
 
270
+ if (fieldFormat === "date-time") {
271
+ targetModel[fieldKey] = toDateTimeLocalInputValue(rawValue);
272
+ continue;
273
+ }
274
+
275
+ if (fieldFormat === "time") {
276
+ targetModel[fieldKey] = toTimeInputValue(rawValue);
277
+ continue;
278
+ }
279
+
127
280
  targetModel[fieldKey] = rawValue == null ? "" : String(rawValue);
128
281
  }
129
282
  }