@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
@@ -30,6 +30,6 @@ test("arePermissionListsEqual returns false for different permission sets", () =
30
30
  });
31
31
 
32
32
  test("hasPermission supports namespace wildcard matches", () => {
33
- assert.equal(hasPermission(["crud_contacts.*"], "crud_contacts.update"), true);
34
- assert.equal(hasPermission(["crud_contacts.*"], "crud_projects.update"), false);
33
+ assert.equal(hasPermission(["crud.contacts.*"], "crud.contacts.update"), true);
34
+ assert.equal(hasPermission(["crud.contacts.*"], "crud.projects.update"), false);
35
35
  });
@@ -1,7 +1,7 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { computed, ref } from "vue";
4
- import { resolveEnabledRef } from "../src/client/composables/refValueHelpers.js";
4
+ import { resolveEnabledRef } from "../src/client/composables/support/refValueHelpers.js";
5
5
 
6
6
  test("resolveEnabledRef unwraps refs", () => {
7
7
  assert.equal(resolveEnabledRef(ref(true)), true);
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { ref } from "vue";
4
- import { hasResolvedQueryData } from "../src/client/composables/resourceLoadStateHelpers.js";
4
+ import { hasResolvedQueryData } from "../src/client/composables/support/resourceLoadStateHelpers.js";
5
5
 
6
6
  test("hasResolvedQueryData returns true when the query succeeded", () => {
7
7
  const query = {
@@ -4,7 +4,7 @@ import {
4
4
  extractRouteParamNames,
5
5
  resolveScopedRoutePathname,
6
6
  resolveRouteParamNamesInOrder
7
- } from "../src/client/composables/routeTemplateHelpers.js";
7
+ } from "../src/client/composables/support/routeTemplateHelpers.js";
8
8
 
9
9
  test("extractRouteParamNames reads dynamic params from route templates", () => {
10
10
  assert.deepEqual(
@@ -6,7 +6,7 @@ import {
6
6
  normalizeOwnershipFilter,
7
7
  resolveApiSuffix,
8
8
  resolveResourceMessages
9
- } from "../src/client/composables/scopeHelpers.js";
9
+ } from "../src/client/composables/support/scopeHelpers.js";
10
10
 
11
11
  test("resolveResourceMessages merges defaults with resource messages", () => {
12
12
  const messages = resolveResourceMessages(
@@ -11,7 +11,7 @@ import {
11
11
  applyCrudRouteBoundFieldValues,
12
12
  resolveCrudFieldErrors,
13
13
  parseCrudResourceOperationInput
14
- } from "../src/client/composables/crudSchemaFormHelpers.js";
14
+ } from "../src/client/composables/crud/crudSchemaFormHelpers.js";
15
15
 
16
16
  test("normalizeCrudFormFields trims keys, removes invalid entries, and deduplicates", () => {
17
17
  const fields = normalizeCrudFormFields([
@@ -62,6 +62,86 @@ test("buildCrudFormPayload normalizes booleans and numbers while skipping empty
62
62
  });
63
63
  });
64
64
 
65
+ test("buildCrudFormPayload and applyCrudPayloadToForm round-trip date-time fields", () => {
66
+ const fields = [
67
+ { key: "scheduledAt", type: "string", format: "date-time" }
68
+ ];
69
+ const payload = buildCrudFormPayload(fields, {
70
+ scheduledAt: "2024-01-02T03:04"
71
+ });
72
+
73
+ assert.match(payload.scheduledAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z$/);
74
+
75
+ const form = reactive({
76
+ scheduledAt: ""
77
+ });
78
+ applyCrudPayloadToForm(fields, form, payload);
79
+
80
+ assert.equal(form.scheduledAt, "2024-01-02T03:04");
81
+ });
82
+
83
+ test("buildCrudFormPayload normalizes time fields to canonical HH:MM", () => {
84
+ const fields = [
85
+ { key: "fromTime", type: "string", format: "time" },
86
+ { key: "toTime", type: "string", format: "time" }
87
+ ];
88
+
89
+ const payload = buildCrudFormPayload(fields, {
90
+ fromTime: "06:13 PM",
91
+ toTime: "18:45:00"
92
+ });
93
+
94
+ assert.deepEqual(payload, {
95
+ fromTime: "18:13",
96
+ toTime: "18:45"
97
+ });
98
+ });
99
+
100
+ test("buildCrudFormPayload serializes cleared nullable typed fields as null", () => {
101
+ const payload = buildCrudFormPayload(
102
+ [
103
+ { key: "serviceId", type: "integer", nullable: true },
104
+ { key: "fromDate", type: "string", format: "date", nullable: true },
105
+ { key: "scheduledAt", type: "string", format: "date-time", nullable: true },
106
+ { key: "fromTime", type: "string", format: "time", nullable: true }
107
+ ],
108
+ {
109
+ serviceId: null,
110
+ fromDate: "",
111
+ scheduledAt: "",
112
+ fromTime: ""
113
+ }
114
+ );
115
+
116
+ assert.deepEqual(payload, {
117
+ serviceId: null,
118
+ fromDate: null,
119
+ scheduledAt: null,
120
+ fromTime: null
121
+ });
122
+ });
123
+
124
+ test("applyCrudPayloadToForm normalizes time fields for form inputs", () => {
125
+ const fields = [
126
+ { key: "fromTime", type: "string", format: "time" },
127
+ { key: "toTime", type: "string", format: "time" }
128
+ ];
129
+ const form = reactive({
130
+ fromTime: "",
131
+ toTime: ""
132
+ });
133
+
134
+ applyCrudPayloadToForm(fields, form, {
135
+ fromTime: "18:13:00",
136
+ toTime: "06:45 PM"
137
+ });
138
+
139
+ assert.deepEqual(form, {
140
+ fromTime: "18:13",
141
+ toTime: "18:45"
142
+ });
143
+ });
144
+
65
145
  test("applyCrudPayloadToForm maps payload values into reactive form model", () => {
66
146
  const form = reactive({
67
147
  name: "",
@@ -0,0 +1,143 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { resolveCrudListParentDescriptor, resolveCrudListParentRecordTitle, resolveCrudListParentTitleFromItems } from "../src/client/composables/internal/crudListParentTitleSupport.js";
4
+
5
+ const contactChildResource = Object.freeze({
6
+ contract: {
7
+ lookup: {
8
+ containerKey: "lookups"
9
+ }
10
+ },
11
+ fieldMeta: Object.freeze([
12
+ Object.freeze({
13
+ key: "contactId",
14
+ relation: Object.freeze({
15
+ kind: "lookup",
16
+ namespace: "contacts",
17
+ valueKey: "id"
18
+ })
19
+ }),
20
+ Object.freeze({
21
+ key: "serviceId",
22
+ relation: Object.freeze({
23
+ kind: "lookup",
24
+ namespace: "services",
25
+ valueKey: "id"
26
+ })
27
+ })
28
+ ])
29
+ });
30
+
31
+ test("resolveCrudListParentDescriptor selects the nearest lookup route parent", () => {
32
+ const descriptor = resolveCrudListParentDescriptor({
33
+ resource: contactChildResource,
34
+ route: {
35
+ matched: [
36
+ { path: "/w/:workspaceSlug/admin" },
37
+ { path: "/w/:workspaceSlug/admin/contacts/:contactId/availabilities" }
38
+ ],
39
+ params: {
40
+ workspaceSlug: "dogandgroom",
41
+ contactId: "538779"
42
+ }
43
+ },
44
+ recordIdParam: "availabilityRuleId"
45
+ });
46
+
47
+ assert.deepEqual(
48
+ descriptor,
49
+ {
50
+ fieldKey: "contactId",
51
+ routeParamKey: "contactId",
52
+ relationNamespace: "contacts",
53
+ entityLabel: "Contact",
54
+ labelKey: "",
55
+ fieldDescriptor: {
56
+ key: "contactId",
57
+ relation: {
58
+ kind: "lookup",
59
+ valueKey: "id",
60
+ labelKey: "",
61
+ containerKey: "lookups"
62
+ }
63
+ },
64
+ apiUrlTemplate: "/contacts/:contactId"
65
+ }
66
+ );
67
+ });
68
+
69
+ test("resolveCrudListParentTitleFromItems uses the hydrated lookup label", () => {
70
+ const descriptor = resolveCrudListParentDescriptor({
71
+ resource: contactChildResource,
72
+ route: {
73
+ matched: [{ path: "/w/:workspaceSlug/admin/contacts/:contactId/availabilities" }],
74
+ params: {
75
+ workspaceSlug: "dogandgroom",
76
+ contactId: "538779"
77
+ }
78
+ },
79
+ recordIdParam: "availabilityRuleId"
80
+ });
81
+
82
+ const title = resolveCrudListParentTitleFromItems(
83
+ [
84
+ {
85
+ id: 1,
86
+ contactId: 538779,
87
+ lookups: {
88
+ contactId: {
89
+ id: 538779,
90
+ firstName: "Jessica",
91
+ lastName: "Dickinson"
92
+ }
93
+ }
94
+ }
95
+ ],
96
+ descriptor
97
+ );
98
+
99
+ assert.equal(title, "Jessica Dickinson");
100
+ });
101
+
102
+ test("resolveCrudListParentRecordTitle falls back to entity label plus id", () => {
103
+ const title = resolveCrudListParentRecordTitle(
104
+ {
105
+ id: 538779
106
+ },
107
+ {
108
+ entityLabel: "Contact",
109
+ labelKey: ""
110
+ }
111
+ );
112
+
113
+ assert.equal(title, "Contact #538779");
114
+ });
115
+
116
+ test("resolveCrudListParentDescriptor supports parentRouteParamKey aliases", () => {
117
+ const descriptor = resolveCrudListParentDescriptor({
118
+ resource: {
119
+ fieldMeta: [
120
+ {
121
+ key: "staffContactId",
122
+ parentRouteParamKey: "contactId",
123
+ relation: {
124
+ kind: "lookup",
125
+ namespace: "contacts",
126
+ valueKey: "id"
127
+ }
128
+ }
129
+ ]
130
+ },
131
+ route: {
132
+ matched: [{ path: "/w/:workspaceSlug/admin/contacts/:contactId/availabilities" }],
133
+ params: {
134
+ workspaceSlug: "dogandgroom",
135
+ contactId: "538779"
136
+ }
137
+ },
138
+ recordIdParam: "availabilityRuleId"
139
+ });
140
+
141
+ assert.equal(descriptor?.fieldKey, "staffContactId");
142
+ assert.equal(descriptor?.routeParamKey, "contactId");
143
+ });
@@ -3,7 +3,7 @@ import test from "node:test";
3
3
  import {
4
4
  normalizeListSearchConfig,
5
5
  matchesLocalSearch
6
- } from "../src/client/composables/listSearchSupport.js";
6
+ } from "../src/client/composables/support/listSearchSupport.js";
7
7
 
8
8
  test("normalizeListSearchConfig defaults to disabled query search", () => {
9
9
  const config = normalizeListSearchConfig();
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { ref } from "vue";
4
- import { useViewCore } from "../src/client/composables/useViewCore.js";
4
+ import { useViewCore } from "../src/client/composables/runtime/useViewCore.js";
5
5
 
6
6
  test("useViewCore prefers resource isInitialLoading/isFetching signals when provided", () => {
7
7
  const resource = {
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { ref } from "vue";
4
- import { createViewUiRuntime } from "../src/client/composables/viewUiRuntime.js";
4
+ import { createViewUiRuntime } from "../src/client/composables/runtime/viewUiRuntime.js";
5
5
 
6
6
  test("createViewUiRuntime resolves api/list/edit paths with nested params", () => {
7
7
  const runtime = createViewUiRuntime({
@@ -1,95 +0,0 @@
1
- import "@jskit-ai/uploads-image-web/client/styles";
2
- import { createImageUploadRuntime } from "@jskit-ai/uploads-image-web/client/composables/createImageUploadRuntime";
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 createImageUploadRuntime({
33
- endpoint: "/api/settings/profile/avatar",
34
- fieldName: "avatar",
35
- resolveRequestHeaders: async () => ({
36
- "csrf-token": await resolveCsrfToken()
37
- }),
38
- onSelectedFileNameChanged: (fileName) => {
39
- selectedAvatarFileName.value = String(fileName || "");
40
- },
41
- onUploadSuccess: ({ data, uppy }) => {
42
- applySettingsData(data);
43
- queryClient.setQueryData(accountSettingsQueryKey, data);
44
-
45
- const dashboard = uppy.getPlugin("Dashboard");
46
- if (dashboard && typeof dashboard.closeModal === "function") {
47
- dashboard.closeModal();
48
- }
49
-
50
- reportAccountFeedback({
51
- message: "Avatar uploaded.",
52
- severity: "success",
53
- channel: "snackbar",
54
- dedupeKey: "users-web.account-settings-runtime:avatar-uploaded"
55
- });
56
- },
57
- onInvalidResponse: () => {
58
- reportAccountFeedback({
59
- message: "Avatar uploaded, but the response payload was invalid.",
60
- severity: "error",
61
- channel: "banner",
62
- dedupeKey: "users-web.account-settings-runtime:avatar-upload-invalid-response"
63
- });
64
- },
65
- onUploadError: ({ error, response }) => {
66
- const body = response?.body && typeof response.body === "object" ? response.body : {};
67
- const fieldErrors = resolveFieldErrors(body);
68
-
69
- reportAccountFeedback({
70
- message: String(fieldErrors.avatar || body?.error || error?.message || "Unable to upload avatar."),
71
- severity: "error",
72
- channel: "banner",
73
- dedupeKey: "users-web.account-settings-runtime:avatar-upload-error"
74
- });
75
- },
76
- onRestrictionFailed: ({ error }) => {
77
- reportAccountFeedback({
78
- message: String(error?.message || "Selected avatar file does not meet upload restrictions."),
79
- severity: "error",
80
- channel: "banner",
81
- dedupeKey: "users-web.account-settings-runtime:avatar-upload-restriction"
82
- });
83
- },
84
- onUnavailable: () => {
85
- reportAccountFeedback({
86
- message: "Avatar editor is unavailable in this environment.",
87
- severity: "error",
88
- channel: "banner",
89
- dedupeKey: "users-web.account-settings-runtime:avatar-editor-unavailable"
90
- });
91
- }
92
- });
93
- }
94
-
95
- export { createAccountSettingsAvatarUploadRuntime };